import test from 'node:test' import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import { normalizeModuleOrder } from '../admin-test-env-acceptance.mjs' import { mixedReceivingSchedule, receiveMixedContainer, receiveOverseasComplete, receiveOverseasShortage } from './mixed-container-receiving.mjs' function fixtures() { const record = (type, fulfillmentType = 'STANDARD') => ({ label: type, order: { destinationType: type, fulfillmentType }, source: { inboundId: type, inboundCode: type }, expectedCartonMarks: [1, 2, 3, 4].map(i => `${type}#4-${i}`), overseas: { plan: { pallets: [{ palletNo: `PAL-${type}` }, ...(type === 'OVERSEAS' ? [{ palletNo: 'PAL-OVERSEAS-2' }] : [])] } } }) const d = record('DROPSHIP', 'ONE_CLICK_DROPSHIP') d.task = { pallets: [{ palletNo: 'DP-1', cartons: [{ inboundId: 'DROPSHIP', boxCode: 'DROPSHIP#4-1' }] }, { palletNo: 'DP-2', cartons: [2, 3, 4].map(i => ({ inboundId: 'DROPSHIP', boxCode: `DROPSHIP#4-${i}` })) }] } return [d, record('FBA'), record('OVERSEAS')] } test('mixed receiving is registered with prerequisites, not transfer or downstream delivery', () => { assert.deepEqual(normalizeModuleOrder(['mixed-container-receiving']), ['merchant-qualification-fixture', 'mixed-container-receiving']) const source = readFileSync(new URL('dropship-pallet-matrix.mjs', import.meta.url), 'utf8') assert.match(source, /await\s+adminCall\('回查普通海外仓现场逐托实收'/) const start = source.indexOf("const dropship = await prepare('MIXED-DROPSHIP'") const branch = source.slice(start, source.indexOf('if (stopBeforeOverseasPda)', start)) for (const entry of ["'MIXED-FBA', 1, {}, 'FBA'", "'MIXED-OVERSEAS', 1, {}, 'OVERSEAS'", 'allocate(group)', 'progressShipping(group)', 'convertOverseas(group)', 'receiveOverseas(group)', 'MIXED_CONTAINER_INBOUND_COMPLETED']) assert.ok(branch.includes(entry), entry) assert.doesNotMatch(branch, /outboundOverseas|warehouse-transfers/) assert.match(source, /混柜完成后按入仓码查询库存/) assert.match(source, /String\(row.customerId\) === String\(item.order.tenantId\)/) assert.match(source, /String\(p.warehousePalletId\) === String\(row.palletId\)/) assert.match(source, /String\(item.sourceContainerBookingId\) === sourceContainerBookingId/) assert.match(source, /\/prepare\?sourceContainerBookingId=\$\{encodeURIComponent\(sourceContainerBookingId\)\}/) assert.match(source, /普通海外仓四箱预留两托/) assert.match(source, /普通海外仓计划二加二可按三加一实收/) assert.match(source, /普通海外仓预留托盘标签返回有效PDF/) }) test('schedule interleaves three modes and uses FBA cumulative count rather than delta', () => { const steps = mixedReceivingSchedule(fixtures()) assert.deepEqual(steps.map(s => s.path), ['bulk', 'pallet', 'carton', 'carton', 'carton', 'carton', 'bulk', 'pallet']) assert.deepEqual(steps.filter(s => s.path === 'bulk').map(s => [s.count, s.body.packageCount]), [[1, 1], [3, 4]]) assert.deepEqual(steps.filter(s => s.path === 'carton').map(s => s.body.palletCode), ['PAL-OVERSEAS', 'PAL-OVERSEAS', 'PAL-OVERSEAS', 'PAL-OVERSEAS-2']) assert.equal(steps.reduce((sum, s) => sum + s.count, 0), 12) assert.throws(() => mixedReceivingSchedule(fixtures().slice(0, 2))) }) function driver({ earlyComplete = false, wrongModeAccepted = false, drift = false, repeatIncrement = false, changedPayloadAccepted = false, foreignPalletAccepted = false } = {}) { const group = fixtures() const expected = new Map(group.map(r => [r.source.inboundCode, 0])) const schedule = mixedReceivingSchedule(group) const seen = new Map() let index = 0 let complete = false const snapshot = () => ({ status: complete ? 'COMPLETED' : 'SIGNED', totalPackages: 12, receivedPackages: [...expected.values()].reduce((a, b) => a + b, 0), pendingPackages: 12 - [...expected.values()].reduce((a, b) => a + b, 0), inbounds: [...expected].map(([inboundCode, receivedPackages]) => ({ inboundCode, totalPackages: 4, receivedPackages, pendingPackages: 4 - receivedPackages })) }) return { group, marker: 'MIXED-TEST', check: (name, _response, passed) => assert.ok(passed, name), request: async (_method, path, body) => { if (path.endsWith('/pallets/resolve')) return { body: { code: foreignPalletAccepted ? '200' : body.inboundCode === 'FBA' ? 'A0404' : 'A0409', } } if (body.idempotencyKey.endsWith('-duplicate')) { return { body: path.endsWith('/carton') ? { code: 'A0409' } : { code: '200', data: { result: 'ALREADY_RECEIVED', acceptedPackages: repeatIncrement ? 1 : 0 } } } } if (seen.has(body.idempotencyKey)) return { body: { code: changedPayloadAccepted ? '200' : 'A0409' } } return { body: { code: (path.endsWith('/complete') ? earlyComplete : wrongModeAccepted) ? '200' : 'A0409' } } }, call: async (_name, method, path, body) => { if (method === 'GET') return snapshot() if (seen.has(body.idempotencyKey)) return structuredClone(seen.get(body.idempotencyKey)) if (path.endsWith('/complete')) { complete = true const result = snapshot(); seen.set(body.idempotencyKey, result); return result } const step = schedule[index++] assert.ok(path.endsWith(`/receipts/${step.path}`)) const inbound = drift ? 'FBA' : step.record.source.inboundCode expected.set(inbound, expected.get(inbound) + step.count) const result = { acceptedPackages: step.count } seen.set(body.idempotencyKey, result) return result } } } test('mixed receiving executes the whole schedule and completes only after all three rows balance', async () => { const result = await receiveMixedContainer(driver()) assert.equal(result.status, 'COMPLETED') assert.equal(result.receivedPackages, 12) }) test('mixed receiving fails on early-completion acceptance', async () => { await assert.rejects(receiveMixedContainer(driver({ earlyComplete: true }))) }) test('mixed receiving fails if another inbound real reserved pallet is accepted', async () => { await assert.rejects(receiveMixedContainer(driver({ foreignPalletAccepted: true })), /同柜另一票真实预留托盘必须拒绝/) }) test('mixed receiving fails if overseas or dropship can bypass pallet/carton receipt rules', async () => { await assert.rejects(receiveMixedContainer(driver({ wrongModeAccepted: true }))) }) test('mixed receiving detects correct aggregate count allocated to the wrong inbound', async () => { await assert.rejects(receiveMixedContainer(driver({ drift: true }))) }) test('mixed receiving rejects a new-key rescan that accepts another box', async () => { await assert.rejects(receiveMixedContainer(driver({ repeatIncrement: true }))) }) test('mixed receiving rejects same-key different-payload success', async () => { await assert.rejects(receiveMixedContainer(driver({ changedPayloadAccepted: true }))) }) function shortageDriver({ stockPackages = 3, wrongPalletAccepted = false, postCompleteReceiptAccepted = false } = {}) { const record = { order: { tenantId: 'CUSTOMER' }, source: { inboundCode: 'IN-OVERSEAS' }, expectedCartonMarks: [1, 2, 3, 4].map(index => `IN-OVERSEAS#4-${index}`), overseas: { plan: { planNo: 'IP-SHORTAGE', pallets: [{ palletNo: 'PAL-1', warehousePalletId: '900' }] } } } const receipts = new Map() const confirmation = new Map() const state = { received: 0, status: 'SIGNED', planStatus: 'PENDING_RECONCILIATION', confirmations: 0 } const snapshot = () => ({ status: state.status, receivedPackages: state.received, pendingPackages: 4 - state.received }) return { record, marker: 'SHORTAGE-TEST', state, check: (name, _response, passed) => assert.ok(passed, name), request: async (_method, path, body) => { if (/receive-after-(complete|shortage)$/.test(body.idempotencyKey || '')) { return { body: { code: postCompleteReceiptAccepted ? '200' : 'A0404' } } } return { body: { code: path.endsWith('/pallets/resolve') && wrongPalletAccepted ? '200' : 'A0409' } } }, call: async (_name, method, path, body) => { if (method === 'GET') return snapshot() if (path.endsWith('/cartons/resolve')) return { inboundCode: record.source.inboundCode, destinationType: 'OVERSEAS' } if (path.endsWith('/pallets/resolve')) return { compatible: true, palletCode: 'PAL-1' } if (path.endsWith('/inventory/resolve')) return { queryCode: record.source.inboundCode, items: [{ inboundCode: record.source.inboundCode, customerId: 'CUSTOMER', palletId: '900', locationId: '800', availablePackages: stockPackages }] } if (receipts.has(body.idempotencyKey)) return structuredClone(receipts.get(body.idempotencyKey)) if (path.endsWith('/receipts/carton')) { state.received++ const result = { acceptedPackages: 1 }; receipts.set(body.idempotencyKey, result); return result } assert.ok(path.endsWith('/complete')) assert.equal(body.allowDifference, true) state.status = 'COMPLETED_WITH_DIFFERENCE' const result = snapshot(); receipts.set(body.idempotencyKey, result); return result }, adminCall: async (_name, method, path, body, headers) => { if (method === 'GET') return { status: state.planStatus, version: 4, expectedPackageCount: 4, pickedPackageCount: state.received } assert.ok(path.endsWith('/confirm-shortage')) assert.equal(body.expectedPackageCount, 4); assert.equal(body.receivedPackageCount, 3) const key = headers['Idempotency-Key'] if (confirmation.has(key)) return confirmation.get(key) state.planStatus = 'PARTIAL_INBOUND'; state.confirmations++ const result = { status: state.planStatus }; confirmation.set(key, result); return result } } } test('ordinary overseas shortage preserves four expected cartons and stocks three exactly once', async () => { const input = shortageDriver() const result = await receiveOverseasShortage(input) assert.equal(result.shortageVerified, true) assert.equal(result.plan.expectedPackageCount, 4) assert.equal(result.plan.pickedPackageCount, 3) assert.equal(input.state.confirmations, 1) }) test('ordinary overseas shortage fails if an unreserved pallet passes validation', async () => { await assert.rejects(receiveOverseasShortage(shortageDriver({ wrongPalletAccepted: true }))) }) test('ordinary overseas shortage fails if stock includes an unreceived carton', async () => { await assert.rejects(receiveOverseasShortage(shortageDriver({ stockPackages: 4 }))) }) test('ordinary overseas shortage fails if a missing carton can be received after confirmation', async () => { await assert.rejects(receiveOverseasShortage(shortageDriver({ postCompleteReceiptAccepted: true })), /短少确认后不能继续收取未入库箱/) }) function completeDriver(stockPackages = 4, postCompleteReceiptAccepted = false) { const input = shortageDriver({ stockPackages, postCompleteReceiptAccepted }) const call = input.call input.call = async (name, method, path, body) => { if (path.endsWith('/complete')) { assert.equal(body.allowDifference, false) assert.equal(input.state.received, 4) return { status: 'COMPLETED', receivedPackages: 4, pendingPackages: 0 } } return call(name, method, path, body) } input.adminCall = async (_name, method) => { assert.equal(method, 'GET') return { status: 'COMPLETED', expectedPackageCount: 4, pickedPackageCount: input.state.received } } return input } test('ordinary overseas full receipt uses normal completion and stocks all four cartons', async () => { const result = await receiveOverseasComplete(completeDriver()) assert.equal(result.normalCompletionVerified, true) assert.equal(result.stock.items[0].availablePackages, 4) }) test('ordinary overseas full receipt rejects stock inconsistent with actual received cartons', async () => { await assert.rejects(receiveOverseasComplete(completeDriver(3))) }) test('ordinary overseas full receipt fails if another receipt is accepted after completion', async () => { await assert.rejects(receiveOverseasComplete(completeDriver(4, true)), /正常完成后新键扫码不能重新收货/) })