fix(dispatch): invalidate resource evidence and bind uploads to review rounds
This commit is contained in:
@@ -50,7 +50,7 @@ function model(fault = '') {
|
||||
}
|
||||
return response({...prior.result,replayed:true})
|
||||
}
|
||||
if (method === 'GET' && path.endsWith('/loading-documents')) return response({ dispatchNo: 'DS-ONE', stage: 'LOADING', complete: true, photos: LOADING_PHOTO_SLOTS.map((slot, index) => ({ slot, fileId: String(index + 1) })) })
|
||||
if (method === 'GET' && path.endsWith('/loading-documents')) return response({ dispatchNo: 'DS-ONE', stage: 'LOADING', reviewRound: '1', complete: true, photos: LOADING_PHOTO_SLOTS.map((slot, index) => ({ slot, fileId: String(index + 1) })) })
|
||||
if (path === '/api/platform/v1/last-leg/fleet/trips/TRIP') return response(trip)
|
||||
if (path === '/api/platform/v1/last-leg/dispatches/DS-ONE/items') return response({dispatchNo:'DS-ONE',recordVersion:nativeVersion,items:items()})
|
||||
if (path === '/api/platform/v1/last-leg/dispatches/DS-ONE') {
|
||||
|
||||
@@ -87,7 +87,7 @@ function fixture(fault = '', nativeMode = false) {
|
||||
if (method === 'GET' && normalized === `${BASE}/DS-OWN` && path.startsWith(PDA)) return response({ ...data, items: items.map((i, n) => ({ ...i, positions: [{ positionId: `6${n}`, availablePackages: i.scheduledPackages }] })) })
|
||||
if (method === 'GET' && normalized.endsWith('/items')) return response({ dispatchNo: data.dispatchNo, recordVersion: data.recordVersion, status: 'ACTIVE', stage: data.stage, items: cargo })
|
||||
if (method === 'GET' && normalized.endsWith('/route')) return response({ dispatchNo: data.dispatchNo, recordVersion: data.recordVersion, stops: tripProof.stops })
|
||||
if (method === 'GET' && path.endsWith('/loading-documents')) return response({ dispatchNo: 'DS-OWN', stage: 'LOADING', complete: true, photos: LOADING_PHOTO_SLOTS.map((slot, index) => ({ slot, fileId: String(index + 1) })) })
|
||||
if (method === 'GET' && path.endsWith('/loading-documents')) return response({ dispatchNo: 'DS-OWN', stage: 'LOADING', reviewRound: '1', complete: true, photos: LOADING_PHOTO_SLOTS.map((slot, index) => ({ slot, fileId: String(index + 1) })) })
|
||||
if (method === 'GET' && normalized.endsWith('/loading')) return response({ ...data, manifestHash: 'c'.repeat(64), loadingComplete: { allowed: true }, pallets: items.map((i, n) => ({ ...i, ...(fault === 'missing-measurements' ? {} : { palletSpaces: n === 2 ? 2 : 1, weightKg: n === 2 ? 37 : 10, volumeCbm: n === 2 ? 0.7 : 0.2, deliveryIndex: nativeMode || n === 2 ? 1 : n + 1,
|
||||
evidence: [['palletSpaces', n === 2 ? 2 : 1], ['weightKg', n === 2 ? 37 : 10], ['volumeCbm', n === 2 ? 0.7 : 0.2]].map(([field, value]) => ({ field, value, actual: true, sourceType: 'MEASURED', sourceBusinessNo: i.inboundCode, observedAt: '2026-10-19T01:00:00Z' })) }) })) })
|
||||
if (method === 'GET') { assert.ok(normalized === `${BASE}/DS-OWN` || normalized === `${BASE}/DS-FULL`, raw); return response(data) }
|
||||
|
||||
@@ -114,7 +114,7 @@ function fixture(fault = '') {
|
||||
if (method === 'GET' && normalized.endsWith('/items')) return envelope({ dispatchNo: next ? 'DP-NEXT' : 'DP-NEW', recordVersion: next ? 0 : recordVersion, status: 'ACTIVE', stage: next ? 'BEFORE_LOADING' : stage, items: next ? [] : [nativeItem()] })
|
||||
if (method === 'GET' && normalized.endsWith('/route')) return envelope({ dispatchNo: 'DP-NEW', recordVersion, stops })
|
||||
if (method === 'GET' && path.startsWith(PDA) && normalized.endsWith('/DP-NEW')) return envelope({ ...detail(false), items: [nativeItem()] })
|
||||
if (method === 'GET' && path.endsWith('/loading-documents')) return envelope({ dispatchNo: 'DP-NEW', stage: 'LOADING', complete: true, photos: LOADING_PHOTO_SLOTS.map((slot, index) => ({ slot, fileId: String(index + 1) })) })
|
||||
if (method === 'GET' && path.endsWith('/loading-documents')) return envelope({ dispatchNo: 'DP-NEW', stage: 'LOADING', reviewRound: '1', complete: true, photos: LOADING_PHOTO_SLOTS.map((slot, index) => ({ slot, fileId: String(index + 1) })) })
|
||||
if (method === 'GET' && normalized.endsWith('/loading')) return envelope({ ...detail(false), manifestHash: 'b'.repeat(64), loadingComplete: { allowed: true }, pallets: [{ warehousePalletId: '100', palletNo: context.palletCode, scheduledPackages: 2, pickedPackages: 2,
|
||||
deliveryIndex: 1, ...(fault === 'missing-measurements' ? {} : { palletSpaces: 1, weightKg: 37, volumeCbm: 0.7, evidence: [['palletSpaces', 2], ['weightKg', 37], ['volumeCbm', 0.7]].map(([field, value]) => ({ field, value, actual: true, sourceType: 'MEASURED', sourceBusinessNo: 'EX-NEW', observedAt: '2026-10-19T02:00:00Z' })) }) }] })
|
||||
if (method === 'GET' && normalized.endsWith('/delivery-arrangement')) return envelope({ ...detail(false), cargoHash: 'a'.repeat(64), loadingCompletionHash: 'b'.repeat(64), arrange: { allowed: true }, groups: [{ groupKey: 'GROUP-A', customerId: CUSTOMER, destination, groupCargoHash: 'c'.repeat(64), externalFreightHash: 'd'.repeat(64), feeCoverageCargoHash: 'c'.repeat(64), blockReasons: [], requiredServices: ['DELIVERY_FEE'], cargo: [{ ...nativeItem(), packages: 2 }] }] })
|
||||
|
||||
@@ -20,7 +20,7 @@ const receipt = () => ({ environment: 'TEST', marker: 'TEST-NATIVE', dispatchNo:
|
||||
|
||||
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,
|
||||
const state = { dispatchNo: 'DP-A', recordVersion: '9007199254740993', dispatchRecordVersion: '11', reviewRound: '1', stage: initialStage,
|
||||
complete: false, editable: true, photos: [{ slot: LOADING_PHOTO_SLOTS[0], fileId: 'existing' }] }
|
||||
let saved = false
|
||||
const context = { async checkpoint() { saved = true },
|
||||
@@ -28,7 +28,7 @@ for (const initialStage of ['LOADING', 'AFTER_LOADING']) test(`loading documents
|
||||
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.match(path, /idempotencyKey=[a-f0-9]{64}&reviewRound=1$/); 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}` })
|
||||
@@ -39,6 +39,7 @@ for (const initialStage of ['LOADING', 'AFTER_LOADING']) test(`loading documents
|
||||
assert.equal(method, 'PUT')
|
||||
if (replays.has(body.idempotencyKey)) return ok(replays.get(body.idempotencyKey))
|
||||
assert.equal(body.recordVersion, state.recordVersion)
|
||||
assert.equal(body.uploadKey, new URL(proof.loadingDocuments.uploads.at(-1).path, 'https://unused.test').searchParams.get('idempotencyKey'))
|
||||
state.photos.push({ slot: path.split('/').at(-1), fileId: body.fileId })
|
||||
state.recordVersion = String(BigInt(state.recordVersion) + 1n)
|
||||
state.complete = state.photos.length === 5
|
||||
@@ -54,7 +55,7 @@ for (const initialStage of ['LOADING', 'AFTER_LOADING']) test(`loading documents
|
||||
|
||||
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 state = { dispatchNo: 'DP-A', stage: 'LOADING', reviewRound: '1', 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) {
|
||||
@@ -127,7 +128,7 @@ 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('/loading-documents')) return ok({ dispatchNo: 'DP-A', stage: 'LOADING', reviewRound: '1', 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')
|
||||
|
||||
@@ -296,9 +296,10 @@ export async function ensureFleetLoadingDocuments(context, module, proof, token)
|
||||
const read = () => command(context, module, 'GET', path, undefined, '回查本车装车资料', token)
|
||||
let documents = await read()
|
||||
const initialStage = documents.stage
|
||||
const reviewRound = id(documents.reviewRound)
|
||||
const complete = value => LOADING_PHOTO_SLOTS.every(slot => value.photos?.some(photo => photo.slot === slot && id(photo.fileId)))
|
||||
const validate = value => {
|
||||
if (value.dispatchNo !== proof.dispatchNo || !['LOADING', 'AFTER_LOADING', 'DELIVERING'].includes(value.stage)
|
||||
if (value.dispatchNo !== proof.dispatchNo || !/^[1-9]\d*$/.test(reviewRound) || id(value.reviewRound) !== reviewRound || !['LOADING', 'AFTER_LOADING', 'DELIVERING'].includes(value.stage)
|
||||
|| value.complete !== complete(value)) throw Error('本车装车资料身份、阶段或五框完整性不匹配')
|
||||
}
|
||||
validate(documents)
|
||||
@@ -307,7 +308,8 @@ export async function ensureFleetLoadingDocuments(context, module, proof, token)
|
||||
if (documents.photos?.some(photo => photo.slot === slot && id(photo.fileId))) continue
|
||||
if (!documents.editable || typeof context.requestMultipart !== 'function') throw Error('缺少本车装车照片写入资格或上传能力')
|
||||
const content = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aH1kAAAAASUVORK5CYII=', 'base64')
|
||||
const upload = { slot, path: `${path}/${slot}/uploads?idempotencyKey=${key(proof, `PHOTO_UPLOAD_${slot}`)}`,
|
||||
const uploadKey = key(proof, `PHOTO_UPLOAD_${slot}`)
|
||||
const upload = { slot, path: `${path}/${slot}/uploads?idempotencyKey=${uploadKey}&reviewRound=${reviewRound}`,
|
||||
name: `${proof.marker}-TEST-${slot}.png`, sha256: createHash('sha256').update(content).digest('hex'), status: 'PREPARED' }
|
||||
if (proof.loadingDocuments.uploads.some(item => item.slot === slot)) throw Error('该照片上传已开始,保留原键和结果排查,不自动重发')
|
||||
proof.loadingDocuments.uploads.push(upload); await context.checkpoint?.()
|
||||
@@ -319,7 +321,7 @@ export async function ensureFleetLoadingDocuments(context, module, proof, token)
|
||||
await context.checkpoint?.()
|
||||
const photo = context.apiSuccess(module, '上传本轮TEST合成装车照片', response, '', OPTIONS)
|
||||
if (response?.http !== 200 || String(response?.body?.code) !== '200' || photo?.slot !== slot || !id(photo.fileId)) throw Error('装车照片上传未确认,停止绑定及发车')
|
||||
const body = { recordVersion: documents.recordVersion, idempotencyKey: key(proof, `PHOTO_BIND_${slot}`), fileId: id(photo.fileId) }
|
||||
const body = { recordVersion: documents.recordVersion, idempotencyKey: key(proof, `PHOTO_BIND_${slot}`), fileId: id(photo.fileId), uploadKey }
|
||||
const bound = await command(context, module, 'PUT', `${path}/${slot}`, body, '保存本车装车照片', token)
|
||||
validate(bound)
|
||||
check(context, module, '绑定回执保留本槽文件', bound.photos.some(item => item.slot === slot && id(item.fileId) === id(photo.fileId)))
|
||||
|
||||
Reference in New Issue
Block a user