import test from 'node:test' import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import { verifyFleetFeeCoverage, coveragePlan } from './fleet-fee-coverage-acceptance.mjs' const BASE = '/api/platform/v1/last-leg/dispatches', PDA = '/api/pda/v1/last-leg/dispatches', customerId = '900719925474099301' const clone = value => structuredClone(value) function fixture(mode = 'FULL', fault = '', needsLoading = false) { const cases = ['FULL', 'PARTIAL'].map((mode, index) => ({ marker: 'CURRENT-TEST', mode, dispatchNo: `DS-${index + 1}`, customerId, transferBody: { plannedDeliveryAt: '2026-10-22T01:00:00Z', loadingType: 'PALLET', appointmentAccount: 'SELF', fees: [{ direction: 'RECEIVABLE', feeType: 'DELIVERY_FEE', feeName: '派送费', amount: 17.5, currency: 'USD' }] } })) const source = cases.find(c => c.mode === mode), calls = [], assertions = [], ledger = new Map(), saved = new Map() const states = cases.map((c, n) => ({ dispatchNo: c.dispatchNo, recordVersion: 0, stage: needsLoading && c === source ? 'LOADING' : 'AFTER_LOADING', items: [1, 2].map(i => ({ dispatchItemId: `${n + 1}1${i}`, sourceKey: `PLAN:${n + 1}2${i}`, sourceVersion: 'a'.repeat(64), customerId, warehousePalletId: `${n + 1}3${i}`, planPalletId: `${n + 1}2${i}`, inboundCode: `IN-${n + 1}-${i}`, destination: { key: `FBA:A${i}`, warehouseCode: `A${i}` }, scheduledPackages: 2, pickedPackages: needsLoading && c === source ? 0 : 2 })) })) if (fault === 'overlap') states[1].items[0].warehousePalletId = states[0].items[0].warehousePalletId const triples = cases.map((c, n) => ({ dispatchNo: c.dispatchNo, tripNo: `TR-${n + 1}`, warehouseId: '92', stops: [{ index: 0, destinationKey: 'ORIGIN:92' }, { index: 1, destinationKey: 'FBA:A1' }, { index: 2, destinationKey: 'FBA:A2' }] })) let dirty = false, replayed = false const totals = s => ({ itemCount: s.items.length, palletCount: s.items.length, packageCount: 4, pickedPackageCount: s.items.reduce((n, i) => n + i.pickedPackages, 0), shippedPackageCount: 0 }) const detail = s => ({ dispatchNo: s.dispatchNo, status: 'ACTIVE', stage: s.stage, recordVersion: s.recordVersion, counts: totals(s), pickingReconciled: s.items.every(i => i.pickedPackages === 2), dispatchInfo: { route: 'A1,A2' }, orderDetails: s.items.map(i => ({ customerId, sourceKey: i.sourceKey, inboundCode: i.inboundCode, destinationFbaWarehouse: i.destination.warehouseCode })) }) const cargo = s => s.items.map((i, n) => ({ ...i, orderId: `4${s.dispatchNo.slice(-1)}${n + 1}`, sourceRegion: 'FIRSTLEG', sourceInboundId: `9007199254740994${s.dispatchNo.slice(-1)}${n + 1}`, packages: 2 })) const groups = s => cargo(s).map((row, n) => ({ groupKey: `G-${s.dispatchNo}-${n}`, customerId, destination: row.destination, groupCargoHash: `${n + 1}`.repeat(64), externalFreightHash: 'b'.repeat(64), externalQuotes: [], feeCoverageCargoHash: `${n + 1}`.repeat(64), cargo: [row], blockReasons: [], requiredServices: ['DELIVERY_FEE'], feeSources: fault === 'missing-source' && n === 1 ? [] : [{ financeFeeId: `9007199254740995${s.dispatchNo.slice(-1)}${n}`, customerId: fault === 'foreign-customer' && n === 0 ? '999' : customerId, currency: 'CNY', amount: fault === 'mutated-original' && saved.has(s.dispatchNo) ? 201 : 200, sourceVersion: 1, sourceHash: `fee-hash-${n}`, status: 'AVAILABLE', services: ['DELIVERY_FEE'], sourceKeys: [row.sourceKey] }] })) const ack = (s, action, result = {}) => ({ dispatchNo: s.dispatchNo, action, recordVersion: s.recordVersion, status: 'ACTIVE', stage: s.stage, counts: totals(s), result, replayed: false }) const response = (data, message = '') => ({ http: 200, body: { code: message ? 'A0409' : '200', message, data: clone(data) }, requestId: 'OFFLINE-REQ', traceId: 'OFFLINE-TRACE' }) const context = { marker: source.marker, createdResources: { fleetFeeCoverageCases: cases, fleetTrips: triples }, 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), number = normalized.slice(BASE.length + 1).split('/')[0] const state = states.find(s => s.dispatchNo === number), commandKey = `${normalized}:${body?.idempotencyKey}` if (method === 'POST' && ledger.has(commandKey)) { assert.deepEqual(body, ledger.get(commandKey).body); replayed = true; return response({ ...clone(ledger.get(commandKey).data), replayed: true }) } const remember = data => { ledger.set(commandKey, { body: clone(body), data: clone(data) }); return response(data) } if (method === 'GET' && path === BASE) return response({ records: states.filter(s => s.dispatchNo === params.get('dispatchNo')).map(s => ({ ...detail(s), warehouseId: '92' })) }) if (path.includes('/outbound-orders?') || raw.includes('/outbound-orders?')) return response({ records: dirty ? [{ outboundNo: 'OUT-DIRTY', dispatchNo: source.dispatchNo }] : [], total: dirty ? 1 : 0, page: 1, size: 200 }) if (path.match(/\/outbound-orders\/OUT-DS-[12]$/)) { const n = path.slice(path.lastIndexOf('OUT-') + 4), s = states.find(s => s.dispatchNo === n), p = saved.get(n); return response({ outbound: { outboundNo: `OUT-${n}`, dispatchNo: n, status: 'WAIT_OUTBOUND' }, deliveries: p.groups.map((g, i) => ({ deliveryNo: `DEL-${n}-${i}`, status: 'WAIT_DELIVERY' })) }) } const delivery = path.match(/\/delivery-orders\/DEL-(DS-[12])-(\d)$/) if (delivery) { assert.equal(params.get('customerId'), customerId) const s = states.find(s => s.dispatchNo === delivery[1]), index = Number(delivery[2]), group = groups(s)[index], input = saved.get(s.dispatchNo).groups[index] const references = input.feeCoverage.map(selection => ({ source: { ...group.feeSources.find(f => f.financeFeeId === selection.financeFeeId), sourceType: 'INBOUND_FEE', sourceInboundId: group.cargo[0].sourceInboundId, ...(fault === 'snapshot-source-amount' ? { amount: 999 } : {}) }, services: selection.services })) const evidence = input.feeCoverage.length ? { cargoHash: group.feeCoverageCargoHash, cargo: fault === 'wrong-cargo' ? cargo(s) : group.cargo, references: fault === 'missing-reference' ? [] : references, operatorId: 'OP', confirmedAt: replayed && fault === 'rewrite-confirmation' ? '2026-10-19T02:00:00Z' : '2026-10-19T01:00:00Z' } : null let fees = input.fees.map(f => ({ id: `8${index}`, feeDirection: f.direction, feeType: f.feeType, currency: f.currency, amount: f.amount, reviewStatus: 'APPROVED', sourceStatus: 'ACTIVE' })) if (fault === 'duplicate-new-fee' && index === 0 || fault === 'replay-adds-fee' && replayed) fees.push({ id: '999', feeDirection: 'RECEIVABLE', feeType: 'DELIVERY_FEE', currency: 'USD', amount: 17.5, reviewStatus: 'APPROVED', sourceStatus: 'ACTIVE' }) if (fault === 'charge-covered-destination') fees = index === 0 ? [{ id: '999', feeDirection: 'RECEIVABLE', feeType: 'DELIVERY_FEE', currency: 'USD', amount: 17.5, reviewStatus: 'APPROVED', sourceStatus: 'ACTIVE' }] : [] return response({ order: { dispatchNo: s.dispatchNo, deliveryNo: `DEL-${s.dispatchNo}-${index}`, customerId: fault === 'wrong-delivery-customer' ? '999' : customerId, outboundNo: `OUT-${s.dispatchNo}`, destinationWarehouseCode: group.destination.warehouseCode }, fees, feeCoverage: evidence }) } assert.ok(state, `${method} ${raw}`) if (path.startsWith(PDA) && method === 'GET' && normalized.endsWith(number)) return response({ ...detail(state), items: state.items.map((i, n) => ({ ...i, positions: [{ positionId: `6${n}`, availablePackages: 2 }] })) }) if (method === 'POST' && normalized.endsWith('/pick')) { assert.ok(token); if (body.recordVersion !== state.recordVersion) return response(null, '公共版本已变化'); const itemId = normalized.split('/').at(-2); state.items.find(i => i.dispatchItemId === itemId).pickedPackages += body.packages; state.recordVersion++; return remember(ack(state, 'PICK')) } if (method === 'GET' && normalized.endsWith('/loading')) return response({ ...detail(state), manifestHash: 'c'.repeat(64), loadingComplete: { allowed: true }, pallets: state.items.map((i, n) => ({ ...i, palletNo: `PAL-${n}`, deliveryIndex: n + 1, palletSpaces: 1, weightKg: 20, volumeCbm: 0.2, evidence: ['palletSpaces', 'weightKg', 'volumeCbm'].map(field => ({ field, value: field === 'palletSpaces' ? 1 : field === 'weightKg' ? 20 : 0.2, actual: true, sourceType: 'MEASURED', sourceBusinessNo: i.inboundCode, observedAt: '2026-10-19T01:00:00Z' })) })) }) if (method === 'POST' && normalized.endsWith('/loading-complete')) { assert.equal(body.recordVersion, state.recordVersion); assert.equal(state.items.every(i => i.pickedPackages === 2), true); state.stage = 'AFTER_LOADING'; state.recordVersion++; return remember(ack(state, 'LOADING_COMPLETE', { confirmedLoad: { manifestHash: body.manifestHash, pallets: body.measurements } })) } if (method === 'GET' && path.endsWith('/items')) return response({ dispatchNo: number, recordVersion: state.recordVersion, status: 'ACTIVE', stage: state.stage, items: state.items }) if (method === 'GET' && path.endsWith('/route')) return response({ dispatchNo: number, recordVersion: state.recordVersion, stops: triples.find(t => t.dispatchNo === number).stops }) if (method === 'GET' && path.endsWith('/delivery-arrangement')) return response({ ...detail(state), cargoHash: 'a'.repeat(64), loadingCompletionHash: 'c'.repeat(64), arrange: { allowed: !saved.has(number) }, groups: groups(state), arrangement: saved.has(number) ? { dispatchNo: number } : null }) if (method === 'POST' && path.endsWith('/transfer-delivery')) { assert.ok(body.idempotencyKey); assert.equal(body.recordVersion, state.recordVersion); assert.equal(state.stage, 'AFTER_LOADING') if (body.cargoHash !== 'a'.repeat(64)) { if (fault === 'failed-command-writes') dirty = true; return response(null, '装车或实际货物已经变化') } if (body.groups.some(g => g.feeCoverage.some(f => f.sourceVersion !== 1))) return response(null, '原费用版本已变化') if (source.mode === 'FULL' && body.groups.some(g => g.fees.length)) return response(null, '本组服务已全部覆盖,不得重复登记应收') if (source.mode === 'PARTIAL' && !body.groups.some(g => g.fees.length)) return response(null, '本组尚有未覆盖的服务费用,请核对') saved.set(number, clone(body)); state.recordVersion++ const result = ack(state, 'ARRANGE_DELIVERY', { dispatchNo: number, cargoHash: body.cargoHash, outbounds: [{ customerId, outboundNo: `OUT-${number}`, status: 'WAIT_OUTBOUND', packageCount: 4 }], deliveries: groups(state).map((g, n) => ({ groupKey: g.groupKey, customerId, destination: g.destination, outboundNo: `OUT-${number}`, deliveryNo: `DEL-${number}-${n}`, status: 'WAIT_DELIVERY', packageCount: 2 })) }) if (fault === 'unknown-write') return { http: 0, body: null, raw: 'connection reset' } return remember(result) } assert.equal(method, 'GET', raw); assert.equal(path, `${BASE}/${number}`); return response(detail(state)) } } return { context, source, cases, calls, assertions, states, groups, clearFault() { fault = '' } } } for (const mode of ['FULL', 'PARTIAL']) test(`${mode} uses native grouped fees, immutable confirmation evidence and original-key replay`, async () => { const f = fixture(mode), proof = await verifyFleetFeeCoverage(f.context, 'coverage', f.source) assert.equal(proof.status, 'VERIFIED'); assert.equal(proof.newReceivable, mode === 'FULL' ? 0 : 17.5); assert.equal(proof.deliveryNos.length, 2) assert.ok(f.assertions.every(a => a.passed)); assert.ok(f.calls.every(c => !/cancel|unbind|pre-arrange|fee-coverage/.test(c.path))) assert.equal(proof.confirmationRequest.recordVersion, 0); assert.equal(proof.confirmationRequest.groups.length, 2) await assert.rejects(verifyFleetFeeCoverage(f.context, 'coverage', f.source), /未执行/) }) test('LOADING performs actual PDA picks, immutable pick replay and explicit loading completion before arranging', async () => { const f = fixture('FULL', '', true), proof = await verifyFleetFeeCoverage(f.context, 'coverage', f.source, { pickerToken: 'OFFLINE-ONLY' }) assert.equal(proof.status, 'VERIFIED') const loading = f.calls.findIndex(c => c.method === 'POST' && c.path.endsWith('/loading-complete')), arrangement = f.calls.findIndex(c => c.method === 'POST' && c.path.endsWith('/transfer-delivery')) assert.ok(loading > f.calls.findIndex(c => c.path.endsWith('/pick'))); assert.ok(arrangement > loading) }) test('reviewed persisted reconciliation only replays original successful native command after actual document readback', async () => { const f = fixture('FULL', 'wrong-cargo'); await assert.rejects(verifyFleetFeeCoverage(f.context, 'coverage', f.source)); assert.ok(f.source.proof.outboundNo) const original = clone(f.source.proof.confirmationRequest), before = f.calls.length; f.clearFault() assert.equal((await verifyFleetFeeCoverage(f.context, 'coverage', f.source, { reconcilePersisted: true })).status, 'VERIFIED') assert.ok(f.calls.slice(before).filter(c => c.method === 'POST').every(c => JSON.stringify(c.body) === JSON.stringify(original))) }) for (const fault of ['foreign-customer', 'missing-source', 'failed-command-writes', 'wrong-cargo', 'missing-reference', 'duplicate-new-fee', 'rewrite-confirmation', 'mutated-original', 'charge-covered-destination', 'replay-adds-fee', 'wrong-delivery-customer', 'overlap', 'unknown-write', 'snapshot-source-amount']) test(`native fee proof rejects ${fault}`, async () => { const f = fixture(fault === 'charge-covered-destination' ? 'PARTIAL' : 'FULL', fault) await assert.rejects(verifyFleetFeeCoverage(f.context, 'coverage', f.source)); assert.notEqual(f.source.proof.status, 'VERIFIED') if (['foreign-customer', 'missing-source', 'overlap'].includes(fault)) assert.equal(f.calls.some(c => c.method === 'POST'), false) if (fault === 'unknown-write') { assert.equal(f.source.proof.status, 'OUTCOME_UNKNOWN'); const last = f.calls.at(-1); assert.equal(last.method, 'POST'); assert.equal(f.context.createdResources.dispatchWriteBoundary.stopped, true) } }) test('two current registered source groups are required before HTTP; missing PDA and ambiguous mixed provenance never guess', async () => { const missing = fixture(); missing.context.createdResources.fleetFeeCoverageCases.pop(); await assert.rejects(verifyFleetFeeCoverage(missing.context, 'coverage', missing.source), /两组不重叠/); assert.equal(missing.calls.length, 0) const noPicker = fixture('FULL', '', true); await assert.rejects(verifyFleetFeeCoverage(noPicker.context, 'coverage', noPicker.source), /PDA身份/); assert.equal(noPicker.calls.some(c => c.method === 'POST'), false) const f = fixture('PARTIAL'), groups = f.groups(f.states[1]); groups[0].cargo.push({ ...groups[0].cargo[0], sourceInboundId: '999' }); assert.throws(() => coveragePlan({ groups }, f.source), /不能唯一映射/) const multi = fixture('PARTIAL'), many = multi.groups(multi.states[1]), third = clone(many[1]); third.groupKey = 'GROUP-THIRD'; third.cargo[0].sourceKey = 'PLAN:999'; third.cargo[0].sourceInboundId = '999'; third.feeSources[0].financeFeeId = '9999'; third.feeSources[0].sourceKeys = ['PLAN:999']; many.push(third) assert.throws(() => coveragePlan({ groups: many }, multi.source), /明确逐组费用/) multi.source.transferBody.groupFees = { [many[1].groupKey]: [{ ...multi.source.transferBody.fees[0], amount: 6 }] } assert.throws(() => coveragePlan({ groups: many }, multi.source), /明确逐组费用/) multi.source.transferBody.groupFees[third.groupKey] = [{ ...multi.source.transferBody.fees[0], amount: 7 }] assert.deepEqual(coveragePlan({ groups: many }, multi.source).groups.map(g => g.fees.map(f => f.amount)), [[], [6], [7]]) const unsafe = fixture(); unsafe.source.customerId = 9007199254740992; assert.throws(() => coveragePlan({ groups: unsafe.groups(unsafe.states[0]) }, unsafe.source), /ID精度无效/) }) test('public native delivery DTO retains grouped fees and requires version and loading hashes', () => { const dto = readFileSync(new URL('../../lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchDeliveryModels.java', import.meta.url), 'utf8') assert.ok(dto.includes('record ArrangeDelivery(Long recordVersion')); assert.ok(dto.includes('String loadingCompletionHash')); assert.ok(dto.includes('List groups')) })