优化
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { controlledPalletMeasurements } from './fleet-trip-fulfillment.mjs'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { confirmFleetLoading } from './fleet-trip-fulfillment.mjs'
|
||||
|
||||
const MODULE = 'fleet-cross-customer-acceptance'
|
||||
const ROOT = '/api/platform/v1/last-leg'
|
||||
@@ -10,57 +11,82 @@ const encode = encodeURIComponent
|
||||
const scoped = (path, customerId) => `${path}?${new URLSearchParams({ customerId })}`
|
||||
const canonical = value => JSON.stringify(normalize(value))
|
||||
function normalize(value) {
|
||||
// Route stops and other business arrays are ordered. Raw readers return stable ID order.
|
||||
if (Array.isArray(value)) return value.map(normalize)
|
||||
return value && typeof value === 'object' ? Object.fromEntries(Object.keys(value).sort().map(key=>[key,normalize(value[key])])) : value
|
||||
return value && typeof value === 'object' ? Object.fromEntries(Object.keys(value).sort().map(key => [key, normalize(value[key])])) : value
|
||||
}
|
||||
function integer(value) {
|
||||
if (!['number','string'].includes(typeof value) || !/^(0|[1-9][0-9]*)$/.test(String(value)) || !Number.isSafeInteger(Number(value))) throw new Error('Invalid inventory integer')
|
||||
if (!['number', 'string'].includes(typeof value) || !/^(0|[1-9][0-9]*)$/.test(String(value)) || !Number.isSafeInteger(Number(value))) throw new Error('Invalid inventory integer')
|
||||
return Number(value)
|
||||
}
|
||||
const buckets = ['availablePackages','holdPackages','allocatedPackages','pickedPackages','receivingPackages']
|
||||
const balanceBuckets = [...buckets,'inTransitPackages','procurementPackages']
|
||||
|
||||
const buckets = ['availablePackages', 'holdPackages', 'allocatedPackages', 'pickedPackages', 'receivingPackages']
|
||||
const balanceBuckets = [...buckets, 'inTransitPackages', 'procurementPackages']
|
||||
export const fixtureContract = Object.freeze({
|
||||
stage: 'TWO_FRESH_RECEIVED_DISPATCHES_BOUND_AND_TRANSFERRED_NOT_PICKED',
|
||||
fixture: 'marker, warehouseId, tripNo, pdaToken, members[2]: {marker, customerId, incomingNo, dispatchNo, outboundNo, deliveryNo, balanceId, positionId, palletId, palletNo, receiptVerified:true}',
|
||||
preparation: 'Two independently qualified current-run customers; each formally received one physical pallet/box in the same warehouse; draft lock/pre-arrange separately; bind both to one assigned qualified vehicle/driver trip; transfer each to its own outbound and delivery. No picking yet.',
|
||||
reader: 'readInventory(scope) => {scope:{marker,customerId,warehouseId,coverage:"CUSTOMER_WAREHOUSE_COMPLETE"}, balances:all raw rows including zero, positions:all raw rows including zero, ledger:all immutable movement rows}. Retain camelCase IDs and every quantity bucket; never substitute a grouped public SKU page.',
|
||||
helperLimit: 'Existing external-receipt helper selects plan.cargo[0].customerId and requires an existing own-cargo capacity plan; external-dispatch helper has one mutable externalReceipt slot. Do not run either twice pretending to create two customers.',
|
||||
boundary: 'This exercises a prerequisite gate rejection, not a mid-transaction rollback. No fault injection, account activation, SQL writes, deployment, cleanup deletion or implicit fixture creation.',
|
||||
stage: 'TWO_FRESH_CUSTOMERS_ONE_REVIEWED_DISPATCH_LOADING_NOT_PICKED',
|
||||
fixture: 'marker, warehouseId, dispatchNo, tripNo, tripProof, pdaToken, optional current-run loadingMeasurements; members[2]: marker/customerId/incomingNo/shared dispatchNo/dispatchItemId/balanceId/positionId/palletId/palletNo/receiptVerified',
|
||||
preparation: 'Two independently qualified customers formally received one pallet/box each in the same warehouse. One native Create with both actual source selections; automatic unique Fleet record, resources and both native reviews. No picking or delivery arrangement.',
|
||||
reader: 'readInventory(scope) returns all raw balances/positions/immutable movements including zero, exact marker/customer/warehouse and CUSTOMER_WAREHOUSE_COMPLETE coverage.',
|
||||
boundary: 'Prerequisite rejection is separate from an explicitly injected real mid-transaction rollback. Actual loading needs actual evidence; absent measurement fields block. No environment loader, SQL or implicit account lifecycle.',
|
||||
})
|
||||
|
||||
/** Import/default call is network-free. Explicit execute:true requires separately authorized TEST execution.
|
||||
* Tokens stay in the caller, never in the persisted proof. Failure retains fixture and exact request keys.
|
||||
*/
|
||||
export async function verifyFleetCrossCustomer(context, fixture, { execute = false, readInventory, verifyDepartureRollback } = {}) {
|
||||
export const afterLoadingContinuationContract = Object.freeze({
|
||||
...fixtureContract,
|
||||
stage: 'TWO_OWNED_CUSTOMERS_ONE_DISPATCH_AFTER_LOADING_NOT_ARRANGED',
|
||||
preparation: 'Explicit continuation only: prior failed proof with verified gate and complete AFTER_PICK_1; original successful loadingCommand/confirmedLoad; current AFTER_LOADING with no arrangement or execution documents.',
|
||||
boundary: 'Reconcile original identities, complete picked inventory and original loading receipt through current reads before any new write. Never repeat picking/loading or rewrite the prior proof.',
|
||||
})
|
||||
|
||||
export async function verifyFleetCrossCustomer(context, fixture, { execute = false, readInventory, verifyDepartureRollback, completionPolling } = {}) {
|
||||
if (!execute) return fixtureContract
|
||||
if (!context?.marker || fixture?.marker !== context.marker || fixture.stage !== fixtureContract.stage
|
||||
|| !fixture.tripNo || !fixture.warehouseId || !fixture.pdaToken || typeof readInventory !== 'function'
|
||||
|| fixture.members?.length !== 2 || new Set(fixture.members.map(row=>row.customerId)).size !== 2
|
||||
|| fixture.members.some(row=>row.marker!==context.marker || row.receiptVerified!==true
|
||||
|| ['customerId','incomingNo','dispatchNo','outboundNo','deliveryNo','balanceId','positionId','palletId','palletNo'].some(key=>!id(row[key])))) {
|
||||
throw new Error('Requires two distinct current-run customers, immutable receipt references and a complete raw inventory reader')
|
||||
}
|
||||
for (const key of ['dispatchNo','outboundNo','deliveryNo','positionId','palletId']) {
|
||||
if (new Set(fixture.members.map(row=>id(row[key]))).size!==2) throw new Error(`Cross-customer fixture reuses ${key}`)
|
||||
}
|
||||
context.createdResources ||= {}
|
||||
return runCrossCustomerCore(context, fixture, { readInventory, verifyDepartureRollback, completionPolling })
|
||||
}
|
||||
|
||||
export async function continueFleetCrossCustomerAfterLoading(context, fixture, { execute = false, readInventory, verifyDepartureRollback, priorProof, completionPolling } = {}) {
|
||||
if (!execute) return afterLoadingContinuationContract
|
||||
if (!priorProof) throw new Error('Explicit prior cross-customer proof is required for AFTER_LOADING continuation')
|
||||
return runCrossCustomerCore(context, { ...fixture, members: structuredClone(fixture?.members) },
|
||||
{ readInventory, verifyDepartureRollback, priorProof, completionPolling })
|
||||
}
|
||||
|
||||
async function runCrossCustomerCore(context, fixture, { readInventory, verifyDepartureRollback, priorProof, completionPolling }) {
|
||||
const continuation = priorProof !== undefined
|
||||
const contract = continuation ? afterLoadingContinuationContract : fixtureContract
|
||||
if (!context?.marker || fixture?.marker !== context.marker || fixture.stage !== contract.stage
|
||||
|| !fixture.dispatchNo || !fixture.tripNo || fixture.tripProof?.dispatchNo !== fixture.dispatchNo || fixture.tripProof.tripNo !== fixture.tripNo
|
||||
|| !context.createdResources?.fleetTrips?.includes(fixture.tripProof)
|
||||
|| !fixture.warehouseId || !fixture.pdaToken || typeof readInventory !== 'function' || fixture.members?.length !== 2
|
||||
|| new Set(fixture.members.map(row => row.customerId)).size !== 2
|
||||
|| fixture.members.some(row => row.marker !== context.marker || row.receiptVerified !== true || row.dispatchNo !== fixture.dispatchNo
|
||||
|| ['customerId', 'incomingNo', 'dispatchItemId', 'balanceId', 'positionId', 'palletId', 'palletNo'].some(field => !id(row[field]))))
|
||||
throw new Error('Requires two distinct current-run customers and one reviewed native dispatch with complete raw inventory evidence')
|
||||
for (const field of ['incomingNo', 'dispatchItemId', 'positionId', 'palletId', 'balanceId'])
|
||||
if (new Set(fixture.members.map(row => id(row[field]))).size !== 2) throw new Error(`Cross-customer fixture reuses ${field}`)
|
||||
const polling = { maxAttempts: 41, intervalMs: 500, deadlineMs: 20000, ...completionPolling }
|
||||
if (!Number.isSafeInteger(polling.maxAttempts) || polling.maxAttempts < 1 || polling.maxAttempts > 121
|
||||
|| !Number.isSafeInteger(polling.intervalMs) || polling.intervalMs < 1 || polling.intervalMs > 2000
|
||||
|| !Number.isSafeInteger(polling.deadlineMs) || polling.deadlineMs < 1 || polling.deadlineMs > 60000)
|
||||
throw new Error('Finite completion polling bounds are required')
|
||||
context.createdResources ??= {}
|
||||
if (context.createdResources.fleetCrossCustomer) throw new Error('Proof already exists; retain failed run and reconcile explicitly')
|
||||
const proof = context.createdResources.fleetCrossCustomer = { marker:context.marker,tripNo:fixture.tripNo,
|
||||
warehouseId:fixture.warehouseId,members:fixture.members.map(({marker,customerId,incomingNo,dispatchNo,outboundNo,deliveryNo,balanceId,positionId,palletId,palletNo})=>({marker,customerId,incomingNo,dispatchNo,outboundNo,deliveryNo,balanceId,positionId,palletId,palletNo})),
|
||||
status:'CHECKING',requests:[],snapshots:{},gateProof:'NOT_RUN',midTransactionRollback:'NOT_TESTED',cleanup:'Caller owns account/session lifecycle; retain all business fixtures' }
|
||||
const check = (name, passed) => {
|
||||
context.assert(MODULE,name,null,Boolean(passed),'',OPTIONS)
|
||||
if (!passed) throw new Error(name)
|
||||
}
|
||||
const proof = context.createdResources.fleetCrossCustomer = { marker: context.marker, dispatchNo: fixture.dispatchNo, tripNo: fixture.tripNo,
|
||||
warehouseId: fixture.warehouseId, members: fixture.members.map(({ marker, customerId, incomingNo, dispatchNo, dispatchItemId, balanceId, positionId, palletId, palletNo }) =>
|
||||
({ marker, customerId, incomingNo, dispatchNo, dispatchItemId, balanceId, positionId, palletId, palletNo })),
|
||||
status: 'CHECKING', requests: [], snapshots: {}, gateProof: 'NOT_RUN', midTransactionRollback: 'NOT_TESTED', cleanup: 'Caller owns account/session lifecycle; retain all business fixtures' }
|
||||
const check = (name, passed) => { context.assert(MODULE, name, null, Boolean(passed), '', OPTIONS); if (!passed) throw new Error(name) }
|
||||
const equal = (a, b, name) => check(name, canonical(a) === canonical(b))
|
||||
const key = suffix => `${context.marker}-CROSS-${suffix}`
|
||||
const path = `${ROOT}/dispatches/${encode(fixture.dispatchNo)}`, pdaPath = `${PDA}/${encode(fixture.dispatchNo)}`
|
||||
const tripPath = `${ROOT}/fleet/trips/${encode(fixture.tripNo)}`
|
||||
const request = async (method,path,body,label,pda=false,rejection=false) => {
|
||||
const response = await context.request(method,path,body,pda?fixture.pdaToken:context.token,{})
|
||||
proof.requests.push({method,path,body,label,http:response?.http,code:response?.body?.code,requestId:response?.requestId,traceId:response?.traceId})
|
||||
const denied = response?.http===409 || (response?.http===200 && id(response.body?.code)==='A0409')
|
||||
check(label,Boolean(response?.requestId && response?.traceId) && (rejection?denied && rejection(id(response.body?.message)):ok(response)))
|
||||
const request = async (method, target, body, label, pda = false, rejection) => {
|
||||
const entry = { method, path: target, body, label, status: 'SENDING' }; proof.requests.push(entry); await context.checkpoint?.()
|
||||
let response
|
||||
try { response = await context.request(method, target, body, pda ? fixture.pdaToken : context.token, {}) }
|
||||
catch (error) { entry.status = 'UNKNOWN_RESULT'; await context.checkpoint?.(); throw error }
|
||||
Object.assign(entry, { status: 'RESPONDED', http: response?.http, code: response?.body?.code, requestId: response?.requestId, traceId: response?.traceId })
|
||||
await context.checkpoint?.()
|
||||
const denied = response?.http === 409 || (response?.http === 200 && id(response.body?.code) === 'A0409')
|
||||
const actorDenied = [200,403].includes(response?.http) && id(response.body?.code) === 'A0403'
|
||||
check(label, Boolean(response?.requestId && response?.traceId)
|
||||
&& (rejection === 'ACTOR_FORBIDDEN' ? actorDenied : rejection ? denied && rejection(id(response.body?.message)) : ok(response)))
|
||||
return response.body?.data
|
||||
}
|
||||
const stock = async member => {
|
||||
@@ -81,120 +107,273 @@ export async function verifyFleetCrossCustomer(context, fixture, { execute = fal
|
||||
return raw
|
||||
}
|
||||
const snapshot = async label => {
|
||||
const trip=await request('GET',tripPath,undefined,'Read shared trip')
|
||||
check('Trip members are exactly the two independent customers',trip.trip?.tripNo===fixture.tripNo && id(trip.trip.warehouseId)===id(fixture.warehouseId)
|
||||
&& trip.dispatches?.length===2 && fixture.members.every(member=>trip.dispatches.some(row=>row.dispatchNo===member.dispatchNo && id(row.customerId)===id(member.customerId))))
|
||||
const members=[]
|
||||
for(const member of fixture.members) {
|
||||
const dispatch=await request('GET',`${ROOT}/dispatches/${encode(member.dispatchNo)}`,undefined,'Read customer dispatch')
|
||||
const outbound=await request('GET',scoped(`${ROOT}/outbound-orders/${encode(member.outboundNo)}`,member.customerId),undefined,'Read customer outbound')
|
||||
const delivery=await request('GET',scoped(`${ROOT}/delivery-orders/${encode(member.deliveryNo)}`,member.customerId),undefined,'Read customer delivery')
|
||||
const task=await request('GET',`${PDA}/${encode(member.dispatchNo)}`,undefined,'Read PDA task',true)
|
||||
check('Execution records retain exact customer, warehouse and dispatch ownership',dispatch.dispatchNo===member.dispatchNo
|
||||
&& id(dispatch.overseasWarehouseId)===id(fixture.warehouseId) && dispatch.items?.length===1
|
||||
&& id(dispatch.items[0].warehousePalletId)===id(member.palletId)
|
||||
&& outbound.outbound?.outboundNo===member.outboundNo && outbound.outbound.dispatchNo===member.dispatchNo
|
||||
&& id(outbound.outbound.overseasWarehouseId)===id(fixture.warehouseId) && outbound.deliveries?.length===1
|
||||
&& outbound.deliveries[0].deliveryNo===member.deliveryNo && id(outbound.deliveries[0].customerId)===id(member.customerId)
|
||||
&& delivery.order?.deliveryNo===member.deliveryNo && id(delivery.order.customerId)===id(member.customerId)
|
||||
&& delivery.order.dispatchNo===member.dispatchNo && delivery.order.outboundNo===member.outboundNo
|
||||
&& task.dispatchNo===member.dispatchNo && id(task.warehouseId)===id(fixture.warehouseId) && task.tripNo===fixture.tripNo)
|
||||
members.push({dispatch,outbound,delivery,task,stock:await stock(member)})
|
||||
const trip = await request('GET', tripPath, undefined, 'Read automatic unique Fleet resource record')
|
||||
check('One trip binds exactly this one native cross-customer dispatch', trip.trip?.tripNo === fixture.tripNo
|
||||
&& id(trip.trip.warehouseId) === id(fixture.warehouseId) && trip.dispatches?.length === 1 && trip.dispatches[0].dispatchNo === fixture.dispatchNo)
|
||||
const dispatch = await request('GET', path, undefined, 'Read native whole dispatch')
|
||||
const items = await request('GET', `${path}/items`, undefined, 'Read actual per-customer cargo')
|
||||
const task = await request('GET', pdaPath, undefined, 'Read bound warehouse native PDA task', true)
|
||||
check('Native main/items/PDA retain both true customers and physical pallets', dispatch.dispatchNo === fixture.dispatchNo
|
||||
&& items.dispatchNo === fixture.dispatchNo && task.dispatchNo === fixture.dispatchNo && id(task.warehouseId) === id(fixture.warehouseId)
|
||||
&& id(dispatch.recordVersion) === id(items.recordVersion) && id(task.recordVersion) === id(dispatch.recordVersion)
|
||||
&& items.items?.length === 2 && task.items?.length === 2 && fixture.members.every(member =>
|
||||
items.items.some(i => id(i.dispatchItemId) === member.dispatchItemId && id(i.customerId) === member.customerId
|
||||
&& id(i.warehousePalletId) === member.palletId && i.palletNo === member.palletNo && integer(i.scheduledPackages) === 1)
|
||||
&& task.items.some(i => id(i.dispatchItemId) === member.dispatchItemId && id(i.customerId) === member.customerId && i.palletNo === member.palletNo)))
|
||||
const members = []
|
||||
for (const member of proof.members) {
|
||||
let outbound = null, delivery = null
|
||||
if (member.outboundNo || member.deliveryNo) {
|
||||
check('Customer execution documents are both explicit', member.outboundNo && member.deliveryNo)
|
||||
outbound = await request('GET', scoped(`${ROOT}/outbound-orders/${encode(member.outboundNo)}`, member.customerId), undefined, 'Read customer outbound')
|
||||
delivery = await request('GET', scoped(`${ROOT}/delivery-orders/${encode(member.deliveryNo)}`, member.customerId), undefined, 'Read customer delivery')
|
||||
check('Execution documents keep customer, warehouse and shared main ownership', outbound.outbound?.outboundNo === member.outboundNo
|
||||
&& outbound.outbound.dispatchNo === fixture.dispatchNo && id(outbound.outbound.overseasWarehouseId) === id(fixture.warehouseId)
|
||||
&& outbound.deliveries?.length === 1 && outbound.deliveries[0].deliveryNo === member.deliveryNo
|
||||
&& id(outbound.deliveries[0].customerId) === member.customerId && delivery.order?.deliveryNo === member.deliveryNo
|
||||
&& id(delivery.order.customerId) === member.customerId && delivery.order.dispatchNo === fixture.dispatchNo && delivery.order.outboundNo === member.outboundNo)
|
||||
}
|
||||
members.push({ outbound, delivery, stock: await stock(member) })
|
||||
}
|
||||
const value={trip,members};proof.snapshots[label]=value;return value
|
||||
const value = { trip, dispatch, items, task, members }; proof.snapshots[label] = value; return value
|
||||
}
|
||||
const equal = (a,b,name)=>check(name,canonical(a)===canonical(b))
|
||||
const pick = async (member,index) => {
|
||||
const before=await snapshot(`BEFORE_PICK_${index}`),task=before.members[index].task
|
||||
check('PDA exposes the single real current-run pallet and position',task.items?.length===1 && integer(task.items[0].scheduledPackages)===1
|
||||
&& integer(task.items[0].pickedPackages)===0 && task.items[0].palletNo===member.palletNo && task.items[0].positions?.length===1
|
||||
&& id(task.items[0].positions[0].id)===id(member.positionId) && integer(task.items[0].positions[0].availablePackages)===1)
|
||||
const body={positionId:member.positionId,packages:1,productQuantity:1,idempotencyKey:key(`PICK-${index}`),remark:context.marker}
|
||||
const path=`${PDA}/${encode(member.dispatchNo)}/items/${encode(task.items[0].dispatchItemId)}/pick`
|
||||
await request('POST',path,body,'Pick this customer only',true)
|
||||
await request('POST',`${PDA}/${encode(member.dispatchNo)}/complete`,{idempotencyKey:key(`COMPLETE-${index}`)},'Complete this customer picking',true)
|
||||
const after=await snapshot(`AFTER_PICK_${index}`)
|
||||
equal(before.members[1-index].stock,after.members[1-index].stock,'Other customer inventory unchanged by picking')
|
||||
const expected=structuredClone(before.members[index].stock)
|
||||
for(const collection of ['balances','positions']) {
|
||||
const row=expected[collection].find(row=>id(row.id)===id(collection==='balances'?member.balanceId:member.positionId))
|
||||
row.availablePackages=integer(row.availablePackages)-1
|
||||
}
|
||||
const counts=raw=>Object.fromEntries(['balances','positions'].map(collection=>[collection,raw[collection].map(row=>({id:id(row.id),...Object.fromEntries((collection==='balances'?balanceBuckets:buckets).map(field=>[field,integer(row[field])]))}))]))
|
||||
equal(counts(expected),counts(after.members[index].stock),'Only selected customer position and balance lose one box')
|
||||
const old=before.members[index].stock.ledger,newRows=after.members[index].stock.ledger.filter(row=>!old.some(prior=>id(prior.id)===id(row.id)))
|
||||
equal(old,after.members[index].stock.ledger.filter(row=>old.some(prior=>id(prior.id)===id(row.id))),'Immutable old movements preserved')
|
||||
check('Single ALLOCATE/PICK/SHIP sequence has exact source and quantity',newRows.length===3 && ['ALLOCATE','PICK','SHIP'].every(type=>newRows.filter(row=>row.businessType===type
|
||||
&& id(row.positionId)===id(member.positionId) && row.businessOrderNo===member.dispatchNo && Number(row.deltaPackages)===(type==='SHIP'?-1:1)).length===1))
|
||||
await request('POST',path,body,'Replay exact customer pick',true)
|
||||
equal(after,await snapshot(`PICK_REPLAY_${index}`),'Pick replay changes neither customer nor any execution record')
|
||||
const pick = async (member, index) => {
|
||||
const before = await snapshot(`BEFORE_PICK_${index}`), item = before.task.items.find(i => id(i.dispatchItemId) === member.dispatchItemId)
|
||||
check('PDA exposes exactly this customer real current-run pallet and position', integer(item.scheduledPackages) === 1 && integer(item.pickedPackages) === 0
|
||||
&& item.palletNo === member.palletNo && item.positions?.length === 1 && id(item.positions[0].positionId) === member.positionId
|
||||
&& integer(item.positions[0].availablePackages) === 1)
|
||||
const body = { recordVersion: before.task.recordVersion, positionId: member.positionId, packages: 1, productQuantity: 1,
|
||||
idempotencyKey: key(`PICK-${index}`), remark: context.marker }
|
||||
const pickPath = `${pdaPath}/items/${encode(member.dispatchItemId)}/pick`
|
||||
await request('POST', pickPath, body, 'Pick this customer only', true)
|
||||
const after = await snapshot(`AFTER_PICK_${index}`)
|
||||
equal(before.members[1-index].stock, after.members[1-index].stock, 'Other customer inventory unchanged by picking')
|
||||
const expected = structuredClone(before.members[index].stock)
|
||||
for (const collection of ['balances', 'positions']) expected[collection].find(row => id(row.id) === (collection === 'balances' ? member.balanceId : member.positionId)).availablePackages = 0
|
||||
const counts = raw => Object.fromEntries(['balances','positions'].map(collection => [collection, raw[collection].map(row => ({ id: id(row.id),
|
||||
...Object.fromEntries((collection === 'balances' ? balanceBuckets : buckets).map(field => [field, integer(row[field])])) }))]))
|
||||
equal(counts(expected), counts(after.members[index].stock), 'Only selected customer position and balance lose one box')
|
||||
const old = before.members[index].stock.ledger, newRows = after.members[index].stock.ledger.filter(row => !old.some(prior => id(prior.id) === id(row.id)))
|
||||
equal(old, after.members[index].stock.ledger.filter(row => old.some(prior => id(prior.id) === id(row.id))), 'Immutable old movements preserved')
|
||||
check('Single ALLOCATE/PICK/SHIP sequence has exact source and quantity', newRows.length === 3 && ['ALLOCATE','PICK','SHIP'].every(type => newRows.filter(row =>
|
||||
row.businessType === type && id(row.positionId) === member.positionId && row.businessOrderNo === fixture.dispatchNo && Number(row.deltaPackages) === (type === 'SHIP' ? -1 : 1)).length === 1))
|
||||
await request('POST', pickPath, body, 'Replay exact customer pick', true)
|
||||
equal(after, await snapshot(`PICK_REPLAY_${index}`), 'Pick replay changes neither customer nor any execution record')
|
||||
}
|
||||
try {
|
||||
let initial=await snapshot('INITIAL')
|
||||
check('Minimal fresh fixture has one available box per customer and no prior execution',initial.trip.trip.status==='PLANNED'
|
||||
&& initial.members.every((row,index)=>row.dispatch.status==='PENDING_DEPARTURE' && integer(row.dispatch.packageCount)===1
|
||||
&& integer(row.dispatch.pickedPackageCount)===0 && row.outbound.outbound.status==='WAIT_OUTBOUND' && row.delivery.order.status==='WAIT_DELIVERY'
|
||||
&& integer(row.stock.positions.find(p=>id(p.id)===id(fixture.members[index].positionId)).availablePackages)===1
|
||||
&& row.stock.balances.every(balance=>balanceBuckets.filter(field=>field!=='availablePackages').every(field=>integer(balance[field])===0))
|
||||
&& row.stock.balances.reduce((sum,balance)=>sum+integer(balance.availablePackages),0)===1))
|
||||
let detail=initial.trip
|
||||
for(const member of fixture.members)detail=await request('POST',`${tripPath}/dispatches/${encode(member.dispatchNo)}/review`,
|
||||
{recordVersion:detail.trip.recordVersion,decision:'APPROVED'},'Review each customer dispatch separately')
|
||||
check('Both customer dispatches approved',detail.dispatches.every(row=>row.review?.status==='APPROVED'))
|
||||
await pick(fixture.members[0],0)
|
||||
const rejectedBefore=await snapshot('GATE_BEFORE')
|
||||
check('Precisely one customer is ready',rejectedBefore.members[0].dispatch.status==='READY_TO_DEPART' && integer(rejectedBefore.members[1].dispatch.pickedPackageCount)===0)
|
||||
await request('POST',`${PDA}/${encode(fixture.members[0].dispatchNo)}/depart`,
|
||||
{recordVersion:rejectedBefore.trip.trip.recordVersion,idempotencyKey:key('UNREADY-DEPART'),remark:context.marker},'Unready second customer blocks whole trip before execution',true,
|
||||
message=>message.includes(fixture.members[1].dispatchNo) && message.includes('请完成拣货并核平全部箱数'))
|
||||
equal(rejectedBefore,await snapshot('GATE_AFTER'),'Rejected departure leaves both complete inventories/outbounds/deliveries/trip unchanged')
|
||||
proof.gateProof='PREREQUISITE_REJECTION_VERIFIED'
|
||||
await pick(fixture.members[1],1)
|
||||
const loading=await request('GET',`${tripPath}/loading`,undefined,'Read ready shared loading')
|
||||
check('Both real pallets ready for loading',!loading.blockReason && loading.cargo?.length===2)
|
||||
detail=await request('GET',tripPath,undefined,'Read current trip route')
|
||||
const measured=controlledPalletMeasurements(loading.cargo,detail.trip.stops)
|
||||
const loaded=await request('POST',`${tripPath}/loading`,{recordVersion:loading.recordVersion,manifestHash:loading.manifestHash,pallets:measured,reason:context.marker},'Confirm controlled TEST measurements')
|
||||
const reviewed=await request('POST',`${tripPath}/loading-review`,{recordVersion:loaded.recordVersion,decision:'APPROVED'},'Approve whole-trip loading')
|
||||
const ready=await snapshot('READY')
|
||||
const departBody={recordVersion:reviewed.trip.recordVersion,idempotencyKey:key('DEPART'),remark:context.marker}
|
||||
const departPath=`${PDA}/${encode(fixture.members[0].dispatchNo)}/depart`
|
||||
if(verifyDepartureRollback!==undefined) {
|
||||
check('Explicit rollback verifier is callable',typeof verifyDepartureRollback==='function')
|
||||
const rollback=await verifyDepartureRollback({context,fixture,ready,departPath,departBody,snapshot})
|
||||
check('Mid-transaction rollback has exact owned verification',rollback?.verified===true && rollback.scope==='OWNED_TWO_CUSTOMER_DEPARTURE_ROLLBACK')
|
||||
equal(ready,await snapshot('ROLLBACK_RESTORED'),'Mid-transaction rollback restores both complete customer execution and inventory snapshots')
|
||||
proof.rollbackEvidence=rollback;proof.midTransactionRollback='VERIFIED'
|
||||
let cargoBasis, originalLoadingHash
|
||||
if (continuation) {
|
||||
const prior = priorProof.snapshots?.AFTER_PICK_1, command = fixture.tripProof.loadingCommand
|
||||
const confirmed = command?.result?.result?.confirmedLoad
|
||||
check('Continuation has the original failed owned proof and verified prerequisite gate', priorProof.status === 'FAILED'
|
||||
&& priorProof.marker === context.marker && priorProof.dispatchNo === fixture.dispatchNo && priorProof.tripNo === fixture.tripNo
|
||||
&& id(priorProof.warehouseId) === id(fixture.warehouseId) && priorProof.gateProof === 'PREREQUISITE_REJECTION_VERIFIED'
|
||||
&& priorProof.members?.length === 2 && !priorProof.arrangeRequest && !priorProof.departure
|
||||
&& !priorProof.members.some(member => member.outboundNo || member.deliveryNo)
|
||||
&& Array.isArray(priorProof.requests) && priorProof.requests.every(entry => ['GET','HEAD','OPTIONS'].includes(entry.method) || entry.status === 'RESPONDED'))
|
||||
const identityFields = ['marker','customerId','incomingNo','dispatchNo','dispatchItemId','balanceId','positionId','palletId','palletNo']
|
||||
check('Continuation preserves each original customer, source, item, balance, position and physical pallet', fixture.members.every((member,index) =>
|
||||
identityFields.every(field => id(member[field]) === id(priorProof.members[index]?.[field]))))
|
||||
check('Prior AFTER_PICK_1 is a complete reviewed picked but not shipped native snapshot', prior?.dispatch?.dispatchNo === fixture.dispatchNo
|
||||
&& prior.dispatch.status === 'ACTIVE' && prior.dispatch.stage === 'LOADING' && prior.dispatch.reviewState === 'APPROVED'
|
||||
&& prior.dispatch.reviews?.dispatchReview?.status === 'APPROVED' && prior.dispatch.reviews?.loadingReview?.status === 'APPROVED'
|
||||
&& integer(prior.dispatch.counts?.packageCount) === 2 && integer(prior.dispatch.counts?.pickedPackageCount) === 2
|
||||
&& integer(prior.dispatch.counts?.shippedPackageCount) === 0 && prior.task?.pickingReconciled === true
|
||||
&& prior.task.dispatchNo === fixture.dispatchNo && id(prior.task.warehouseId) === id(fixture.warehouseId)
|
||||
&& prior.task.stage === 'LOADING' && id(prior.task.recordVersion) === id(prior.dispatch.recordVersion)
|
||||
&& prior.items?.dispatchNo === fixture.dispatchNo && id(prior.items.recordVersion) === id(prior.dispatch.recordVersion)
|
||||
&& prior.items.items?.length === 2 && prior.task.items?.length === 2
|
||||
&& prior.members?.length === 2 && prior.members.every(row => row.stock && !row.outbound && !row.delivery)
|
||||
&& prior.dispatch.documents?.outbounds?.length === 0 && prior.dispatch.documents?.deliveries?.length === 0)
|
||||
check('Original loading command has a successful explicit receipt for this main and both exact physical pallets', command?.path === `${pdaPath}/loading-complete`
|
||||
&& command.body?.idempotencyKey && command.body.manifestHash && command.result?.dispatchNo === fixture.dispatchNo
|
||||
&& command.result.action === 'LOADING_COMPLETE' && command.result.idempotencyKey === command.body.idempotencyKey
|
||||
&& command.result.status === 'ACTIVE' && command.result.stage === 'AFTER_LOADING' && command.result.result?.loadingCompletionHash
|
||||
&& id(command.body.recordVersion) === id(prior.dispatch.recordVersion)
|
||||
&& /^(0|[1-9]\d*)$/.test(id(command.result.recordVersion))
|
||||
&& /^(0|[1-9]\d*)$/.test(id(command.body.recordVersion))
|
||||
&& BigInt(id(command.result.recordVersion)) > BigInt(id(command.body.recordVersion))
|
||||
&& integer(command.result.counts?.packageCount) === 2 && integer(command.result.counts?.pickedPackageCount) === 2
|
||||
&& integer(command.result.counts?.shippedPackageCount) === 0
|
||||
&& confirmed?.manifestHash === command.body.manifestHash && confirmed.operatorId && Number.isFinite(Date.parse(confirmed.confirmedAt))
|
||||
&& canonical(confirmed) === canonical(fixture.tripProof.confirmedLoad)
|
||||
&& Array.isArray(command.body.measurements) && command.body.measurements.length === 2
|
||||
&& canonical(command.body.measurements) === canonical(confirmed.pallets)
|
||||
&& new Set(confirmed.pallets.map(row => id(row.warehousePalletId))).size === 2
|
||||
&& fixture.members.every(member => confirmed.pallets.some(row => id(row.warehousePalletId) === member.palletId
|
||||
&& Number.isSafeInteger(row.palletSpaces) && row.palletSpaces > 0
|
||||
&& ['weightKg','volumeCbm'].every(field => ['number','string'].includes(typeof row[field])
|
||||
&& /^(?:0|[1-9]\d*)(?:\.\d+)?$/.test(String(row[field])) && Number.isFinite(Number(row[field])) && Number(row[field]) > 0)
|
||||
&& Number.isSafeInteger(row.deliveryIndex) && row.deliveryIndex > 0 && fixture.tripProof.stops?.[row.deliveryIndex]?.destinationKey)))
|
||||
proof.continued = true
|
||||
proof.continuation = { mode: 'EXPLICIT_AFTER_LOADING', priorProofSHA256: createHash('sha256').update(canonical(priorProof)).digest('hex'),
|
||||
priorStatus: priorProof.status, gateProofSource: 'PRIOR_PROOF', originalAfterPickSnapshot: structuredClone(prior),
|
||||
originalLoadingCommand: structuredClone(command), originalConfirmedLoad: structuredClone(confirmed) }
|
||||
proof.gateProof = priorProof.gateProof
|
||||
await context.checkpoint?.()
|
||||
const initial = await snapshot('CONTINUATION_INITIAL')
|
||||
check('Current main is exactly the unarranged confirmed loading version with two picked and zero shipped packages', initial.dispatch.status === 'ACTIVE'
|
||||
&& initial.dispatch.stage === 'AFTER_LOADING' && initial.task.stage === 'AFTER_LOADING' && initial.trip.trip.status === 'PLANNED'
|
||||
&& id(initial.dispatch.recordVersion) === id(command.result.recordVersion) && initial.task.pickingReconciled === true
|
||||
&& initial.dispatch.reviewState === 'APPROVED' && initial.dispatch.reviews?.dispatchReview?.status === 'APPROVED'
|
||||
&& initial.dispatch.reviews?.loadingReview?.status === 'APPROVED' && integer(initial.dispatch.counts.packageCount) === 2
|
||||
&& integer(initial.dispatch.counts.pickedPackageCount) === 2 && integer(initial.dispatch.counts.shippedPackageCount) === 0
|
||||
&& initial.dispatch.documents?.outbounds?.length === 0 && initial.dispatch.documents?.deliveries?.length === 0
|
||||
&& id(initial.trip.trip.vehicleId) === id(fixture.tripProof.vehicleId) && id(initial.trip.trip.driverId) === id(fixture.tripProof.driverId)
|
||||
&& id(prior.trip?.trip?.vehicleId) === id(fixture.tripProof.vehicleId) && id(prior.trip?.trip?.driverId) === id(fixture.tripProof.driverId)
|
||||
&& canonical(initial.trip.trip.stops) === canonical(fixture.tripProof.stops)
|
||||
&& initial.items.items.every(item => integer(item.pickedPackages) === 1) && initial.task.items.every(item => integer(item.pickedPackages) === 1))
|
||||
equal(prior.members.map(row => row.stock), initial.members.map(row => row.stock), 'Continuation current complete inventory equals original AFTER_PICK_1 inventory')
|
||||
const sourceItems = items => items.map(item => Object.fromEntries(['dispatchItemId','customerId','sourceKey','sourceVersion','sourceType',
|
||||
'planPalletId','returnReceiptId','relabelNo','warehousePalletId','palletNo','orderNo','inboundCode','destination','scheduledPackages','pickedPackages']
|
||||
.map(field => [field,item[field]]))).sort((a,b) => id(a.dispatchItemId).localeCompare(id(b.dispatchItemId)))
|
||||
equal(sourceItems(prior.items.items), sourceItems(initial.items.items), 'Continuation preserves both picked source items and destinations')
|
||||
const loading = await request('GET', `${pdaPath}/loading`, undefined, 'Reconcile current native manifest against original successful loading receipt', true)
|
||||
check('Current loading preview preserves the original manifest, confirmed receipt and two reconciled physical pallets', loading.dispatchNo === fixture.dispatchNo
|
||||
&& loading.status === 'ACTIVE' && loading.stage === 'AFTER_LOADING' && id(loading.recordVersion) === id(command.result.recordVersion)
|
||||
&& loading.pickingReconciled === true && loading.manifestHash === confirmed.manifestHash
|
||||
&& canonical(loading.confirmedLoad) === canonical(confirmed) && loading.pallets?.length === 2
|
||||
&& new Set(loading.pallets.map(row => id(row.warehousePalletId))).size === 2
|
||||
&& fixture.members.every(member => loading.pallets.some(row => id(row.warehousePalletId) === member.palletId
|
||||
&& row.palletNo === member.palletNo && integer(row.scheduledPackages) === 1 && integer(row.pickedPackages) === 1)))
|
||||
proof.continuation.currentLoadingPreview = loading
|
||||
cargoBasis = initial
|
||||
originalLoadingHash = command.result.result.loadingCompletionHash
|
||||
} else {
|
||||
const initial = await snapshot('INITIAL')
|
||||
check('Fresh native fixture is reviewed LOADING with both customer receipts untouched', initial.trip.trip.status === 'PLANNED'
|
||||
&& initial.dispatch.status === 'ACTIVE' && initial.dispatch.stage === 'LOADING' && initial.dispatch.reviewState === 'APPROVED'
|
||||
&& initial.dispatch.reviews?.dispatchReview?.status === 'APPROVED' && initial.dispatch.reviews?.loadingReview?.status === 'APPROVED'
|
||||
&& integer(initial.dispatch.counts.packageCount) === 2 && integer(initial.dispatch.counts.pickedPackageCount) === 0
|
||||
&& initial.dispatch.documents?.deliveries?.length === 0 && initial.members.every((row,index) =>
|
||||
integer(row.stock.positions.find(p => id(p.id) === fixture.members[index].positionId).availablePackages) === 1
|
||||
&& row.stock.balances.every(balance => balanceBuckets.filter(field => field !== 'availablePackages').every(field => integer(balance[field]) === 0))
|
||||
&& row.stock.balances.reduce((sum,balance) => sum + integer(balance.availablePackages), 0) === 1))
|
||||
await pick(proof.members[0], 0)
|
||||
const rejectedBefore = await snapshot('GATE_BEFORE')
|
||||
check('Precisely one customer picked while main remains LOADING', integer(rejectedBefore.dispatch.counts.pickedPackageCount) === 1
|
||||
&& rejectedBefore.dispatch.stage === 'LOADING' && rejectedBefore.task.pickingReconciled === false)
|
||||
const preview = await request('GET', `${pdaPath}/loading`, undefined, 'Read incomplete native loading manifest', true)
|
||||
await request('POST', `${pdaPath}/loading-complete`, { recordVersion: rejectedBefore.task.recordVersion, idempotencyKey: key('UNREADY-LOAD'),
|
||||
manifestHash: preview.manifestHash, measurements: [], remark: context.marker }, 'Unready second customer blocks explicit loading', true,
|
||||
message => /拣货.*核平|尚未核平/.test(message))
|
||||
await request('POST', `${pdaPath}/depart`, { recordVersion: rejectedBefore.task.recordVersion, idempotencyKey: key('UNREADY-DEPART'), remark: context.marker },
|
||||
'Unready second customer blocks whole trip before execution', true, message => /装车|派送/.test(message))
|
||||
equal(rejectedBefore, await snapshot('GATE_AFTER'), 'Rejected departure leaves both complete inventories/outbounds/deliveries/trip unchanged')
|
||||
proof.gateProof = 'PREREQUISITE_REJECTION_VERIFIED'
|
||||
await pick(proof.members[1], 1)
|
||||
await confirmFleetLoading(context, MODULE, fixture.tripProof, fixture.pdaToken, fixture.loadingMeasurements)
|
||||
cargoBasis = proof.snapshots.AFTER_PICK_1
|
||||
}
|
||||
const departed=await request('POST',departPath,departBody,'Depart whole trip once',true)
|
||||
check('Departure includes both customer dispatches',departed.tripNo===fixture.tripNo && departed.status==='IN_TRANSIT' && integer(departed.shippedPackages)===2
|
||||
&& canonical(departed.dispatches.map(row=>row.dispatchNo).sort())===canonical(fixture.members.map(row=>row.dispatchNo).sort()))
|
||||
const after=await snapshot('DEPARTED')
|
||||
check('Both outbounds completed and trip in transit',after.trip.trip.status==='IN_TRANSIT' && after.members.every(row=>row.dispatch.status==='DEPARTED' && row.outbound.outbound.status==='OUTBOUNDED'))
|
||||
equal(ready.members.map(row=>row.stock),after.members.map(row=>row.stock),'Departure does not deduct either inventory again')
|
||||
const replay=await request('POST',departPath,departBody,'Replay exact shared departure',true)
|
||||
check('Shared departure replay is explicit',replay.replayed===true)
|
||||
equal(after,await snapshot('DEPART_REPLAY'),'Departure replay preserves both complete inventories and execution facts')
|
||||
for(const [index,member] of fixture.members.entries()) {
|
||||
const before=await snapshot(`BEFORE_SIGN_${index}`),path=scoped(`${ROOT}/delivery-orders/${encode(member.deliveryNo)}`,member.customerId)
|
||||
await request('PUT',scoped(`${ROOT}/delivery-orders/${encode(member.deliveryNo)}/status`,member.customerId),
|
||||
{status:'DELIVERING',expectedVersion:before.members[index].delivery.order.version,remark:context.marker},'Start only this customer delivery')
|
||||
const documentPath=scoped(`${ROOT}/delivery-orders/${encode(member.deliveryNo)}/documents`,member.customerId)
|
||||
const docs=await request('GET',documentPath,undefined,'Read this delivery document version')
|
||||
const uploaded=await context.requestMultipart('POST',scoped(`${ROOT}/delivery-orders/${encode(member.deliveryNo)}/documents/RECEIPT/files`,member.customerId),
|
||||
{name:`${context.marker}-${index}-receipt.txt`,type:'text/plain',content:new TextEncoder().encode(`${context.marker} synthetic receipt ${member.deliveryNo}`)})
|
||||
proof.requests.push({method:'POST',path:scoped(`${ROOT}/delivery-orders/${encode(member.deliveryNo)}/documents/RECEIPT/files`,member.customerId),label:'Upload own synthetic receipt',http:uploaded?.http,code:uploaded?.body?.code,requestId:uploaded?.requestId,traceId:uploaded?.traceId})
|
||||
check('Receipt uploaded through correct delivery endpoint',ok(uploaded) && id(uploaded.body?.data?.fileId))
|
||||
const documents=(docs.categories||[]).filter(row=>row.category!=='RECEIPT').map(row=>({category:row.category,fileIds:(row.files||[]).map(file=>file.fileId)}))
|
||||
documents.push({category:'RECEIPT',fileIds:[id(uploaded.body.data.fileId)]})
|
||||
await request('PUT',documentPath,{expectedVersion:docs.version,idempotencyKey:key(`SIGN-${index}`),bolNo:docs.bolNo||null,documents},'Attach receipt to independently complete this delivery')
|
||||
const signed=await snapshot(`SIGNED_${index}`)
|
||||
check('This customer independently signed',signed.members[index].delivery.order.status==='DELIVERED')
|
||||
equal(before.members[1-index].delivery,signed.members[1-index].delivery,'Other customer delivery is not signed or changed implicitly')
|
||||
equal(before.members.map(row=>row.stock),signed.members.map(row=>row.stock),'Signing does not mutate either customer inventory')
|
||||
// Retain the independently readable result as evidence, not a fabricated completion command.
|
||||
await request('GET',path,undefined,'Read signed customer delivery')
|
||||
const arrangement = await request('GET', `${path}/delivery-arrangement`, undefined, 'Read native loaded per-customer delivery arrangement')
|
||||
if (continuation) check('Continuation has no prior arrangement and retains the original loading completion hash', arrangement.arrangement == null
|
||||
&& arrangement.loadingCompletionHash === originalLoadingHash && id(arrangement.recordVersion) === id(fixture.tripProof.loadingCommand.result.recordVersion))
|
||||
check('Loaded arrangement groups cover exactly both real customers', arrangement.dispatchNo === fixture.dispatchNo && arrangement.status === 'ACTIVE'
|
||||
&& arrangement.stage === 'AFTER_LOADING' && arrangement.arrange?.allowed === true && arrangement.cargoHash && arrangement.loadingCompletionHash
|
||||
&& arrangement.groups?.length === 2 && proof.members.every(m => arrangement.groups.some(g => id(g.customerId) === m.customerId && g.cargo?.length
|
||||
&& g.cargo.every(c => id(c.customerId) === m.customerId && id(c.warehousePalletId) === m.palletId) && g.blockReasons?.length === 0
|
||||
&& g.destination?.key === cargoBasis.items.items.find(i => id(i.customerId) === m.customerId)?.destination?.key)))
|
||||
const arrangeBody = { recordVersion: arrangement.recordVersion, idempotencyKey: key('ARRANGE'), cargoHash: arrangement.cargoHash,
|
||||
loadingCompletionHash: arrangement.loadingCompletionHash, groups: arrangement.groups.map(g => ({ groupKey: g.groupKey, groupCargoHash: g.groupCargoHash,
|
||||
plannedDeliveryAt: new Date(Date.now() + 21600000).toISOString(), deliveryMethod: 'TRUCK_PALLET', pickupMethod: 'WAREHOUSE_PICKUP',
|
||||
loadingType: 'PALLET', vehicleType: 'TRUCK', vehicleSize: '本轮实际装载', appointmentAccount: 'SELF', remark: `${context.marker} TEST合成资料`,
|
||||
externalFreightHash: g.externalFreightHash, feeCoverageCargoHash: g.feeCoverageCargoHash, feeCoverage: [],
|
||||
fees: (g.requiredServices || []).map(feeType => ({ feeType, feeName: 'TEST本轮派送费', direction: 'RECEIVABLE', currency: 'CNY', amount: 30 })) })) }
|
||||
proof.arrangeRequest = arrangeBody
|
||||
const arranged = await request('POST', `${path}/transfer-delivery`, arrangeBody, 'Arrange both customers after real loading before departure')
|
||||
check('Native arrangement creates customer-isolated outbounds and deliveries', arranged.dispatchNo === fixture.dispatchNo && arranged.stage === 'AFTER_LOADING'
|
||||
&& arranged.result?.deliveries?.length === 2 && arranged.result.outbounds?.length === 2
|
||||
&& new Set(arranged.result.deliveries.map(d => d.deliveryNo)).size === 2 && new Set(arranged.result.outbounds.map(o => o.outboundNo)).size === 2)
|
||||
for (const [index, member] of proof.members.entries()) {
|
||||
const deliveries = arranged.result.deliveries.filter(d => id(d.customerId) === member.customerId)
|
||||
check('Each customer has exactly its own source-specific delivery and outbound', deliveries.length === 1 && integer(deliveries[0].packageCount) === 1
|
||||
&& arranged.result.outbounds.some(o => id(o.customerId) === member.customerId && o.outboundNo === deliveries[0].outboundNo && integer(o.packageCount) === 1))
|
||||
Object.assign(member, { outboundNo: deliveries[0].outboundNo, deliveryNo: deliveries[0].deliveryNo })
|
||||
Object.assign(fixture.members[index], { outboundNo: member.outboundNo, deliveryNo: member.deliveryNo })
|
||||
}
|
||||
proof.status='VERIFIED';return proof
|
||||
} catch(error) {proof.status='FAILED';proof.error=error.message;throw error}
|
||||
const arrangedReplay = await request('POST', `${path}/transfer-delivery`, arrangeBody, 'Replay exact native customer arrangement')
|
||||
check('Native arrangement replay keeps original documents and main version', arrangedReplay.replayed === true && id(arrangedReplay.recordVersion) === id(arranged.recordVersion)
|
||||
&& canonical(arrangedReplay.result) === canonical(arranged.result))
|
||||
const ready = await snapshot('READY'), departBody = { recordVersion: ready.task.recordVersion, idempotencyKey: key('DEPART'), remark: context.marker }
|
||||
const departPath = `${pdaPath}/depart`
|
||||
if (verifyDepartureRollback !== undefined) {
|
||||
check('Explicit rollback verifier is callable', typeof verifyDepartureRollback === 'function')
|
||||
const rollback = await verifyDepartureRollback({ context, fixture, ready, departPath, departBody, snapshot })
|
||||
check('Mid-transaction rollback has exact owned verification', rollback?.verified === true && rollback.scope === 'OWNED_TWO_CUSTOMER_DEPARTURE_ROLLBACK')
|
||||
equal(ready, await snapshot('ROLLBACK_RESTORED'), 'Mid-transaction rollback restores both complete customer execution and inventory snapshots')
|
||||
proof.rollbackEvidence = rollback; proof.midTransactionRollback = 'VERIFIED'
|
||||
}
|
||||
const departed = await request('POST', departPath, departBody, 'Depart one cross-customer native dispatch once', true)
|
||||
check('Departure includes both customers in the one actual native main', departed.dispatchNo === fixture.dispatchNo && departed.status === 'ACTIVE'
|
||||
&& departed.stage === 'DELIVERING' && integer(departed.result?.shippedPackages) === 2 && departed.result.deliveries?.length === 2)
|
||||
proof.departure = departed
|
||||
const after = await snapshot('DEPARTED')
|
||||
check('Both customer outbounds completed and main delivering', after.trip.trip.status === 'IN_TRANSIT' && after.dispatch.stage === 'DELIVERING'
|
||||
&& after.members.every(row => row.outbound.outbound.status === 'OUTBOUNDED' && row.delivery.order.status === 'DELIVERING'))
|
||||
equal(ready.members.map(row => row.stock), after.members.map(row => row.stock), 'Departure does not deduct either inventory again')
|
||||
const replay = await request('POST', departPath, departBody, 'Original PDA actor exact replay of native departure', true)
|
||||
check('Shared departure replay is explicit and returns original result', replay.replayed === true && id(replay.recordVersion) === id(departed.recordVersion)
|
||||
&& canonical(replay.result) === canonical(departed.result))
|
||||
const replayed = await snapshot('DEPART_REPLAY')
|
||||
equal(after, replayed, 'Departure replay preserves both complete inventories and execution facts')
|
||||
await request('POST', `${path}/depart`, departBody, 'Different platform actor cannot reuse the PDA departure command key', false, 'ACTOR_FORBIDDEN')
|
||||
equal(replayed, await snapshot('FOREIGN_ACTOR_DEPART_REJECTED'), 'Different actor rejection preserves both complete inventories and execution facts')
|
||||
for (const [index, member] of proof.members.entries()) {
|
||||
const before = await snapshot(`BEFORE_SIGN_${index}`), documentPath = scoped(`${ROOT}/delivery-orders/${encode(member.deliveryNo)}/documents`, member.customerId)
|
||||
const docs = await request('GET', documentPath, undefined, 'Read this customer delivery document version')
|
||||
const uploadPath = scoped(`${ROOT}/delivery-orders/${encode(member.deliveryNo)}/documents/RECEIPT/files`, member.customerId)
|
||||
const uploadEntry = { method: 'POST', path: uploadPath, label: 'Upload own synthetic receipt', status: 'SENDING' }; proof.requests.push(uploadEntry); await context.checkpoint?.()
|
||||
let uploaded
|
||||
try { uploaded = await context.requestMultipart('POST', uploadPath, { name: `${context.marker}-${index}-TEST-receipt.txt`, type: 'text/plain',
|
||||
content: new TextEncoder().encode(`${context.marker} synthetic TEST receipt ${member.deliveryNo}`) }) }
|
||||
catch (error) { uploadEntry.status = 'UNKNOWN_RESULT'; await context.checkpoint?.(); throw error }
|
||||
Object.assign(uploadEntry, { status: 'RESPONDED', http: uploaded?.http, code: uploaded?.body?.code, requestId: uploaded?.requestId, traceId: uploaded?.traceId }); await context.checkpoint?.()
|
||||
check('Receipt uploaded through correct customer endpoint', ok(uploaded) && uploaded.requestId && uploaded.traceId && id(uploaded.body?.data?.fileId))
|
||||
const documents = (docs.categories || []).filter(row => row.category !== 'RECEIPT').map(row => ({ category: row.category, fileIds: (row.files || []).map(file => file.fileId) }))
|
||||
documents.push({ category: 'RECEIPT', fileIds: [id(uploaded.body.data.fileId)] })
|
||||
await request('PUT', documentPath, { expectedVersion: docs.version, idempotencyKey: key(`SIGN-${index}`), bolNo: docs.bolNo || null, documents }, 'Attach receipt to independently complete this delivery')
|
||||
const signed = await snapshot(`SIGNED_${index}`)
|
||||
check('This customer independently signed', signed.members[index].delivery.order.status === 'DELIVERED')
|
||||
equal(before.members[1-index].delivery, signed.members[1-index].delivery, 'Other customer delivery is not signed or changed implicitly')
|
||||
equal(before.members.map(row => row.stock), signed.members.map(row => row.stock), 'Signing does not mutate either customer inventory')
|
||||
}
|
||||
const started = Date.now()
|
||||
proof.completionPolling = { ...polling, status: 'WAITING', attempts: [] }
|
||||
for (let attempt = 1; attempt <= polling.maxAttempts; attempt++) {
|
||||
if (Date.now() - started >= polling.deadlineMs) break
|
||||
const entry = { attempt, elapsedMs: Date.now() - started, status: 'READING' }
|
||||
proof.completionPolling.attempts.push(entry)
|
||||
const current = await request('GET', path, undefined, 'Read bounded automatic delivery completion progress')
|
||||
check('Completion progress retains the exact normal main, counts and both execution-document owners', current.dispatchNo === fixture.dispatchNo
|
||||
&& current.status === 'ACTIVE' && ['DELIVERING','DELIVERED'].includes(current.stage)
|
||||
&& /^(0|[1-9]\d*)$/.test(id(current.recordVersion)) && integer(current.counts?.packageCount) === 2
|
||||
&& integer(current.counts?.pickedPackageCount) === 2 && integer(current.counts?.shippedPackageCount) === 2
|
||||
&& current.documents?.outbounds?.length === 2 && current.documents?.deliveries?.length === 2
|
||||
&& proof.members.every(member => current.documents.outbounds.some(row => id(row.customerId) === member.customerId && row.outboundNo === member.outboundNo)
|
||||
&& current.documents.deliveries.some(row => id(row.customerId) === member.customerId && row.deliveryNo === member.deliveryNo && row.outboundNo === member.outboundNo)))
|
||||
Object.assign(entry, { status: 'RESPONDED', stage: current.stage, recordVersion: current.recordVersion, elapsedMs: Date.now() - started })
|
||||
if (entry.elapsedMs >= polling.deadlineMs) break
|
||||
if (current.stage === 'DELIVERED') { proof.completionPolling.status = 'COMPLETED'; break }
|
||||
await context.checkpoint?.()
|
||||
const remaining = polling.deadlineMs - (Date.now() - started)
|
||||
if (attempt < polling.maxAttempts && remaining > 0) await new Promise(resolve => setTimeout(resolve, Math.min(polling.intervalMs, remaining)))
|
||||
}
|
||||
proof.completionPolling.elapsedMs = Date.now() - started
|
||||
if (proof.completionPolling.status !== 'COMPLETED') {
|
||||
proof.completionPolling.status = 'TIMED_OUT'
|
||||
await context.checkpoint?.()
|
||||
throw new Error('Automatic delivery completion did not reach DELIVERED within the explicit read-only polling bounds')
|
||||
}
|
||||
const final = await snapshot('FINAL')
|
||||
check('All normal customer signatures aggregate native delivery completion', final.dispatch.status === 'ACTIVE' && final.dispatch.stage === 'DELIVERED')
|
||||
proof.status = 'VERIFIED'; return proof
|
||||
} catch (error) {
|
||||
proof.status = 'FAILED'; proof.error = error.message
|
||||
if (proof.completionPolling?.status === 'WAITING') {
|
||||
proof.completionPolling.status = 'FAILED'; proof.completionPolling.error = error.message
|
||||
const attempt = proof.completionPolling.attempts.at(-1)
|
||||
if (attempt?.status === 'READING') attempt.status = 'FAILED'
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user