import assert from 'node:assert/strict' import fs from 'node:fs' import path from 'node:path' import test from 'node:test' import { fileURLToPath } from 'node:url' import { prepareAppointmentFixture } from './phase4-overseas-inbound-operations.mjs' const here = path.dirname(fileURLToPath(import.meta.url)) const source = fs.readFileSync(path.join(here, 'phase4-overseas-inbound-operations.mjs'), 'utf8') test('module covers the frozen Admin inbound-plan write surface', () => { for (const value of ['/container-preview', '/convert-container', '/pallets/supplement', '/fba-appointments/batch', '/fba-appointments/${encodeURIComponent(appointmentNo)}']) assert.ok(source.includes(value), value) assert.equal(source.includes("`${PLAN_BASE}/convert`"), false) assert.match(source, /snapshotVersion/) assert.match(source, /snapshotHash/) assert.match(source, /'Idempotency-Key'/) assert.match(source, /重复转换返回同一入仓计划/) assert.match(source, /FBA预约同单号异载荷返回A0409/) assert.match(source, /request\(context, 'DELETE'/) assert.match(source, /cancelReason/) assert.match(source, /cargo-verification/) assert.match(source, /appointments\/repartition/) assert.doesNotMatch(source, /appointmentStatus: 'CANCELLED'/) }) // An independent HTTP authority model: records original facts separately from // verification and active allocations, and rejects stale commands before mutation. function authority({ failSave = false, mutateOrder = false, unknown = 0, drift = false, packages = 4 } = {}) { const state = { calls: [], appointments: [], verification: null, order: { order: { id: 'order1', customerId: 'customer1', packageCount: packages, receivedPackageCount: 0, version: 7, sourceInboundId: 'inbound1' }, cargo: [{ packageCount: packages }] }, history: new Map() } const commands = new Map() const clone = value => structuredClone(value) const response = (data, code = '200') => ({ http: code === '200' ? 200 : code === 'A0409' ? 409 : code === 'A0400' ? 400 : 503, body: { code, data: clone(data) } }) const sourceFacts = { sourceKey: 'ORDER:order1', customerId: 'customer1', destinationKey: 'FBA:TEST1', originWarehouseId: 'warehouse1', canFollowup: true, sourceVersion: 'source-v1', packages } const fresh = allocations => { const id = String(state.appointments.length + 1) const value = { id, batchKey: `FBA_APPOINTMENT:${id}`, appointmentNo: `APPT-${id}`, version: 0, appointmentStatus: 'PENDING', replacementBatchKeys: [], items: allocations.map(a => ({ customerId: a.customerId, sourceKey: a.sourceKey, destinationKey: sourceFacts.destinationKey, packageCount: a.packageCount, sourceVersion: sourceFacts.sourceVersion, cargoVerificationVersion: state.verification.version })) } state.appointments.push(value) return value } const context = { marker: 'offline-new-four-box', assert(_module, name, _response, condition, evidence) { assert.ok(condition, `${name}: ${evidence}`) }, addStep() {}, async request(method, path, body) { state.calls.push({ method, path, body: clone(body) }) const pathname = decodeURIComponent(path.split('?')[0]) const commandKey = body?.idempotencyKey && `${pathname}/${body.idempotencyKey}` const fingerprint = JSON.stringify(body) if (commandKey && commands.has(commandKey)) { const old = commands.get(commandKey) return old.fingerprint === fingerprint ? clone(old.response) : response(null, 'A0409') } const commit = data => { const result = response(data) if (commandKey) commands.set(commandKey, { fingerprint, response: clone(result) }) return result } if (pathname.endsWith('/orders/order1')) return response(state.order) if (pathname.endsWith('/cargo-verification')) { if (method === 'GET') { const allocated = state.appointments.filter(a => !a.replacementBatchKeys.length) .flatMap(a => a.items).reduce((n, i) => n + i.packageCount, 0) return response({ source: sourceFacts, verification: state.verification, allocatedPackages: String(allocated), availablePackages: String((state.verification?.packageCount ?? packages) - allocated), unknownAllocationCount: String(unknown), sourceChanged: drift }) } if (body.expectedSourceVersion !== sourceFacts.sourceVersion || body.expectedVersion !== (state.verification?.version ?? 0)) return response(null, 'A0409') if (failSave) return response(null, 'S0503') state.verification = { version: body.expectedVersion + 1, sourceVersion: sourceFacts.sourceVersion, packageCount: body.packageCount } if (mutateOrder) state.order.order.packageCount = body.packageCount return commit(state.verification) } if (pathname.endsWith('/fba-appointments/batch')) { const created = fresh([{ customerId: 'customer1', sourceKey: sourceFacts.sourceKey, packageCount: state.verification.packageCount }]) Object.assign(created, body.details) return commit(created) } if (pathname.includes('/batches/')) return response({ appointment: state.appointments.find(a => a.batchKey === pathname.split('/').at(-1)) }) if (pathname.endsWith('/appointments/repartition')) { const originals = body.originals.map(o => state.appointments.find(a => a.appointmentNo === o.appointmentNo)) if (originals.some((a, i) => !a || a.version !== body.originals[i].version || a.replacementBatchKeys.length)) return response(null, 'A0409') const total = items => items.reduce((n, i) => n + i.packageCount, 0) if (total(originals.flatMap(a => a.items)) !== total(body.batches.flatMap(b => b.allocations))) return response(null, 'A0400') const appointments = body.batches.map(b => fresh(b.allocations)) for (const a of originals) { a.version++; a.replacementBatchKeys = appointments.map(a => a.batchKey) } return commit({ appointments, replacedBatchKeys: originals.map(a => a.batchKey) }) } const match = /\/fba-appointments\/(APPT-\d+)(\/cancellations)?$/.exec(pathname) assert.ok(match, `Unexpected request ${method} ${path}`) const appointment = state.appointments.find(a => a.appointmentNo === match[1]) if (match[2]) return response({ total: String((state.history.get(match[1]) || []).length), records: state.history.get(match[1]) || [] }) if (method === 'GET') return response(appointment) if (method === 'PUT') { if (body.version !== appointment.version) return response(null, 'A0409') appointment.version++; Object.assign(appointment, body.details) return commit(appointment) } assert.equal(method, 'DELETE') const old = state.history.get(match[1]) if (old) return old[0].version === body.version + 1 && old[0].cancelReason === body.cancelReason ? response(null) : response(null, 'A0409') if (body.version !== appointment.version) return response(null, 'A0409') appointment.version++; appointment.appointmentStatus = 'CANCELLED' state.history.set(match[1], [{ version: appointment.version, cancelReason: body.cancelReason, snapshot: clone(appointment) }]) return response(null) } } return { context, state } } const fixture = { customerId: 'customer1', plan: { planNo: 'PLAN-1', overseasWarehouseId: 'warehouse1' }, fbaLines: [{ id: 'line1', orderId: 'order1', fbaWarehouseCode: 'TEST1', destinationWarehouseId: 'destination1' }] } test('four real boxes retain original order facts through verification, cancel, split and merge', async () => { const { context, state } = authority() const result = await prepareAppointmentFixture(context, fixture) assert.equal(result.terminalStatus, 'PENDING') assert.equal(state.order.order.packageCount, 4) assert.equal(state.order.order.version, 7) assert.deepEqual(state.verification, { version: 2, sourceVersion: 'source-v1', packageCount: 4 }) assert.equal(state.appointments.length, 4) assert.equal(state.history.size, 1) assert.equal(state.history.get(result.cancelledAppointmentNo).length, 1) assert.equal(result.replacedBatchKeys.length, 3) assert.equal(result.splitBatchKeys.length, 2) const active = state.appointments.filter(a => a.appointmentStatus === 'PENDING' && !a.replacementBatchKeys.length) assert.equal(active.length, 1) assert.equal(active[0].batchKey, result.batchKey) assert.equal(active[0].items.reduce((n, i) => n + i.packageCount, 0), 4) }) for (const [name, mode, expected] of [ ['unknown write outcome', { failSave: true }, /stop, retain/], ['original order changed by verification', { mutateOrder: true }, /不覆盖原订单/], ['unknown source allocations', { unknown: 1 }, /核实数量和版本/], ['source drift', { drift: true }, /核实数量和版本/], ]) test(`${name} stops before creating any appointment`, async () => { const { context, state } = authority(mode) await assert.rejects(() => prepareAppointmentFixture(context, fixture), expected) assert.equal(state.appointments.length, 0) assert.equal(state.calls.some(c => c.path.endsWith('/fba-appointments/batch')), false) }) test('minimal two-box sample splits into one box each and merges without inflating source quantity', async () => { const { context, state } = authority({ packages: 2 }) const result = await prepareAppointmentFixture(context, fixture) const split = state.appointments.filter(a => result.splitBatchKeys.includes(a.batchKey)) assert.deepEqual(split.map(a => a.items.reduce((n, i) => n + i.packageCount, 0)), [1, 1]) assert.equal(state.appointments.find(a => a.batchKey === result.batchKey).items[0].packageCount, 2) assert.equal(state.order.order.packageCount, 2) }) test('module covers reversible inventory commands with idempotency and ledger proof', () => { for (const endpoint of ['/hold', '/release', '/adjust', '/ledger']) assert.ok(source.includes(endpoint), endpoint) assert.match(source, /PDA完成柜形成的库存/) assert.match(source, /库存冻结同键异载荷返回A0409/) assert.match(source, /盘点调整同键异载荷返回A0409/) assert.match(source, /库存数量恢复基线/) assert.match(source, /types\.filter\(\(type\) => type === 'ADJUST'\)\.length === 2/) }) test('module stays inside inbound and inventory scope', () => { for (const forbidden of ['/api/pda/', '/outbound', '/delivery', '/dispatch', '/ship', '/pick']) { assert.equal(source.toLowerCase().includes(forbidden), false, forbidden) } assert.doesNotMatch(source, /\bfetch\s*\(|process\.env|https?:\/\//) })