fix(lastleg): repair dispatch cancellation and return execution contracts

This commit is contained in:
2026-10-18 16:34:14 +08:00
parent 66a0beb653
commit aa33514ec6
64 changed files with 2061 additions and 213 deletions
@@ -1,6 +1,6 @@
import test from 'node:test'
import { relabelOutboundMarksMatch, inventoryQuantitySnapshot, inventorySnapshot, dispatchInventorySources, inventorySourcesPresent, dispatchStockConservation, verifyDepartureReadiness, verifyPartialOrderCompletion, verifyFulfillmentRootStatus,
completeReturnedException, returnedExceptionFactsMatch, verifyCustomerSignedDocuments, createSignedDocumentIsolationCustomer,
createDeliveryReturn, completeReturnedException, returnedExceptionFactsMatch, verifyCustomerSignedDocuments, createSignedDocumentIsolationCustomer,
dispatchDestinationConservation } from './phase5-overseas-fulfillment.mjs'
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
@@ -105,7 +105,7 @@ test('phase-five module proves receipt-save completion, cross-region convergence
"visitResult: 'SATISFIED'", '/exception-orders',
'/return-inbound-options', '/mark-return', "returnDirection: 'OVERSEAS_WAREHOUSE'",
'returnSources', 'outboundPalletItemId', 'inventoryPositionId',
'value.returnSources.some', 'returnInbound?.deliveryNo === deliveryNo',
'value.returnSources.some', 'returnCandidate?.deliveryNo === deliveryNo',
"idempotencyKey: key(context, 'DELIVERY-RETURN')", '派送退回同键同载荷重放',
'RETURN_PENDING_WITH_COMPLETED_VISIT',
]) assert.ok(source.includes(token), token)
@@ -183,6 +183,68 @@ function contextFor(request) {
}
const ok = data => ({ http: 200, body: { code: '200', data } })
function deliveryReturnFixture() {
return { lastMileOrderId: '9007199254740993', lastMileOrderNo: 'LM-1', inboundOrderId: '81', inboundCode: 'IN-A',
customerId: '100', overseasWarehouseId: 'WH-1', deliveryNo: 'DL-1', outboundNo: 'OB-1',
returnSources: [{ outboundPalletItemId: '51', inventoryPositionId: '61', returnCartonId: null, maxReturnPackages: 2 }] }
}
const returnScope = { customerId: '100', warehouseId: 'WH-1', deliveryNo: 'DL-1', outboundNo: 'OB-1' }
for (const ordinaryInbound of ['81', null, undefined]) test(`phase5 mark-return sends the actual master key; inbound=${ordinaryInbound}`, async () => {
const candidate = deliveryReturnFixture(); candidate.inboundOrderId = ordinaryInbound
if (!ordinaryInbound) candidate.inboundCode = null
const context = contextFor((method, path, body) => {
if (method === 'GET') { assert.match(path, /return-inbound-options/); return ok([candidate]) }
assert.equal(method, 'POST'); assert.match(path, /mark-return/)
assert.equal(body.lastMileOrderId, candidate.lastMileOrderId)
assert.equal(Object.hasOwn(body, 'inboundOrderId'), false)
assert.deepEqual(body.items, [{ outboundPalletItemId: '51', inventoryPositionId: '61', packageCount: 1 }])
assert.equal(body.returnDirection, 'OVERSEAS_WAREHOUSE')
assert.ok(body.idempotencyKey)
return ok({ exceptionNo: 'EX-1', status: 'PENDING', version: 0 })
})
const result = await createDeliveryReturn(context, returnScope)
assert.equal(result.returned.data.exceptionNo, 'EX-1')
assert.equal(context.calls.filter(row => row.method === 'POST').length, 1)
assert.deepEqual(context.assertions.filter(row => !row.passed), [])
})
test('phase5 keeps the selected master and its own source together when candidates share no inbound relation', async () => {
const first = deliveryReturnFixture(); first.inboundOrderId = null; first.inboundCode = null; first.returnSources = []
const selected = { ...deliveryReturnFixture(), lastMileOrderId: '9007199254740995', lastMileOrderNo: 'LM-2', inboundOrderId: null, inboundCode: null,
returnSources: [{ outboundPalletItemId: '52', inventoryPositionId: '62', maxReturnPackages: 1 }] }
const context = contextFor((method, path, body) => {
if (method === 'GET') return ok([first, selected])
assert.equal(body.lastMileOrderId, selected.lastMileOrderId)
assert.equal(Object.hasOwn(body, 'inboundOrderId'), false)
assert.deepEqual(body.items, [{ outboundPalletItemId: '52', inventoryPositionId: '62', packageCount: 1 }])
return ok({ exceptionNo: 'EX-2' })
})
await createDeliveryReturn(context, returnScope)
assert.equal(context.calls.filter(row => row.method === 'POST').length, 1)
})
for (const [name, mutate] of [
['missing-master', row => { delete row.lastMileOrderId }], ['zero-master', row => { row.lastMileOrderId = '0' }],
['foreign-customer', row => { row.customerId = 'OTHER' }], ['foreign-warehouse', row => { row.overseasWarehouseId = 'OTHER' }],
['wrong-delivery', row => { row.deliveryNo = 'DL-OTHER' }], ['wrong-outbound', row => { row.outboundNo = 'OB-OTHER' }],
['missing-source', row => { row.returnSources = [] }], ['missing-position', row => { row.returnSources[0].inventoryPositionId = null }],
['ambiguous-source-type', row => { row.returnSources[0].returnCartonId = '701' }],
]) test(`phase5 refuses ${name} before any mark-return write even with an inbound ID`, async () => {
const candidate = deliveryReturnFixture(); mutate(candidate)
const context = contextFor((method) => { assert.equal(method, 'GET'); return ok([candidate]) })
assert.equal(await createDeliveryReturn(context, returnScope), null)
assert.equal(context.calls.length, 1)
assert.ok(context.assertions.some(row => !row.passed))
})
test('the phase5 lifecycle calls the tested request path and replays its exact returned body', () => {
assert.match(source, /const returnRequest = await createDeliveryReturn\(context, \{ customerId, warehouseId, deliveryNo, outboundNo \}\)/)
assert.match(source, /const \{ returned, returnBody \} = returnRequest/)
assert.doesNotMatch(source, /inboundOrderId:\s*id\(returnCandidate/)
assert.match(source, /lastMileOrderId: id\(returnCandidate\.lastMileOrderId\)/)
})
test('partial completion rejects different Orders and an already delivered pending fragment', async () => {
for (const second of [{ orderId: 'OTHER', processStatus: 'WAIT_SHIP' },
{ orderId: 'ORDER', processStatus: 'DELIVERED' }]) {