Files
baiyue-firstleg/scripts/admin-test-env-modules/fleet-external-dispatch-acceptance-contract.test.mjs
T
2026-10-21 14:30:41 +08:00

146 lines
20 KiB
JavaScript

import test from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { verifyFleetExternalDispatch } from './fleet-external-dispatch-acceptance.mjs'
const BASE = '/api/platform/v1/last-leg/dispatches', PDA = '/api/pda/v1/last-leg/dispatches', TRIP = '/api/platform/v1/last-leg/fleet/trips/TR-CURRENT'
const OWN = '900719925474099301', EXT = '900719925474099302', clone = value => structuredClone(value)
function fixture(fault = '', nativeMode = false) {
const calls = [], assertions = [], ledger = new Map()
const externalCustomer = nativeMode ? OWN : EXT
const destination = n => ({ key: `FBA:A${nativeMode ? 1 : n}`, warehouseCode: `A${nativeMode ? 1 : n}` })
const item = (n, customerId = OWN) => ({ dispatchItemId: `11${n}`, sourceKey: `PLAN:2${n}`, sourceVersion: 'a'.repeat(64), sourceType: 'INBOUND_PLAN', planPalletId: `2${n}`, warehousePalletId: `3${n}`, palletNo: `PAL-${n}`,
customerId, inboundCode: n === 3 ? 'WT-CURRENT' : nativeMode ? `WT-OWN-${n}` : `IN-${n}`, orderNo: n === 3 ? 'WT-CURRENT' : nativeMode ? `WT-OWN-${n}` : `ORDER-${n}`, destination: destination(n === 3 ? 1 : n), scheduledPackages: nativeMode ? 1 : n === 3 ? 3 : 2, pickedPackages: 0 })
const items = [item(1), item(2, nativeMode ? EXT : OWN)], fullItems = [item(4)], external = item(3, externalCustomer)
const segment = { fromIndex: 0, toIndex: 1, occupied: { palletSpaces: 4, weightKg: 57, volumeCbm: 1.1 }, remaining: { palletSpaces: 26, weightKg: 17943, volumeCbm: 88.9 } }
const bookingCargo = { destinationKey: 'FBA:A1', packageCount: nativeMode ? 1 : 3, palletSpaces: 2, weightKg: 37, volumeCbm: 0.7, priceBasis: 'PALLET', unitPrice: 15, currency: 'CNY' }
const accepted = { source: { incomingNo: 'WT-CURRENT', inboundCode: 'WT-CURRENT', warehouseId: '92', destinationKey: 'FBA:A1', status: 'READY_FOR_DISPATCH', receiptHash: 'd'.repeat(64), receivedPackages: nativeMode ? 1 : 3,
pallets: [{ warehousePalletId: '33', palletNo: 'PAL-3', packages: nativeMode ? 1 : 3, weightKg: 37, volumeCbm: 0.7 }] }, pallets: [{ warehousePalletId: '33', palletSpaces: 2, weightKg: 37, volumeCbm: 0.7 }], operatorId: 'OP', confirmedAt: '2026-10-19T01:00:00Z' }
const currentCase = { marker: 'CURRENT', mode: 'PARTIAL', dispatchNo: 'DS-OWN', customerId: OWN, transferBody: { plannedDeliveryAt: '2026-10-22T01:00:00Z', loadingType: 'PALLET', appointmentAccount: 'SELF', fees: [{ direction: 'RECEIVABLE', feeType: 'DELIVERY_FEE', feeName: '未覆盖派送费', amount: 17.5, currency: 'USD' }] } }
const full = { ...clone(currentCase), mode: 'FULL', dispatchNo: 'DS-FULL' }
const tripProof = { tripNo: 'TR-CURRENT', dispatchNo: 'DS-OWN', warehouseId: '92', vehicleId: '71', driverId: '72', stops: [{ index: 0, destinationKey: 'ORIGIN:92' }, { index: 1, destinationKey: 'FBA:A1' }, { index: 2, destinationKey: 'FBA:A2' }],
externalReceipt: { marker: 'CURRENT', status: 'VERIFIED', incomingNo: 'WT-CURRENT', bookingNo: 'BK-CURRENT', warehousePalletId: '33', palletNo: 'PAL-3', createRequest: { customerId: externalCustomer }, finalSegments: [segment], finalBooking: { cargo: bookingCargo, agreedAmount: 30 } } }
if (nativeMode) tripProof.stops.pop()
let recordVersion = 0, fleetVersion = 0, stage = fault === 'already-loaded' ? 'DELIVERING' : 'LOADING', bound = false, planned = false, review = true, saved
const booking = () => ({ bookingNo: 'BK-CURRENT', customerId: externalCustomer, status: bound ? 'BOUND' : 'RECEIVED', incomingNo: 'WT-CURRENT', dispatchNo: bound ? 'DS-OWN' : null, recordVersion: bound ? 1 : 0, cargo: bookingCargo, agreedAmount: 30, receipt: accepted })
const counts = rows => ({ itemCount: rows.length, palletCount: rows.length, packageCount: rows.reduce((n, i) => n + i.scheduledPackages, 0), pickedPackageCount: rows.reduce((n, i) => n + i.pickedPackages, 0), shippedPackageCount: 0 })
const detail = (full = false) => ({ dispatchNo: full ? 'DS-FULL' : 'DS-OWN', recordVersion: full ? 0 : recordVersion, status: 'ACTIVE', stage: full ? 'DELIVERING' : stage,
reviewState: review ? 'APPROVED' : 'DRAFT', reviews: { dispatchReview: { status: review ? 'APPROVED' : 'NOT_SUBMITTED' }, loadingReview: { status: review ? 'APPROVED' : 'NOT_SUBMITTED' } },
counts: counts(full ? fullItems : items), pickingReconciled: !full && items.every(i => i.pickedPackages === i.scheduledPackages), dispatchInfo: { route: 'A1,A2' },
documents: stage==='DELIVERING'&&!full?documents():{outbounds:[],deliveries:[]},
orderDetails: (full ? fullItems : items).map(i => ({ sourceKey: i.sourceKey, customerId: i.customerId, inboundCode: i.inboundCode, destinationFbaWarehouse: i.destination.warehouseCode })) })
const ack = (action, result = {}) => ({ ...detail(), action, result, replayed: false })
const groups = () => {
const raw = items.map((i, index) => {
const isExternal = i.sourceKey === 'PLAN:23', groupKey = nativeMode ? `G-${i.customerId}` : `G-${i.sourceKey}`
return { groupKey, customerId: i.customerId, destination: i.destination, groupCargoHash: `${index + 1}`.repeat(64), feeCoverageCargoHash: `${index + 1}`.repeat(64), externalFreightHash: 'b'.repeat(64), blockReasons: [], requiredServices: isExternal ? [] : ['DELIVERY_FEE'],
cargo: [{ ...i, sourceRegion: isExternal || nativeMode ? 'LASTLEG' : 'FIRSTLEG', sourceInboundId: isExternal || nativeMode ? null : `9007199254740994${index}`, orderId: `4${index}`, packages: i.scheduledPackages }],
externalQuotes: isExternal ? [{ customerId: externalCustomer, groupKey, bookingNo: 'BK-CURRENT', inboundCode: 'WT-CURRENT', currency: 'CNY', amount: fault === 'stale-price' ? 50 : 30, sourceHash: 'b'.repeat(64) }] : [],
feeSources: isExternal || nativeMode ? [] : [{ financeFeeId: `9007199254740995${index}`, customerId: OWN, currency: 'CNY', amount: 200, sourceVersion: 1, sourceHash: `FEE-${index}`, status: 'AVAILABLE', services: ['DELIVERY_FEE'], sourceKeys: [i.sourceKey] }] }
})
if (!nativeMode) return raw
const merged = new Map()
for (const group of raw) { const previous = merged.get(group.groupKey); if (previous) { previous.cargo.push(...group.cargo); previous.externalQuotes.push(...group.externalQuotes) } else merged.set(group.groupKey, group) }
return [...merged.values()].map(g => ({ ...g, requiredServices: g.cargo.some(c => !g.externalQuotes.some(q => q.inboundCode === c.inboundCode)) ? ['DELIVERY_FEE'] : [] }))
}
const documents=()=>({outbounds:[...new Set(items.map(i=>i.customerId))].map(customerId=>({customerId,outboundNo:customerId===OWN?'OUT-OWN':'OUT-EXT',status:'OUTBOUNDED',packageCount:items.filter(i=>i.customerId===customerId).reduce((n,i)=>n+i.scheduledPackages,0)})),deliveries:groups().map((g,n)=>({groupKey:g.groupKey,customerId:g.customerId,destination:g.destination,outboundNo:g.customerId===EXT?'OUT-EXT':'OUT-OWN',deliveryNo:`DEL-${n}`,status:'DELIVERING',packageCount:g.cargo.reduce((n,c)=>n+c.packages,0)}))})
const trip = () => ({ trip: { tripNo: 'TR-CURRENT', warehouseId: '92', vehicleId: '71', driverId: '72', recordVersion: fleetVersion, stops: tripProof.stops }, dispatches: [{ dispatchNo: fault === 'second-dispatch' ? 'DS-OTHER' : 'DS-OWN' }] })
const response = (data, message = '') => ({ http: 200, body: { code: message ? 'A0409' : '200', message, data: clone(data) }, requestId: 'OFFLINE-REQ', traceId: 'OFFLINE-TRACE' })
const context = { marker: 'CURRENT', createdResources: nativeMode ? { fleetTrips: [tripProof] } : { fleetTrips: [tripProof, { dispatchNo: 'DS-FULL', tripNo: 'TR-FULL', warehouseId: '92' }], fleetFeeCoverageCases: [full, currentCase] }, checkpoint: async () => {},
assert(_module, name, _response, passed) { assertions.push({ name, passed }) }, addStep(_module, name, _response, passed) { assertions.push({ name, passed }) }, addCoverageGap() {}, apiSuccess(_m, _l, r) { return r.body?.data },
async request(method, raw, body, token) {
calls.push({ method, path: raw, body: clone(body), token })
const path = raw.split('?')[0], params = new URLSearchParams(raw.split('?')[1]), normalized = path.replace(PDA, BASE), key = `${normalized}:${body?.idempotencyKey}`
if (method !== 'GET' && body?.idempotencyKey && ledger.has(key)) { const original = ledger.get(key); assert.deepEqual(body, original.body); if (fault === 'replay-row-drift' && path.endsWith('/items')) items[0].dispatchItemId = '999'; if(path.endsWith('/loading-complete')&&token!==original.token)return {http:200,body:{code:'A0403',message:'该请求标识不属于当前操作员'},requestId:'R',traceId:'T'}; return response({ ...clone(original.data), replayed: true }) }
const remember = data => { if (body?.idempotencyKey) ledger.set(key, { body: clone(body), data: clone(data), token }); return response(data) }
if (path === `${TRIP}/capacity-plan`) {
if (method === 'POST') { assert.equal(body.recordVersion, fleetVersion); assert.equal(body.pallets.length, 3); assert.equal(body.pallets.find(p => p.warehousePalletId === '33').palletSpaces, 2); planned = true; fleetVersion++ }
return response({ recordVersion: fleetVersion, manifestHash: 'c'.repeat(64), planValid: items.length === 2 || planned || fault === 'stale-plan',
cargo: items.map(i => ({ dispatchNo: 'DS-OWN', warehousePalletId: i.warehousePalletId, packages: i.scheduledPackages })),
capacityPlan: { pallets: items.slice(0, 2).map((i, n) => ({ warehousePalletId: i.warehousePalletId, palletSpaces: 1, weightKg: 10, volumeCbm: 0.2, deliveryIndex: nativeMode ? 1 : n + 1 })) },
availableSegments: planned && fault === 'double-capacity' ? [{ ...segment, remaining: { ...segment.remaining, palletSpaces: 25 } }] : [segment] })
}
if (path === `${TRIP}/bookings`) return response({ tripRecordVersion: fleetVersion, records: [booking()] })
if (path === `${TRIP}/bookings/BK-CURRENT/dispatch`) { assert.equal(body.dispatchNo, 'DS-OWN'); assert.equal(body.tripRecordVersion, fleetVersion); assert.equal(body.recordVersion, 0); bound = true; fleetVersion++; return remember({ tripRecordVersion: fleetVersion, booking: booking(), replayed: false }) }
if (path === TRIP) return response(trip())
if (path === `${BASE}/candidates`) {
assert.equal(params.get('warehouseId'), '92'); assert.equal(params.get('sourceStage'), 'RECEIVED'); assert.equal(params.get('orderNo'), 'WT-CURRENT')
return response({ complete: true, snapshotAt: '2026-10-19', candidates: [{ selection: { sourceType: 'INBOUND_PLAN', planPalletId: '23', returnReceiptId: null, relabelNo: null, warehousePalletId: '33', sourceVersion: 'a'.repeat(64) },
sourceKey: 'PLAN:23', sourceVersion: 'a'.repeat(64), customerId: fault === 'foreign-customer' ? '999' : externalCustomer, warehouseId: '92', inboundCode: 'WT-CURRENT', palletNo: 'PAL-3', availablePackages: nativeMode ? 1 : 3, destination: destination(1), appointmentBatches: [], blockReasons: [] }] })
}
if (path === BASE) return response({ records: [{ ...detail(params.get('dispatchNo') === 'DS-FULL'), warehouseId: '92' }] })
if (raw.includes('/outbound-orders?')) return response({ records: [], total: 0, page: 1, size: 200 })
if (path.includes('/finance-fee-sources')) {
assert.equal(params.get('customerId'), externalCustomer); assert.equal(params.get('businessNo'), nativeMode ? 'DEL-0' : 'DEL-2')
return response([{ id: '980', sourceFeeId: '970', customerId: fault === 'foreign-source-fee' ? externalCustomer === OWN ? EXT : OWN : externalCustomer, direction: 'RECEIVABLE', feeItemCode: 'DELIVERY_FEE', inboundCode: 'WT-CURRENT', businessNo: nativeMode ? 'DEL-0' : 'DEL-2', sourceStatus: 'ACTIVE', reviewStatus: 'APPROVED', originalCurrency: 'CNY', originalAmount: 30, chargeableQuantity: fault === 'wrong-quantity' ? 1 : 2, unitPrice: 15, quantityUnit: '托位' }])
}
const delivery = path.match(/\/delivery-orders\/DEL-(\d)$/)
if (delivery) {
const index=Number(delivery[1]),group=groups()[index],who=group.customerId
assert.equal(params.get('customerId'),who)
const fees=group.externalQuotes.length?[{id:'980',feeDirection:'RECEIVABLE',feeType:'DELIVERY_FEE',amount:fault==='stale-price'?50:30,currency:'CNY',sourceStatus:'ACTIVE',reviewStatus:'APPROVED'}]:[]
if(fault==='duplicate-fee'&&fees.length)fees.push(clone(fees[0]))
return response({order:{customerId:who,dispatchNo:'DS-OWN',deliveryNo:`DEL-${index}`,outboundNo:who===EXT?'OUT-EXT':'OUT-OWN',destinationWarehouseCode:group.destination.warehouseCode,status:'DELIVERING',packageCount:group.cargo.reduce((n,c)=>n+c.packages,0)},fees,feeCoverage:null})
}
const isFull = path.startsWith(`${BASE}/DS-FULL`), data = detail(isFull), cargo = isFull ? fullItems : items
if (method === 'GET' && normalized === `${BASE}/DS-OWN` && path.startsWith(PDA)) return response({ ...data, items: items.map((i, n) => ({ ...i, positions: [{ positionId: `6${n}`, availablePackages: i.scheduledPackages }] })) })
if (method === 'GET' && normalized.endsWith('/items')) return response({ dispatchNo: data.dispatchNo, recordVersion: data.recordVersion, status: 'ACTIVE', stage: data.stage, items: cargo })
if (method === 'GET' && normalized.endsWith('/route')) return response({ dispatchNo: data.dispatchNo, recordVersion: data.recordVersion, stops: tripProof.stops })
if (method === 'GET' && normalized.endsWith('/loading')) return response({ ...data, manifestHash: 'c'.repeat(64), loadingComplete: { allowed: true }, pallets: items.map((i, n) => ({ ...i, ...(fault === 'missing-measurements' ? {} : { palletSpaces: n === 2 ? 2 : 1, weightKg: n === 2 ? 37 : 10, volumeCbm: n === 2 ? 0.7 : 0.2, deliveryIndex: nativeMode || n === 2 ? 1 : n + 1,
evidence: [['palletSpaces', n === 2 ? 2 : 1], ['weightKg', n === 2 ? 37 : 10], ['volumeCbm', n === 2 ? 0.7 : 0.2]].map(([field, value]) => ({ field, value, actual: true, sourceType: 'MEASURED', sourceBusinessNo: i.inboundCode, observedAt: '2026-10-19T01:00:00Z' })) }) })) })
if (method === 'GET') { assert.ok(normalized === `${BASE}/DS-OWN` || normalized === `${BASE}/DS-FULL`, raw); return response(data) }
if (path.endsWith('/pick') && body.recordVersion !== recordVersion) return response(null, '公共版本已变化')
assert.equal(body.recordVersion, recordVersion, raw)
if (path.endsWith('/return-to-review')) { stage = 'BEFORE_LOADING'; review = false; recordVersion++; return remember(ack('RETURN_TO_REVIEW')) }
if (path.endsWith('/items')) { assert.equal(stage, 'BEFORE_LOADING'); assert.equal(body.cargo.length, 1); items.push(external); recordVersion++; fleetVersion++; if (fault === 'unknown-add') return { http: 0, body: null }; return remember(ack('ADD_CARGO')) }
if (path.endsWith('/submit-review') || path.endsWith('/dispatch-review') || path.endsWith('/loading-review')) { if (path.endsWith('/loading-review') && fault !== 'review-not-applied') { review = true; stage = 'LOADING' }; recordVersion++; return remember(ack('REVIEW')) }
if (path.endsWith('/pick')) { assert.ok(token); const itemId = path.split('/').at(-2); items.find(i => i.dispatchItemId === itemId).pickedPackages += body.packages; recordVersion++; return remember(ack('PICK')) }
if (path.endsWith('/loading-complete')) { assert.equal(items.every(i => i.pickedPackages === i.scheduledPackages), true); stage = 'DELIVERING'; recordVersion++; return remember(ack('LOADING_COMPLETE', { confirmedLoad: { manifestHash: body.manifestHash, pallets: body.measurements } })) }
throw Error(`Unexpected ${method} ${raw}`)
} }
const options = nativeMode ? {pickerToken:'OFFLINE-ONLY'} : {coverageCase:currentCase,pickerToken:'OFFLINE-ONLY'}
return { context, tripProof, currentCase, calls, assertions, items, options }
}
test('native external flow adds actual received cargo to same whole, re-reviews, scans, loads and arranges all customer groups', async () => {
const f = fixture(), proof = await verifyFleetExternalDispatch(f.context, 'external', f.tripProof, f.options)
assert.equal(proof.status, 'VERIFIED'); assert.equal(proof.dispatchNo, 'DS-OWN'); assert.equal(proof.sourceFees.length, 1); assert.equal(proof.sourceFees[0].chargeableQuantity, 2)
assert.equal(proof.financeAcknowledgement, 'NOT_VERIFIED'); assert.equal(f.currentCase.proof.status, 'VERIFIED'); assert.equal(f.currentCase.proof.feeCoverageSelection,'NOT_APPLICABLE_AUTOMATIC_DELIVERY')
assert.ok(f.assertions.every(a => a.passed)); assert.equal(f.calls.some(c => /pre-arrange|dispatches\/bind|dispatches\/unbind|\/cancel/.test(c.path)), false)
assert.equal(f.calls.filter(c => c.method === 'POST' && c.path === BASE).length, 0)
assert.ok(f.calls.findIndex(c => c.path.endsWith('/return-to-review')) < f.calls.findIndex(c => c.path.endsWith('/items') && c.method === 'POST'))
assert.equal(f.calls.some(c => c.path.endsWith('/transfer-delivery')||c.path.endsWith('/depart')),false)
assert.equal(f.items.length, 3); await assert.rejects(verifyFleetExternalDispatch(f.context, 'external', f.tripProof, f.options), /尚未装入/)
})
for (const fault of ['foreign-customer', 'stale-plan', 'double-capacity', 'review-not-applied', 'stale-price', 'duplicate-fee', 'foreign-source-fee', 'wrong-quantity', 'replay-row-drift', 'second-dispatch', 'already-loaded', 'unknown-add']) test(`native external proof rejects ${fault}`, async () => {
const f = fixture(fault); await assert.rejects(verifyFleetExternalDispatch(f.context, 'external', f.tripProof, f.options)); assert.notEqual(f.tripProof.externalDispatch.status, 'VERIFIED')
if (['foreign-customer', 'already-loaded'].includes(fault)) assert.equal(f.calls.some(c => c.method === 'POST'), false)
if (fault === 'unknown-add') { assert.equal(f.tripProof.externalDispatch.status, 'OUTCOME_UNKNOWN'); assert.equal(f.calls.at(-1).path.endsWith('/items'), true); assert.equal(f.context.createdResources.dispatchWriteBoundary.stopped, true) }
})
test('no external callback injection can replace real registered PARTIAL sources, current PDA or durable journal', async () => {
for (const mutate of [f => { delete f.options.pickerToken }, f => { delete f.context.checkpoint }]) {
const f = fixture(); mutate(f); await assert.rejects(verifyFleetExternalDispatch(f.context, 'external', f.tripProof, f.options)); assert.equal(f.calls.length, 0)
}
})
test('server binding requires the same existing whole dispatch and public cargo commands use versioned ADD_CARGO', () => {
const service = readFileSync(new URL('../../lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetBookingService.java', import.meta.url), 'utf8')
assert.ok(service.includes('identity.tripId()!=trip.getId()')); assert.ok(service.includes('planning.validateIncomingDispatch'))
const source = readFileSync(new URL('./fleet-external-dispatch-acceptance.mjs', import.meta.url), 'utf8')
assert.equal(source.includes('/pre-arrange'), false); assert.equal(source.includes('draftToken'), false); assert.ok(source.includes('/return-to-review')); assert.ok(source.includes("'ADD_CARGO'"))
})
test('ordinary current sources automatically deliver without manual fee configuration',async()=>{
const f=fixture('',true),proof=await verifyFleetExternalDispatch(f.context,'external-native',f.tripProof,f.options)
assert.equal(proof.status,'VERIFIED');assert.equal(proof.fulfillmentMode,'AUTOMATIC_DELIVERY')
assert.equal(proof.deliveries.length,2);assert.equal(proof.deliveries.reduce((n,d)=>n+d.packageCount,0),3)
assert.ok(proof.deliveries.every(d=>d.status==='DELIVERING'));assert.ok(proof.loadingRequest.idempotencyKey)
assert.equal(f.calls.some(c=>c.path.endsWith('/transfer-delivery')||c.path.endsWith('/depart')),false)
assert.equal(proof.sourceFees[0].originalAmount,30)
})
for(const fault of ['foreign-customer','double-capacity','review-not-applied','stale-price','duplicate-fee','foreign-source-fee','wrong-quantity','replay-row-drift','already-loaded','missing-measurements'])test(`automatic external flow rejects ${fault}`,async()=>{
const f=fixture(fault,true);await assert.rejects(verifyFleetExternalDispatch(f.context,'external-native',f.tripProof,f.options));assert.notEqual(f.tripProof.externalDispatch.status,'VERIFIED')
})