feat(dispatch): unify whole vehicle scheduling, source ownership and staged execution
This commit is contained in:
@@ -3,7 +3,7 @@ import test from 'node:test'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { fulfillmentHistorySnapshot, validTwoFragmentTree, validSignatureGate,
|
||||
completionCounts, exactCompletionOutboxProof, rootCompletionTimeMatches, resolveF40MerchantCredentials,
|
||||
f40TransferBody, f40LocalDateTime, validFragmentCabinetIdentity, validUnloadedRemainder,
|
||||
f40CreateDispatchBody, resolveF40LoadingMeasurements, sendFragment, f40LocalDateTime, validFragmentCabinetIdentity, validUnloadedRemainder,
|
||||
validRemainderCancellationGate, exactCancelledRemainderOutboxProof, cancelUnloadedRemainder,
|
||||
verifyRootProjections, verifyRemainderTaxIdentity, validFreshReceiptSave, preserveReceivingEvidence,
|
||||
resolveF40ReceivingWarehouse, run } from './phase5-root-fulfillment.mjs'
|
||||
@@ -153,24 +153,52 @@ test('F40 retains partial and failed receiving raw snapshots without exporting i
|
||||
assert.match(source,/proof.receiptReplayProofs \|\|= \[\]/)
|
||||
})
|
||||
|
||||
test('F40 transfer uses the current formal Transfer DTO including required positive fee and same LocalDateTime tool',() => {
|
||||
const dto=readFileSync(new URL('../../lastleg-service/src/main/java/com/baiyue/lastleg/delivery/dto/DeliveryRequests.java',import.meta.url),'utf8')
|
||||
const vehicle={carrierName:'offline-fleet',vehiclePlate:'offline-plate',driverName:'offline-driver',driverPhone:'19900000000'}
|
||||
const now=new Date('2026-09-30T01:02:03.456Z'),body=f40TransferBody('offline',1,vehicle,now)
|
||||
for(const field of ['idempotencyKey','plannedDeliveryAt','deliveryMethod','pickupMethod','loadingType','fees']) {
|
||||
assert.match(dto,new RegExp(`\\b${field}\\b`));assert.ok(body[field])
|
||||
}
|
||||
assert.equal(body.plannedDeliveryAt,'2026-10-02T01:02:03')
|
||||
assert.equal(f40LocalDateTime(now),'2026-09-30T01:02:03')
|
||||
assert.equal(body.deliveryMethod,'TRUCK_PALLET');assert.equal(body.pickupMethod,'WAREHOUSE_PICKUP')
|
||||
assert.equal(body.loadingType,'PALLET');assert.equal(body.vehicleSize,'15托')
|
||||
for(const field of Object.keys(vehicle))assert.equal(body[field],vehicle[field])
|
||||
assert.equal(body.fees.length,1)
|
||||
assert.deepEqual(Object.fromEntries(['feeType','feeName','direction','currency','amount'].map(field=>[field,body.fees[0][field]])),
|
||||
{feeType:'DELIVERY_FEE',feeName:'派送费',direction:'RECEIVABLE',currency:'USD',amount:10})
|
||||
assert.match(source,/f40TransferBody\(context.marker,index,vehicle\)/)
|
||||
assert.match(source,/estimatedArrivalAt: f40LocalDateTime\(/)
|
||||
function nativeF40Candidate() {
|
||||
const sourceVersion = 'a'.repeat(64)
|
||||
return { sourceKey: 'PLAN:9007199254740993', sourceVersion, customerId: '90', warehouseId: '80',
|
||||
inboundCode: 'IN-F40', availablePackages: '1', destination: { key: 'FBA:ONT8', type: 'FBA', warehouseCode: 'ONT8' },
|
||||
selection: { sourceType: 'INBOUND_PLAN', planPalletId: '9007199254740993', warehousePalletId: '9007199254740994',
|
||||
returnReceiptId: null, relabelNo: null, sourceVersion }, blockReasons: [], appointmentBatches: [{
|
||||
batchKey: 'FBA_APPOINTMENT:9007199254740995', customerId: '90', recordVersion: '9007199254740996',
|
||||
sources: [{ sourceKey: 'ORDER:9007199254740997', destinationKey: 'FBA:ONT8', packageCount: 1 }] }] }
|
||||
}
|
||||
|
||||
test('F40 native Create uses frozen candidate source and exact FBA batch version including zero, without legacy vehicle/Transfer fields', () => {
|
||||
const candidate = nativeF40Candidate()
|
||||
const body = f40CreateDispatchBody('offline', '80', '90', [candidate])
|
||||
assert.equal(body.warehouseId, '80')
|
||||
assert.equal(body.cargo[0].planPalletId, '9007199254740993')
|
||||
assert.equal(body.cargo[0].warehousePalletId, '9007199254740994')
|
||||
assert.equal(body.cargo[0].sourceVersion, candidate.sourceVersion)
|
||||
assert.deepEqual(body.batchAllocations, [{ planPalletId: '9007199254740993', batchKey: 'FBA_APPOINTMENT:9007199254740995',
|
||||
customerId: '90', sourceKey: 'ORDER:9007199254740997', destinationKey: 'FBA:ONT8', packageCount: 1, recordVersion: '9007199254740996' }])
|
||||
candidate.appointmentBatches[0].recordVersion = '0'
|
||||
assert.equal(f40CreateDispatchBody('offline', '80', '90', [candidate]).batchAllocations[0].recordVersion, '0')
|
||||
assert.equal(body.vehicle, null); assert.equal(body.driver, null)
|
||||
assert.equal('vehiclePlate' in body, false); assert.equal('plannedDeliveryAt' in body, false)
|
||||
assert.equal(f40LocalDateTime(new Date('2026-09-30T01:02:03.456Z')), '2026-09-30T01:02:03')
|
||||
const dto = readFileSync(new URL('../../lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchCommands.java', import.meta.url), 'utf8')
|
||||
assert.match(dto, /record Create/)
|
||||
for (const field of ['warehouseId', 'cargo', 'batchAllocations', 'sourceVersion']) assert.ok(dto.includes(field), field)
|
||||
})
|
||||
|
||||
test('F40 native Create rejects foreign/ambiguous sources and incomplete batch allocation instead of guessing source or version', () => {
|
||||
for (const mutate of [value => { value.customerId = '91' }, value => { value.warehouseId = '81' },
|
||||
value => { value.sourceVersion = 'stale' }, value => { value.selection.sourceVersion = 'stale' },
|
||||
value => { value.selection.planPalletId = 9007199254740992 }, value => { value.availablePackages = '9007199254740993' },
|
||||
value => { value.blockReasons = ['NOT_RECEIVED'] }, value => { value.appointmentBatches[0].recordVersion = null },
|
||||
value => { value.appointmentBatches[0].recordVersion = 9007199254740992 },
|
||||
value => { value.appointmentBatches[0].customerId = '91' },
|
||||
value => { value.appointmentBatches[0].sources[0].packageCount = 2 },
|
||||
value => { value.appointmentBatches[0].sources[0].destinationKey = 'FBA:OTHER' },
|
||||
value => { value.appointmentBatches.push(structuredClone(value.appointmentBatches[0])) }]) {
|
||||
const candidate = nativeF40Candidate(); mutate(candidate)
|
||||
assert.throws(() => f40CreateDispatchBody('offline', '80', '90', [candidate]))
|
||||
}
|
||||
const candidate = nativeF40Candidate()
|
||||
assert.throws(() => f40CreateDispatchBody('offline', '80', '90', [candidate, candidate]))
|
||||
})
|
||||
|
||||
test('F40 credentials prefer a complete current fixture pair and never combine fixture with configured customer',() => {
|
||||
const options={username:'offline-admin',password:'offline-admin-secret',merchantUsername:'configured-customer',merchantPassword:'configured-secret'}
|
||||
const current={username:'fixture-customer',password:'fixture-secret',tenantId:'90'}
|
||||
@@ -537,3 +565,130 @@ test('early F40 failures become FAILED with only a safe stage and preserve the o
|
||||
assert.equal(withLogin.createdResources.phase5RootFulfillment.finalStatus,'FAILED')
|
||||
assert.equal(JSON.stringify(withLogin.createdResources.phase5RootFulfillment).includes(secretError.message),false)
|
||||
})
|
||||
|
||||
|
||||
function nativeFragmentFixture() {
|
||||
const context = fixture(), events = [], calls = []
|
||||
const producer = { tenantId: '90', inboundCode: 'IN-F40', sourceInboundId: '901', sourceOrderId: '100',
|
||||
rootOrderId: '100', containerBookingId: '902' }
|
||||
const rootProof = { fragments: [producer], currentStage: 'DISPATCH_AND_SIGNATURE_1' }
|
||||
context.context = { f40BaseMarker: 'offline-F40', f40RootFulfillmentProof: rootProof }
|
||||
const measurement = { environment: 'TEST', marker: 'offline-F40-SEND-0', dispatchNo: 'DS-F40', pallets: [] }
|
||||
context.context.f40LoadingMeasurements = { 'DS-F40': measurement }
|
||||
context.checkpoint = async () => { events.push('checkpoint') }
|
||||
const delivery = { customerId: '90', deliveryNo: 'DL-F40', outboundNo: 'OB-F40', packageCount: '1',
|
||||
destination: { key: 'FBA:ONT8', warehouseCode: 'ONT8' } }
|
||||
let created, departed, signed = false
|
||||
context.request = async (method, path, body, token) => {
|
||||
calls.push({ method, path, body: structuredClone(body), token })
|
||||
let data
|
||||
if (method === 'GET' && path.includes('/candidates?')) {
|
||||
assert.equal(new URL(path, 'https://offline.invalid').searchParams.get('warehouseId'), '80')
|
||||
assert.equal(new URL(path, 'https://offline.invalid').searchParams.has('sourceStage'), false)
|
||||
data = { complete: true, candidates: [nativeF40Candidate()] }
|
||||
} else if (method === 'POST' && path.endsWith('/dispatches')) {
|
||||
events.push('create')
|
||||
if (created) data = { ...created, replayed: true }
|
||||
else data = created = { dispatchNo: 'DS-F40', status: 'ACTIVE', stage: 'BEFORE_LOADING', recordVersion: '0',
|
||||
result: { dispatchNo: 'DS-F40', changedItemIds: ['9007199254740998'] } }
|
||||
} else if (method === 'POST' && path.endsWith('/depart')) {
|
||||
events.push('depart')
|
||||
assert.equal(token, 'pda-memory')
|
||||
assert.equal(body.recordVersion, '9007199254740999')
|
||||
if (departed) {
|
||||
assert.deepEqual(body, calls.find(call => call.path.endsWith('/depart')).body)
|
||||
data = { ...departed, replayed: true }
|
||||
} else data = departed = { dispatchNo: 'DS-F40', status: 'ACTIVE', stage: 'DELIVERING', recordVersion: '9007199254741000',
|
||||
result: { shippedPackages: '1', deliveries: [delivery], outbounds: [{ customerId: '90', outboundNo: 'OB-F40' }] } }
|
||||
} else if (method === 'GET' && path.includes('/delivery-orders/')) {
|
||||
data = { order: { id: '9007199254741001', customerId: '90', outboundNo: 'OB-F40', deliveryNo: 'DL-F40',
|
||||
version: '9007199254741002', status: departed ? 'DELIVERING' : 'WAIT_DELIVERY' }, skus: [{ inboundCode: 'IN-F40', packageCount: 1 }] }
|
||||
} else if (method === 'GET' && path.endsWith('/outbound-orders/OB-F40')) {
|
||||
data = { outbound: { customerId: '90', outboundNo: 'OB-F40', status: 'OUTBOUNDED' }, pallets: [{ inboundCode: 'IN-F40' }] }
|
||||
} else if (method === 'GET' && path.endsWith('/dispatches/DS-F40')) {
|
||||
assert.equal(signed, true); events.push('normal-completed')
|
||||
data = { dispatchNo: 'DS-F40', status: 'ACTIVE', stage: 'DELIVERED', recordVersion: '9007199254741004' }
|
||||
} else throw new Error(`Unexpected offline wire ${method} ${path}`)
|
||||
return { http: 200, body: { code: '200', data } }
|
||||
}
|
||||
const deps = {
|
||||
async fulfillmentEvidenceContext(current) { events.push('evidence'); return current },
|
||||
async prepareFleetTrip(current, module, dispatchNo, warehouseId) {
|
||||
events.push('prepare-review'); assert.equal(dispatchNo, 'DS-F40'); assert.equal(warehouseId, '80')
|
||||
return { dispatchNo, warehouseId, tripNo: 'TR-F40', recordVersion: '1' }
|
||||
},
|
||||
async pickAll(current, token, dispatchNo) { events.push('pick'); assert.equal(token, 'pda-memory'); assert.equal(dispatchNo, 'DS-F40'); return true },
|
||||
async confirmFleetLoading(current, module, proof, token, actual) {
|
||||
events.push('loading-complete'); assert.equal(token, 'pda-memory'); assert.equal(actual, measurement)
|
||||
if (!actual) throw new Error('实际测量缺少有效palletSpaces')
|
||||
return '9007199254740998'
|
||||
},
|
||||
async arrangeDispatchDelivery() { events.push('arrange'); return { deliveries: [delivery],
|
||||
outbounds: [{ customerId: '90', outboundNo: 'OB-F40', packageCount: '1' }], data: { result: { deliveries: [delivery] } } } },
|
||||
async verifyDepartureReadiness() { events.push('read-current-depart-version'); return { data: { recordVersion: '9007199254740999' } } },
|
||||
async saveReceiptAndComplete(current, actual, customerId, index) {
|
||||
events.push('receipt'); assert.equal(actual, delivery); assert.equal(customerId, '90'); assert.equal(index, 0)
|
||||
signed = true
|
||||
return { order: { customerId: '90', deliveryNo: 'DL-F40', status: 'DELIVERED', version: '9007199254741003', signedAt: '2026-10-19T01:00:00' } }
|
||||
},
|
||||
async finishFleetTrip() { events.push('finish'); return { status: 'VERIFIED' } },
|
||||
}
|
||||
return { context, producer, rootProof, deps, events, calls, measurement }
|
||||
}
|
||||
|
||||
test('F40 fragment uses native creation, reviews, real PDA scan, explicit loading/token/evidence, arrangement, refreshed departure version and normal signature', async () => {
|
||||
const f = nativeFragmentFixture()
|
||||
const result = await sendFragment(f.context, f.producer, { warehouseId: '80', token: 'pda-memory' }, 'merchant-memory', 0, f.deps)
|
||||
assert.equal(result.outboundNo, 'OB-F40'); assert.equal(result.receiptVersion, '9007199254741003')
|
||||
assert.equal(result.signedAt, '2026-10-19T01:00:00')
|
||||
assert.deepEqual(f.events.filter(event => event !== 'checkpoint'), ['evidence', 'create', 'create', 'prepare-review', 'pick',
|
||||
'loading-complete', 'arrange', 'read-current-depart-version', 'depart', 'depart', 'receipt', 'normal-completed', 'finish'])
|
||||
assert.equal(f.calls.some(call => /pre-arrange|\/status|\/api\/merchant\//.test(call.path)), false)
|
||||
assert.equal(f.calls.some(call => call.method === 'PUT'), false)
|
||||
assert.equal(f.rootProof.dispatches[0].stage, 'DELIVERED')
|
||||
assert.equal(f.rootProof.dispatches[0].departureReplay.replayed, true)
|
||||
assert.equal(f.context.steps.every(step => step.passed), true)
|
||||
})
|
||||
|
||||
test('F40 missing real measurement evidence stops before arranging, departing or signing while keeping the failed source proof', async () => {
|
||||
const f = nativeFragmentFixture()
|
||||
delete f.context.context.f40LoadingMeasurements
|
||||
f.deps.confirmFleetLoading = async (_context, _module, _proof, token, actual) => {
|
||||
assert.equal(token, 'pda-memory'); assert.equal(actual, undefined)
|
||||
throw new Error('实际测量缺少有效palletSpaces')
|
||||
}
|
||||
await assert.rejects(sendFragment(f.context, f.producer, { warehouseId: '80', token: 'pda-memory' }, 'merchant-memory', 0, f.deps), /实际测量缺少/)
|
||||
assert.equal(f.events.includes('arrange'), false)
|
||||
assert.equal(f.calls.some(call => call.path.endsWith('/depart')), false)
|
||||
assert.equal(f.events.includes('receipt'), false)
|
||||
assert.equal(f.rootProof.dispatches[0].dispatchNo, 'DS-F40')
|
||||
assert.equal(f.rootProof.dispatches[0].stage, 'FAILED')
|
||||
assert.equal(f.rootProof.dispatches[0].failedAction, 'LOADING_COMPLETE')
|
||||
assert.equal(f.events.at(-1), 'checkpoint')
|
||||
})
|
||||
|
||||
test('F40 measurement reader receives the exact current TEST source scope and preserves its returned evidence without inferred values', async () => {
|
||||
const f = nativeFragmentFixture(), response = { arbitrary: 'opaque-original-proof' }
|
||||
f.context.marker = 'offline-F40-SEND-0'
|
||||
f.context.context.f40LoadingMeasurements = async scope => {
|
||||
assert.deepEqual(scope, { environment: 'TEST', marker: 'offline-F40-SEND-0', dispatchNo: 'DS-F40',
|
||||
warehouseId: '80', sourceInboundId: '901', inboundCode: 'IN-F40' })
|
||||
return response
|
||||
}
|
||||
assert.equal(await resolveF40LoadingMeasurements(f.context, 'DS-F40', f.producer, '80'), response)
|
||||
})
|
||||
|
||||
test('F40 fresh receipt revision evidence preserves string versions above safe integer precision', () => {
|
||||
const oldVersion = '9007199254740993', newVersion = '9007199254740994'
|
||||
const documents = { version: oldVersion, uploadedCategoryCount: 1, categories: [{ category: 'RECEIPT', files: [{ fileId: '9007199254740995' }] }], replayed: false }
|
||||
const before = { documents, delivery: { order: { version: oldVersion, status: 'DELIVERED', signedAt: '2026-10-19T01:00:00' },
|
||||
operations: [{ id: '9007199254740996', operationNode: 'RECEIPT_AUTO_COMPLETE' }] }, revisions: [{ version: oldVersion }] }
|
||||
const after = structuredClone(before)
|
||||
after.documents.version = newVersion; after.delivery.order.version = newVersion
|
||||
after.delivery.operations.push({ id: '9007199254740997', operationNode: 'SAVE_DOCUMENTS', result: '保存派送资料:1/3类',
|
||||
operatedAt: '2026-10-19T01:01:00', operatorName: 'TEST' })
|
||||
after.revisions.push({ version: newVersion, savedAt: '2026-10-19T01:01:00', operatorId: 'platform', snapshot: after.documents })
|
||||
assert.equal(validFreshReceiptSave(before, after), true)
|
||||
after.documents.version = '9007199254740995'
|
||||
assert.equal(validFreshReceiptSave(before, after), false)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user