优化
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { LOADING_PHOTO_SLOTS, ensureFleetLoadingDocuments, reuseFleetResources } from './fleet-trip-fulfillment.mjs'
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { prepareFleetTrip, confirmFleetLoading, controlledPalletMeasurements, finishFleetTrip } from './fleet-trip-fulfillment.mjs'
|
||||
@@ -17,6 +18,69 @@ const receipt = () => ({ environment: 'TEST', marker: 'TEST-NATIVE', dispatchNo:
|
||||
{ warehousePalletId: PALLET, palletSpaces: 2, deliveryIndex: 1, evidence: [evidence('palletSpaces', 2)] },
|
||||
] })
|
||||
|
||||
for (const initialStage of ['LOADING', 'AFTER_LOADING']) test(`loading documents fill missing slots without replacing existing files from ${initialStage}`, async () => {
|
||||
const proof = { marker: 'TEST-DOCS', dispatchNo: 'DP-A' }, calls = [], replays = new Map()
|
||||
const state = { dispatchNo: 'DP-A', recordVersion: '9007199254740993', dispatchRecordVersion: '11', stage: initialStage,
|
||||
complete: false, editable: true, photos: [{ slot: LOADING_PHOTO_SLOTS[0], fileId: 'existing' }] }
|
||||
let saved = false
|
||||
const context = { async checkpoint() { saved = true },
|
||||
assert(_m, label, _r, passed) { assert.ok(passed, label) }, apiSuccess(_m, _l, response) { return response.body.data },
|
||||
async requestMultipart(method, path, file, token) {
|
||||
assert.ok(saved); saved = false; assert.equal(token, 'PDA-TEST'); assert.equal(method, 'POST')
|
||||
const slot = path.split('/').at(-2)
|
||||
assert.match(path, /idempotencyKey=[a-f0-9]{64}$/); assert.equal(file.type, 'image/png')
|
||||
assert.equal(file.content.subarray(1, 4).toString(), 'PNG'); calls.push(slot)
|
||||
assert.equal(proof.loadingDocuments.uploads.at(-1).status, 'PREPARED')
|
||||
return ok({ slot, fileId: `new-${slot}` })
|
||||
},
|
||||
async request(method, path, body, token) {
|
||||
assert.equal(token, 'PDA-TEST')
|
||||
if (method === 'GET') return ok(state)
|
||||
assert.equal(method, 'PUT')
|
||||
if (replays.has(body.idempotencyKey)) return ok(replays.get(body.idempotencyKey))
|
||||
assert.equal(body.recordVersion, state.recordVersion)
|
||||
state.photos.push({ slot: path.split('/').at(-1), fileId: body.fileId })
|
||||
state.recordVersion = String(BigInt(state.recordVersion) + 1n)
|
||||
state.complete = state.photos.length === 5
|
||||
if (state.complete && initialStage === 'AFTER_LOADING') state.stage = 'DELIVERING'
|
||||
replays.set(body.idempotencyKey, structuredClone(state)); return ok(state)
|
||||
} }
|
||||
await ensureFleetLoadingDocuments(context, 'docs', proof, 'PDA-TEST')
|
||||
assert.deepEqual(calls, LOADING_PHOTO_SLOTS.slice(1))
|
||||
assert.equal(state.photos[0].fileId, 'existing')
|
||||
assert.equal(state.stage, initialStage === 'LOADING' ? 'LOADING' : 'DELIVERING')
|
||||
assert.equal(proof.loadingDocuments.result.complete, true)
|
||||
})
|
||||
|
||||
for (const fault of ['checkpoint', 'upload-unknown', 'wrong-slot', 'replay-drift', 'early-departure']) test(`loading document ${fault} stops before loading confirmation`, async () => {
|
||||
const proof = { marker: 'TEST-DOCS', dispatchNo: 'DP-A' }, calls = []
|
||||
const state = { dispatchNo: 'DP-A', stage: 'LOADING', recordVersion: '1', complete: false, editable: true, photos: [] }
|
||||
const context = { async checkpoint() { if (fault === 'checkpoint') throw Error('checkpoint failed') },
|
||||
assert(_m, label, _r, passed) { assert.ok(passed, label) }, apiSuccess(_m, _l, response) { return response?.body?.data },
|
||||
async requestMultipart(_method, path) {
|
||||
calls.push(path)
|
||||
if (fault === 'upload-unknown') throw Error('unknown upload')
|
||||
return ok({ slot: fault === 'wrong-slot' ? 'RECEIPT' : 'LOADING_SCENE', fileId: 'new-file' })
|
||||
},
|
||||
async request(method, path) {
|
||||
calls.push(path)
|
||||
if (method === 'GET') return ok(state)
|
||||
if (state.photos.length && fault === 'replay-drift') return ok({ ...state, recordVersion: '3' })
|
||||
state.photos = [{ slot: 'LOADING_SCENE', fileId: 'new-file' }]
|
||||
if (fault === 'early-departure') state.stage = 'DELIVERING'
|
||||
return ok(state)
|
||||
} }
|
||||
await assert.rejects(ensureFleetLoadingDocuments(context, 'docs', proof, 'PDA-TEST'))
|
||||
assert.equal(calls.some(path => path.endsWith('/loading-complete')), false)
|
||||
if (fault === 'checkpoint') assert.equal(calls.length, 1)
|
||||
if (fault === 'upload-unknown') {
|
||||
assert.equal(proof.loadingDocuments.uploads[0].status, 'UNKNOWN')
|
||||
const sent = calls.filter(path => path.includes('/uploads?')).length
|
||||
await assert.rejects(ensureFleetLoadingDocuments(context, 'docs', proof, 'PDA-TEST'), /不自动重发/)
|
||||
assert.equal(calls.filter(path => path.includes('/uploads?')).length, sent)
|
||||
}
|
||||
})
|
||||
|
||||
test('real Jackson long counts accept safe decimal strings without changing pallet IDs', () => {
|
||||
const expected = controlledPalletMeasurements([pallet()], stops, receipt().pallets)
|
||||
for (const counts of [{ scheduledPackages: '3', pickedPackages: '3' }, { scheduledPackages: '3', pickedPackages: 3 }]) {
|
||||
@@ -37,7 +101,7 @@ function server(fault = '') {
|
||||
counts: { packageCount: 3, pickedPackageCount: 0, shippedPackageCount: 0 }, reviews: {}, publishLooseboardDemand: false,
|
||||
dispatchInfo: { estimatedArrivalAt: '2026-10-21T10:00:00Z', fbaAppointments: [{ appointmentNo: 'FBA-ORIGINAL' }] }, vehicleInfo: {} }
|
||||
const trip = { tripNo: 'TR-A', status: 'PLANNED', warehouseId: '92', recordVersion: 4, tripPurpose: 'DELIVERY', stops }
|
||||
let driverVersion = 0, uploadComplete = false, driverReviewed = false, nextPlate = '', checkpointError = false, nextTrip, nextDetail
|
||||
let driverVersion = 0, uploadComplete = false, driverReviewed = fault === 'fixed-master', nextPlate = fault === 'fixed-master' ? 'TEST-EXISTING' : '', checkpointError = false, nextTrip, nextDetail
|
||||
let mutatedItems = false, mutatedLoading = false
|
||||
const claims = new Map([['VEHICLE', 'TR-A'], ['DRIVER', 'TR-A']])
|
||||
const bump = () => detail.recordVersion = String(BigInt(detail.recordVersion) + 1n)
|
||||
@@ -57,6 +121,9 @@ function server(fault = '') {
|
||||
},
|
||||
async request(method, path, body, token) {
|
||||
calls.push({ method, path, body: structuredClone(body), actor: token ? 'PDA' : 'PLATFORM' })
|
||||
if (method === 'GET' && path.endsWith('/loading-documents')) return ok({ dispatchNo: 'DP-A', stage: 'LOADING', complete: true, photos: LOADING_PHOTO_SLOTS.map((slot, index) => ({ slot, fileId: String(index + 1) })) })
|
||||
if (method === 'GET' && path.endsWith(`/master-data/vehicles/${VID}`)) return ok({ id: VID, status: 'IN_SERVICE', region: 'OVERSEAS', ownership: 'OWNED' })
|
||||
if (method === 'GET' && path.includes(`/fleet/drivers/${DRIVER}?`)) return ok({ id: DRIVER, active: true, reviewStatus: 'APPROVED', details: { ownership: 'OWNED' }, qualification: { canDispatch: true, reason: 'ELIGIBLE' } })
|
||||
if (fault === 'unknown-edit' && method === 'PUT' && path === D) throw new Error('transport result unknown')
|
||||
if (path.endsWith('/master-data/vehicles')) {
|
||||
assert.equal(body.status, 'IN_SERVICE'); assert.match(body.remark, /TEST/); nextPlate = body.plateNumber.toUpperCase()
|
||||
@@ -232,6 +299,48 @@ test('native preparation edits existing master then submits both reviews, discov
|
||||
assert.ok(s.assertions.every(a => a.passed))
|
||||
})
|
||||
|
||||
test('fixed customer prepares a real trip without creating vehicles, drivers or uploading licenses', async () => {
|
||||
const s = server('fixed-master')
|
||||
s.context.options = { fixedCustomerId: '123', fixedFleetResources: [{ vehicleId: VID, driverId: DRIVER }] }
|
||||
const proof = await prepare(s)
|
||||
assert.equal(proof.reusedMasterData, true)
|
||||
assert.equal(proof.preparation, 'VERIFIED')
|
||||
assert.equal(proof.vehicleId, VID)
|
||||
assert.equal(proof.driverId, DRIVER)
|
||||
assert.equal(s.calls.some(call => call.file), false)
|
||||
assert.ok(s.calls.filter(call => call.method !== 'GET').every(call => call.path.startsWith(D)))
|
||||
})
|
||||
|
||||
test('fixed fleet resources block missing, locally occupied and ineligible pairs without master writes', async () => {
|
||||
const pair = { vehicleId: VID, driverId: DRIVER }
|
||||
for (const fault of ['missing', 'occupied', 'vehicle-status', 'driver-qualification']) {
|
||||
const proof = {}, calls = []
|
||||
const context = { options: { fixedFleetResources: fault === 'missing' ? [] : [pair] },
|
||||
createdResources: { fleetTrips: fault === 'occupied' ? [{ ...pair }] : [proof] },
|
||||
async request(method, path) {
|
||||
calls.push(path); assert.equal(method, 'GET')
|
||||
return path.includes('/vehicles/') ? ok({ id: VID, status: fault === 'vehicle-status' ? 'DISPATCHED' : 'IN_SERVICE', region: 'OVERSEAS', ownership: 'OWNED' })
|
||||
: ok({ id: DRIVER, active: true, reviewStatus: 'APPROVED', details: { ownership: 'OWNED' }, qualification: { canDispatch: fault !== 'driver-qualification', reason: 'ELIGIBLE' } })
|
||||
}, apiSuccess(_m, _l, response) { return response.body.data } }
|
||||
await assert.rejects(reuseFleetResources(context, 'test', proof), /固定车辆司机/)
|
||||
if (['missing', 'occupied'].includes(fault)) assert.equal(calls.length, 0)
|
||||
}
|
||||
})
|
||||
|
||||
test('verified resource closure makes a fixed pair available again', async () => {
|
||||
const proof = {}
|
||||
const context = { options: { fixedFleetResources: [{ vehicleId: VID, driverId: DRIVER }] },
|
||||
createdResources: { fleetTrips: [{ vehicleId: VID, driverId: DRIVER, closure: { status: 'CARGO_COMPLETED_AND_RESOURCES_RELEASED' } }, proof] },
|
||||
async request(method, path) {
|
||||
assert.equal(method, 'GET')
|
||||
return path.includes('/vehicles/') ? ok({ id: VID, status: 'IN_SERVICE', region: 'OVERSEAS', ownership: 'OWNED' })
|
||||
: ok({ id: DRIVER, active: true, reviewStatus: 'APPROVED', details: { ownership: 'OWNED' }, qualification: { canDispatch: true, reason: 'ELIGIBLE' } })
|
||||
}, apiSuccess(_m, _l, response) { return response.body.data } }
|
||||
await reuseFleetResources(context, 'test', proof)
|
||||
assert.equal(proof.vehicleId, VID)
|
||||
assert.equal(proof.driverId, DRIVER)
|
||||
})
|
||||
|
||||
test('zero-scan LOADING must explicitly return to review before editing and pass a new review round', async () => {
|
||||
const s = server(); s.detail.stage = 'LOADING'; s.detail.reviewState = 'APPROVED'
|
||||
const proof = await prepare(s)
|
||||
@@ -253,7 +362,7 @@ test('authoritative uppercase vehicle plate discovers both the prepared and subs
|
||||
assert.equal(proof.tripNo, 'TR-A')
|
||||
s.trip.status = 'IN_TRANSIT'
|
||||
const closure = await finishFleetTrip(s.context, 'native', proof)
|
||||
assert.equal(closure.nextDispatch.marker, marker); assert.equal(closure.nextDispatch.vehicle, undefined)
|
||||
assert.equal(closure.nextDispatch.marker, marker); assert.deepEqual(closure.nextDispatch.vehicle, proof.vehicle)
|
||||
assert.equal(closure.nextDispatch.tripNo, 'TR-NEXT'); assert.equal(closure.nextDispatch.finalStatus, 'CANCELLED')
|
||||
const queries = s.calls.filter(c => c.path.startsWith('/api/platform/v1/last-leg/fleet/trips?'))
|
||||
assert.equal(queries.length, 2)
|
||||
|
||||
Reference in New Issue
Block a user