This commit is contained in:
2026-09-30 17:13:11 +08:00
parent babc7de4b6
commit 32fb9ec7b3
26 changed files with 1484 additions and 70 deletions
@@ -3,7 +3,9 @@ import test from 'node:test'
import { readFileSync } from 'node:fs'
import { fulfillmentHistorySnapshot, validTwoFragmentTree, validSignatureGate,
completionCounts, exactCompletionOutboxProof, resolveF40MerchantCredentials,
f40TransferBody, f40LocalDateTime, validFragmentCabinetIdentity, run } from './phase5-root-fulfillment.mjs'
f40TransferBody, f40LocalDateTime, validFragmentCabinetIdentity, validUnloadedRemainder,
validRemainderCancellationGate, exactCancelledRemainderOutboxProof, cancelUnloadedRemainder,
verifyRootProjections, validFreshReceiptSave, preserveReceivingEvidence, run } from './phase5-root-fulfillment.mjs'
import { loadOneFragment } from './phase3-order-splitting.mjs'
import { verifyPalletLabels } from './phase3-loading-finalize.mjs'
import { awaitProducerConsumption, convertReceivingContainers, runReceiving,
@@ -30,6 +32,70 @@ const fixture = () => ({steps:[],token:'offline',marker:'offline-only',createdRe
addCoverageGap(){},apiSuccess(_module,_label,response){return response?.body?.data},
})
test('F40 unified projections require exact root and customer session with final COMPLETED or unfinished DELIVERED',async()=>{
for (const completed of [true,false]) {
const context=fixture(),calls=[],snapshot={root:{status:completed?'COMPLETED':'TRANSPORTING',histories:history('OVERSEAS_DELIVERED')}}
context.request=async(method,path,body,token)=>{
calls.push({method,path,token})
return {http:200,body:{code:'200',data:{summary:{orderId:'100',mainStatus:completed?'COMPLETED':'DELIVERED'}}}}
}
const result=await verifyRootProjections(context,'90','100','customer-session',snapshot)
assert.deepEqual(result.platform,result.customer)
assert.equal(calls[0].path,'/api/platform/v1/orders/100?tenantId=90')
assert.equal(calls[1].path,'/api/merchant/v1/orders/100')
assert.equal(calls[1].token,'customer-session')
assert.equal(context.steps.every(step=>step.passed),true)
}
})
test('F40 customer mismatched root or prematurely completed projection fails closed',async()=>{
for (const summary of [{orderId:'101',mainStatus:'COMPLETED'},{orderId:'100',mainStatus:'DELIVERED'}]) {
const context=fixture()
context.request=async(_method,path)=>({http:200,body:{code:'200',data:{summary:
path.startsWith('/api/platform')?{orderId:'100',mainStatus:'COMPLETED'}:summary}}})
await assert.rejects(verifyRootProjections(context,'90','100','customer-session',{root:{status:'COMPLETED',histories:[]}}),
/平台客户统一状态/)
}
})
test('F40 README documents all three independent modes and fail-closed receiving/zero child event evidence',()=>{
const readme=readFileSync(new URL('./README.md',import.meta.url),'utf8')
for(const value of ['CHILD_FIRST','ROOT_SIGNED_REMAINDER_CANCELLED','orderStopAtApproved=true',
'PROCESSING','WAIT_SPLIT','A0409','phase4ReceivingEvidenceReader','零事件子行'])assert.ok(readme.includes(value),value)
assert.match(source,/proof\.beforeReceiptReplayOutbox = await waitState/)
assert.match(source,/outboxCounts\(proof.beforeReceiptReplayOutbox\) === outboxCounts\(proof.outboxProof\)/)
assert.match(source,/validFreshReceiptSave\(beforeFresh,afterFresh\)/)
assert.match(source,/JSON.stringify\(afterFresh\) === JSON.stringify\(receiptProof.afterSameKey\)/)
})
test('fresh receipt save allows exactly one document version/revision/audit while business completion stays unchanged',()=>{
const documents={version:'2',uploadedCategoryCount:1,categories:[{category:'RECEIPT',files:[{fileId:'1'}]}],replayed:false}
const before={documents,delivery:{order:{version:'2',status:'DELIVERED',signedAt:'2026-09-30T16:00:00'},
operations:[{id:'1',operationNode:'RECEIPT_AUTO_COMPLETE'}],skus:[{packageCount:1}],fees:[]},revisions:[{version:'2'}]}
const after=structuredClone(before);after.documents.version='3';after.delivery.order.version='3'
after.delivery.operations.push({id:'2',operationNode:'SAVE_DOCUMENTS',result:'保存派送资料:1/3类',
operatedAt:'2026-09-30T16:01:00',operatorName:'platform'})
after.revisions.push({version:'3',savedAt:'2026-09-30T16:01:00',operatorId:'platform',snapshot:after.documents})
assert.equal(validFreshReceiptSave(before,after),true)
for(const mutate of [
value=>value.documents.version='4',value=>value.delivery.order.version='2',
value=>value.delivery.order.signedAt='2026-09-30T16:01:00',value=>value.delivery.order.status='RETURNED',
value=>value.documents.categories[0].files[0].fileId='2',value=>value.delivery.skus[0].packageCount=2,
value=>value.delivery.fees.push({amount:1}),value=>value.delivery.operations[0].id='999',
value=>value.delivery.operations[1].operationNode='RECEIPT_AUTO_COMPLETE',
value=>value.delivery.operations.push({...value.delivery.operations[1],id:'3'}),
value=>value.revisions.pop(),value=>value.revisions[0].version='1',
]){const bad=structuredClone(after);mutate(bad);assert.equal(validFreshReceiptSave(before,bad),false)}
})
test('F40 retains partial and failed receiving raw snapshots without exporting identity credentials',()=>{
const proof={},scope={createdResources:{phase4OverseasPda:{receivingCompletionEvidence:{}}},token:'secret'}
preserveReceivingEvidence(proof,scope)
scope.createdResources.phase4OverseasPda.receivingCompletionEvidence['20']={status:'UNPROVEN',stages:{
BEFORE_COMPLETE:{captured:true,evidence:{balances:[]}},AFTER_COMPLETE:{captured:false,evidence:{balances:[{id:'1'}]}}}}
assert.deepEqual(proof.receivingCompletionEvidence,scope.createdResources.phase4OverseasPda.receivingCompletionEvidence)
assert.equal(JSON.stringify(proof).includes('secret'),false)
assert.match(source,/finally \{ preserveReceivingEvidence\(proof,scope\);/)
assert.match(source,/proof.receiptReplayProofs \|\|= \[\]/)
})
test('F40 transfer uses the current formal Transfer DTO including required positive fee and same LocalDateTime tool',() => {
const dto=readFileSync(new URL('../../lastleg-service/src/main/java/com/baiyue/lastleg/delivery/dto/DeliveryRequests.java',import.meta.url),'utf8')
const vehicle={carrierName:'offline-fleet',vehiclePlate:'offline-plate',driverName:'offline-driver',driverPhone:'19900000000'}
@@ -175,17 +241,141 @@ test('exported receiving warehouse resolver continues to choose only configured
]}}})
assert.deepEqual(await resolveSharedWarehouse(context),{id:'193000000000200003',name:'offline'})
})
test('complete orchestrator calls actual source chain and blocks missing Outbox evidence; no fake callback, SQL or cancellation substitute',() => {
test('complete orchestrator calls actual source chain and blocks missing Outbox evidence; no fake callback or SQL',() => {
assert.equal(typeof run,'function')
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'])
assert.match(source,new RegExp(`await ${name}\\(`))
assert.match(source,/rootOrderId, sourceOrderIds:\[rootOrderId,childOrderId\]/)
assert.match(source,/rootOrderId,\s*sourceOrderIds:\[rootOrderId,childOrderId\]/)
assert.match(source,/HTTP_VERIFIED_OUTBOX_PENDING/)
assert.match(source,/finally \{ proof.currentStage = 'PDA_IDENTITY_CLEANUP'; await disablePicker/)
assert.doesNotMatch(source,/['"`]\/internal\/|UPDATE\s+ord_|INSERT\s+INTO|\/cancel|\/complete-delivery/)
assert.match(source,/finally \{ preserveReceivingEvidence\(proof,scope\); proof.currentStage = 'PDA_IDENTITY_CLEANUP'; await disablePicker/)
assert.doesNotMatch(source,/['"`]\/internal\/|UPDATE\s+ord_|INSERT\s+INTO|\/complete-delivery/)
})
const oneLoadedTree = () => ({...tree(),cumulativeLoadedPackageCount:1,currentRemainderPackageCount:1,
children:tree().children.slice(0,2).map(row=>({...row,loadingPlanId:'30',containerBookingId:'20'}))})
const approvedChild = () => fulfillmentHistorySnapshot({order:{id:'101',tenantId:'90',orderRole:'CHILD',
rootOrderId:'100',parentOrderId:'100',status:'APPROVED',version:1,orderNo:'R-1',inboundCode:'I-1',completedAt:null},statusHistory:[]})
const unshippedInbound = () => ({id:'301',tenantId:'90',orderId:'101',orderNo:'R-1',inboundCode:'I-1',
processStatus:'WAIT_SPLIT',orderStatusSnapshot:'APPROVED',totalPackageCount:1,containerNo:null,cabinetNo:null})
const cancelled = () => ({root:{status:'COMPLETED',histories:history('OVERSEAS_DELIVERED','FULFILLMENT_COMPLETED')},
child:{status:'CANCELLED',rawOrder:{completedAt:null},histories:history('CANCELLED')},
rootInbound:{processStatus:'DELIVERED',orderStatusSnapshot:'COMPLETED'}})
const cancellationOutbox = () => {const value=outbox();value.orders[1].count=0;value.orders[1].publishedCount=0;return value}
test('unloaded remainder requires real raw APPROVED child, exact customer/root/source and one remaining physical box',() => {
const child=approvedChild(),inbound=unshippedInbound(),tree=oneLoadedTree()
assert.equal(validUnloadedRemainder(tree,child,inbound,'90','100','101'),true)
for(const mutate of [
value => value.child.status='PROCESSING',value => value.child.status='ACCEPTED',
value => value.child.rootOrderId='999',value => value.child.parentOrderId='999',
value => value.child.rawOrder.tenantId='91',value => value.child.orderId='102',
value => value.child.histories=history('OVERSEAS_DELIVERED'),
value => value.inbound.orderId='102',value => value.inbound.tenantId='91',
value => value.inbound.inboundCode='ANOTHER',value => value.inbound.orderNo='ANOTHER',
value => value.inbound.processStatus='LOADED',value => value.inbound.totalPackageCount=2,
value => value.inbound.orderStatusSnapshot='PROCESSING',value => value.inbound.containerNo='ACTUAL-CABINET',
value => value.inbound.cabinetNo='ACTUAL-CABINET',value => value.tree.children.push(tree.children[0]),
value => value.tree.children[0].sourceOrderId='101',value => value.tree.children[1].loadingPlanId='31',
value => value.tree.children[1].containerBookingId='21',value => value.tree.currentRemainderPackageCount=0,
]) {
const value={tree:oneLoadedTree(),child:approvedChild(),inbound:unshippedInbound()};mutate(value)
assert.equal(validUnloadedRemainder(value.tree,value.child,value.inbound,'90','100','101'),false)
}
})
test('cancelled remainder cannot substitute for child signature in either full-signature mode',() => {
assert.equal(validRemainderCancellationGate(cancelled()),true)
assert.equal(validSignatureGate(cancelled(),'MASTER',true),false)
assert.equal(validSignatureGate(cancelled(),'CHILD',true),false)
for(const mutate of [
value=>value.root.histories=history('FULFILLMENT_COMPLETED'),
value=>value.root.histories.push({action:'FULFILLMENT_COMPLETED'}),
value=>value.child.status='COMPLETED',value=>value.child.histories.push({action:'OVERSEAS_DELIVERED'}),
value=>value.child.histories.push({action:'CANCELLED'}),value=>value.child.rawOrder.completedAt='2026-09-30T01:00:00',
value=>value.rootInbound.orderStatusSnapshot='APPROVED',value=>value.rootInbound.processStatus='RECEIVED',
]){const value=cancelled();mutate(value);assert.equal(validRemainderCancellationGate(value),false)}
})
test('cancellation Outbox requires exactly one published root completion and explicit zero child completion counts',() => {
assert.equal(exactCancelledRemainderOutboxProof(cancellationOutbox(),'90','100','101'),true)
assert.equal(exactCompletionOutboxProof(cancellationOutbox(),'90','100',['100','101']),false)
for(const mutate of [
value=>value.orders.pop(),value=>value.orders[1].count=1,value=>value.orders[1].publishedCount=1,
value=>value.orders[1].pendingCount=1,value=>value.orders[1].failedCount=1,
value=>value.orders[1].count=null,value=>delete value.orders[1].failedCount,
value=>value.orders[1].rootOrderId='999',value=>value.orders[1].customerId='91',
value=>value.orders[1].orderId='100',value=>value.orders[1].eventType='ORDER_CANCELLED',
value=>value.orders[0].count=2,value=>value.orders[0].publishedCount=0,value=>value.readOnly=false,
]){const value=cancellationOutbox();mutate(value);assert.equal(exactCancelledRemainderOutboxProof(value,'90','100','101'),false)}
})
function cancellationFixture(change=null) {
const context=fixture(),calls=[],proof={snapshots:[]}
const rootDetail={order:{id:'100',tenantId:'90',orderRole:'MASTER',rootOrderId:'100',status:'APPROVED',
version:1,orderNo:'R',inboundCode:'I',completedAt:null},statusHistory:history('OVERSEAS_DELIVERED')}
const childDetail={order:approvedChild().rawOrder,statusHistory:[]}
const rootInbound={summary:{processStatus:'DELIVERED',orderStatusSnapshot:'APPROVED'}}
const childInbound={summary:unshippedInbound()},tree=oneLoadedTree()
let count=0
context.context={f40BaseMarker:'offline-only',f40ReadOnlyOutboxEvidence:async args=>{
assert.deepEqual(args,{customerId:'90',rootOrderId:'100',sourceOrderIds:['100','101'],
eventType:'FIRST_LEG_ORDER_FULFILLMENT_COMPLETED'})
return cancellationOutbox()
}}
if(change)change({rootDetail,childDetail,childInbound,tree})
context.request=async(method,path,body)=>{
calls.push({method,path,body})
if(method==='POST'){
assert.equal(path,'/api/platform/v1/orders/first-leg/101/cancel?tenantId=90')
assert.ok(body.reason.includes('offline-only'))
if(count++)return {http:200,body:{code:'A0409'}}
childDetail.order.status='CANCELLED';childDetail.order.version++
childDetail.statusHistory=history('CANCELLED')
rootDetail.order.status='COMPLETED';rootDetail.order.version++
rootDetail.order.completedAt='2026-09-30T01:00:00'
rootDetail.statusHistory=history('OVERSEAS_DELIVERED','FULFILLMENT_COMPLETED')
rootInbound.summary.orderStatusSnapshot='COMPLETED'
return {http:200,body:{code:'200',data:null}}
}
let data
if(path.includes('/fulfillment-tree?'))data=tree
else if(path.includes('/orders/first-leg/100?'))data=rootDetail
else if(path.includes('/orders/first-leg/101?'))data=childDetail
else if(path.includes('/inbounds/300?'))data=rootInbound
else if(path.includes('/inbounds/301?'))data=childInbound
else throw new Error(`unexpected offline request ${path}`)
return {http:200,body:{code:'200',data:structuredClone(data)}}
}
return {context,proof,calls}
}
test('formal cancellation helper rereads physical eligibility, uses public cancel and accepts replay rejection only with unchanged facts and Outbox',async()=>{
const {context,proof,calls}=cancellationFixture()
const result=await cancelUnloadedRemainder(context,proof,'90','100','101','300','301')
assert.equal(validRemainderCancellationGate(result),true)
assert.equal(calls.filter(row=>row.method==='POST').length,2)
assert.equal(proof.snapshots.length,3)
assert.deepEqual(proof.cancelReplayOutbox.before,proof.cancelReplayOutbox.after)
assert.equal(context.steps.every(step=>step.passed),true)
})
test('formal cancellation helper stops before any mutation for PROCESSING, unsigned root or already loaded child',async()=>{
for(const change of [value=>value.childDetail.order.status='PROCESSING',
value=>value.rootDetail.order.status='PROCESSING',value=>value.rootDetail.statusHistory=[],
value=>value.childInbound.summary.processStatus='LOADED',value=>value.tree.children.push(tree().children[2])]) {
const {context,proof,calls}=cancellationFixture(change)
await assert.rejects(cancelUnloadedRemainder(context,proof,'90','100','101','300','301'),/仅取消真实签收根下未装 APPROVED/)
assert.equal(calls.some(row=>row.method!=='GET'),false)
}
})
test('cancel scenario uses new approved-only order and exactly root shipment; both existing full-signature modes remain independent',()=>{
assert.match(source,/const cancelRemainder = signatureOrder === 'ROOT_SIGNED_REMAINDER_CANCELLED'/)
assert.match(source,/orderStopAtApproved:cancelRemainder/)
assert.match(source,/approvedRoot.status === 'APPROVED'/)
assert.match(source,/if \(cancelRemainder\) \{[\s\S]*?proof.unloadedRemainder[\s\S]*?\} else \{[\s\S]*?second = await fragment/)
assert.match(source,/const producers = cancelRemainder \? \[first\] : \[first,second\]/)
assert.match(source,/signatureOrder === 'CHILD_FIRST' \? \[second,first\] : producers/)
assert.match(source,/if \(cancelRemainder\) await cancelUnloadedRemainder/)
const orderSource=readFileSync(new URL('./orders.mjs',import.meta.url),'utf8')
assert.match(orderSource,/context.context\?\.orderStopAtApproved === true/)
})
test('early F40 failures become FAILED with only a safe stage and preserve the original exception',async() => {
const context=fixture();context.context={};context.options={}