优化
This commit is contained in:
@@ -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={}
|
||||
|
||||
Reference in New Issue
Block a user