test(fba): verify signed delivery ACK loss and original event recovery

This commit is contained in:
2026-10-03 08:13:04 +08:00
parent ef1eef3278
commit 51bf4e5558
11 changed files with 499 additions and 57 deletions
@@ -290,8 +290,9 @@ test('complete orchestrator calls actual source chain and blocks missing Outbox
for(const name of ['orders','inbound-measurement','phase3-customs','phase3-outbound-dispatch',
'phase3-carrier-schedule-queries'])assert.ok(source.includes(`'${name}'`))
for(const name of ['loadOneFragment','verifyPalletLabels','awaitProducerConsumption',
'convertReceivingContainers','prepareTerminalDrayage','runReceiving','pickAll','saveReceiptAndComplete'])
'convertReceivingContainers','prepareTerminalDrayage','runReceiving','pickAll','withDeliveryEventRecovery'])
assert.match(source,new RegExp(`await ${name}\\(`))
assert.match(source,/\(\) => saveReceiptAndComplete\(context, deliveries\[0\], customerId, index\)/)
assert.match(source,/rootOrderId,\s*sourceOrderIds:\[rootOrderId,childOrderId\]/)
assert.match(source,/HTTP_VERIFIED_OUTBOX_PENDING/)
assert.match(source,/finally \{ preserveReceivingEvidence\(proof,scope\); proof.currentStage = 'PDA_IDENTITY_CLEANUP'; await disablePicker/)
@@ -5,6 +5,8 @@ import { stableLoadedCartonMarks } from './phase3-loading.mjs'
import { awaitProducerConsumption, convertReceivingContainers, runReceiving, resolveSharedWarehouse,
resolvePickerAccess, createPicker, disablePicker } from './phase4-overseas-pda.mjs'
import { pickAll, verifyDepartureReadiness, saveReceiptAndComplete } from './phase5-overseas-fulfillment.mjs'
import { isDeepStrictEqual } from 'node:util'
import { assertTestDeliveryFaultProof } from '../test-delivery-fault-proof.mjs'
import { prepareTerminalDrayage } from './phase5-terminal-drayage.mjs'
import { prepareTaxCustoms, verifyTaxDocumentsAfterCompletion } from './phase5-root-tax-customs.mjs'
import { prepareFleetTrip, confirmFleetLoading, finishFleetTrip } from './fleet-trip-fulfillment.mjs'
@@ -432,7 +434,10 @@ async function sendFragment(context, producer, picker, merchantToken, index) {
query(`/api/platform/v1/last-leg/delivery-orders/${deliveries[0].deliveryNo}/status`,{customerId}),
{status:'DELIVERING',expectedVersion:Number(beforeDelivery.order.version),remark:context.marker})
check(context,'F40 派送处于配送中后保存签收',delivering.deliveryStatus === 'DELIVERING',`delivery=${deliveries[0].deliveryNo}`)
const completed = await saveReceiptAndComplete(context, deliveries[0], customerId, index)
const completed = await withDeliveryEventRecovery(context, {
marker: context.context.f40BaseMarker, customerId: id(customerId), deliveryId: id(beforeDelivery.order.id),
sourceInboundId: id(producer.sourceInboundId), sourceOrderId: id(producer.sourceOrderId), rootOrderId: id(producer.rootOrderId),
}, id(picker.warehouseId), index, () => saveReceiptAndComplete(context, deliveries[0], customerId, index))
check(context, 'F40 实际签收资料驱动派送完成', completed?.order?.status === 'DELIVERED', `delivery=${deliveries[0].deliveryNo}`)
check(context,'F40 签收完成记录真实签收时间', Boolean(completed.order.signedAt), `delivery=${deliveries[0].deliveryNo}`)
const tripClosure = await finishFleetTrip(context, MODULE, tripProof)
@@ -440,6 +445,51 @@ async function sendFragment(context, producer, picker, merchantToken, index) {
receiptVersion: completed.order.version, signedAt: completed.order.signedAt, receiptSnapshot:completed, tripNo: tripProof.tripNo, tripClosure }
}
/** Only the first actual FBA signature is faulted; caller performs the normal receipt command. */
export async function withDeliveryEventRecovery(context, faultScope, warehouseId, index, sign) {
const executor = context.context.f40DeliveryEventFaultExecutor
if (!executor || index !== 0) return sign()
const proof = context.createdResources.phase5RootFulfillment
if (proof.deliveryEventRecovery) throw Error('F40 delivery recovery already attempted')
const reader = context.context.f40ReadOnlyInventoryEvidence
if (typeof reader !== 'function') throw Error('F40 complete inventory evidence reader required')
const record = proof.deliveryEventRecovery = { scope: faultScope, status: 'PREPARING', restored: false }
const inventoryScope = { marker: faultScope.marker, customerId: faultScope.customerId, warehouseId }
try {
record.inventoryBefore = await reader(inventoryScope)
record.preparationAttempted = true
await executor.prepare(faultScope)
record.status = 'SIGNING'
const result = await sign()
record.status = 'RECOVERING'
record.proof = await executor.readProof(faultScope)
assertTestDeliveryFaultProof(record.proof, faultScope)
if (record.proof.restored !== true) throw Error('F40 delivery producer restoration missing')
record.inventoryAfter = await reader(inventoryScope)
if (record.inventoryBefore?.scope?.coverage !== 'CUSTOMER_WAREHOUSE_COMPLETE'
|| record.inventoryBefore.scope.customerId !== faultScope.customerId
|| record.inventoryBefore.scope.warehouseId !== warehouseId
|| !['balances','positions','ledger'].every(key => Array.isArray(record.inventoryBefore[key]))
|| !isDeepStrictEqual(record.inventoryBefore, record.inventoryAfter)) throw Error('F40 full inventory drift during delivery recovery')
record.inventoryUnchanged = true
record.status = 'VERIFIED'
return result
} catch (error) {
record.status = 'FAILED'
throw error
} finally {
if (record.preparationAttempted) try {
record.restoration = await executor.cleanup()
record.restored = record.restoration?.restored === true && record.restoration?.healthy === true && record.restoration?.proxyStopped === true
if (!record.restored) throw Error('F40 delivery restoration incomplete')
} catch (error) {
record.status = 'RESTORATION_FAILED'
record.restored = false
throw error
}
}
}
/** Explicit TEST orchestrator, called by the parent acceptance context; never synthesizes an event. */
export async function run(context) {
try { return await runRootFulfillment(context) }