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 } }