test(fulfillment): reconcile exact source positions and inventory balances

This commit is contained in:
2026-09-30 13:15:20 +08:00
parent eae630e60d
commit 74f0d823da
2 changed files with 140 additions and 16 deletions
@@ -1,5 +1,5 @@
import test from 'node:test'
import { relabelOutboundMarksMatch, inventoryQuantitySnapshot, verifyPartialOrderCompletion,
import { relabelOutboundMarksMatch, inventoryQuantitySnapshot, inventorySnapshot, verifyPartialOrderCompletion,
completeReturnedException, verifyCustomerSignedDocuments, createSignedDocumentIsolationCustomer } from './phase5-overseas-fulfillment.mjs'
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
@@ -169,12 +169,17 @@ test('same dispatch-return exception reaches actual restored inventory and retri
let uploads = 0
const source = { outboundPalletItemId: '51', inventoryPositionId: '61', maxReturnPackages: 1 }
const context = contextFor((method, path, body) => {
if (path.includes('/inventory/balances/1/positions')) return ok([{ id: '3', balanceId: '1',
if (path.includes('/inventory/balances/1/positions')) return ok(path.includes('palletNo=') ? (completed ? [{ id: '3', balanceId: '1',
customerId: '100', overseasWarehouseId: 'WH-1', inboundCode: 'IN-A', palletId: '71',
palletNo: 'RET-TAIL-CURRENT', warehouseLocationId: '72', totalPackages: 1, availablePackages: 1 }])
palletNo: 'RET-TAIL-CURRENT', warehouseLocationId: '72', totalPackages: 1, availablePackages: 1,
holdPackages: 0, allocatedPackages: 0, pickedPackages: 0, receivingPackages: 0 }] : [])
: [{ id: 'OLD', balanceId: '1', customerId: '100', overseasWarehouseId: 'WH-1', inboundCode: 'IN-A',
palletNo: 'SOURCE', totalPackages: 8, availablePackages: 8, holdPackages: 0, allocatedPackages: 0, pickedPackages: 0, receivingPackages: 0 },
...(completed ? [{ id: '3', balanceId: '1', customerId: '100', overseasWarehouseId: 'WH-1', inboundCode: 'IN-A', palletId: '71',
palletNo: 'RET-TAIL-CURRENT', warehouseLocationId: '72', totalPackages: 1, availablePackages: 1, holdPackages: 0, allocatedPackages: 0, pickedPackages: 0, receivingPackages: 0 }] : [])])
if (path.includes('/inventory/balances')) return ok({ total: 1, records: [{ id: '1', customerId: '100',
overseasWarehouseId: 'WH-1', inboundCode: 'IN-A', totalPackages: completed ? 9 : 8,
availablePackages: completed ? 9 : 8, holdPackages: 0, allocatedPackages: 0, pickedPackages: 0 }] })
overseasWarehouseId: 'WH-1', totalPackages: completed ? 9 : 8,
availablePackages: completed ? 9 : 8, holdPackages: 0, allocatedPackages: 0, pickedPackages: 0, receivingPackages: 0, inTransitPackages: 0, procurementPackages: 0 }] })
if (path.includes('/inventory/ledger')) return ok({ total: completed ? 1 : 0, records: completed ? [{ id: '2',
customerId: '100', inboundCode: 'IN-A', businessType: 'RETURN_STOCK_IN', businessOrderNo: 'EX-1',
outboundOrderNo: 'OB-1', deltaPackages: 1, beforePackages: 0, afterPackages: 1 }] : [] })
@@ -303,3 +308,77 @@ test('incomplete SIGNED-B producer remains a blocker and never logs in as the pr
assert.ok(context.assertions.some(row => !row.passed))
assert.equal(context.context.phase5TailVerification, undefined)
})
function inventoryFixture() {
const position = (positionId, inboundCode, packages) => ({ id: positionId, balanceId: '1', customerId: '100',
overseasWarehouseId: 'WH-1', inboundCode, inboundOrderId: `ORDER-${inboundCode}`, palletId: `PALLET-${positionId}`,
palletNo: `P-${positionId}`, batchNo: 'B-1', warehouseLocationId: 'LOCATION-1', warehouseLocationCode: 'L-1',
totalPackages: packages, availablePackages: packages, holdPackages: 0, allocatedPackages: 0, pickedPackages: 0, receivingPackages: 0 })
return { balance: { id: '1', customerId: '100', overseasWarehouseId: 'WH-1', skuCode: 'MIXED',
totalPackages: 9, availablePackages: 9, holdPackages: 0, allocatedPackages: 0, pickedPackages: 0,
receivingPackages: 0, inTransitPackages: 4, procurementPackages: 2 },
positions: [position('11', 'IN-A', 3), position('12', 'IN-A-1', 2), position('13', 'IN-B', 4)] }
}
function inventoryContext(fixture) {
return contextFor((method, path) => path.includes('/positions') ? ok(fixture.positions)
: ok({ records: [fixture.balance], total: 1 }))
}
test('inventory evidence resolves missing Balance inboundCode through exact positions and retains an independent shared balance', async () => {
const fixture = inventoryFixture()
const context = inventoryContext(fixture)
const snapshot = await inventorySnapshot(context, '100', 'WH-1', ['IN-A', 'IN-A', 'IN-B'], 'stock')
assert.equal(snapshot.length, 1)
assert.equal(snapshot[0].id, '1')
assert.equal(snapshot[0].totalPackages, 7)
assert.deepEqual(snapshot[0].inboundCodes, ['IN-A', 'IN-B'])
assert.deepEqual(snapshot[0].matchingPositionIds, ['11', '13'])
assert.equal(snapshot[0].balance.totalPackages, 9)
assert.equal(snapshot[0].balance.inTransitPackages, 4)
assert.equal(snapshot[0].balance.procurementPackages, 2)
assert.equal(snapshot[0].positions.length, 3)
assert.deepEqual(context.assertions.filter(row => !row.passed), [])
assert.equal(context.calls.filter(call => call.path.includes('/positions')).length, 1)
const childOnly = inventoryFixture()
childOnly.positions = [childOnly.positions[1]]
childOnly.balance.totalPackages = 2; childOnly.balance.availablePackages = 2
assert.deepEqual(await inventorySnapshot(inventoryContext(childOnly), '100', 'WH-1', ['IN-A'], 'stock'), [])
})
test('inventory evidence rejects foreign balances and positions, mismatched balance IDs, duplicate identities, and broken bucket reconciliation', async () => {
for (const mutate of [
fixture => { fixture.balance.customerId = 'OTHER' },
fixture => { fixture.balance.overseasWarehouseId = 'OTHER' },
fixture => { fixture.positions[0].customerId = 'OTHER' },
fixture => { fixture.positions[0].overseasWarehouseId = 'OTHER' },
fixture => { fixture.positions[0].balanceId = 'OTHER' },
fixture => { fixture.positions[0].id = fixture.positions[1].id },
fixture => { fixture.positions[0].inboundCode = '' },
fixture => { fixture.positions[0].receivingPackages = 1 },
fixture => { fixture.balance.inTransitPackages = null },
fixture => { delete fixture.positions[0].receivingPackages },
fixture => { fixture.balance.availablePackages = 8 },
]) {
const fixture = inventoryFixture(); mutate(fixture)
const context = inventoryContext(fixture)
assert.equal(await inventorySnapshot(context, '100', 'WH-1', ['IN-A'], 'stock'), null)
assert.ok(context.assertions.some(row => !row.passed))
}
})
test('inventory evidence captures zero drift independently for every balance bucket, position identity, and unselected sibling position', async () => {
const fixture = inventoryFixture()
const original = await inventorySnapshot(inventoryContext(fixture), '100', 'WH-1', ['IN-A'], 'stock')
for (const field of ['inTransitPackages', 'procurementPackages']) {
const changed = inventoryFixture(); changed.balance[field] += 1
assert.notDeepEqual(await inventorySnapshot(inventoryContext(changed), '100', 'WH-1', ['IN-A'], 'stock'), original)
}
for (const field of ['palletId', 'palletNo', 'batchNo', 'warehouseLocationId', 'warehouseLocationCode', 'inboundOrderId']) {
const changed = inventoryFixture(); changed.positions[2][field] = 'DIFFERENT'
assert.notDeepEqual(await inventorySnapshot(inventoryContext(changed), '100', 'WH-1', ['IN-A'], 'stock'), original)
}
for (const field of ['holdPackages', 'allocatedPackages', 'pickedPackages', 'receivingPackages']) {
const changed = inventoryFixture(); changed.positions[2][field] += 1; changed.balance[field] += 1
assert.notDeepEqual(await inventorySnapshot(inventoryContext(changed), '100', 'WH-1', ['IN-A'], 'stock'), original)
}
})