import { controlledPalletMeasurements } from './fleet-trip-fulfillment.mjs' const MODULE = 'fleet-cross-customer-acceptance' const ROOT = '/api/platform/v1/last-leg' const PDA = '/api/pda/v1/last-leg/dispatches' const OPTIONS = { scenario: 'fleet-cross-customer-departure', deploymentBlocker: true } const id = value => value == null ? '' : String(value) const ok = response => response?.http === 200 && id(response.body?.code) === '200' 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 } 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') return Number(value) } 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.', }) /** 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 } = {}) { 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 ||= {} 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 key = suffix => `${context.marker}-CROSS-${suffix}` 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))) return response.body?.data } const stock = async member => { const scope={marker:context.marker,customerId:member.customerId,warehouseId:fixture.warehouseId} const raw=await readInventory(scope) check('Inventory reader proves complete customer/warehouse scope',canonical(raw?.scope)===canonical({...scope,coverage:'CUSTOMER_WAREHOUSE_COMPLETE'})) for(const collection of ['balances','positions','ledger'])check(`Complete ${collection} rows have unique identities and customer ownership`,Array.isArray(raw[collection]) && new Set(raw[collection].map(row=>id(row.id))).size===raw[collection].length && raw[collection].every(row=>id(row.id) && id(row.customerId)===id(member.customerId) && id(row.overseasWarehouseId)===id(fixture.warehouseId))) check('Original receipt balance and physical position are retained even at zero',raw.balances.some(row=>id(row.id)===id(member.balanceId)) && raw.positions.some(row=>id(row.id)===id(member.positionId) && id(row.balanceId)===id(member.balanceId) && id(row.palletId)===id(member.palletId) && row.palletNo===member.palletNo)) for(const balance of raw.balances) { for(const field of balanceBuckets) integer(balance[field]) const positions=raw.positions.filter(row=>id(row.balanceId)===id(balance.id)) for(const field of buckets) check(`Raw balance and every position conserve ${field}`,integer(balance[field])===positions.reduce((sum,row)=>sum+integer(row[field]),0)) } check('No orphan stock positions',raw.positions.every(row=>raw.balances.some(balance=>id(balance.id)===id(row.balanceId)))) 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 value={trip,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') } 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' } 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') } proof.status='VERIFIED';return proof } catch(error) {proof.status='FAILED';proof.error=error.message;throw error} }