Files
baiyue-firstleg/scripts/admin-test-env-modules/fleet-fee-coverage-acceptance-contract.test.mjs
T
2026-10-20 10:35:52 +08:00

123 lines
16 KiB
JavaScript

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<ArrangeGroup> groups'))
})