178 lines
14 KiB
JavaScript
178 lines
14 KiB
JavaScript
import { receivingProgressMatches } from './dropship-outbound-edge-cases.mjs'
|
|
|
|
export async function run(context) {
|
|
const { runMixedContainer } = await import('./dropship-pallet-matrix.mjs')
|
|
return runMixedContainer(context)
|
|
}
|
|
|
|
export function mixedReceivingSchedule(group) {
|
|
const fba = group.filter(r => r.order.fulfillmentType === 'STANDARD' && r.order.destinationType === 'FBA')
|
|
const overseas = group.filter(r => r.order.fulfillmentType === 'STANDARD' && r.order.destinationType === 'OVERSEAS')
|
|
const dropship = group.filter(r => r.order.fulfillmentType === 'ONE_CLICK_DROPSHIP')
|
|
if (group.length !== 3 || fba.length !== 1 || overseas.length !== 1 || dropship.length !== 1
|
|
|| dropship[0].task?.pallets?.length !== 2) throw new Error('混柜必须包含普通FBA、普通海外仓及一件代发双托三票')
|
|
const [f, o, d] = [fba[0], overseas[0], dropship[0]]
|
|
const fbaPallet = f.overseas.plan.pallets?.[0]?.palletNo
|
|
const overseasPallets = o.overseas.plan.pallets || []
|
|
if (!fbaPallet || overseasPallets.length !== 2 || overseasPallets.some(pallet => !pallet.palletNo)
|
|
|| o.expectedCartonMarks.length !== 4 || f.expectedCartonMarks.length < 2) throw new Error('混柜普通订单缺少真实托盘或分批箱数')
|
|
const palletStep = pallet => ({ record: d, path: 'pallet', count: pallet.cartons.filter(c =>
|
|
String(c.inboundId) === String(d.source.inboundId)).length, body: { sourcePalletNo: pallet.palletNo } })
|
|
return [
|
|
{ record: f, path: 'bulk', count: 1, body: { inboundCode: f.source.inboundCode, palletCode: fbaPallet, packageCount: 1 } },
|
|
palletStep(d.task.pallets[0]),
|
|
...o.expectedCartonMarks.map((cartonMark, index) => ({ record: o, path: 'carton', count: 1,
|
|
body: { cartonMark, palletCode: overseasPallets[index === 3 ? 1 : 0].palletNo } })),
|
|
{ record: f, path: 'bulk', count: f.expectedCartonMarks.length - 1,
|
|
body: { inboundCode: f.source.inboundCode, palletCode: fbaPallet, packageCount: f.expectedCartonMarks.length } },
|
|
palletStep(d.task.pallets[1]),
|
|
]
|
|
}
|
|
|
|
export async function receiveMixedContainer({ group, call, check, request, marker }) {
|
|
const base = '/api/pda/v1/last-leg/receiving-containers/current'
|
|
const steps = mixedReceivingSchedule(group)
|
|
const expected = group.map(r => ({ inboundCode: r.source.inboundCode, total: r.expectedCartonMarks.length, received: 0 }))
|
|
for (const record of group.filter(r => r.order.fulfillmentType === 'ONE_CLICK_DROPSHIP' || r.order.destinationType === 'OVERSEAS')) {
|
|
const before = await call('混柜错误收货模式前回查', 'GET', base)
|
|
const invalid = await request('POST', `${base}/receipts/bulk`, {
|
|
inboundCode: record.source.inboundCode, palletCode: record.overseas.plan.pallets[0].palletNo,
|
|
packageCount: 1, idempotencyKey: `${marker}-forbid-bulk-${record.label}`,
|
|
})
|
|
check('普通海外仓和一件代发均不能绕过各自规则使用FBA批量收货', invalid, invalid.body?.code === 'A0409')
|
|
const after = await call('混柜错误收货模式拒绝后回查', 'GET', base)
|
|
check('混柜错误收货模式拒绝后零漂移', null, JSON.stringify(before) === JSON.stringify(after))
|
|
}
|
|
for (const [index, step] of steps.entries()) {
|
|
const before = await call('混柜交替收货前进度', 'GET', base)
|
|
check('混柜每步前逐票和柜级进度准确', null, receivingProgressMatches(before, expected))
|
|
const premature = await request('POST', `${base}/complete`,
|
|
{ allowDifference: false, idempotencyKey: `${marker}-early-${index}` })
|
|
check('混柜任一票未齐不得正常完成', premature, premature.body?.code === 'A0409')
|
|
const afterRejected = await call('混柜提前完成拒绝后回查', 'GET', base)
|
|
check('混柜提前完成拒绝后零漂移', null, JSON.stringify(afterRejected) === JSON.stringify(before))
|
|
const path = `${base}/receipts/${step.path}`
|
|
const body = { ...step.body, idempotencyKey: `${marker}-mixed-receive-${index}` }
|
|
const received = await call(`${step.record.label}交替收货`, 'POST', path, body)
|
|
check('混柜本次只收当前操作对应箱数', null, Number(received.acceptedPackages) === step.count)
|
|
const replay = await call('混柜收货同键重放', 'POST', path, body)
|
|
check('混柜同键重放结果稳定', null, JSON.stringify(replay) === JSON.stringify(received))
|
|
expected.find(row => row.inboundCode === step.record.source.inboundCode).received += step.count
|
|
const after = await call('混柜每步后逐票回查', 'GET', base)
|
|
check('混柜交替收货不串票不重计', null, receivingProgressMatches(after, expected))
|
|
const changedBody = step.path === 'pallet' ? { ...body, sourcePalletNo: `${body.sourcePalletNo}-CHANGED` }
|
|
: step.path === 'bulk' ? { ...body, packageCount: body.packageCount + 1 }
|
|
: { ...body, palletCode: `${body.palletCode}-CHANGED` }
|
|
const changed = await request('POST', path, changedBody)
|
|
check('混柜收货同键异载荷必须冲突', changed, changed.body?.code === 'A0409')
|
|
const duplicate = await request('POST', path, { ...body, idempotencyKey: `${body.idempotencyKey}-duplicate` })
|
|
check('混柜同货物新键重复操作不重复收货', duplicate, step.path === 'carton'
|
|
? duplicate.body?.code === 'A0409'
|
|
: duplicate.body?.code === '200' && duplicate.body?.data?.result === 'ALREADY_RECEIVED'
|
|
&& Number(duplicate.body?.data?.acceptedPackages) === 0)
|
|
const afterRetries = await call('混柜重复和冲突请求后回查全部票', 'GET', base)
|
|
check('混柜冲突及新键重复后全柜事实不变', null, JSON.stringify(afterRetries) === JSON.stringify(after))
|
|
}
|
|
const body = { allowDifference: false, idempotencyKey: `${marker}-mixed-complete` }
|
|
const completed = await call('混柜全部核平后正常完成入库', 'POST', `${base}/complete`, body)
|
|
check('混柜三票全收且普通完成', null,
|
|
completed.status === 'COMPLETED' && receivingProgressMatches(completed, expected))
|
|
const replay = await call('混柜入库完成重放', 'POST', `${base}/complete`, body)
|
|
check('混柜完成重放不重复入库', null, JSON.stringify(replay) === JSON.stringify(completed))
|
|
return completed
|
|
}
|
|
|
|
export async function receiveOverseasComplete({ record, call, adminCall, check, marker }) {
|
|
const base = '/api/pda/v1/last-leg/receiving-containers/current'
|
|
const inboundCode = record.source.inboundCode
|
|
const plan = record.overseas.plan
|
|
const pallet = plan.pallets[0]
|
|
const expected = record.expectedCartonMarks.length
|
|
check('普通海外仓全收场景具备四个真实箱码和预留托盘', null, expected === 4 && Boolean(pallet?.palletNo))
|
|
for (const [index, cartonMark] of record.expectedCartonMarks.entries()) {
|
|
const carton = await call('全收场景解析本柜箱码', 'POST', `${base}/cartons/resolve`, { cartonMark })
|
|
check('全收箱码属于当前普通海外仓票', null, carton.inboundCode === inboundCode && carton.destinationType === 'OVERSEAS')
|
|
const resolved = await call('全收场景校验预留托盘', 'POST', `${base}/pallets/resolve`, { inboundCode, palletCode: pallet.palletNo })
|
|
check('全收预留托盘校验通过', null, resolved.compatible === true && resolved.palletCode === pallet.palletNo)
|
|
const receipt = await call('全收场景逐箱实收', 'POST', `${base}/receipts/carton`,
|
|
{ cartonMark, palletCode: pallet.palletNo, idempotencyKey: `${marker}-complete-carton-${index}` })
|
|
check('全收本次只接收当前一箱', null, Number(receipt.acceptedPackages) === 1)
|
|
}
|
|
const body = { allowDifference: false, idempotencyKey: `${marker}-ordinary-complete` }
|
|
const completed = await call('四箱全收后正常完成', 'POST', `${base}/complete`, body)
|
|
check('四箱全收正常完成且无差异', null, completed.status === 'COMPLETED'
|
|
&& Number(completed.receivedPackages) === expected && Number(completed.pendingPackages) === 0)
|
|
const replay = await call('正常完成原键重放', 'POST', `${base}/complete`, body)
|
|
check('正常完成重放不重复入库', null, JSON.stringify(replay) === JSON.stringify(completed))
|
|
const finalPlan = await adminCall('回查全收最终计划', 'GET',
|
|
`/api/platform/v1/last-leg/inbound-plans/${encodeURIComponent(plan.planNo)}?customerId=${encodeURIComponent(record.order.tenantId)}`)
|
|
check('全收计划完成且应收实收均为四箱', null, finalPlan.status === 'COMPLETED'
|
|
&& Number(finalPlan.expectedPackageCount) === expected && Number(finalPlan.pickedPackageCount) === expected)
|
|
const stock = await call('回查全收可用库存', 'POST', '/api/pda/v1/last-leg/inventory/resolve', { code: inboundCode })
|
|
check('全收库存只有本票实际收到的四箱', null, stock.queryCode === inboundCode && stock.items?.length > 0
|
|
&& stock.items.every(row => row.inboundCode === inboundCode && String(row.customerId) === String(record.order.tenantId)
|
|
&& row.locationId && plan.pallets.some(item => String(item.warehousePalletId) === String(row.palletId)))
|
|
&& stock.items.reduce((sum, row) => sum + Number(row.availablePackages), 0) === expected)
|
|
return { completed, plan: finalPlan, stock, receivingVerified: true, normalCompletionVerified: true }
|
|
}
|
|
|
|
/** 逐箱短少只按本轮真实已收箱入库,验证后台确认、重放和库存归属。 */
|
|
export async function receiveOverseasShortage({ record, call, adminCall, check, request, marker }) {
|
|
const base = '/api/pda/v1/last-leg/receiving-containers/current'
|
|
const inboundCode = record.source.inboundCode
|
|
const plan = record.overseas.plan
|
|
const pallet = plan.pallets[0]
|
|
const expected = record.expectedCartonMarks.length
|
|
const actual = expected - 1
|
|
check('普通海外仓短少场景具备四个真实箱码和预留托盘', null, expected === 4 && Boolean(pallet?.palletNo))
|
|
const before = await call('错误托盘校验前回查', 'GET', base)
|
|
const wrong = await request('POST', `${base}/pallets/resolve`, { inboundCode, palletCode: `${marker}-UNRESERVED` })
|
|
check('普通海外仓非本票预留托盘在校验时拒绝', wrong, wrong.body?.code === 'A0409')
|
|
const unchanged = await call('错误托盘校验后回查', 'GET', base)
|
|
check('错误托盘校验后实收零漂移', null, JSON.stringify(before) === JSON.stringify(unchanged))
|
|
for (const [index, cartonMark] of record.expectedCartonMarks.slice(0, actual).entries()) {
|
|
const carton = await call('短少场景解析本柜箱码', 'POST', `${base}/cartons/resolve`, { cartonMark })
|
|
check('箱码属于当前普通海外仓票', null, carton.inboundCode === inboundCode && carton.destinationType === 'OVERSEAS')
|
|
const resolved = await call('短少场景校验预留托盘', 'POST', `${base}/pallets/resolve`, { inboundCode, palletCode: pallet.palletNo })
|
|
check('预留托盘校验通过', null, resolved.compatible === true && resolved.palletCode === pallet.palletNo)
|
|
const body = { cartonMark, palletCode: pallet.palletNo, idempotencyKey: `${marker}-shortage-carton-${index}` }
|
|
const receipt = await call('短少场景逐箱实收', 'POST', `${base}/receipts/carton`, body)
|
|
check('本次只接收当前一箱', null, Number(receipt.acceptedPackages) === 1)
|
|
const replay = await call('逐箱实收同键重放', 'POST', `${base}/receipts/carton`, body)
|
|
check('逐箱重放不重复计数', null, JSON.stringify(receipt) === JSON.stringify(replay))
|
|
const duplicate = await request('POST', `${base}/receipts/carton`, { ...body, idempotencyKey: `${body.idempotencyKey}-duplicate` })
|
|
check('同箱新键重扫拒绝', duplicate, duplicate.body?.code === 'A0409')
|
|
}
|
|
const premature = await request('POST', `${base}/complete`, { allowDifference: false, idempotencyKey: `${marker}-shortage-normal-complete` })
|
|
check('短少场景拒绝正常完成', premature, premature.body?.code === 'A0409')
|
|
const completeBody = { allowDifference: true, idempotencyKey: `${marker}-shortage-onsite-complete` }
|
|
const completed = await call('带短少结束现场收货', 'POST', `${base}/complete`, completeBody)
|
|
check('现场结束保留短少一箱', null, completed.status === 'COMPLETED_WITH_DIFFERENCE'
|
|
&& Number(completed.receivedPackages) === actual && Number(completed.pendingPackages) === 1)
|
|
const completeReplay = await call('带短少结束同键重放', 'POST', `${base}/complete`, completeBody)
|
|
check('现场结束重放稳定', null, JSON.stringify(completeReplay) === JSON.stringify(completed))
|
|
const planPath = `/api/platform/v1/last-leg/inbound-plans/${encodeURIComponent(plan.planNo)}`
|
|
const customerId = String(record.order.tenantId)
|
|
const pending = await adminCall('读取短少待核平计划', 'GET', `${planPath}?customerId=${encodeURIComponent(customerId)}`)
|
|
check('短少计划等待后台确认', null, pending.status === 'PENDING_RECONCILIATION'
|
|
&& Number(pending.expectedPackageCount) === expected && Number(pending.pickedPackageCount) === actual
|
|
&& Number.isInteger(pending.version))
|
|
const confirmBody = { customerId, version: pending.version, expectedPackageCount: expected,
|
|
receivedPackageCount: actual, reason: `${marker}逐箱核对短少一箱` }
|
|
const headers = { 'Idempotency-Key': `${marker}-shortage-confirm` }
|
|
const confirmed = await adminCall('后台确认普通海外仓短少入库', 'POST', `${planPath}/confirm-shortage`, confirmBody, headers)
|
|
check('普通海外仓短少确认成功', null, confirmed.status === 'PARTIAL_INBOUND')
|
|
const confirmReplay = await adminCall('后台短少确认同键重放', 'POST', `${planPath}/confirm-shortage`, confirmBody, headers)
|
|
check('后台短少确认重放不重复入库', null, JSON.stringify(confirmReplay) === JSON.stringify(confirmed))
|
|
const finalPlan = await adminCall('回查短少入库最终计划', 'GET', `${planPath}?customerId=${encodeURIComponent(customerId)}`)
|
|
check('短少入库保留原应收及实际箱数', null, finalPlan.status === 'PARTIAL_INBOUND'
|
|
&& Number(finalPlan.expectedPackageCount) === expected && Number(finalPlan.pickedPackageCount) === actual)
|
|
const stock = await call('回查短少入库可用库存', 'POST', '/api/pda/v1/last-leg/inventory/resolve', { code: inboundCode })
|
|
check('短少库存只有本票真实已收三箱', null, stock.queryCode === inboundCode && Array.isArray(stock.items)
|
|
&& stock.items.length > 0 && stock.items.every(row => row.inboundCode === inboundCode
|
|
&& String(row.customerId) === customerId && row.locationId
|
|
&& plan.pallets.some(item => String(item.warehousePalletId) === String(row.palletId)))
|
|
&& stock.items.reduce((sum, row) => sum + Number(row.availablePackages), 0) === actual)
|
|
return { completed, plan: finalPlan, stock, receivingVerified: true, shortageVerified: true }
|
|
}
|