540 lines
37 KiB
JavaScript
540 lines
37 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import test from 'node:test'
|
|
import { readFileSync } from 'node:fs'
|
|
import { fulfillmentHistorySnapshot, validTwoFragmentTree, validSignatureGate,
|
|
completionCounts, exactCompletionOutboxProof, rootCompletionTimeMatches, resolveF40MerchantCredentials,
|
|
f40TransferBody, f40LocalDateTime, validFragmentCabinetIdentity, validUnloadedRemainder,
|
|
validRemainderCancellationGate, exactCancelledRemainderOutboxProof, cancelUnloadedRemainder,
|
|
verifyRootProjections, verifyRemainderTaxIdentity, validFreshReceiptSave, preserveReceivingEvidence,
|
|
resolveF40ReceivingWarehouse, run } from './phase5-root-fulfillment.mjs'
|
|
import { loadOneFragment } from './phase3-order-splitting.mjs'
|
|
import { verifyPalletLabels } from './phase3-loading-finalize.mjs'
|
|
import { awaitProducerConsumption, convertReceivingContainers, runReceiving,
|
|
resolveSharedWarehouse } from './phase4-overseas-pda.mjs'
|
|
|
|
const source = readFileSync(new URL('./phase5-root-fulfillment.mjs', import.meta.url),'utf8')
|
|
|
|
test('根完成UTC时刻与上海本地签收最大值一致,拒绝时区偏移、较早来源和缺失证据', () => {
|
|
const times = ['2026-10-05T21:55:00.000', '2026-10-05T21:56:07.922']
|
|
assert.equal(rootCompletionTimeMatches('2026-10-05T13:56:07.922', times), true)
|
|
assert.equal(rootCompletionTimeMatches('2026-10-05T13:56:07.922Z', [...times].reverse()), true)
|
|
assert.equal(rootCompletionTimeMatches('2026-10-05T13:56:07.922Z', ['2026-10-05T21:56:07.922+08:00']), true)
|
|
for (const value of ['2026-10-05T21:56:07.922', '2026-10-05T13:55:00', null]) {
|
|
assert.equal(rootCompletionTimeMatches(value, times), false)
|
|
}
|
|
assert.equal(rootCompletionTimeMatches('2026-10-05T13:56:07.922', []), false)
|
|
assert.equal(rootCompletionTimeMatches('2026-10-05T13:56:07.922', ['invalid']), false)
|
|
})
|
|
const history = (...actions) => actions.map((action,index) => ({id:String(index+1),action}))
|
|
const tree = () => ({parentOrderId:'100',originalPackageCount:2,cumulativeLoadedPackageCount:2,
|
|
currentRemainderPackageCount:0,children:[
|
|
{orderId:'100',sourceOrderId:'100',splitRole:'LOADED',packageCount:1},
|
|
{orderId:'101',sourceOrderId:'100',splitRole:'REMAINDER',packageCount:1},
|
|
{orderId:'101',sourceOrderId:'101',splitRole:'LOADED',packageCount:1},
|
|
]})
|
|
const signed = () => ({root:{status:'COMPLETED',histories:history('OVERSEAS_DELIVERED','FULFILLMENT_COMPLETED')},
|
|
child:{status:'COMPLETED',histories:history('OVERSEAS_DELIVERED')},
|
|
rootInbound:{processStatus:'DELIVERED',orderStatusSnapshot:'COMPLETED'}})
|
|
const outbox = () => ({readOnly:true,customerId:'90',rootOrderId:'100',orders:['100','101'].map(orderId => ({
|
|
orderId,customerId:'90',rootOrderId:'100',eventType:'FIRST_LEG_ORDER_FULFILLMENT_COMPLETED',
|
|
count:1,publishedCount:1,pendingCount:0,failedCount:0,
|
|
}))})
|
|
const fixture = () => ({steps:[],token:'offline',marker:'offline-only',createdResources:{},
|
|
addStep(_module,_label,_response,passed){this.steps.push({passed})},
|
|
assert(_module,_label,_response,passed){this.steps.push({passed})},
|
|
addCoverageGap(){},apiSuccess(_module,_label,response){return response?.body?.data},
|
|
})
|
|
|
|
function remainderTaxFixture(taxMode) {
|
|
const context=fixture(),calls=[]
|
|
const evidence={taxMode,customerId:'90',rootOrderId:'100',childOrderId:'101',rootOrderNo:'ORDER-ROOT',
|
|
rawChild:{id:'101',tenantId:'90',taxMode},rootInbound:{orderId:'100',taxMode},
|
|
childInbound:{orderId:'101',tenantId:'90',taxMode}}
|
|
const rows=[{orderId:'100',tenantId:'90',taxMode,relation:{role:'PARENT',rootOrderId:'100'}},
|
|
{orderId:'101',tenantId:'90',taxMode,relation:{role:'CHILD',rootOrderId:'100',parentOrderId:'100'}}]
|
|
context.request=async(method,path,body,token)=>{
|
|
calls.push({method,path,body,token})
|
|
const filter=new URL(path,'https://offline.invalid').searchParams.get('taxMode')
|
|
const records=filter&&filter!==taxMode?[]:structuredClone(rows)
|
|
return {http:200,body:{code:'200',data:{records,total:records.length}}}
|
|
}
|
|
return {context,evidence,calls,rows}
|
|
}
|
|
test('raw remainder and actual inbound summary taxMode agree for BUY and TAX and customer filters retain exact parent/child',async()=>{
|
|
for(const mode of ['BUY_DECLARATION','TAX_REFUND']){
|
|
const f=remainderTaxFixture(mode)
|
|
await verifyRemainderTaxIdentity(f.context,f.evidence,'own-customer')
|
|
assert.equal(f.calls.length,3)
|
|
assert.ok(f.calls.every(c=>c.method==='GET'&&c.token==='own-customer'&&!c.path.includes('/customs')))
|
|
assert.deepEqual(f.evidence.matchingTax.records.map(r=>r.orderId),['100','101'])
|
|
assert.equal(f.evidence.otherTax.total,0)
|
|
assert.match(f.calls[1].path,new RegExp('taxMode='+mode))
|
|
}
|
|
})
|
|
test('missing or wrong raw remainder taxMode is rejected before customer/customs calls despite valid inbound projection',async()=>{
|
|
for(const mode of ['BUY_DECLARATION','TAX_REFUND'])for(const actual of [null,undefined,mode==='BUY_DECLARATION'?'TAX_REFUND':'BUY_DECLARATION']){
|
|
const f=remainderTaxFixture(mode);f.evidence.rawChild.taxMode=actual
|
|
await assert.rejects(verifyRemainderTaxIdentity(f.context,f.evidence,'own-customer'),/原始余量子订单继承/)
|
|
assert.equal(f.calls.length,0)
|
|
}
|
|
})
|
|
test('wrong inbound tax, missing filtered child, foreign customer or wrong parent identity cannot pass',async()=>{
|
|
for(const mutate of [f=>f.evidence.childInbound.taxMode=null,f=>f.rows.pop(),f=>f.rows[1].tenantId='91',
|
|
f=>f.rows[1].relation.parentOrderId='999',f=>f.rows[1].taxMode='BUY_DECLARATION']) {
|
|
const f=remainderTaxFixture('TAX_REFUND');mutate(f)
|
|
await assert.rejects(verifyRemainderTaxIdentity(f.context,f.evidence,'own-customer'))
|
|
}
|
|
const f=remainderTaxFixture('TAX_REFUND'),original=f.context.request
|
|
f.context.request=async(...args)=>{const r=await original(...args);if(args[1].includes('taxMode=BUY_DECLARATION'))r.body.data={records:[f.rows[1]],total:1};return r}
|
|
await assert.rejects(verifyRemainderTaxIdentity(f.context,f.evidence,'own-customer'),/otherTax/)
|
|
})
|
|
|
|
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'}
|
|
const now=new Date('2026-09-30T01:02:03.456Z'),body=f40TransferBody('offline',1,vehicle,now)
|
|
for(const field of ['idempotencyKey','plannedDeliveryAt','deliveryMethod','pickupMethod','loadingType','fees']) {
|
|
assert.match(dto,new RegExp(`\\b${field}\\b`));assert.ok(body[field])
|
|
}
|
|
assert.equal(body.plannedDeliveryAt,'2026-10-02T01:02:03')
|
|
assert.equal(f40LocalDateTime(now),'2026-09-30T01:02:03')
|
|
assert.equal(body.deliveryMethod,'TRUCK_PALLET');assert.equal(body.pickupMethod,'WAREHOUSE_PICKUP')
|
|
assert.equal(body.loadingType,'PALLET');assert.equal(body.vehicleSize,'15托')
|
|
for(const field of Object.keys(vehicle))assert.equal(body[field],vehicle[field])
|
|
assert.equal(body.fees.length,1)
|
|
assert.deepEqual(Object.fromEntries(['feeType','feeName','direction','currency','amount'].map(field=>[field,body.fees[0][field]])),
|
|
{feeType:'DELIVERY_FEE',feeName:'派送费',direction:'RECEIVABLE',currency:'USD',amount:10})
|
|
assert.match(source,/f40TransferBody\(context.marker,index,vehicle\)/)
|
|
assert.match(source,/estimatedArrivalAt: f40LocalDateTime\(/)
|
|
})
|
|
test('F40 credentials prefer a complete current fixture pair and never combine fixture with configured customer',() => {
|
|
const options={username:'offline-admin',password:'offline-admin-secret',merchantUsername:'configured-customer',merchantPassword:'configured-secret'}
|
|
const current={username:'fixture-customer',password:'fixture-secret',tenantId:'90'}
|
|
assert.deepEqual(resolveF40MerchantCredentials({options,context:{merchantQualificationFixture:current}}),
|
|
{username:'fixture-customer',password:'fixture-secret',customerId:'90'})
|
|
assert.deepEqual(resolveF40MerchantCredentials({options,context:{}}),
|
|
{username:'configured-customer',password:'configured-secret',customerId:null})
|
|
for(const incomplete of [{username:'fixture-only',tenantId:'90'},{password:'fixture-only',tenantId:'90'},
|
|
{username:'fixture-customer',password:'fixture-secret'}])
|
|
assert.equal(resolveF40MerchantCredentials({options,context:{merchantQualificationFixture:incomplete}}),null)
|
|
assert.equal(resolveF40MerchantCredentials({options:{merchantUsername:'incomplete'},context:{}}),null)
|
|
assert.ok(source.indexOf("'F40 正式订单创建客户与本轮登录客户一致'") < source.indexOf("await stage(scope, 'inbound-measurement')"))
|
|
})
|
|
test('F40 reads formal nested domestic cabinet workbench and compares booking/plan/task identity instead of inventing GET containers/id',() => {
|
|
const controller=readFileSync(new URL('../../firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformContainerController.java',import.meta.url),'utf8')
|
|
const dto=readFileSync(new URL('../../firstleg/src/main/java/com/baiyue/firstleg/terminaldrayage/dto/ContainerWorkbenchDtos.java',import.meta.url),'utf8')
|
|
assert.match(controller,/@GetMapping\("\/domestic\/\{id\}"\)/)
|
|
assert.doesNotMatch(controller,/@GetMapping\("\/\{id\}"\)/)
|
|
assert.match(dto,/record WorkbenchItem\(ContainerBookingEntity container, String workbenchStage/)
|
|
assert.match(source,/`\/api\/platform\/v1\/containers\/domestic\/\$\{result.containerId\}`/)
|
|
assert.doesNotMatch(source,/`\/api\/platform\/v1\/containers\/\$\{result.containerId\}`/)
|
|
const fragment={containerId:'20',planId:'30',taskId:'40',oceanBookingId:'50'},task={id:'40',containerBookingId:'20',loadingPlanId:'30'}
|
|
const workbench={container:{id:'20',cabinetNo:'D0001',containerNo:'OFFLINE',oceanBookingId:'50'}}
|
|
assert.equal(validFragmentCabinetIdentity(workbench,fragment,task),true)
|
|
assert.equal(validFragmentCabinetIdentity(workbench,{...fragment,containerId:'21'},task),false)
|
|
assert.equal(validFragmentCabinetIdentity(workbench,fragment,{...task,containerBookingId:'21'}),false)
|
|
assert.equal(validFragmentCabinetIdentity(workbench,fragment,{...task,loadingPlanId:'31'}),false)
|
|
assert.equal(validFragmentCabinetIdentity(workbench,fragment,{...task,id:'41'}),false)
|
|
assert.equal(validFragmentCabinetIdentity(workbench,{...fragment,oceanBookingId:'51'},task),false)
|
|
assert.equal(validFragmentCabinetIdentity(workbench.container,fragment,task),false)
|
|
})
|
|
|
|
test('formal tree requires one root physical source plus its exact real remainder child and conserves two boxes',() => {
|
|
assert.equal(validTwoFragmentTree(tree(),'100','101'),true)
|
|
for (const change of [
|
|
value => value.parentOrderId='999', value => value.currentRemainderPackageCount=1,
|
|
value => value.children[2].sourceOrderId='999', value => value.children[1].sourceOrderId='999',
|
|
value => value.children[2].packageCount=2,
|
|
value => value.children.push({splitRole:'UNKNOWN',orderId:'999'}),
|
|
]) {const value=tree();change(value);assert.equal(validTwoFragmentTree(value,'100','101'),false)}
|
|
})
|
|
test('F40 separates immutable physical task cartons from current actual FBA receiving prefix scans',() => {
|
|
assert.match(source,/cartonMarks: stableLoadedCartonMarks\(task.items\)/)
|
|
assert.match(source,/receivingCartonMarks:\[`\$\{current.summary.inboundCode\}#PDA-CONTRACT-1`\]/)
|
|
assert.doesNotMatch(source,/cartonMarks:\s*\[`\$\{current.summary.inboundCode\}#PDA-CONTRACT/)
|
|
})
|
|
test('raw Order and original status history remain in evidence including operator, reason and timestamp',() => {
|
|
const detail={order:{id:'100',orderRole:'MASTER',version:3,status:'TRANSPORTING'},statusHistory:[
|
|
{id:'11',orderId:'100',action:'OVERSEAS_DELIVERED',operatorName:'OVERSEAS_DELIVERY_EVENT',
|
|
reason:'signed actual delivery',occurredAt:'2026-09-30T01:00:00',fromStatus:'TRANSPORTING',toStatus:'TRANSPORTING'},
|
|
]}
|
|
const snapshot=fulfillmentHistorySnapshot(detail)
|
|
assert.deepEqual(snapshot.rawOrder,detail.order)
|
|
assert.deepEqual(snapshot.histories,detail.statusHistory)
|
|
assert.deepEqual(completionCounts(snapshot),{own:1,final:0})
|
|
})
|
|
test('root shipment signed first preserves unfinished root while effective child remains pending',() => {
|
|
const snapshot=signed();snapshot.root.status='TRANSPORTING'
|
|
snapshot.root.histories=history('OVERSEAS_DELIVERED');snapshot.child={status:'TRANSPORTING',histories:[]}
|
|
snapshot.rootInbound.orderStatusSnapshot='TRANSPORTING'
|
|
assert.equal(validSignatureGate(snapshot,'MASTER'),true)
|
|
snapshot.root.status='COMPLETED'
|
|
assert.equal(validSignatureGate(snapshot,'MASTER'),false)
|
|
})
|
|
test('child signed before root remains an independent completed child and cannot finish unsigned root',() => {
|
|
const snapshot=signed();snapshot.root={status:'TRANSPORTING',histories:[]}
|
|
snapshot.rootInbound={processStatus:'IN_TRANSIT',orderStatusSnapshot:'TRANSPORTING'}
|
|
assert.equal(validSignatureGate(snapshot,'CHILD'),true)
|
|
assert.equal(validSignatureGate(snapshot,'MASTER'),false)
|
|
snapshot.root.histories=history('OVERSEAS_DELIVERED')
|
|
assert.equal(validSignatureGate(snapshot,'CHILD'),false)
|
|
})
|
|
test('all signed requires root own fact, exactly one aggregate final history and real Firstleg delivered completion',() => {
|
|
assert.equal(validSignatureGate(signed(),'MASTER',true),true)
|
|
for(const change of [
|
|
value => value.root.histories=history('FULFILLMENT_COMPLETED'),
|
|
value => value.root.histories.push({action:'FULFILLMENT_COMPLETED'}),
|
|
value => value.child.histories.push({action:'OVERSEAS_DELIVERED'}),
|
|
value => value.child.status='CANCELLED',
|
|
value => value.rootInbound.orderStatusSnapshot='TRANSPORTING',
|
|
value => value.rootInbound.processStatus='RECEIVED',
|
|
]) {const value=signed();change(value);assert.equal(validSignatureGate(value,'MASTER',true),false)}
|
|
})
|
|
test('Outbox requires exact customer/root/source aggregate membership, unique completion and PUBLISHED',() => {
|
|
assert.equal(exactCompletionOutboxProof(outbox(),'90','100',['101','100']),true)
|
|
for(const change of [
|
|
value => value.readOnly=false, value => value.customerId='91', value => value.rootOrderId='999',
|
|
value => value.orders[0].rootOrderId='999', value => value.orders[0].customerId='91',
|
|
value => value.orders[1].orderId='100', value => value.orders[0].count=2,
|
|
value => value.orders[1].publishedCount=0, value => value.orders[1].pendingCount=1,
|
|
value => value.orders[0].failedCount=1, value => value.orders[0].eventType='OVERSEAS_DELIVERED',
|
|
value => value.orders.pop(),
|
|
]) {const value=outbox();change(value);assert.equal(exactCompletionOutboxProof(value,'90','100',['100','101']),false)}
|
|
})
|
|
test('exported fragment producer preserves fail closed supplier precondition and never writes without actual source master',async() => {
|
|
const context=fixture(),calls=[]
|
|
context.request=async(method,path) => {calls.push([method,path]);return {http:200,body:{code:200,data:{records:[]}}}}
|
|
assert.equal(await loadOneFragment(context,{sourceOrderNo:'offline-only'}),null)
|
|
assert.equal(calls.length,1)
|
|
assert.equal(calls[0][0],'GET')
|
|
assert.match(calls[0][1],/category=BOOKING&status=ACTIVE/)
|
|
})
|
|
test('exported pallet label helper preserves FBA eligibility without introducing writes for other destinations',async() => {
|
|
const context=fixture();context.request=async() => {throw new Error('unexpected HTTP')}
|
|
assert.equal(await verifyPalletLabels(context,{destinationType:'OVERSEAS'},'1'),null)
|
|
assert.equal(await verifyPalletLabels(context,{destinationType:'FBA',fulfillmentType:'ONE_CLICK_DROPSHIP'},'1'),null)
|
|
})
|
|
test('exported consumer polling and conversion fail closed when real producer consumption is absent',async() => {
|
|
const context=fixture(),calls=[]
|
|
context.request=async(method,path) => {calls.push([method,path]);return {http:200,body:{code:200,data:{records:[]}}}}
|
|
assert.equal(await awaitProducerConsumption(context,[]),true)
|
|
assert.equal(await convertReceivingContainers(context,[{containerNo:'OFFLINE'}]),false)
|
|
assert.equal(calls.every(([method]) => method==='GET'),true)
|
|
assert.equal(context.steps.at(-1).passed,false)
|
|
})
|
|
test('exported receiving rejects unmatched current producer instead of borrowing any history container',async() => {
|
|
const context=fixture();context.request=async(_method,path) => ({http:200,body:{code:200,data:
|
|
path.endsWith('/candidates')?[{containerNo:'ANOTHER',sourceContainerBookingId:'999',totalPackages:1}]:null}})
|
|
await runReceiving(context,'offline-pda',null,null,{containerNo:'EXPECTED',containerBookingId:'100'},false,{attempts:1})
|
|
assert.equal(context.steps.some(step => step.passed === false),true)
|
|
})
|
|
test('exported receiving warehouse resolver continues to choose only configured active official warehouse',async() => {
|
|
const context=fixture();context.request=async() => ({http:200,body:{code:200,data:{records:[
|
|
{id:'193000000000200003',warehouseType:'OVERSEAS',status:'ACTIVE',warehouseName:'offline'},
|
|
]}}})
|
|
assert.deepEqual(await resolveSharedWarehouse(context),{id:'193000000000200003',name:'offline'})
|
|
})
|
|
|
|
function warehouseSelectionFixture() {
|
|
const producers = [1, 2].map(n => ({ tenantId: '90', containerNo: `CONTAINER-${n}`,
|
|
containerBookingId: `BOOKING-${n}`, sourceInboundId: `INBOUND-${n}`, inboundCode: `CODE-${n}` }))
|
|
const orders = producers.map((p, n) => ({ id: `LASTLEG-${n}`, customerId: p.tenantId,
|
|
sourceContainerBookingId: p.containerBookingId, sourceInboundId: p.sourceInboundId,
|
|
inboundCode: p.inboundCode, containerNo: p.containerNo, overseasWarehouseId: 'actual-warehouse',
|
|
overseasWarehouseCode: 'ACTUAL' }))
|
|
const warehouses = [{ id: 'other-warehouse', warehouseName: 'other', warehouseCode: 'OTHER',
|
|
warehouseType: 'OVERSEAS', status: 'ACTIVE' },
|
|
{ id: 'actual-warehouse', warehouseName: 'actual', warehouseCode: 'ACTUAL',
|
|
warehouseType: 'OVERSEAS', status: 'ACTIVE' }]
|
|
const calls = [], context = fixture()
|
|
context.request = async (method, path) => {
|
|
calls.push({ method, path })
|
|
const rows = path.includes('/warehouse-admin?') ? warehouses
|
|
: orders.filter(row => row.containerNo === new URL(path, 'https://offline.invalid').searchParams.get('containerNo'))
|
|
return { http: 200, body: { code: 200, data: { records: rows, total: rows.length } } }
|
|
}
|
|
return { producers, orders, warehouses, calls, context }
|
|
}
|
|
|
|
test('F40 selects actual source warehouse irrespective of authority list order and only performs GET', async () => {
|
|
for (const reverse of [false, true]) {
|
|
const f = warehouseSelectionFixture()
|
|
if (reverse) { f.warehouses.reverse(); f.orders.reverse(); f.producers.reverse() }
|
|
const warehouse = await resolveF40ReceivingWarehouse(f.context, f.producers)
|
|
assert.equal(warehouse.id, 'actual-warehouse')
|
|
assert.equal(warehouse.code, 'ACTUAL')
|
|
assert.equal(warehouse.sourceOrders.length, 2)
|
|
assert.ok(f.calls.every(row => row.method === 'GET'))
|
|
}
|
|
assert.ok(source.indexOf('await awaitProducerConsumption(scope,producers)')
|
|
< source.indexOf('await resolveF40ReceivingWarehouse(scope,producers)'))
|
|
assert.ok(source.indexOf('await resolveF40ReceivingWarehouse(scope,producers)')
|
|
< source.indexOf('await createPicker(scope, 7'))
|
|
assert.doesNotMatch(source, /resolveSharedWarehouse|19300000000020000[34]/)
|
|
})
|
|
|
|
test('F40 warehouse resolution rejects wrong customer, booking, inbound identity, duplicates and missing assignment', async () => {
|
|
for (const mutate of [
|
|
f => { f.orders[0].customerId = 'foreign' },
|
|
f => { f.orders[0].sourceContainerBookingId = 'foreign' },
|
|
f => { f.orders[0].sourceInboundId = 'foreign' },
|
|
f => { f.orders[0].inboundCode = 'foreign' },
|
|
f => { f.orders[0].containerNo = 'foreign' },
|
|
f => { f.orders[0].overseasWarehouseId = '' },
|
|
f => { f.orders.push({ ...f.orders[0] }) },
|
|
]) {
|
|
const f = warehouseSelectionFixture(); mutate(f)
|
|
await assert.rejects(resolveF40ReceivingWarehouse(f.context, f.producers), /来源柜客户入仓身份/)
|
|
assert.ok(f.calls.every(row => !row.path.includes('/warehouse-admin?')))
|
|
}
|
|
})
|
|
|
|
test('F40 warehouse resolution blocks different receiving warehouses and invalid or ambiguous official master data', async () => {
|
|
const mixed = warehouseSelectionFixture(); mixed.orders[1].overseasWarehouseId = 'other-warehouse'
|
|
await assert.rejects(resolveF40ReceivingWarehouse(mixed.context, mixed.producers), /同一实际接柜仓/)
|
|
for (const mutate of [
|
|
f => { f.warehouses.pop() },
|
|
f => { f.warehouses[1].status = 'DISABLED' },
|
|
f => { f.warehouses[1].warehouseType = 'DOMESTIC' },
|
|
f => { f.warehouses[1].warehouseCode = 'WRONG' },
|
|
f => { f.warehouses.push({ ...f.warehouses[1] }) },
|
|
]) {
|
|
const f = warehouseSelectionFixture(); mutate(f)
|
|
await assert.rejects(resolveF40ReceivingWarehouse(f.context, f.producers), /唯一启用海外收货仓/)
|
|
assert.ok(f.calls.every(row => row.method === 'GET'))
|
|
}
|
|
})
|
|
|
|
test('F40 warehouse resolution rejects empty or duplicate producers before HTTP', async () => {
|
|
for (const select of [() => [], f => [f.producers[0], f.producers[0]],
|
|
f => [{ ...f.producers[0], sourceInboundId: '' }]]) {
|
|
const f = warehouseSelectionFixture()
|
|
await assert.rejects(resolveF40ReceivingWarehouse(f.context, select(f)), /唯.*本轮实装来源/)
|
|
assert.equal(f.calls.length, 0)
|
|
}
|
|
})
|
|
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','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/)
|
|
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})
|
|
let frozenCancel
|
|
context.request=async(method,path,body,token,headers)=>{
|
|
calls.push({method,path,body,headers})
|
|
if(method==='POST'){
|
|
assert.equal(path,'/api/platform/v1/orders/first-leg/101/cancel?tenantId=90')
|
|
assert.ok(body.reason.includes('offline-only'))
|
|
assert.ok(headers?.['Idempotency-Key'])
|
|
if(count++)return {http:200,body:{code:headers['Idempotency-Key']===frozenCancel.key && JSON.stringify(body)===frozenCancel.payload ? '200' : 'A0409'}}
|
|
assert.equal(body.expectedVersion,childDetail.order.version)
|
|
frozenCancel={key:headers['Idempotency-Key'],payload:JSON.stringify(body)}
|
|
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)
|
|
const writes=calls.filter(row=>row.method==='POST')
|
|
assert.equal(writes.length,4)
|
|
assert.deepEqual(writes[1].body,writes[0].body)
|
|
assert.deepEqual(writes[1].headers,writes[0].headers)
|
|
assert.equal(writes[2].headers['Idempotency-Key'],writes[0].headers['Idempotency-Key'])
|
|
assert.notEqual(writes[3].headers['Idempotency-Key'],writes[0].headers['Idempotency-Key'])
|
|
assert.ok(writes[3].body.expectedVersion>writes[0].body.expectedVersion)
|
|
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={}
|
|
await assert.rejects(run(context),/F40 本轮客户完整凭据有效/)
|
|
assert.equal(context.createdResources.phase5RootFulfillment.finalStatus,'FAILED')
|
|
assert.equal(context.createdResources.phase5RootFulfillment.failedStage,'CUSTOMER_IDENTITY')
|
|
assert.equal('error' in context.createdResources.phase5RootFulfillment,false)
|
|
const secretError=new Error('offline-secret-body-not-recorded')
|
|
const withLogin=fixture();withLogin.context={};withLogin.options={merchantUsername:'offline',merchantPassword:'offline'}
|
|
withLogin.request=async()=>{throw secretError}
|
|
await assert.rejects(run(withLogin),error=>error === secretError)
|
|
assert.equal(withLogin.createdResources.phase5RootFulfillment.finalStatus,'FAILED')
|
|
assert.equal(JSON.stringify(withLogin.createdResources.phase5RootFulfillment).includes(secretError.message),false)
|
|
})
|