feat(dispatch): unify whole vehicle scheduling, source ownership and staged execution

This commit is contained in:
2026-10-19 23:40:42 +08:00
parent e0677e7ae7
commit 9dd34791f7
346 changed files with 29245 additions and 12602 deletions
@@ -1,9 +1,11 @@
import test from 'node:test'
import { relabelOutboundMarksMatch, inventoryQuantitySnapshot, inventorySnapshot, dispatchInventorySources, inventorySourcesPresent, dispatchStockConservation, verifyDepartureReadiness, verifyPartialOrderCompletion, verifyFulfillmentRootStatus,
createDeliveryReturn, completeReturnedException, returnedExceptionFactsMatch, verifyCustomerSignedDocuments, createSignedDocumentIsolationCustomer,
dispatchDestinationConservation } from './phase5-overseas-fulfillment.mjs'
dispatchDestinationConservation, pickAll, arrangeDispatchDelivery, fulfillmentEvidenceContext } from './phase5-overseas-fulfillment.mjs'
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import { readFile, mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { parseArguments } from '../admin-test-env-acceptance.mjs'
@@ -27,27 +29,26 @@ test('一箱实收从四箱派送退回仅保持RETURN_PENDING,全部实收才
}
})
test('route equals actual item destinations and frozen selected candidates, then conserves split deliveries',()=>{
const fixture={planPalletIds:['10','11','12'],destinationWarehouseCodes:['ONT8','LAX9']}
const detail={route:'ONT8,LAX9',items:[{planPalletId:'10',fbaWarehouseCode:'ONT8'},
{planPalletId:'11',destinationWarehouseCode:'LAX9'},{planPalletId:'12',fbaWarehouseCode:'ONT8'}]}
const deliveries=[{destinationWarehouseCode:'LAX9'},{destinationWarehouseCode:'ONT8'}]
assert.equal(dispatchDestinationConservation(detail,fixture),true)
assert.equal(dispatchDestinationConservation(detail,fixture,deliveries),true)
for(const mutate of [
value=>value.detail.route='ONT8',value=>value.detail.route='ONT8,LAX9,OTHER',
value=>value.detail.route='ONT8,ONT8,LAX9',value=>value.detail.items[1].destinationWarehouseCode='OTHER',
value=>value.detail.items[0].destinationWarehouseCode='REASSIGNED',
value=>value.detail.items[1].planPalletId='99',value=>value.detail.items[1].planPalletId='10',
value=>value.fixture.destinationWarehouseCodes.push('UNSELECTED'),value=>value.deliveries.pop(),
value=>value.deliveries.push({destinationWarehouseCode:'OTHER'}),
value=>value.deliveries.push({destinationWarehouseCode:'ONT8'}),
]) {
const value=structuredClone({detail,fixture,deliveries});mutate(value)
assert.equal(dispatchDestinationConservation(value.detail,value.fixture,value.deliveries),false)
test('native route conserves source keys and destinations while same destination may contain multiple customers', () => {
const fixture = { sourceKeys: ['PLAN:10', 'PLAN:11', 'PLAN:12'], destinationWarehouseCodes: ['ONT8', 'LAX9'] }
const detail = { dispatchInfo: { route: 'ONT8,LAX9' }, orderDetails: [
{ sourceKey: 'PLAN:10', customerId: '100', destinationFbaWarehouse: 'ONT8' },
{ sourceKey: 'PLAN:11', customerId: '200', destinationFbaWarehouse: 'LAX9' },
{ sourceKey: 'PLAN:12', customerId: '200', destinationFbaWarehouse: 'ONT8' }] }
const deliveries = [{ customerId: '100', destination: { key: 'FBA:ONT8', warehouseCode: 'ONT8' } },
{ customerId: '200', destination: { key: 'FBA:ONT8', warehouseCode: 'ONT8' } },
{ customerId: '200', destination: { key: 'FBA:LAX9', warehouseCode: 'LAX9' } }]
assert.equal(dispatchDestinationConservation(detail, fixture), true)
assert.equal(dispatchDestinationConservation(detail, fixture, deliveries), true)
for (const mutate of [value => { value.detail.dispatchInfo.route = 'ONT8' },
value => { value.detail.dispatchInfo.route = 'ONT8,ONT8,LAX9' },
value => { value.detail.orderDetails[1].destinationFbaWarehouse = 'OTHER' },
value => { value.detail.orderDetails[1].sourceKey = 'PLAN:99' },
value => { value.fixture.destinationWarehouseCodes.push('UNSELECTED') },
value => { value.deliveries.pop() }, value => { value.deliveries.push(value.deliveries[0]) }]) {
const value = structuredClone({ detail, fixture, deliveries }); mutate(value)
assert.equal(dispatchDestinationConservation(value.detail, value.fixture, value.deliveries), false)
}
assert.match(source,/dispatchDestinationConservation\(beforeEdit.data,source.dispatch\)/)
assert.match(source,/dispatchDestinationConservation\(beforeEdit.data,source.dispatch,deliveries\)/)
})
test('phase-five fulfillment is a registered default HTTP module after the real phase-four dispatch fixture', () => {
@@ -61,40 +62,38 @@ test('phase-five fulfillment is a registered default HTTP module after the real
}
})
test('phase-five module covers dispatch edit, transfer, PDA pick/depart and delivery state', () => {
for (const token of [
"const DISPATCH_BASE = '/api/platform/v1/last-leg/dispatches'",
"const PDA_DISPATCH_BASE = '/api/pda/v1/last-leg/dispatches'",
'query(MERCHANT_DELIVERY_BASE, { dispatchNo })', '/items/${encodeURIComponent(id(item.dispatchItemId))}/pick',
'/complete`', '/depart`', '/outbound-orders/${encodeURIComponent(outboundNo)}',
'/delivery-orders/${encodeURIComponent(currentNo)}/status',
]) assert.ok(source.includes(token), token)
assert.match(source, /WAIT_OUTBOUND/)
assert.match(source, /WAIT_DELIVERY/)
assert.match(source, /READY_TO_DEPART/)
assert.match(source, /OUTBOUNDED/)
assert.match(source, /DELIVERING/)
assert.match(source, /expectedVersion: deliveryVersion/)
assert.match(source, /transferDeliveries\(transferred\.data\)/)
assert.match(source, /new Set\(destinationKeys\)\.size === deliveries\.length/)
assert.match(source, /deliveries\.reduce\(\(sum, value\) => sum \+ Number\(value\?\.packageCount \|\| 0\), 0\)/)
test('native fulfillment uses real public DTOs and scans before loading, arranging and departing', () => {
for (const token of ["const DISPATCH_BASE = '/api/platform/v1/last-leg/dispatches'",
"const PDA_DISPATCH_BASE = '/api/pda/v1/last-leg/dispatches'", '/transfer-delivery', '/delivery-arrangement',
'cargoHash: value.cargoHash', 'loadingCompletionHash: value.loadingCompletionHash',
'recordVersion: version(detail.data.recordVersion)', 'positionId: id',
'departed.data.result?.shippedPackages', 'outboundRef.outboundNo']) {
if (token === 'positionId: id') assert.match(source, /positionId: source.position \? id\(source.position.positionId\)/)
else assert.ok(source.includes(token), token)
}
assert.doesNotMatch(source, /READY_TO_DEPART|PENDING_DEPARTURE|DISPATCH-COMPLETE|\/items\/.*['"]DELETE['"]/)
assert.match(source, /data.status === 'ACTIVE' && data.stage === 'AFTER_LOADING'/)
assert.match(source, /departed.data.stage === 'DELIVERING'/)
const run = source.slice(source.indexOf('export async function run'))
assert.ok(run.indexOf('prepareFleetTrip(') < run.indexOf('await pickAll('))
assert.ok(run.indexOf('await pickAll(') < run.indexOf('await confirmFleetLoading('))
assert.ok(run.indexOf('await confirmFleetLoading(') < run.indexOf('await arrangeDispatchDelivery('))
assert.ok(run.indexOf('await arrangeDispatchDelivery(') < run.indexOf('await verifyDepartureReadiness('))
assert.ok(run.indexOf('全部客户正常签收汇总整车DELIVERED') < run.indexOf('await finishFleetTrip('))
assert.ok(run.indexOf('await finishFleetTrip(') < run.indexOf('const returnRequest'))
})
test('phase-five module proves the client delivery create, list and nested detail contract', () => {
assert.match(source, /const MERCHANT_DELIVERY_BASE = '\/api\/merchant\/v1\/last-leg\/delivery-orders'/)
test('client reads its own native-arranged deliveries and full nested document contract', () => {
assert.match(source, /loginMerchant\(context, customerId\)/)
assert.match(source, /context\.options\?\.merchantUsername/)
assert.match(source, /\/api\/merchant\/v1\/iam\/tenant\/profile/)
assert.match(source, /id\(profile\?\.id\) === customerId/)
assert.match(source, /客户创建派送并原子转出库单/)
assert.match(source, /deliveryMethod: 'TRUCK_PALLET'/)
assert.match(source, /fees: \[\{ feeType: 'DELIVERY_FEE', feeName: '派送费', direction: 'RECEIVABLE',\s*currency: 'USD', amount: 10/)
assert.match(source, /context.options\?.merchantUsername/)
assert.match(source, /id\(profile\?.id\) === customerId/)
assert.match(source, /客户分页回查自身派送单/)
assert.match(source, /客户读取自身派送详情/)
assert.match(source, /merchantDetail\.data\?\.order\?\.deliveryNo/)
assert.match(source, /merchantDetail\.data\?\.skus/)
assert.match(source, /merchantDetail\.data\?\.fees/)
assert.match(source, /merchantDetail\.data\?\.operations/)
assert.match(source, /merchantDetail.data\?.order\?.deliveryNo/)
assert.match(source, /merchantDetail.data\?.skus/)
assert.match(source, /merchantDetail.data\?.fees/)
assert.match(source, /merchantDetail.data\?.operations/)
assert.doesNotMatch(source, /merchant\(context, merchantToken, 'POST'.*创建派送/)
})
test('phase-five module proves receipt-save completion, cross-region convergence, customer visit, exception and return', () => {
@@ -134,10 +133,9 @@ test('phase-five module uses marker-owned fixtures and always disables its tempo
})
test('phase-five writes carry idempotency and record deployment blockers instead of passing empty fixtures', () => {
assert.match(source, /idempotencyKey: key\(context, 'TRANSFER'\)/)
assert.match(source, /idempotencyKey: key\(context, 'TRANSFER', dispatchNo\)/)
assert.match(source, /idempotencyKey: key\(context, 'DISPATCH-PICK'/)
assert.match(source, /idempotencyKey: key\(context, 'DISPATCH-COMPLETE'\)/)
assert.match(source, /idempotencyKey: key\(context, 'DISPATCH-DEPART'\)/)
assert.match(source, /idempotencyKey: key\(context, 'DISPATCH-DEPART', dispatchNo\)/)
assert.match(source, /addCoverageGap\(MODULE, SCENARIO/)
assert.match(source, /deploymentBlocker: true/)
})
@@ -153,9 +151,10 @@ test('relabel marks are checked on the authoritative outbound DTO without weaken
assert.match(source, /relabel\?\.marker !== context\.marker/)
})
test('manual exception retries only a transport failure with the identical business body', () => {
assert.match(source, /manualException\.response\.http === 0/)
assert.match(source, /人工异常连接失败后使用同载荷重试/)
test('unknown writes preserve the original command and stop instead of transport retry', () => {
assert.match(source, /entry.status = 'UNKNOWN'/)
assert.match(source, /存在未知写结果/)
assert.doesNotMatch(source, /人工异常连接失败后使用同载荷重试/)
assert.match(source, /manualExceptionBody/)
})
@@ -497,34 +496,28 @@ test('inventory evidence captures zero drift independently for every balance buc
}
})
test('departure gate requires the PDA task COMPLETED, server canDepart, and independent matching dispatch READY_TO_DEPART', async () => {
for (const [taskStatus, canDepart, dispatchStatus, dispatchNo, expected] of [
['COMPLETED', true, 'READY_TO_DEPART', 'DS-1', true],
['READY_TO_DEPART', true, 'READY_TO_DEPART', 'DS-1', false],
['PICKING', true, 'READY_TO_DEPART', 'DS-1', false],
['COMPLETED', false, 'READY_TO_DEPART', 'DS-1', false],
['COMPLETED', true, 'PENDING_DEPARTURE', 'DS-1', false],
['COMPLETED', true, 'DEPARTED', 'DS-1', false],
['COMPLETED', true, 'READY_TO_DEPART', 'DS-OTHER', false],
]) {
const context = contextFor((method, path) => path.startsWith('/api/pda/')
? ok({ dispatchNo: 'DS-1', status: taskStatus, canDepart }) : ok({ dispatchNo, status: dispatchStatus }))
assert.equal(Boolean(await verifyDepartureReadiness(context, 'PDA', 'DS-1')), expected)
test('departure requires independent matching native stage, shared version, reconciled picks and both action gates', async () => {
const valid = { dispatchNo: 'DS-1', status: 'ACTIVE', stage: 'AFTER_LOADING', recordVersion: '9007199254740993',
pickingReconciled: true, actions: { DEPART: { allowed: true } } }
for (const change of [null, { stage: 'LOADING' }, { stage: 'DELIVERING' }, { status: 'CANCELLED' },
{ dispatchNo: 'OTHER' }, { recordVersion: '9007199254740994' }, { pickingReconciled: false },
{ actions: { DEPART: { allowed: false } } }]) {
const context = contextFor((method, path) => ok(path.startsWith('/api/pda/') ? valid : { ...valid, ...change }))
assert.equal(Boolean(await verifyDepartureReadiness(context, 'PDA', 'DS-1')), change == null)
assert.equal(context.calls.length, 2)
assert.equal(context.calls.some(call => call.method !== 'GET'), false)
assert.equal(context.assertions.some(row => !row.passed), !expected)
}
})
test('G05 freezes actual PDA balance IDs and measures raw sources independently from grouped representatives without inbound filters', async () => {
const task = { dispatchNo: 'DS-1', warehouseId: 'WH-1', items: [
{ positions: [{ id: '11', balanceId: '1' }, { id: '12', balanceId: '2' }, { id: '11', balanceId: '1' }] }] }
{ positions: [{ positionId: '11', balanceId: '1' }, { positionId: '12', balanceId: '2' }, { positionId: '11', balanceId: '1' }] }] }
assert.deepEqual(dispatchInventorySources(task, 'DS-1', 'WH-1'), { sourceBalanceIds: ['1', '2'], sourcePositionIds: ['11', '12'],
sourcePositionBalances: [{ positionId: '11', balanceId: '1' }, { positionId: '12', balanceId: '2' }] })
assert.equal(dispatchInventorySources(task, 'OTHER', 'WH-1'), null)
assert.equal(dispatchInventorySources(task, 'DS-1', 'OTHER'), null)
assert.equal(dispatchInventorySources({ ...task, items: [{ positions: [{ id: '11' }] }] }, 'DS-1', 'WH-1'), null)
assert.equal(dispatchInventorySources({ ...task, items: [{ positions: [{ positionId: '11' }] }] }, 'DS-1', 'WH-1'), null)
let shipped = false
const source = balanceId => ({ id: balanceId, customerId: '100', overseasWarehouseId: 'WH-1',
destinationWarehouseCode: 'FBA', skuCode: 'MIXED', totalPackages: balanceId === '1' ? 3 : 6,
@@ -761,3 +754,129 @@ test('all fragments completed still requires raw aggregate history and matching
? ok({ summary: { orderId: '1002', mainStatus: 'COMPLETED' } }) : originalRequest(...args)
assert.equal((await verifyFulfillmentRootStatus(foreignProjection.context, foreignProjection.args)).passed, false)
})
test('native PDA scans use exact current versions including zero, immutable replay and stale-version rejection without implicit loading', async () => {
const values = ['0', '9007199254740993', '9007199254740994']
let currentVersion = values[0], picked = 0, firstBody, firstResult
const context = contextFor((method, path, body) => {
if (method === 'GET') return ok({ dispatchNo: 'DS-1', status: 'ACTIVE', stage: 'LOADING', recordVersion: currentVersion,
pickingReconciled: picked === 2, counts: { packageCount: 2, pickedPackageCount: picked },
items: [{ dispatchItemId: '9007199254740995', customerId: '100', scheduledPackages: 2, pickedPackages: picked,
positions: [{ positionId: '9007199254740996', balanceId: '9007199254740997', availablePackages: 1 },
{ positionId: '9007199254740998', balanceId: '9007199254740999', availablePackages: 1 }] }] })
assert.match(path, /items\/9007199254740995\/pick$/)
assert.equal(Object.hasOwn(body, 'expectedVersion'), false)
assert.equal(typeof body.positionId, 'string')
if (body.idempotencyKey.endsWith('-STALE')) {
assert.equal(body.recordVersion, '0'); return { http: 200, body: { code: 'A0409' } }
}
if (firstBody?.idempotencyKey === body.idempotencyKey) {
assert.deepEqual(body, firstBody); return ok({ ...firstResult, replayed: true })
}
assert.equal(body.recordVersion, currentVersion)
picked += 1; currentVersion = values[picked]
firstBody = structuredClone(body)
firstResult = { dispatchNo: 'DS-1', status: 'ACTIVE', stage: 'LOADING', recordVersion: currentVersion,
result: { dispatchItemId: '9007199254740995', pickedPackages: picked }, counts: { pickedPackageCount: picked } }
return ok(firstResult)
})
assert.equal(await pickAll(context, 'PDA-IN-MEMORY', 'DS-1'), true)
assert.equal(picked, 2)
assert.equal(context.calls.some(call => /loading-complete|\/complete|\/depart/.test(call.path)), false)
assert.deepEqual(context.assertions.filter(row => !row.passed), [])
})
function nativeArrangementPreview(recordVersion = '0') {
return { dispatchNo: 'DS-1', recordVersion, status: 'ACTIVE', stage: 'AFTER_LOADING', cargoHash: 'CARGO',
loadingCompletionHash: 'LOAD', arrange: { allowed: true }, groups: ['100', '200'].map(customerId => ({
groupKey: `GROUP-${customerId}`, customerId, destination: { key: 'FBA:ONT8', warehouseCode: 'ONT8' },
groupCargoHash: `GROUP-HASH-${customerId}`, externalFreightHash: 'FREIGHT', feeCoverageCargoHash: 'COVERAGE',
requiredServices: ['DELIVERY_FEE'], feeSources: [], cargo: [{ customerId, packages: 1 }], blockReasons: [] })) }
}
test('native arrangement carries frozen group and cost hashes, preserves all customer outbounds, and replays the exact original command', async () => {
for (const recordVersion of ['0', '9007199254740993']) {
const preview = nativeArrangementPreview(recordVersion)
let original, result
const context = contextFor((method, path, body) => {
if (method === 'GET') { assert.match(path, /delivery-arrangement$/); return ok(preview) }
assert.match(path, /transfer-delivery$/)
if (original) { assert.deepEqual(body, original); return ok({ ...result, replayed: true }) }
original = structuredClone(body)
assert.equal(body.recordVersion, recordVersion)
assert.equal(body.cargoHash, 'CARGO'); assert.equal(body.loadingCompletionHash, 'LOAD')
assert.deepEqual(body.groups.map(row => row.groupKey), ['GROUP-100', 'GROUP-200'])
assert.ok(body.groups.every(row => row.externalFreightHash === 'FREIGHT' && row.feeCoverageCargoHash === 'COVERAGE'))
result = { dispatchNo: 'DS-1', status: 'ACTIVE', stage: 'AFTER_LOADING', recordVersion: '9007199254740994',
result: { dispatchNo: 'DS-1', cargoHash: 'CARGO', outbounds: ['100', '200'].map(customerId => ({
customerId, outboundNo: `OB-${customerId}`, status: 'WAIT_OUTBOUND', packageCount: 1 })),
deliveries: ['100', '200'].map(customerId => ({ customerId, outboundNo: `OB-${customerId}`,
deliveryNo: `DL-${customerId}`, destination: { key: 'FBA:ONT8', warehouseCode: 'ONT8' }, status: 'WAIT_DELIVERY', packageCount: 1 })) } }
return ok(result)
})
const arranged = await arrangeDispatchDelivery(context, 'DS-1')
assert.deepEqual(arranged.outbounds.map(row => row.outboundNo), ['OB-100', 'OB-200'])
assert.equal(context.calls.filter(call => call.method === 'POST').length, 2)
assert.deepEqual(context.assertions.filter(row => !row.passed), [])
}
})
test('arrangement cannot run before loading or with missing hashes, groups or server gate', async () => {
for (const mutate of [value => { value.stage = 'LOADING' }, value => { value.status = 'CANCELLED' },
value => { value.cargoHash = '' }, value => { value.loadingCompletionHash = '' },
value => { value.groups = [] }, value => { value.arrange.allowed = false },
value => { value.groups[0].blockReasons = ['MISSING_FEE'] }]) {
const preview = nativeArrangementPreview(); mutate(preview)
const context = contextFor((method) => { assert.equal(method, 'GET'); return ok(preview) })
assert.equal(await arrangeDispatchDelivery(context, 'DS-1'), null)
assert.ok(context.assertions.some(row => !row.passed))
}
})
for (const [name, outcome] of [
['transport', { http: 0, body: { code: '' } }],
['infrastructure', { http: 503, body: { code: 'S0503' } }],
['malformed success', { http: 200, body: { code: '200', data: null } }],
['missing business response', { http: 200, body: {} }],
]) test(`write intent persists and ${name} stops commands and checkpoint restarts`, async () => {
const dir = await mkdtemp(join(tmpdir(), 'phase5-contract-'))
try {
const context = contextFor(async (method, path, body) => {
const saved = JSON.parse(await readFile(context.createdResources.phase5OverseasFulfillment.checkpointPath, 'utf8'))
assert.equal(saved.requests.at(-1).status, 'SENDING')
assert.equal(saved.requests.at(-1).body.recordVersion, '0')
assert.equal(saved.requests.at(-1).body.idempotencyKey, body.idempotencyKey)
assert.equal(JSON.stringify(saved).includes('SECRET-CREDENTIAL'), false)
return { ...outcome, requestId: 'REQ-UNKNOWN', traceId: 'TRACE-UNKNOWN' }
})
context.options.output = join(dir, 'report.json')
context.createdResources.phase5OverseasFulfillment = {}
const scoped = await fulfillmentEvidenceContext(context)
const body = { recordVersion: '0', idempotencyKey: 'ORIGINAL-COMMAND', password: 'SECRET-CREDENTIAL' }
await assert.rejects(scoped.request('POST', '/api/platform/v1/last-leg/dispatches/DS-1/depart', body), /结果未知/)
await assert.rejects(scoped.request('POST', '/api/platform/v1/last-leg/dispatches/DS-1/depart', { ...body, idempotencyKey: 'NEW-KEY' }), /未知写结果/)
assert.equal(context.calls.length, 1)
const saved = JSON.parse(await readFile(context.createdResources.phase5OverseasFulfillment.checkpointPath, 'utf8'))
assert.equal(saved.status, 'UNKNOWN'); assert.equal(saved.requests[0].status, 'UNKNOWN')
assert.equal(saved.requests[0].requestId, 'REQ-UNKNOWN')
await assert.rejects(fulfillmentEvidenceContext(context), /已存在本轮履约检查点/)
} finally { await rm(dir, { recursive: true, force: true }) }
})
test('default normal completion returns before exception writes; explicit legacy return stays after its original receipt guards and before closure', () => {
const run = source.slice(source.indexOf('export async function run'))
const start = run.indexOf('if (context.context?.phase5TailVerification?.runReturnScenario !== true)')
const exception = run.indexOf('const manualExceptionBody')
assert.ok(start > run.indexOf("savedVisit.data?.visit?.status !== 'COMPLETED'"))
const normalBranch = run.slice(start, exception)
assert.ok(normalBranch.includes("finalStatus = 'DELIVERED_WITH_COMPLETED_VISIT'"))
assert.match(normalBranch, /await finishFleetTrip/)
assert.match(normalBranch, /return\s*}/)
assert.doesNotMatch(normalBranch, /createDeliveryReturn|completeReturnedException|exception-orders|mark-return/)
const explicitBranch = run.slice(exception)
assert.ok(explicitBranch.indexOf('const returnReceipt = await completeReturnedException') < explicitBranch.indexOf('await finishFleetTrip'))
assert.match(source, /const sourceBalanceIds = owned.inventoryProof\?.sourceBalanceIds/)
assert.match(source, /原returnSource位置必须存在于正式排车冻结真实余额中/)
})