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
+391 -289
View File
@@ -6,7 +6,8 @@ import { AsyncLocalStorage } from 'node:async_hooks'
import { AcceptanceRun, parseArguments, validateExecutionOptions, buildAcceptanceSummary } from './admin-test-env-acceptance.mjs'
import { redactEvidence } from './fba-core-regression-test-env.mjs'
import { createTestEvidenceReaders, buildEvidenceRemoteScript, validateEvidenceConfig } from './fba-core-regression-evidence.mjs'
import { prepareFleetTrip, confirmFleetLoading, finishFleetTrip, controlledPalletMeasurements } from './admin-test-env-modules/fleet-trip-fulfillment.mjs'
import { prepareFleetTrip, finishFleetTrip } from './admin-test-env-modules/fleet-trip-fulfillment.mjs'
import { createBody, candidatePage, markerCandidates, dispatchIo, readDispatch, version as dispatchVersion, replay as replayDispatch } from './admin-test-env-modules/grouped-dispatch-acceptance.mjs'
import { verifyFleetBookingReservation } from './admin-test-env-modules/fleet-booking-reservation-acceptance.mjs'
import { verifyFleetFeeCoverage } from './admin-test-env-modules/fleet-fee-coverage-acceptance.mjs'
import { verifyFleetExternalReceipt } from './admin-test-env-modules/fleet-external-receipt-acceptance.mjs'
@@ -14,8 +15,8 @@ import { verifyFleetExternalDispatch } from './admin-test-env-modules/fleet-exte
import { verifyFleetHandover, cleanupFleetHandoverResources } from './admin-test-env-modules/fleet-handover-acceptance.mjs'
import { verifyFleetTripCosts } from './admin-test-env-modules/fleet-trip-cost-acceptance.mjs'
import { verifyFleetReturnRedispatch, waitForRedispatchSignatureAudit, selectReturnSources } from './admin-test-env-modules/fleet-return-redispatch-acceptance.mjs'
import { resolvePickerAccess, createPicker, disablePicker } from './admin-test-env-modules/phase4-overseas-pda.mjs'
import { pickAll, saveReceiptAndComplete } from './admin-test-env-modules/phase5-overseas-fulfillment.mjs'
import { resolvePickerAccess, createPicker, disablePicker, pickerFixtureSuffix } from './admin-test-env-modules/phase4-overseas-pda.mjs'
import { saveReceiptAndComplete } from './admin-test-env-modules/phase5-overseas-fulfillment.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const MODULE = 'fleet-specialized-acceptance'
@@ -23,10 +24,55 @@ const OPTIONS = { scenario: MODULE, deploymentBlocker: true }
const id = value => value == null ? '' : String(value)
const success = response => response?.http === 200 && id(response?.body?.code) === '200'
const key = () => `fleet-${randomUUID()}`
const NATIVE_PROTOCOL = 'NATIVE_DISPATCH_V1'
const nativeDeparture = (value, dispatchNo) => value?.action === 'DEPART' && value.dispatchNo === dispatchNo
&& value.status === 'ACTIVE' && value.stage === 'DELIVERING' && Number.isFinite(Date.parse(value.result?.departedAt))
&& Number.isSafeInteger(Number(value.result.shippedPackages)) && Number(value.result.shippedPackages) > 0
const instant = value => typeof value === 'string' && /^\d{4}-\d\d-\d\dT\d\d:\d\d(?::\d\d(?:\.\d+)?)?$/.test(value) ? `${value}Z` : value
const sameInstant = (a, b) => Number.isFinite(Date.parse(instant(a))) && Date.parse(instant(a)) === Date.parse(instant(b))
const deliveryFacts = rows => JSON.stringify(rows.map(row => [id(row.customerId), row.deliveryNo, row.outboundNo, Number(row.packageCount)]).sort())
export function nativeFleetDeliveries(detail) {
if (!detail?.dispatchNo || !Array.isArray(detail.documents?.deliveries) || !detail.documents.deliveries.length) throw Error('Native delivery references missing')
const rows = new Map(), owners = new Map()
for (const row of detail.documents.deliveries) {
if (!row.deliveryNo || !row.outboundNo || !id(row.customerId) || !Number.isSafeInteger(Number(row.packageCount)) || Number(row.packageCount) <= 0)
throw Error('Native delivery customer, business identity or quantity missing')
if (owners.has(row.deliveryNo) && owners.get(row.deliveryNo) !== id(row.customerId)) throw Error('Delivery cannot belong to two customers')
const identity = `${row.customerId}:${row.deliveryNo}`, prior = rows.get(identity)
if (prior && (prior.outboundNo !== row.outboundNo || Number(prior.packageCount) !== Number(row.packageCount) || prior.groupKey !== row.groupKey))
throw Error('Duplicate native delivery references disagree')
owners.set(row.deliveryNo, id(row.customerId)); rows.set(identity, row)
}
return [...rows.values()]
}
export function selectNativeFleetFullSource(context, original, candidates) {
const source = context.createdResources?.phase4OverseasDispatch
if (source?.marker !== context.marker || source.status !== 'VERIFIED' || original.data.dispatchNo !== source.dispatchNo
|| original.data.status !== 'ACTIVE' || original.data.stage !== 'BEFORE_LOADING' || Number(original.data.counts.pickedPackageCount) !== 0
|| Number(original.data.counts.shippedPackageCount) !== 0 || id(original.data.recordVersion) !== id(source.recordVersion)
|| original.items.items.length < 2 || new Set(original.items.items.map(i => i.destination?.key)).size !== 2)
throw Error('FULL/PARTIAL require the unchanged current-run two-destination native main')
const history = source.routeLifecycle?.stages?.find(s => s.label === '新增同目的地第二托')?.snapshot?.items
if (!history?.length) throw Error('Current-run released duplicate source evidence missing')
const own = original.items.items
const eligible = markerCandidates(context, candidates).filter(c => id(c.customerId) === id(source.customerId)
&& id(c.warehouseId) === id(original.warehouseId) && history.some(i => i.sourceKey === c.sourceKey
&& id(i.warehousePalletId) === id(c.selection.warehousePalletId))
&& own.every(i => i.sourceKey !== c.sourceKey && id(i.warehousePalletId) !== id(c.selection.warehousePalletId)))
if (eligible.length !== 1) throw Error('Need exactly one current-run released, free, non-overlapping FULL pallet; never borrow historical inventory')
return eligible[0]
}
/** Install before login/prerequisites. A caught unknown write must not permit another request. */
export function guardFleetWrites(context, checkpoint, { guardFetch = false } = {}) {
let stopped = null, sequence = 0
const stop = entry => {
context.createdResources ??= {}
context.createdResources.fleetWriteBoundary = { status: 'STOPPED', method: entry.method, path: entry.path,
outcome: entry.status, requestId: entry.requestId, traceId: entry.traceId }
}
const scope = new AsyncLocalStorage(), originalFetch = globalThis.fetch
const direct = { async requestWithBody(method, path, body, token, headers, request) {
const nativeResponse = await originalFetch(request), copy = nativeResponse.clone()
@@ -62,7 +108,7 @@ export function guardFleetWrites(context, checkpoint, { guardFetch = false } = {
? { field, value } : { field, name: value.name, type: value.type,
base64: Buffer.from(await value.arrayBuffer()).toString('base64') })
}
try { await checkpoint(entry) } catch (error) { stopped = error; throw error }
try { await checkpoint(entry) } catch (error) { entry.status = 'NOT_SENT'; stopped = error; stop(entry); throw error }
try {
const response = await scope.run(true, () => original.apply(this, arguments))
const http = response?.http, payload = response?.body, code = id(payload?.code)
@@ -79,7 +125,7 @@ export function guardFleetWrites(context, checkpoint, { guardFetch = false } = {
responseBody: redactFleetWrite(response?.body), finishedAt: new Date().toISOString() })
if (binary) entry.binary = { contentType: response.contentType, bytes: response.bytes.length,
sha256: createHash('sha256').update(response.bytes).digest('hex'), validPdf: Boolean(pdf) }
if (!definite) stopped = Error(`Unknown write outcome: ${method} ${path}; preserve original request, do not resend`)
if (!definite) { stopped = Error(`Unknown write outcome: ${method} ${path}; preserve original request, do not resend`); stop(entry) }
await checkpoint(entry)
if (stopped) throw stopped
return response
@@ -88,8 +134,9 @@ export function guardFleetWrites(context, checkpoint, { guardFetch = false } = {
if (entry.status === 'SENDING') {
entry.status = 'UNKNOWN_RESULT'; entry.finishedAt = new Date().toISOString()
entry.raw = boundedFleetDiagnostic(error instanceof Error ? error.message : String(error))
await checkpoint(entry)
stop(entry); await checkpoint(entry)
}
stop(entry)
throw stopped
}
}
@@ -171,7 +218,7 @@ export function fleetExecutionPlan(config) {
outputDir: config.outputDir || `reports/fleet-${config.runId}`, producer: config.resumeReport ? `Reviewed ${config.resumeStage} checkpoint in the same original fixture` : 'phase4-overseas-dispatch with its normal fresh prerequisites',
resumeReport: config.resumeReport || null, resumeStage: config.resumeStage || null, resumeSha256: config.resumeSha256 || null,
stages: config.resumeStage === 'RETURN_SOURCE_SELECTION_BEFORE_RETURN'
? ['Verify exact source-selection-failure report SHA256 and three definitive delivery starts', 'Read-only original DEPART/handover/cost/cargo/resources and DELIVERING versions',
? ['Verify exact source-selection-failure report SHA256 and three definitive native delivery reads', 'Read-only original DEPART/handover/cost/cargo/resources and DELIVERING versions',
'Recheck repaired A/B return candidates and complete source quantities before any write', 'Create fresh bound PDA session',
'Original sample actual return and redispatch; no repeated original departure/handover/cost/start-delivery',
'Wait for coordinator pre-signature A0/B1 event evidence gate', 'New signature and source ACK evidence; close new and original resources']
@@ -184,9 +231,9 @@ export function fleetExecutionPlan(config) {
afterCosts ? 'Match original verified cost snapshot; no cost commands' : 'Original trip operating costs with actual allocation availability',
'Retain original DEPART; no picking or departure commands',
'In-transit handover and PDA readback', 'Original delivery signatures and resource release', 'Close newly created test sessions']
: ['FULL original coverage', 'formal cancel/unbind/re-arrange', 'PARTIAL original coverage', 'reservation lifecycle',
'external actual receipt', 'external dispatch and agreed fee', 'Finance consumer persistence', 'each dispatch PDA picking',
'atomic whole-trip departure', 'actual in-transit handover and PDA readback', 'same-fixture optional H5 checkpoint',
: ['two disjoint current-run native sources', 'independent FULL loading/coverage/departure/POD/resource closure', 'PARTIAL original main and reservation lifecycle',
'external actual receipt', 'external dispatch and agreed fee', 'Finance consumer persistence', 'one native main PDA picking and explicit actual loading',
'native current-version whole-main departure and cross-end exact replay', 'actual in-transit handover and PDA readback', 'same-fixture optional H5 checkpoint',
'per-destination receipt and replacement-resource release', 'explicit handover-fixture cleanup'],
boundaries: 'TEST only; serial fresh fixtures; no SQL writes, deployment or configuration changes. Failure retains original requests/resources. No automatic rerun/resume.',
limits: 'Finance pool proves consumer persistence; producer ACK requires separate event evidence. UI and remaining fleet product scope are separate.' }
@@ -195,28 +242,33 @@ export function fleetExecutionPlan(config) {
export function reviewedFullCheckpoint(report, stage = 'FULL_COVERAGE') {
const resources = report?.createdResources, proof = resources?.fleetAcceptance, full = resources?.fleetFeeCoverageCases?.[0]
const trip = resources?.fleetTrips?.find(row => row.tripNo === proof?.tripNo)
if (proof?.protocol !== NATIVE_PROTOCOL || !proof.dispatchNo || !trip || trip.dispatchNo !== proof.dispatchNo
|| JSON.stringify(proof.dispatchNos) !== JSON.stringify([proof.dispatchNo]) || resources.dispatchWriteBoundary?.outcomeUnknown
|| resources.fleetWriteBoundary?.status === 'STOPPED'
|| (proof.departure && !nativeDeparture(proof.departure, proof.dispatchNo)))
throw Error('Only current native single-dispatch reports may resume; legacy or unknown outcomes require explicit reconciliation')
if (stage === 'RETURN_SOURCE_SELECTION_BEFORE_RETURN') {
const attempt = trip?.returnRedispatch, originals = attempt?.originalDeliveries, writes = attempt?.requests?.filter(row => row.method !== 'GET')
const failures = report?.steps?.filter(row => row.passed === false)
if (report?.success !== false || proof?.stage !== 'RETURN_REDISPATCH' || proof.status !== 'CHECKING'
|| proof.marker !== attempt?.marker || trip?.costAcceptance?.status !== 'VERIFIED' || trip.handover?.status !== 'VERIFIED'
|| proof.departure?.status !== 'IN_TRANSIT' || proof.departure.tripNo !== trip.tripNo
|| !nativeDeparture(proof.departure, proof.dispatchNo)
|| trip.closure || proof.closure || proof.receipts?.length || attempt.status !== 'FAILED'
|| attempt.failure !== 'Fresh redispatch fixture requires source A with at least 3 boxes and a separate domestic source B'
|| attempt.exceptionNo || attempt.sources || attempt.newTripNo || attempt.originalSignatures
|| originals?.length !== 3 || new Set(originals.map(row => row.deliveryNo)).size !== 3 || writes?.length !== 3
|| originals?.length !== 3 || new Set(originals.map(row => row.deliveryNo)).size !== 3 || writes?.length !== 0
|| originals.some(row => id(row.customerId) !== id(proof.customerId) || id(row.overseasWarehouseId) !== id(proof.warehouseId)
|| !proof.dispatchNos.includes(row.dispatchNo) || row.status !== 'WAIT_DELIVERY' || row.signedAt || Number(row.signedPackages || 0))
|| row.dispatchNo !== proof.dispatchNo || row.status !== 'DELIVERING' || row.signedAt || Number(row.signedPackages || 0))
|| attempt.requests.some(row => row.status !== 'RESPONDED' || row.http !== 200 || id(row.code) !== '200' || !row.requestId || !row.traceId)
|| writes.some(row => {
const original = originals.find(item => item.deliveryNo === row.result?.deliveryNo)
return !original || row.method !== 'PUT' || row.path !== `/api/platform/v1/last-leg/delivery-orders/${original.deliveryNo}/status?customerId=${proof.customerId}`
|| row.body?.status !== 'DELIVERING' || id(row.body.expectedVersion) !== id(original.version)
|| row.result.deliveryStatus !== 'DELIVERING' || row.result.replayed !== false
}) || new Set(writes.map(row => row.result?.deliveryNo)).size !== 3
|| attempt.requests.length !== 3 || attempt.requests.some(row => {
const original = originals.find(item => item.deliveryNo === row.result?.order?.deliveryNo)
return row.method !== 'GET' || Object.hasOwn(row.result || {}, 'replayed') || !original
|| row.path !== `/api/platform/v1/last-leg/delivery-orders/${original.deliveryNo}?customerId=${proof.customerId}`
|| JSON.stringify(row.result.order) !== JSON.stringify(original)
}) || new Set(attempt.requests.map(row => row.result?.order?.deliveryNo)).size !== 3
|| resources.fleetCostActors?.status !== 'DISABLED' || failures?.length !== 1 || failures[0].name !== '模块执行'
|| failures[0].evidence !== attempt.failure)
throw Error('Return source recovery requires the exact known candidate failure and three definitive delivery starts, with no return/signature writes')
throw Error('Return source recovery requires the exact native candidate failure, three already-delivering documents and no return/signature writes')
return { resources, marker: proof.marker, returnSourceSelection: true }
}
if (stage === 'DEPARTED_BEFORE_COSTS' || stage === 'DEPARTED_AFTER_COSTS_BEFORE_HANDOVER' || stage === 'DEPARTED_VEHICLE_CREATED') {
@@ -234,7 +286,7 @@ export function reviewedFullCheckpoint(report, stage = 'FULL_COVERAGE') {
|| creation[0].result.status !== 'IN_SERVICE' || handover.replacement?.driverId))
throw Error('Created vehicle recovery requires the exact definitive successful creation and no later handover write')
if (report?.success !== false || proof?.stage !== (afterCosts ? 'IN_TRANSIT_HANDOVER' : 'TRIP_OPERATING_COSTS') || proof.status !== 'CHECKING'
|| proof.marker !== costs?.marker || proof.departure?.status !== 'IN_TRANSIT' || proof.departure.tripNo !== trip?.tripNo
|| proof.marker !== costs?.marker || !nativeDeparture(proof.departure, proof.dispatchNo)
|| !proof.departureRequest?.idempotencyKey || !costs?.departure?.snapshot?.confirmedLoad?.manifestHash
|| costs.status !== (afterCosts ? 'VERIFIED' : 'FAILED') || !Array.isArray(costs.costs) || costs.costs.length !== (afterCosts ? 2 : 0) || !costs.requests?.length
|| costs.requests.some(row => row.status !== 'RESPONDED' || !row.requestId || !row.traceId
@@ -255,7 +307,7 @@ export function reviewedFullCheckpoint(report, stage = 'FULL_COVERAGE') {
if (report?.success !== false || proof?.stage !== 'RECEIPTS_AND_RESOURCE_RELEASE'
|| trip?.closure?.status !== 'CARGO_COMPLETED_AND_RESOURCES_RELEASED'
|| proof.closure?.status !== trip.closure.status || trip.externalReceipt?.status !== 'VERIFIED'
|| trip.externalDispatch?.status !== 'VERIFIED' || trip.reassignment?.status !== 'VERIFIED'
|| trip.externalDispatch?.status !== 'VERIFIED' || trip.externalDispatch.dispatchNo !== proof.dispatchNo || trip.reassignment?.status !== 'REJECTED_WITHOUT_MUTATION'
|| !proof.receipts?.length || proof.finance?.status !== 'CONSUMER_PERSISTED'
|| !failures?.length || failures.some(row => !['本次车次货物结案及车辆司机释放完成', '模块执行'].includes(row.name)))
throw Error('Terminal reconciliation requires completed original business proofs and only the known aggregate assertion failure')
@@ -265,7 +317,7 @@ export function reviewedFullCheckpoint(report, stage = 'FULL_COVERAGE') {
const confirmed = stage === 'EXTERNAL_RECEIPT_CONFIRMED'
const partial = resources?.fleetFeeCoverageCases?.find(row => row.mode === 'PARTIAL'), receipt = trip?.externalReceipt
if (report?.success !== false || proof?.stage !== 'EXTERNAL_RECEIPT' || proof.status !== 'CHECKING'
|| full?.proof?.status !== 'VERIFIED' || partial?.proof?.status !== 'VERIFIED' || partial.dispatchNo !== trip?.dispatchNo
|| full?.proof?.status !== 'VERIFIED' || proof.fullExecution?.closure?.status !== 'CARGO_COMPLETED_AND_RESOURCES_RELEASED' || partial?.proof || partial?.dispatchNo !== trip?.dispatchNo
|| trip.reservation?.status !== 'VERIFIED' || receipt?.status !== 'CHECKING' || trip.externalDispatch
|| (confirmed ? (!receipt.receiptRequest?.idempotencyKey || !receipt.requests?.some(row => row.label === '运营按实际1托位和新单价30确认实收' && row.method === 'POST' && row.http === 200 && String(row.code) === '200')) : receipt.receiptRequest)
|| !receipt.palletizeRequest?.idempotencyKey || !receipt.warehousePalletId || !proof.picker?.membershipId || !proof.pickerMarker
@@ -276,20 +328,29 @@ export function reviewedFullCheckpoint(report, stage = 'FULL_COVERAGE') {
if (stage === 'EXTERNAL_RECEIPT_READY') {
const partial = resources?.fleetFeeCoverageCases?.find(row => row.mode === 'PARTIAL')
if (report?.success !== false || proof?.stage !== 'RESERVATIONS' || proof.status !== 'CHECKING'
|| full?.proof?.status !== 'VERIFIED' || partial?.proof?.status !== 'VERIFIED' || partial.dispatchNo !== trip?.dispatchNo
|| full?.proof?.status !== 'VERIFIED' || proof.fullExecution?.closure?.status !== 'CARGO_COMPLETED_AND_RESOURCES_RELEASED' || partial?.proof || partial?.dispatchNo !== trip?.dispatchNo
|| trip.reservation?.status !== 'VERIFIED' || trip.externalReceipt || trip.externalDispatch || proof.picker
|| !report.steps?.some(row => row.name === '创建海外PDA仓库操作员71' && row.code === 'A0409' && row.message === '手机号已存在'))
throw Error('Receipt-ready checkpoint requires verified coverage/reservations and an explicitly rejected operator creation with no receipt writes')
return { resources, marker: proof.marker, receiptReady: true }
}
if (report?.success !== false || proof?.stage !== 'FULL_COVERAGE' || proof.status !== 'CHECKING' || full?.mode !== 'FULL'
|| full.marker !== proof.marker || full.dispatchNo !== trip?.dispatchNo || full.proof?.status !== 'CHECKING'
|| full.marker !== proof.marker || full.dispatchNo === trip?.dispatchNo || !resources.fleetTrips.some(t => t.dispatchNo === full.dispatchNo && t.tripNo === proof.fullTripNo)
|| !['CHECKING','UNPROVEN'].includes(full.proof?.status) || proof.fullExecution?.departureRequest
|| !full.proof.requests?.length
|| resources.fleetFeeCoverageCases.length !== 1 || !proof.originalDispatch || proof.originalDispatch.dispatchNo !== full.dispatchNo)
|| full.proof.requests.some(row => row.status !== 'RESPONDED' || !row.requestId || !row.traceId)
|| resources.fleetFeeCoverageCases.length !== 2 || !proof.originalDispatch?.detail || proof.originalDispatch.detail.dispatchNo !== proof.dispatchNo)
throw Error('Only a reviewed read-only FULL_COVERAGE failure can resume; mutated/unknown outcomes require case-specific reconciliation')
const persisted = stage === 'FULL_COVERAGE_PERSISTED'
if (persisted ? (!full.proof.outboundNo || !full.proof.deliveryNos?.length || !full.proof.confirmationRequest?.idempotencyKey
|| !full.proof.requests.some(row => row.label === '正式保存本次原费用服务覆盖并转派送' && row.method === 'POST' && row.http === 200 && String(row.code) === '200'))
|| !full.proof.confirmationRequest.groups?.length || full.proof.confirmationRequest.recordVersion == null
|| !full.proof.requests.some(row => row.method === 'POST' && row.path === `/api/platform/v1/last-leg/dispatches/${full.dispatchNo}/transfer-delivery`
&& row.http === 200 && String(row.code) === '200' && row.status === 'RESPONDED'
&& JSON.stringify(row.body) === JSON.stringify(full.proof.confirmationRequest)
&& row.result?.action === 'ARRANGE_DELIVERY' && row.result.dispatchNo === full.dispatchNo
&& row.result.status === 'ACTIVE' && row.result.stage === 'AFTER_LOADING'
&& row.result.result?.outbounds?.some(o => o.outboundNo === full.proof.outboundNo)
&& JSON.stringify(row.result.result?.deliveries?.map(d => d.deliveryNo).sort()) === JSON.stringify([...full.proof.deliveryNos].sort())))
: (full.proof.requests.some(row => row.method !== 'GET') || full.proof.outboundNo))
throw Error('Mutated/unknown outcomes require the exact successful command and persisted document reconciliation')
return { resources, marker: proof.marker, persisted }
@@ -298,58 +359,36 @@ export function reviewedFullCheckpoint(report, stage = 'FULL_COVERAGE') {
export async function verifyFleetTerminalState(context) {
const proof = context.createdResources.fleetAcceptance
const trip = context.createdResources.fleetTrips.find(row => row.tripNo === proof.tripNo)
const partial = context.createdResources.fleetFeeCoverageCases.find(row => row.mode === 'PARTIAL')
const evidence = proof.terminalReadback = { status: 'CHECKING', requests: [], deliveries: [], dispatches: [] }
function check(label, passed, response) {
context.assert(MODULE, label, response, Boolean(passed), '', OPTIONS)
if (!passed) throw Error(label)
}
async function get(path) {
const result = await context.request('GET', path)
evidence.requests.push({ path, http: result.http, code: result.body?.code, requestId: result.requestId, traceId: result.traceId })
check('终态只读回查成功', success(result) && result.requestId && result.traceId, result)
return result.body.data
}
check('结案助手已完成货物与资源核对', trip.closure?.status === 'CARGO_COMPLETED_AND_RESOURCES_RELEASED')
const current = await get(`/api/platform/v1/last-leg/fleet/trips/${encodeURIComponent(trip.tripNo)}`)
check('当前车次已完成且两项资源结束事实与原证明一致', current.trip?.status === 'COMPLETED'
&& current.trip.completedAt === trip.closure.finalTrip.completedAt
&& id(current.trip.recordVersion) === id(trip.closure.finalTrip.recordVersion)
&& JSON.stringify(current.trip.vehicleRelease) === JSON.stringify(trip.closure.finalTrip.vehicleRelease)
&& JSON.stringify(current.trip.driverRelease) === JSON.stringify(trip.closure.finalTrip.driverRelease)
&& current.trip.vehicleRelease && current.trip.driverRelease)
evidence.trip = current.trip
const expectedDispatches = [partial.dispatchNo, trip.externalDispatch.dispatchNo].sort()
check('终态车次仍关联全部原执行单', JSON.stringify(current.dispatches.map(row => row.dispatchNo).sort()) === JSON.stringify(expectedDispatches))
for (const number of expectedDispatches) {
const dispatch = await get(`/api/platform/v1/last-leg/dispatches/${encodeURIComponent(number)}`)
check('原执行单已发车且拣货实发数量守恒', dispatch.dispatchNo === number && dispatch.status === 'DEPARTED'
&& Number(dispatch.packageCount) === Number(dispatch.shippedPackageCount)
&& Number(dispatch.pickedPackageCount) === Number(dispatch.shippedPackageCount))
evidence.dispatches.push(dispatch)
}
check('整车最终箱数与原一次发车事实一致', evidence.dispatches.reduce((n, row) => n + Number(row.shippedPackageCount), 0) === Number(proof.departure.shippedPackages))
const expectedDeliveries = [...partial.proof.deliveryNos, trip.externalDispatch.deliveryNo].sort()
check('签收证明覆盖全部目的地派送', JSON.stringify(proof.receipts.map(row => row.deliveryNo).sort()) === JSON.stringify(expectedDeliveries))
for (const number of expectedDeliveries) {
const delivery = await get(`/api/platform/v1/last-leg/delivery-orders/${encodeURIComponent(number)}?customerId=${encodeURIComponent(proof.customerId)}`)
check('每张原派送保留完成及签收时间', delivery.order?.deliveryNo === number && delivery.order.status === 'DELIVERED'
&& delivery.order.signedAt === proof.receipts.find(row => row.deliveryNo === number).signedAt)
evidence.deliveries.push(delivery.order)
}
for (const probe of trip.closure.probes) {
const detail = await get(`/api/platform/v1/last-leg/fleet/trips/${encodeURIComponent(probe.tripNo)}`)
check('资源占用测试计划均已正式取消', detail.trip?.status === 'CANCELLED')
}
const pool = await get(`/api/platform/finance/v1/fee-pool?customerId=${encodeURIComponent(proof.customerId)}&direction=RECEIVABLE`)
const fees = pool.filter(row => id(row.sourceId) === id(proof.finance.source.sourceFeeId) && Number(row.sourceVersion) === Number(proof.finance.source.sourceVersion))
check('原外接应收仍唯一且金额正确', fees.length === 1 && fees[0].amount === 30 && fees[0].currency === 'CNY'
&& id(fees[0].customerId) === id(proof.customerId) && fees[0].sourceType === 'OVERSEAS_DELIVERY_FEE')
evidence.fees = fees; evidence.status = 'VERIFIED'
proof.status = 'HTTP_CHAIN_VERIFIED_WITH_ACK_GAP'; proof.stage = 'FINISHED'
return proof
const check = (label,passed) => { context.assert(MODULE,label,null,Boolean(passed),'',OPTIONS);if(!passed)throw Error(label) }
async function get(path) { const r=await context.request('GET',path);evidence.requests.push({path,http:r.http,code:r.body?.code,requestId:r.requestId,traceId:r.traceId});check('终态只读回查成功',success(r)&&r.requestId&&r.traceId);return r.body.data }
check('原生结案助手已完成货物与资源核对', proof.protocol===NATIVE_PROTOCOL && trip?.dispatchNo===proof.dispatchNo && trip.closure?.status==='CARGO_COMPLETED_AND_RESOURCES_RELEASED')
const current=await get(`/api/platform/v1/last-leg/fleet/trips/${encodeURIComponent(trip.tripNo)}`)
check('当前车次已完成且两项资源结束事实与原证明一致',current.trip?.status==='COMPLETED'
&& current.trip.completedAt===trip.closure.finalTrip.completedAt && id(current.trip.recordVersion)===id(trip.closure.finalTrip.recordVersion)
&& JSON.stringify(current.trip.vehicleRelease)===JSON.stringify(trip.closure.finalTrip.vehicleRelease)
&& JSON.stringify(current.trip.driverRelease)===JSON.stringify(trip.closure.finalTrip.driverRelease)
&& current.trip.vehicleRelease&&current.trip.driverRelease&&current.dispatches?.length===1&&current.dispatches[0].dispatchNo===proof.dispatchNo)
evidence.trip=current.trip
const detail=await get(`/api/platform/v1/last-leg/dispatches/${encodeURIComponent(proof.dispatchNo)}`)
check('原生主单全部正常派送完成且数量守恒',detail.dispatchNo===proof.dispatchNo&&detail.status==='ACTIVE'&&detail.stage==='DELIVERED'
&& Number(detail.counts?.packageCount)===Number(detail.counts?.shippedPackageCount)&&Number(detail.counts?.pickedPackageCount)===Number(detail.counts?.shippedPackageCount)
&& Number(detail.counts?.shippedPackageCount)===Number(proof.departure.result.shippedPackages))
evidence.dispatches.push(detail)
const refs=nativeFleetDeliveries(detail)
check('签收证明恰好覆盖主单每客户派送一次',JSON.stringify(proof.receipts.map(r=>`${r.customerId}:${r.deliveryNo}`).sort())===JSON.stringify(refs.map(r=>`${r.customerId}:${r.deliveryNo}`).sort()))
for(const ref of refs){const delivery=await get(`/api/platform/v1/last-leg/delivery-orders/${encodeURIComponent(ref.deliveryNo)}?customerId=${encodeURIComponent(id(ref.customerId))}`)
check('每张原派送保留客户和真实签收时间',delivery.order?.deliveryNo===ref.deliveryNo&&id(delivery.order.customerId)===id(ref.customerId)&&delivery.order.status==='DELIVERED'
&& delivery.order.signedAt===proof.receipts.find(r=>r.deliveryNo===ref.deliveryNo&&r.customerId===id(ref.customerId)).signedAt);evidence.deliveries.push(delivery.order)}
const probe=trip.closure.nextDispatch
check('原生空排车占用保护证明完整',trip.closure.subsequentTripClaimProtection==='VERIFIED_NATIVE_EMPTY_DISPATCH'&&probe?.dispatchNo&&probe.finalStatus==='CANCELLED')
const cancelled=await get(`/api/platform/v1/last-leg/dispatches/${encodeURIComponent(probe.dispatchNo)}`)
check('本轮资源核对原生空排车已取消',cancelled.dispatchNo===probe.dispatchNo&&cancelled.status==='CANCELLED'&&Number(cancelled.counts?.packageCount)===0&&Number(cancelled.counts?.pickedPackageCount)===0)
const customerId=id(proof.finance.customerId),pool=await get(`/api/platform/finance/v1/fee-pool?customerId=${encodeURIComponent(customerId)}&direction=RECEIVABLE`)
const fees=pool.filter(row=>id(row.sourceId)===id(proof.finance.source.sourceFeeId)&&id(row.sourceVersion)===id(proof.finance.source.sourceVersion))
check('原外接应收仍唯一且金额正确',fees.length===1&&Number(fees[0].amount)===30&&fees[0].currency==='CNY'&&id(fees[0].customerId)===customerId&&fees[0].sourceType==='OVERSEAS_DELIVERY_FEE')
evidence.fees=fees;evidence.status='VERIFIED';proof.status='HTTP_CHAIN_VERIFIED_WITH_ACK_GAP';proof.stage='FINISHED';return proof
}
/** All business checks happen before creating replacement test sessions or issuing a business command. */
export async function verifyDepartedRecovery(context, { costsAlreadyVerified = false, createdVehicle, returnSourceReady = false } = {}) {
const proof = context.createdResources.fleetAcceptance
@@ -369,7 +408,7 @@ export async function verifyDepartedRecovery(context, { costsAlreadyVerified = f
}
const current = await get(base)
check(current.trip?.status === 'IN_TRANSIT' && id(current.trip.recordVersion) === id(expectedTrip.recordVersion)
&& current.trip.departedAt === proof.departure.departedAt && id(current.trip.warehouseId) === id(proof.warehouseId)
&& sameInstant(current.trip.departedAt, proof.departure.result.departedAt) && id(current.trip.warehouseId) === id(proof.warehouseId)
&& id(current.trip.vehicleId) === id(expectedTrip.vehicleId) && id(current.trip.driverId) === id(expectedTrip.driverId)
&& !current.trip.vehicleRelease && !current.trip.driverRelease, 'trip version, resources or departure changed')
const history = await get(`${base}/history?page=1&size=100`)
@@ -405,34 +444,37 @@ export async function verifyDepartedRecovery(context, { costsAlreadyVerified = f
const expected = trip.handover.requests.filter(row => row.method === 'GET' && row.path.startsWith(`${base}/handovers?`)).at(-1)?.result
check(expected && Number(handovers.total) === 1 && same(handovers.records, expected.records), 'original verified handover changed')
} else check(handovers.records?.length === 0 && Number(handovers.total) === 0, 'handover already exists')
check(same(current.dispatches.map(row => row.dispatchNo).sort(), [...proof.dispatchNos].sort())
&& current.dispatches.every(row => id(row.customerId) === id(proof.customerId)), 'trip dispatch binding changed')
for (const number of proof.dispatchNos) {
const detail = await get(`/api/platform/v1/last-leg/dispatches/${encodeURIComponent(number)}`)
const cargo = load.cargo.filter(row => row.dispatchNo === number)
const facts = rows => rows.map(row => [id(row.id ?? row.dispatchItemId), id(row.warehousePalletId), Number(row.packageCount ?? row.packages)])
.sort((a, b) => a[0].localeCompare(b[0]))
check(detail.dispatchNo === number && detail.status === 'DEPARTED' && id(detail.overseasWarehouseId) === id(proof.warehouseId)
&& Number(detail.packageCount) === Number(detail.shippedPackageCount) && Number(detail.pickedPackageCount) === Number(detail.shippedPackageCount)
&& same(facts(detail.items), facts(cargo)), 'dispatch shipped cargo changed')
evidence.dispatchDetails.push(detail)
check(proof.protocol === NATIVE_PROTOCOL && nativeDeparture(proof.departure,proof.dispatchNo)
&& trip.dispatchNo === proof.dispatchNo && current.dispatches?.length === 1 && current.dispatches[0].dispatchNo === proof.dispatchNo
&& JSON.stringify(proof.dispatchNos) === JSON.stringify([proof.dispatchNo]), 'trip dispatch binding changed')
const detail=await get(`/api/platform/v1/last-leg/dispatches/${encodeURIComponent(proof.dispatchNo)}`)
const items=await get(`/api/platform/v1/last-leg/dispatches/${encodeURIComponent(proof.dispatchNo)}/items`)
const cargo=load.cargo
const facts=rows=>{
const totals=new Map()
for(const row of rows){const identity=JSON.stringify([id(row.dispatchItemId),id(row.warehousePalletId),id(row.customerId)]);totals.set(identity,(totals.get(identity)||0)+Number(row.scheduledPackages??row.packages))}
return [...totals].sort(([a],[b])=>a.localeCompare(b))
}
const partial = context.createdResources.fleetFeeCoverageCases.find(row => row.mode === 'PARTIAL')
const deliveryNos = [...partial.proof.deliveryNos, trip.externalDispatch.deliveryNo]
check(new Set(deliveryNos).size === deliveryNos.length, 'duplicate delivery identities')
for (const number of deliveryNos) {
const detail = await get(`/api/platform/v1/last-leg/delivery-orders/${encodeURIComponent(number)}?customerId=${encodeURIComponent(proof.customerId)}`)
const order = detail.order
const started = returnSourceReady ? trip.returnRedispatch.requests.find(row => row.method === 'PUT' && row.result?.deliveryNo === number) : null
check(order?.deliveryNo === number && id(order.customerId) === id(proof.customerId) && proof.dispatchNos.includes(order.dispatchNo)
&& order.status === (returnSourceReady ? 'DELIVERING' : 'WAIT_DELIVERY')
&& id(order.version) === (returnSourceReady ? String(Number(started?.body.expectedVersion) + 1) : '1')
&& (!returnSourceReady || order.vehiclePlate === expectedTrip.vehiclePlate && order.driverName === expectedTrip.driverName && order.driverPhone === expectedTrip.driverPhone)
&& !order.signedAt && !Number(order.signedPackages || 0)
&& !order.completedAt && order.documentSummary?.categories?.every(row => Number(row.fileCount) === 0), 'delivery has started, changed or been signed')
check(detail.dispatchNo===proof.dispatchNo&&detail.status==='ACTIVE'&&detail.stage==='DELIVERING'&&items.dispatchNo===proof.dispatchNo
&& id(items.recordVersion)===id(detail.recordVersion)&&Number(detail.counts?.packageCount)===Number(detail.counts?.shippedPackageCount)
&& Number(detail.counts?.pickedPackageCount)===Number(detail.counts?.shippedPackageCount)&&cargo.every(row=>row.dispatchNo===proof.dispatchNo)
&& same(facts(items.items),facts(cargo))&&proof.departureSnapshot?.detail?.dispatchNo===proof.dispatchNo
&& same(items,proof.departureSnapshot.items),'dispatch shipped cargo changed')
evidence.dispatchDetails.push({detail,items})
const refs=nativeFleetDeliveries(detail)
check(new Set(refs.map(d=>d.deliveryNo)).size===refs.length
&& deliveryFacts(refs)===deliveryFacts(nativeFleetDeliveries(proof.departureSnapshot.detail)),'duplicate or changed delivery identities')
for(const ref of refs){
const response=await get(`/api/platform/v1/last-leg/delivery-orders/${encodeURIComponent(ref.deliveryNo)}?customerId=${encodeURIComponent(id(ref.customerId))}`),order=response.order
const expected=(returnSourceReady?trip.returnRedispatch.originalDeliveries:proof.departureDeliveries)?.find(d=>d.deliveryNo===ref.deliveryNo&&id(d.customerId)===id(ref.customerId))
check(expected&&order?.deliveryNo===ref.deliveryNo&&id(order.customerId)===id(ref.customerId)&&order.dispatchNo===proof.dispatchNo
&& order.outboundNo===ref.outboundNo&&order.status==='DELIVERING'&&id(order.version)===id(expected.version)
&& order.outboundNo===expected.outboundNo&&Number(order.packageCount)===Number(ref.packageCount)&&Number(order.packageCount)===Number(expected.packageCount)
&& (!returnSourceReady||order.vehiclePlate===expectedTrip.vehiclePlate&&order.driverName===expectedTrip.driverName&&order.driverPhone===expectedTrip.driverPhone)
&& !order.signedAt&&!Number(order.signedPackages||0)&&!order.completedAt&&order.documentSummary?.categories?.every(r=>Number(r.fileCount)===0),'delivery has started, changed or been signed')
evidence.deliveries.push(order)
}
check(evidence.deliveries.reduce((n, row) => n + Number(row.packageCount), 0) === Number(proof.departure.shippedPackages), 'delivery quantity mismatch')
check(evidence.deliveries.reduce((n,row)=>n+Number(row.packageCount),0)===Number(proof.departure.result.shippedPackages),'delivery quantity mismatch')
if (createdVehicle) {
const vehicle = await get(`/api/platform/v1/last-leg/master-data/vehicles/${encodeURIComponent(createdVehicle.snapshot.id)}`)
const canonical = value => value && typeof value === 'object' && !Array.isArray(value)
@@ -524,8 +566,8 @@ export async function continueReviewedReturnSources(context, { verify = verifyRe
run.picker = { membershipId: picker.membershipId, username: picker.username }; await context.checkpoint()
const partial = context.createdResources.fleetFeeCoverageCases.find(row => row.mode === 'PARTIAL')
const result = await redispatch(context, MODULE, trip, { customerId: run.customerId, warehouseId: run.warehouseId,
pickerToken: picker.token, dispatchNos: run.dispatchNos, deliveryNos: [...partial.proof.deliveryNos, trip.externalDispatch.deliveryNo], sourceSelectionRecovery: true })
run.receipts = result.originalSignatures.map(row => ({ deliveryNo: row.deliveryNo, signedAt: row.signedAt }))
pickerToken: picker.token, dispatchNos: run.dispatchNos, deliveryNos: nativeFleetDeliveries(verified.dispatchDetails[0].detail).map(ref => ref.deliveryNo), sourceSelectionRecovery: true })
run.receipts = result.originalSignatures.map(row => ({ customerId: id(row.customerId || run.customerId), deliveryNo: row.deliveryNo, signedAt: row.signedAt }))
context.context.saveFleetReturnRedispatchFixture(result)
run.closure = await closeOriginal(context, MODULE, trip)
if (run.closure?.status !== 'CARGO_COMPLETED_AND_RESOURCES_RELEASED') throw Error('Recovered original trip did not close')
@@ -538,8 +580,8 @@ export async function continueReviewedReturnSources(context, { verify = verifyRe
export async function runFleetHandoverStage(context, trip, pickerToken, dispatchDetails, { verify = verifyFleetHandover, onReady, reviewedVehicle } = {}) {
const state = await verify(context, MODULE, trip, pickerToken, dispatchDetails, { reviewedVehicle })
const run = context.createdResources.fleetAcceptance
if (state?.status !== 'VERIFIED' || state.marker !== context.marker || run?.departure?.tripNo !== trip.tripNo
|| run.departure.status !== 'IN_TRANSIT' || !state.originalDeparture?.snapshot?.confirmedLoad || !state.browser
if (state?.status !== 'VERIFIED' || state.marker !== context.marker || run?.tripNo !== trip.tripNo
|| !nativeDeparture(run.departure, trip.dispatchNo) || !state.originalDeparture?.snapshot?.confirmedLoad || !state.browser
|| trip.costAcceptance?.status !== 'VERIFIED' || trip.costAcceptance.marker !== context.marker)
throw Error('Only the current verified DEPART and handover can publish a shared fixture')
const fixture = { marker: context.marker, runId: context.context?.fleetRunId, tripNo: trip.tripNo, warehouseId: trip.warehouseId, dispatchNos: state.dispatchNos,
@@ -556,21 +598,26 @@ export async function runFleetHandoverStage(context, trip, pickerToken, dispatch
export function fleetCostShares(trip, dispatchDetails) {
const external = trip.externalReceipt, externalDispatch = trip.externalDispatch
if (external?.status !== 'VERIFIED' || !external.bookingNo || !external.warehousePalletId || !externalDispatch?.dispatchNo)
if (external?.status !== 'VERIFIED' || !external.bookingNo || !external.warehousePalletId
|| !trip.dispatchNo || externalDispatch?.dispatchNo !== trip.dispatchNo)
throw Error('Cost expectations require this run\'s verified external receipt')
const shares = dispatchDetails.map(row => {
const pallets = [...new Set((row.items || []).map(item => id(item.warehousePalletId)))]
if (!row.dispatchNo || !pallets.length || pallets.some(value => !value)) throw Error('Cost expectations require actual dispatch pallets')
const isExternal = row.dispatchNo === externalDispatch.dispatchNo
if (isExternal && (pallets.length !== 1 || pallets[0] !== id(external.warehousePalletId))) throw Error('External cost source does not match actual receipt')
// The fixture's explicit measurement rule is one space per physical pallet; never derive this from cost totals.
return { businessNo: isExternal ? external.bookingNo : row.dispatchNo, sourceType: isExternal ? 'EXTERNAL' : 'OWN',
dispatchNo: row.dispatchNo, warehousePalletIds: pallets, palletSpaces: pallets.length }
})
const pallets = shares.flatMap(row => row.warehousePalletIds)
if (shares.filter(row => row.sourceType === 'EXTERNAL').length !== 1 || !shares.some(row => row.sourceType === 'OWN')
|| new Set(pallets).size !== pallets.length) throw Error('Cost fixture requires distinct own and external physical pallets')
return shares
const bundle = dispatchDetails?.length === 1 ? dispatchDetails[0] : null
const detail = bundle?.detail, items = bundle?.items
if (detail?.dispatchNo !== trip.dispatchNo || items?.dispatchNo !== trip.dispatchNo
|| id(detail.recordVersion) !== id(items.recordVersion) || !items.items?.length)
throw Error('Cost expectations require the same native dispatch Detail and Items version')
const pallets = [...new Set(items.items.map(item => id(item.warehousePalletId)))].sort()
const measured = trip.confirmedLoad?.pallets
if (pallets.some(value => !value) || !trip.confirmedLoad?.manifestHash || !Array.isArray(measured)
|| measured.length !== pallets.length || new Set(measured.map(row => id(row.warehousePalletId))).size !== measured.length
|| measured.some(row => !pallets.includes(id(row.warehousePalletId)) || !Number.isSafeInteger(row.palletSpaces) || row.palletSpaces < 1))
throw Error('Cost expectations require exact actual loading measurements for every distinct pallet')
const externalId = id(external.warehousePalletId), own = pallets.filter(value => value !== externalId)
if (!pallets.includes(externalId) || !own.length) throw Error('External receipt and own cargo require distinct actual pallets')
const share = (sourceType, businessNo, ids) => ({ businessNo, sourceType, dispatchNo: trip.dispatchNo,
warehousePalletIds: ids, palletSpaces: measured.filter(row => ids.includes(id(row.warehousePalletId)))
.reduce((sum, row) => sum + row.palletSpaces, 0) })
return [share('OWN', trip.dispatchNo, own), share('EXTERNAL', external.bookingNo, [externalId])]
}
export function fleetCostActorJournalData(value) {
@@ -681,204 +728,259 @@ export async function findFleetActorPhone(context, actorSuffix) {
throw Error('No available phone in the fresh actor scope; do not replay user creation')
}
async function nativeRequest(context, method, path, body, token) {
const result = await context.request(method, path, body, token)
if (!success(result) || !result.requestId || !result.traceId) throw Error(`Native fleet request failed: ${method} ${path}`)
return result.body.data
}
export async function readNativeFleetBundle(context, dispatchNo) {
const base = `/api/platform/v1/last-leg/dispatches/${encodeURIComponent(dispatchNo)}`
const detail = await nativeRequest(context, 'GET', base)
const items = await nativeRequest(context, 'GET', `${base}/items`)
if (detail.dispatchNo !== dispatchNo || items.dispatchNo !== dispatchNo || id(dispatchVersion(detail.recordVersion)) !== id(dispatchVersion(items.recordVersion))) throw Error('Native Detail/Items version mismatch')
return { detail, items }
}
// 只读取本轮生产者和运营已确认实测,不从计划托位、计费重量或常数生成实际量。
export async function currentFleetMeasurements(context, trip) {
const path = `/api/platform/v1/last-leg/dispatches/${encodeURIComponent(trip.dispatchNo)}/loading`
const preview = await nativeRequest(context, 'GET', path)
if (preview.dispatchNo !== trip.dispatchNo || preview.status !== 'ACTIVE' || !preview.pallets?.length) throw Error('Current native physical loading preview missing')
const producer = context.createdResources.phase4OverseasDispatch?.loadingMeasurements
if (producer && (producer.marker !== context.marker || producer.environment !== 'TEST' || producer.dispatchNo !== trip.dispatchNo || !Array.isArray(producer.pallets)))
throw Error('Physical measurements must be the exact current-run producer receipt for this dispatch')
const actual = (producer?.pallets || []).map(row => structuredClone(row))
const receipt = trip.externalReceipt?.finalBooking?.receipt
for (const measured of receipt?.pallets || []) {
const source = receipt.source?.pallets?.find(p => id(p.warehousePalletId) === id(measured.warehousePalletId))
if (!source || !Number.isFinite(Date.parse(receipt.confirmedAt)) || !receipt.operatorId || !receipt.source?.incomingNo) throw Error('External actual measurement ownership missing')
actual.push({ warehousePalletId: id(measured.warehousePalletId), palletSpaces: measured.palletSpaces,
weightKg: measured.weightKg ?? source.weightKg, volumeCbm: measured.volumeCbm ?? source.volumeCbm,
deliveryIndex: trip.stops.findIndex(s => s.destinationKey === trip.externalReceipt.finalBooking.cargo.destinationKey),
evidence: ['palletSpaces','weightKg','volumeCbm'].map(field => ({ field, value: field === 'palletSpaces' ? measured[field] : measured[field] ?? source[field],
actual: true, sourceType: 'FLEET_RECEIPT_CONFIRMATION', sourceBusinessNo: receipt.source.incomingNo, observedAt: receipt.confirmedAt })) })
}
if (new Set(actual.map(p => id(p.warehousePalletId))).size !== actual.length) throw Error('Duplicate current-run physical measurement receipts')
for (const pallet of preview.pallets) {
const supplied = actual.find(p => id(p.warehousePalletId) === id(pallet.warehousePalletId))
for (const field of ['palletSpaces','weightKg','volumeCbm']) {
const source = pallet[field] == null ? supplied : pallet, value = source?.[field]
if (!(Number(value) > 0) || !source?.evidence?.some(e => e.field === field && e.actual === true && e.sourceBusinessNo && e.sourceType
&& Number.isFinite(Date.parse(e.observedAt)) && Number(e.value) === Number(value)))
throw Error(`Missing current-run actual ${field} for ${pallet.palletNo || 'unassociated pallet'}; no planning constants or external receipt injection`)
}
const index = pallet.deliveryIndex ?? supplied?.deliveryIndex
if (!Number.isSafeInteger(index) || index < 1 || !trip.stops?.[index]?.destinationKey)
throw Error('Current-run physical pallet has no verified unloading stop')
}
if (actual.some(p => !preview.pallets.some(row => id(row.warehousePalletId) === id(p.warehousePalletId))
&& !receipt?.source?.pallets?.some(row => id(row.warehousePalletId) === id(p.warehousePalletId)))) throw Error('Physical receipts contain a pallet outside this dispatch and its confirmed incoming')
return { environment: 'TEST', marker: context.marker, dispatchNo: trip.dispatchNo, pallets: actual }
}
export async function departNativeFleet(context, trip, pickerToken, run) {
if (!pickerToken || !run || run.dispatchNo !== trip.dispatchNo || run.tripNo !== trip.tripNo || run.departureRequest)
throw Error('Native departure requires owned main and a new original command')
const path = `/api/platform/v1/last-leg/dispatches/${encodeURIComponent(trip.dispatchNo)}`
const before = await readNativeFleetBundle(context, trip.dispatchNo)
const refs = nativeFleetDeliveries(before.detail)
const task = await nativeRequest(context, 'GET', `/api/pda/v1/last-leg/dispatches/${encodeURIComponent(trip.dispatchNo)}`, undefined, pickerToken)
if (before.detail.status !== 'ACTIVE' || before.detail.stage !== 'AFTER_LOADING' || task.dispatchNo !== trip.dispatchNo || task.status !== 'ACTIVE' || task.stage !== 'AFTER_LOADING'
|| id(task.warehouseId) !== id(trip.warehouseId) || id(task.recordVersion) !== id(before.detail.recordVersion) || task.actions?.DEPART?.allowed !== true
|| !before.detail.pickingReconciled || !task.pickingReconciled || Number(before.detail.counts?.packageCount) !== Number(task.counts?.packageCount)
|| Number(task.counts?.packageCount) !== Number(task.counts?.pickedPackageCount) || Number(task.counts?.shippedPackageCount) !== 0
|| before.items.items.some(row => Number(row.pickedPackages) !== Number(row.scheduledPackages))
|| before.items.items.reduce((n,row) => n + Number(row.scheduledPackages), 0) !== Number(task.counts?.packageCount)
|| refs.reduce((n,d) => n + Number(d.packageCount), 0) !== Number(task.counts.packageCount)) throw Error('Native main is not fully loaded and arranged for both ends')
run.departureRequest = { recordVersion: task.recordVersion, idempotencyKey: key(), remark: 'TEST同一原生主单实际发车,不重复扣库存' }
await context.checkpoint?.()
const departed = await nativeRequest(context, 'POST', `/api/pda/v1/last-leg/dispatches/${encodeURIComponent(trip.dispatchNo)}/depart`, run.departureRequest, pickerToken)
if (!nativeDeparture(departed, trip.dispatchNo) || BigInt(dispatchVersion(departed.recordVersion)) <= BigInt(dispatchVersion(task.recordVersion))
|| Number(departed.result.shippedPackages) !== Number(task.counts.packageCount)
|| !Array.isArray(departed.result.deliveries) || deliveryFacts(departed.result.deliveries) !== deliveryFacts(refs))
throw Error('Native departure did not preserve all customer shipment identities and quantities')
run.departure = departed
const replayed = await nativeRequest(context, 'POST', `${path}/depart`, run.departureRequest)
if (!nativeDeparture(replayed, trip.dispatchNo) || replayed.replayed !== true || id(replayed.recordVersion) !== id(departed.recordVersion) || JSON.stringify(replayed.result) !== JSON.stringify(departed.result))
throw Error('Native cross-end departure replay changed the original outcome')
run.departureReplay = replayed
const after = await readNativeFleetBundle(context, trip.dispatchNo)
if (after.detail.status !== 'ACTIVE' || after.detail.stage !== 'DELIVERING' || id(after.detail.recordVersion) !== id(departed.recordVersion)
|| Number(after.detail.counts.shippedPackageCount) !== Number(task.counts.packageCount) || Number(after.detail.counts.packageCount) !== Number(task.counts.packageCount)
|| Number(after.detail.counts.pickedPackageCount) !== Number(after.detail.counts.shippedPackageCount)
|| deliveryFacts(nativeFleetDeliveries(after.detail)) !== deliveryFacts(refs)) throw Error('Native departure readback did not conserve physical quantities')
run.departureSnapshot = after; run.dispatchNos = [trip.dispatchNo]
run.departureDeliveries = []
for (const ref of nativeFleetDeliveries(after.detail)) {
const value = await nativeRequest(context, 'GET', `/api/platform/v1/last-leg/delivery-orders/${encodeURIComponent(ref.deliveryNo)}?customerId=${encodeURIComponent(id(ref.customerId))}`)
if (value.order?.deliveryNo !== ref.deliveryNo || id(value.order.customerId) !== id(ref.customerId) || value.order.dispatchNo !== trip.dispatchNo
|| value.order.status !== 'DELIVERING' || value.order.version == null || Number(value.order.packageCount) !== Number(ref.packageCount)) throw Error('Native departure delivery versions or owners changed')
run.departureDeliveries.push(value.order)
}
await context.checkpoint?.(); return { departed, dispatchDetails: [after] }
}
export async function runFleetScenario(context, { wait = ms => new Promise(resolve => setTimeout(resolve, ms)), onHandoverReady,
costActors, resumeFull = false, resumePersisted = false, resumeReceiptReady = false, resumePalletized = false, resumeReceiptConfirmed = false, terminalReadOnly = false } = {}) {
if (terminalReadOnly) return verifyFleetTerminalState(context)
if (costActors?.marker !== context.marker || !costActors.listOnlyToken || !costActors.deniedToken
|| costActors.listOnlyToken === costActors.deniedToken || [costActors.listOnlyToken, costActors.deniedToken].includes(context.token))
throw Error('Prepare two independent current-run platform cost permission sessions before starting the fleet scenario')
const proof = resumeFull || resumeReceiptReady ? context.createdResources.fleetAcceptance
: context.createdResources.fleetAcceptance = { marker: context.marker, status: 'CHECKING', stage: 'SOURCES', checkpoints: [] }
const resumed = resumeFull || resumeReceiptReady
const proof = resumed ? context.createdResources.fleetAcceptance : context.createdResources.fleetAcceptance = {
protocol: NATIVE_PROTOCOL, marker: context.marker, status: 'CHECKING', stage: 'SOURCES', checkpoints: [] }
if (proof.protocol !== NATIVE_PROTOCOL) throw Error('Legacy multi-dispatch reports cannot enter the native main chain')
function stage(value) { proof.stage = value; proof.checkpoints.push({ stage: value, at: new Date().toISOString() }); console.log(`FLEET_STAGE=${value}`) }
function check(label, value, response) { context.assert(MODULE, label, response, Boolean(value), '', OPTIONS); if (!value) throw Error(label) }
async function request(method, path, body, label, token) {
const response = await context.request(method, path, body, token)
check(label, success(response), response); return response.body.data
}
const source = context.createdResources.phase4OverseasDispatch, receiving = context.createdResources.phase4OverseasPda
check('专项只承接本轮实际已收货及预排资源', source?.marker === context.marker && source.dispatchNo
&& receiving?.marker === context.marker && receiving.finalStatus === 'COMPLETED' && receiving.customerId && receiving.overseasWarehouseId)
if (source?.marker !== context.marker || source.status !== 'VERIFIED' || !source.dispatchNo || receiving?.marker !== context.marker
|| receiving.finalStatus !== 'COMPLETED' || !receiving.customerId || !receiving.overseasWarehouseId) throw Error('Current-run native dispatch and real received warehouse evidence required')
const customerId = id(receiving.customerId), warehouseId = id(receiving.overseasWarehouseId)
proof.customerId = customerId; proof.warehouseId = warehouseId
const dispatchBase = '/api/platform/v1/last-leg/dispatches'
let trip, partial
if (resumeReceiptReady) {
trip = context.createdResources.fleetTrips.find(row => row.tripNo === proof.tripNo)
partial = context.createdResources.fleetFeeCoverageCases.find(row => row.mode === 'PARTIAL')
const current = await request('GET', `/api/platform/v1/last-leg/fleet/trips/${encodeURIComponent(trip.tripNo)}`, undefined, '恢复实收前核对原车次分配与客户执行单')
const dispatch = await request('GET', `${dispatchBase}/${encodeURIComponent(partial.dispatchNo)}`, undefined, '恢复实收前核对原排车尚未拣货或发车')
check('仍是原已核对车辆司机与未执行客户排车', current.trip?.status === 'PLANNED'
&& id(current.trip.vehicleId) === id(trip.vehicleId) && id(current.trip.driverId) === id(trip.driverId)
&& current.dispatches?.length === 1 && current.dispatches[0].dispatchNo === partial.dispatchNo
&& id(current.dispatches[0].customerId) === customerId && dispatch.dispatchNo === partial.dispatchNo
&& Number(dispatch.pickedPackageCount) === 0 && Number(dispatch.shippedPackageCount) === 0)
Object.assign(proof, { customerId, warehouseId, dispatchNo: source.dispatchNo, dispatchNos: [source.dispatchNo] })
const journal = dispatchIo(context, MODULE, proof, 'FLEET-NATIVE')
let full, partial, trip
const template = { plannedDeliveryAt: new Date(Date.now()+21600000).toISOString(), deliveryMethod: 'TRUCK_PALLET', pickupMethod: 'WAREHOUSE_PICKUP',
loadingType: 'PALLET', vehicleType: 'TRUCK', vehicleSize: '本轮实际装载', appointmentAccount: 'SELF', remark: `${context.marker} TEST原费用覆盖`,
fees: [{ feeType: 'DELIVERY_FEE', feeName: '未覆盖原入仓派送费', direction: 'RECEIVABLE', currency: 'CNY', amount: 10 }] }
if (!resumed) {
const original = await readDispatch(journal, source.dispatchNo)
const candidate = selectNativeFleetFullSource(context, original, await candidatePage(journal, warehouseId))
proof.originalDispatch = { detail: original.data, items: original.items, route: original.route }
proof.fullCreateRequest = createBody(context.marker, 'FLEET-FULL-INDEPENDENT', [candidate])
const created = await journal.command('POST', '/api/platform/v1/last-leg/dispatches', proof.fullCreateRequest, '释放的本轮真实第三托创建独立FULL原生排车')
await replayDispatch(journal, '/api/platform/v1/last-leg/dispatches', proof.fullCreateRequest, created, '独立FULL创建原键恢复')
if (!created.dispatchNo || created.dispatchNo === source.dispatchNo) throw Error('FULL must be a distinct newly created native dispatch')
const fullTrip = await prepareFleetTrip(context, MODULE, created.dispatchNo, warehouseId)
trip = await prepareFleetTrip(context, MODULE, source.dispatchNo, warehouseId)
Object.assign(proof, { tripNo: trip.tripNo, fullTripNo: fullTrip.tripNo })
full = { marker: context.marker, mode: 'FULL', dispatchNo: created.dispatchNo, customerId, transferBody: structuredClone(template) }
partial = { marker: context.marker, mode: 'PARTIAL', dispatchNo: source.dispatchNo, customerId, transferBody: structuredClone(template) }
context.createdResources.fleetFeeCoverageCases = [full, partial]
proof.fullExecution = { protocol: NATIVE_PROTOCOL, marker: context.marker, dispatchNo: full.dispatchNo, tripNo: fullTrip.tripNo }
} else {
const original = await request('GET', `${dispatchBase}/${encodeURIComponent(source.dispatchNo)}`, undefined, '读取本轮原始预排实物')
if (resumeFull) check('原样本恢复前排车货物未变且未开始作业', JSON.stringify(original.items) === JSON.stringify(proof.originalDispatch.items)
&& Number(original.pickedPackageCount) === 0 && Number(original.shippedPackageCount) === 0)
proof.originalDispatch = original
trip = resumeFull ? context.createdResources.fleetTrips.find(row => row.tripNo === proof.tripNo)
: await prepareFleetTrip(context, MODULE, source.dispatchNo, warehouseId)
proof.tripNo = trip.tripNo
const tripPath = `/api/platform/v1/last-leg/fleet/trips/${encodeURIComponent(trip.tripNo)}`
function transferBody(vehicle) { return { idempotencyKey: key(), plannedDeliveryAt: new Date(Date.now() + 21600000).toISOString().slice(0, 19),
deliveryMethod: 'TRUCK_PALLET', pickupMethod: 'WAREHOUSE_PICKUP', loadingType: 'PALLET', vehicleType: 'TRUCK', vehicleSize: '30托',
vehiclePlate: vehicle.vehiclePlate, driverName: vehicle.driverName, driverPhone: vehicle.driverPhone, appointmentAccount: 'SELF',
remark: `${context.marker} 原费用覆盖专用验证`, fees: [{ feeType: 'DELIVERY_FEE', feeName: '未覆盖货物派送费', direction: 'RECEIVABLE', currency: 'CNY', amount: 10 }] } }
let detail = await request('GET', tripPath, undefined, '读取当前派车事实供转派送使用')
check('当前车次资源及原客户排车关联仍一致', id(detail.trip.vehicleId) === id(trip.vehicleId)
&& id(detail.trip.driverId) === id(trip.driverId) && detail.dispatches.some(row => row.dispatchNo === source.dispatchNo && id(row.customerId) === customerId))
if (!resumeFull) context.createdResources.fleetFeeCoverageCases = []
const full = resumeFull ? context.createdResources.fleetFeeCoverageCases[0]
: { marker: context.marker, mode: 'FULL', dispatchNo: source.dispatchNo, customerId, transferBody: transferBody(detail.trip) }
if (resumeFull && !resumePersisted) {
full.previousReadOnlyAttempts ||= []; full.previousReadOnlyAttempts.push(full.proof); delete full.proof
} else if (!resumeFull) context.createdResources.fleetFeeCoverageCases.push(full)
stage('FULL_COVERAGE'); await verifyFleetFeeCoverage(context, MODULE, full, { reconcilePersisted: resumePersisted })
stage('CANCEL_FULL_AND_REARRANGE')
detail = await request('GET', tripPath, undefined, '读取取消未执行全覆盖用例前车次版本')
await request('POST', `${tripPath}/dispatches/unbind`, { recordVersion: detail.trip.recordVersion, dispatchNo: full.dispatchNo,
reason: '全覆盖用例验证完毕,正式取消未执行发运并以新排车验证部分覆盖' }, '先解除未执行全覆盖排车关联')
await request('POST', `/api/platform/v1/last-leg/outbound-orders/${encodeURIComponent(full.proof.outboundNo)}/cancel`,
{ reason: '本轮全覆盖验证完毕,正式取消未执行出库后重新安排' }, '取消未执行全覆盖出库')
await request('DELETE', `${dispatchBase}/${encodeURIComponent(full.dispatchNo)}`, undefined, '正式取消原排车释放其托盘锁')
const draftToken = key(), planPalletIds = original.items.map(row => id(row.planPalletId))
check('原排车保留可追溯计划托盘', planPalletIds.length > 0 && planPalletIds.every(value => /^[1-9]\d*$/.test(value)))
proof.rearrange = { originalDispatchNo: full.dispatchNo, draftToken, planPalletIds }
const container = await request('POST', `${dispatchBase}/drafts/containers`, { draftToken, capacity: 30 }, '建立同批当前库存的新排车草稿')
for (const planPalletId of planPalletIds) await request('POST', `${dispatchBase}/draft-locks/lock`, { planPalletId, draftToken, draftContainerId: id(container.id) }, '锁定已正式释放的本轮托盘')
const arranged = await request('POST', `${dispatchBase}/pre-arrange`, { planPalletIds, draftToken }, '以新排车重新安排本轮同批货物')
check('取消重排产生新排车且货量不变', arranged.dispatchNo && arranged.dispatchNo !== full.dispatchNo
&& Number(arranged.packageCount) === Number(original.packageCount))
proof.rearrange.dispatchNo = arranged.dispatchNo
detail = await request('GET', tripPath, undefined, '读取原车次当前可关联版本')
detail = await request('POST', `${tripPath}/dispatches/bind`, { recordVersion: detail.trip.recordVersion, dispatchNo: arranged.dispatchNo,
reason: '本轮取消后新排车重新关联原计划车次' }, '将新排车正式关联原车次')
trip.replacedDispatchNo = trip.dispatchNo; trip.dispatchNo = arranged.dispatchNo
partial = { marker: context.marker, mode: 'PARTIAL', dispatchNo: arranged.dispatchNo, customerId, transferBody: transferBody(detail.trip) }
context.createdResources.fleetFeeCoverageCases.push(partial)
stage('PARTIAL_COVERAGE'); await verifyFleetFeeCoverage(context, MODULE, partial)
stage('RESERVATIONS'); await verifyFleetBookingReservation(context, MODULE, trip, controlledPalletMeasurements)
full = context.createdResources.fleetFeeCoverageCases.find(row => row.mode === 'FULL')
partial = context.createdResources.fleetFeeCoverageCases.find(row => row.mode === 'PARTIAL')
trip = context.createdResources.fleetTrips.find(row => row.dispatchNo === proof.dispatchNo)
if (!full || !partial || partial.dispatchNo !== trip?.dispatchNo || full.dispatchNo === partial.dispatchNo) throw Error('Reviewed native FULL/PARTIAL original identities missing')
if (resumeFull && !resumePersisted && JSON.stringify(await readNativeFleetBundle(context, full.dispatchNo)) !== JSON.stringify(proof.fullBeforeAttempt))
throw Error('Read-only FULL checkpoint changed physical state; review the original commands before any new session or write')
}
const access = await resolvePickerAccess(context)
let picker = null
let picker
try {
if (resumePalletized || resumeReceiptConfirmed) {
const suffix = proof.pickerMarker.replace(/[^A-Za-z0-9]/g, '').slice(-16).toUpperCase()
const username = `pda_overseas_${suffix.toLowerCase()}_71`
check('仅复用本轮创建的临时PDA身份', proof.picker.username === username && access?.role?.id)
const users = await request('GET', `/api/platform/v1/iam/users?${new URLSearchParams({ page: '1', size: '200', keyword: username })}`, undefined, '核对原临时PDA账号绑定及角色')
const user = users?.records?.find(row => id(row.membershipId) === id(proof.picker.membershipId) && row.username === username)
check('原PDA仍仅有本仓作业员权限且不是受保护账号', user && id(user.tenantId) === '0' && !user.protectedAccount
&& id(user.overseasWarehouseId) === warehouseId && user.roleIds?.length === 1 && id(user.roleIds[0]) === id(access.role.id))
picker = { membershipId: user.membershipId, username, token: null }
const active = await request('PUT', `/api/platform/v1/iam/users/${encodeURIComponent(id(user.membershipId))}/status`, { status: 'ACTIVE' }, '为核对原PDA完成请求恢复本轮临时作业员')
check('原临时PDA已恢复启用', active?.status === 'ACTIVE')
const loggedIn = await request('POST', '/api/auth/login', { loginId: username, password: `Pda${suffix}A719` }, '以原生成凭据登录本轮临时PDA', '')
picker.token = loggedIn?.accessToken
} else {
const pickerContext = Object.create(context)
proof.pickerMarker ||= `${context.marker}-FPDA-${randomUUID().slice(0, 8)}`
pickerContext.marker = proof.pickerMarker
picker = access ? await createPicker(pickerContext, 71, warehouseId, access) : null
if (!resumeReceiptReady) {
const fullTrip = context.createdResources.fleetTrips.find(t => t.dispatchNo === full.dispatchNo)
stage('FULL_COVERAGE')
if (!resumed) proof.fullBeforeAttempt = await readNativeFleetBundle(context, full.dispatchNo)
let fullPicker
try {
const child = Object.create(context)
proof.fullExecution.pickerMarker = `${context.marker}-FULL-PDA-${randomUUID().slice(0,8)}`
child.marker = proof.fullExecution.pickerMarker
fullPicker = access ? await createPicker(child, 161, warehouseId, access) : null
if (!fullPicker?.token || !fullPicker.membershipId) throw Error('Independent FULL requires its own bound-warehouse PDA')
proof.fullExecution.picker = { membershipId: fullPicker.membershipId, username: fullPicker.username }
await context.checkpoint?.()
if (!resumePersisted && full.proof) { full.previousReadOnlyAttempts ??= []; full.previousReadOnlyAttempts.push(full.proof); delete full.proof }
await verifyFleetFeeCoverage(context, MODULE, full, { reconcilePersisted: resumePersisted, pickerToken: fullPicker.token,
actualMeasurements: resumePersisted ? undefined : await currentFleetMeasurements(context, fullTrip) })
const fullDeparted = await departNativeFleet(context, fullTrip, fullPicker.token, proof.fullExecution)
proof.fullExecution.receipts = []
for (const [index, ref] of nativeFleetDeliveries(fullDeparted.dispatchDetails[0].detail).entries()) {
const detail = await nativeRequest(context, 'GET', `/api/platform/v1/last-leg/delivery-orders/${encodeURIComponent(ref.deliveryNo)}?customerId=${encodeURIComponent(id(ref.customerId))}`)
const signed = await saveReceiptAndComplete(context, detail.order, id(ref.customerId), 161+index)
if (signed?.order?.status !== 'DELIVERED' || !signed.order.signedAt) throw Error('FULL source requires actual customer POD completion')
proof.fullExecution.receipts.push({customerId:id(ref.customerId),deliveryNo:ref.deliveryNo,signedAt:signed.order.signedAt})
}
proof.fullExecution.closure = await finishFleetTrip(context, MODULE, fullTrip)
if (proof.fullExecution.closure.status !== 'CARGO_COMPLETED_AND_RESOURCES_RELEASED') throw Error('FULL independent resources did not finish')
} finally { await disablePicker(context, fullPicker, 161) }
stage('RESERVATIONS'); await verifyFleetBookingReservation(context, MODULE, trip)
}
check('专项使用本轮绑定起运仓的PDA账号', picker?.token && picker.membershipId)
proof.picker = { membershipId: picker.membershipId, username: picker.username }
stage('EXTERNAL_RECEIPT'); await verifyFleetExternalReceipt(context, MODULE, trip, picker.token, { reconcilePalletized: resumePalletized, reconcileConfirmed: resumeReceiptConfirmed })
stage('EXTERNAL_DISPATCH'); await verifyFleetExternalDispatch(context, MODULE, trip)
if (full.proof?.status !== 'VERIFIED' || proof.fullExecution?.closure?.status !== 'CARGO_COMPLETED_AND_RESOURCES_RELEASED'
|| trip.reservation?.status !== 'VERIFIED' || partial.proof) throw Error('PARTIAL must retain its unpicked real main after independent FULL closure and reservation')
if (resumePalletized || resumeReceiptConfirmed) {
const child = Object.create(context); child.marker = proof.pickerMarker
const suffix = pickerFixtureSuffix(child), username = `pda_overseas_${suffix.toLowerCase()}_71`.slice(0, 64)
const users = await nativeRequest(context, 'GET', `/api/platform/v1/iam/users?${new URLSearchParams({page:'1',size:'200',keyword:username})}`)
const user = users.records?.find(row => id(row.membershipId) === id(proof.picker?.membershipId) && row.username === username)
if (!user || user.protectedAccount || id(user.tenantId) !== '0' || id(user.overseasWarehouseId) !== warehouseId
|| user.roleIds?.length !== 1 || id(user.roleIds[0]) !== id(access?.role?.id)) throw Error('Reviewed PDA identity or warehouse changed')
picker = { membershipId: user.membershipId, username, token: null }
const active = await nativeRequest(context, 'PUT', `/api/platform/v1/iam/users/${encodeURIComponent(id(user.membershipId))}/status`, {status:'ACTIVE'})
if (active.status !== 'ACTIVE') throw Error('Original owned PDA not active')
const login = await nativeRequest(context, 'POST', '/api/auth/login', {loginId:username,password:`Pda${suffix}A719`}, '')
picker.token = login.accessToken
} else {
const child = Object.create(context); proof.pickerMarker = `${context.marker}-FPDA-${randomUUID().slice(0,8)}`; child.marker = proof.pickerMarker
picker = access ? await createPicker(child, 71, warehouseId, access) : null
}
if (!picker?.token || !picker.membershipId) throw Error('Fresh bound-warehouse fleet PDA required')
proof.picker = {membershipId:picker.membershipId,username:picker.username}; await context.checkpoint?.()
stage('EXTERNAL_RECEIPT')
await verifyFleetExternalReceipt(context, MODULE, trip, picker.token, {reconcilePalletized:resumePalletized,reconcileConfirmed:resumeReceiptConfirmed})
stage('EXTERNAL_DISPATCH')
await verifyFleetExternalDispatch(context, MODULE, trip, {pickerToken:picker.token,
actualMeasurements: await currentFleetMeasurements(context, trip), coverageCase:partial})
const external = trip.externalDispatch
stage('FINANCE_CONSUMER')
const sources = await request('GET', `/api/platform/v1/last-leg/finance-fee-sources?${new URLSearchParams({ customerId, businessType: 'DELIVERY_ORDER', businessNo: external.deliveryNo })}`,
undefined, '读取外接应收稳定来源编号和版本')
const target = sources?.filter(row => row.feeItemCode === 'DELIVERY_FEE' && row.direction === 'RECEIVABLE' && row.sourceStatus === 'ACTIVE')
check('外接应收来源只属于本次客户派送及实收', target?.length === 1 && target[0].businessNo === external.deliveryNo
&& id(target[0].customerId) === customerId && Number(target[0].originalAmount) === 30 && target[0].originalCurrency === 'CNY')
const externalCustomer = id(external.customerId)
const sources = await nativeRequest(context, 'GET', `/api/platform/v1/last-leg/finance-fee-sources?${new URLSearchParams({customerId:externalCustomer,businessType:'DELIVERY_ORDER',businessNo:external.deliveryNo})}`)
const target = sources.filter(row => row.feeItemCode === 'DELIVERY_FEE' && row.direction === 'RECEIVABLE' && row.sourceStatus === 'ACTIVE'
&& row.inboundCode === trip.externalReceipt.incomingNo && id(row.customerId) === externalCustomer)
if (target.length !== 1 || Number(target[0].originalAmount) !== 30 || target[0].originalCurrency !== 'CNY') throw Error('Exact current-run external fee source missing')
let matched = []
for (let attempt = 0; attempt < 30; attempt++) {
const pool = await request('GET', `/api/platform/finance/v1/fee-pool?customerId=${encodeURIComponent(customerId)}&direction=RECEIVABLE`, undefined, '等待Finance消费本次外接费用')
check('Finance费用池为当前客户应收列表', Array.isArray(pool) && pool.every(row => id(row.customerId) === customerId && row.direction === 'RECEIVABLE'))
matched = pool.filter(row => id(row.sourceId) === id(target[0].sourceFeeId) && Number(row.sourceVersion) === Number(target[0].sourceVersion))
for (let attempt=0;attempt<30;attempt++) {
const pool = await nativeRequest(context, 'GET', `/api/platform/finance/v1/fee-pool?customerId=${encodeURIComponent(externalCustomer)}&direction=RECEIVABLE`)
if (!Array.isArray(pool) || pool.some(row => id(row.customerId) !== externalCustomer || row.direction !== 'RECEIVABLE')) throw Error('Finance consumer scope mismatch')
matched = pool.filter(row => id(row.sourceId) === id(target[0].sourceFeeId) && id(row.sourceVersion) === id(target[0].sourceVersion))
if (matched.length) break
await wait(1000)
if (attempt<29) await wait(1000)
}
check('Finance真实消费且仅一笔30CNY外接应收', matched.length === 1 && matched[0].sourceType === 'OVERSEAS_DELIVERY_FEE'
&& Number(matched[0].amount) === 30 && matched[0].currency === 'CNY')
proof.finance = { status: 'CONSUMER_PERSISTED', source: target[0], pool: matched, producerAck: 'NOT_VERIFIED' }
if (matched.length !== 1 || matched[0].sourceType !== 'OVERSEAS_DELIVERY_FEE' || Number(matched[0].amount) !== 30 || matched[0].currency !== 'CNY') throw Error('Finance did not persist exactly this external fee')
proof.finance = {status:'CONSUMER_PERSISTED',customerId:externalCustomer,source:target[0],pool:matched,producerAck:'NOT_VERIFIED'}
external.financeAcknowledgement = 'CONSUMER_PERSISTED_PRODUCER_ACK_NOT_VERIFIED'
stage('PICK_EACH_DISPATCH')
const dispatchNos = [partial.dispatchNo, external.dispatchNo]
proof.dispatchNos = dispatchNos
const dispatchDetails = []
for (const [index, dispatchNo] of dispatchNos.entries()) {
const pickingContext = Object.create(context); pickingContext.marker = `${context.marker}-FLEET-PICK-${index}`
check('逐客户正式PDA拣货及完成成功', await pickAll(pickingContext, picker.token, dispatchNo))
dispatchDetails.push(await request('GET', `${dispatchBase}/${encodeURIComponent(dispatchNo)}`, undefined, '回查逐单拣货数量及未发车事实'))
}
check('每张客户排车实际拣足但尚未发车', dispatchDetails.every(row => row.status === 'READY_TO_DEPART'
&& Number(row.pickedPackageCount) === Number(row.packageCount) && Number(row.shippedPackageCount) === 0))
stage('WHOLE_TRIP_DEPARTURE')
const recordVersion = await confirmFleetLoading(context, MODULE, trip)
proof.departureRequest = { recordVersion, idempotencyKey: key(), remark: '本轮自有货与真实外接货统一发车' }
const departPath = `/api/pda/v1/last-leg/dispatches/${encodeURIComponent(partial.dispatchNo)}/depart`
const departed = await request('POST', departPath, proof.departureRequest, 'PDA一次命令发出全车两张客户执行单', picker.token)
check('整车发车包含所有排车且箱数守恒', departed?.tripNo === trip.tripNo && departed.status === 'IN_TRANSIT'
&& JSON.stringify(departed.dispatches.map(row => row.dispatchNo).sort()) === JSON.stringify([...dispatchNos].sort())
&& Number(departed.shippedPackages) === dispatchDetails.reduce((n, row) => n + Number(row.packageCount), 0))
const departedReplay = await request('POST', departPath, proof.departureRequest, '整车发车同键重放', picker.token)
check('发车重放不改变整车实发数量', departedReplay?.replayed === true && Number(departedReplay.shippedPackages) === Number(departed.shippedPackages))
proof.departure = departed
await completeDepartedFleetScenario(context, trip, picker, dispatchDetails, { costActors, onHandoverReady })
const departure = await departNativeFleet(context, trip, picker.token, proof)
await completeDepartedFleetScenario(context, trip, picker, departure.dispatchDetails, {costActors,onHandoverReady})
} finally { await disablePicker(context, picker, 71) }
return proof
}
export async function completeDepartedFleetScenario(context, trip, picker, dispatchDetails, { costActors, onHandoverReady,
costsAlreadyVerified = false, reviewedVehicle,
verifyCosts = verifyFleetTripCosts, handover = runFleetHandoverStage, completeDelivery = saveReceiptAndComplete,
closeTrip = finishFleetTrip, cleanup = cleanupFleetHandoverResources } = {}) {
const proof = context.createdResources.fleetAcceptance, customerId = proof.customerId, warehouseId = proof.warehouseId
const partial = context.createdResources.fleetFeeCoverageCases.find(row => row.mode === 'PARTIAL')
const external = trip.externalDispatch, dispatchNos = proof.dispatchNos
const stage = value => { proof.stage = value; proof.checkpoints.push({ stage: value, at: new Date().toISOString() }); console.log('FLEET_STAGE=' + value) }
const check = (label, ok) => { if (!ok) throw Error(label) }
async function request(method, path) { const r = await context.request(method, path); check('Continuation read failed', success(r)); return r.body.data }
if (!costsAlreadyVerified) {
stage('TRIP_OPERATING_COSTS')
await verifyCosts(context, MODULE, trip, { ...costActors, shares: fleetCostShares(trip, dispatchDetails) })
} else check('Recovery cost proof must remain verified', trip.costAcceptance?.status === 'VERIFIED')
stage('IN_TRANSIT_HANDOVER')
await handover(context, trip, picker.token, dispatchDetails, {
reviewedVehicle,
onReady: onHandoverReady ? async fixture => {
stage('PDA_UI_CHECKPOINT')
await onHandoverReady({ fixture, picker })
} : undefined,
})
stage('RECEIPTS_AND_RESOURCE_RELEASE')
proof.receipts = []
if (context.context.fleetScenario === 'return-redispatch') {
stage('RETURN_REDISPATCH')
const returned = await verifyFleetReturnRedispatch(context, MODULE, trip, { customerId, warehouseId, pickerToken: picker.token,
dispatchNos, deliveryNos: [...partial.proof.deliveryNos, external.deliveryNo] })
proof.receipts = returned.originalSignatures.map(row => ({ deliveryNo: row.deliveryNo, signedAt: row.signedAt }))
context.context.saveFleetReturnRedispatchFixture(returned)
} else {
for (const [index, deliveryNo] of [...partial.proof.deliveryNos, external.deliveryNo].entries()) {
const delivery = await request('GET', `/api/platform/v1/last-leg/delivery-orders/${encodeURIComponent(deliveryNo)}?customerId=${encodeURIComponent(customerId)}`,
undefined, '签收前读取本次实际派送')
const completed = await completeDelivery(context, delivery.order, customerId, index + 71)
check('每个目的地真实保存签收资料后完成', completed?.order?.status === 'DELIVERED' && completed.order.signedAt)
proof.receipts.push({ deliveryNo, signedAt: completed.order.signedAt })
const proof=context.createdResources.fleetAcceptance,customerId=proof.customerId,warehouseId=proof.warehouseId
if(proof.protocol!==NATIVE_PROTOCOL||!nativeDeparture(proof.departure,trip.dispatchNo)||proof.dispatchNo!==trip.dispatchNo
||dispatchDetails?.length!==1||dispatchDetails[0].detail?.dispatchNo!==trip.dispatchNo)throw Error('Post-departure continuation requires one native main bundle and its actual DEPART command')
const stage=value=>{proof.stage=value;proof.checkpoints.push({stage:value,at:new Date().toISOString()});console.log('FLEET_STAGE='+value)}
if(!costsAlreadyVerified){stage('TRIP_OPERATING_COSTS');await verifyCosts(context,MODULE,trip,{...costActors,shares:fleetCostShares(trip,dispatchDetails)})}
else if(trip.costAcceptance?.status!=='VERIFIED')throw Error('Recovery cost proof must remain verified')
stage('IN_TRANSIT_HANDOVER')
await handover(context,trip,picker.token,dispatchDetails.map(bundle=>bundle.detail),{reviewedVehicle,
onReady:onHandoverReady?async fixture=>{stage('PDA_UI_CHECKPOINT');await onHandoverReady({fixture,picker})}:undefined})
stage('RECEIPTS_AND_RESOURCE_RELEASE');proof.receipts=[]
const current=await readNativeFleetBundle(context,trip.dispatchNo),refs=nativeFleetDeliveries(current.detail)
if(context.context.fleetScenario==='return-redispatch'){
stage('RETURN_REDISPATCH')
if(refs.some(ref=>id(ref.customerId)!==id(customerId)))throw Error('Explicit return-redispatch source selection requires the one actual source customer')
const returned=await verifyFleetReturnRedispatch(context,MODULE,trip,{customerId,warehouseId,pickerToken:picker.token,
dispatchNos:[trip.dispatchNo],deliveryNos:refs.map(ref=>ref.deliveryNo)})
proof.receipts=returned.originalSignatures.map(row=>({customerId:id(row.customerId||customerId),deliveryNo:row.deliveryNo,signedAt:row.signedAt}))
context.context.saveFleetReturnRedispatchFixture(returned)
}else{
for(const[index,ref]of refs.entries()){
const delivery=await nativeRequest(context,'GET',`/api/platform/v1/last-leg/delivery-orders/${encodeURIComponent(ref.deliveryNo)}?customerId=${encodeURIComponent(id(ref.customerId))}`)
if(delivery.order?.deliveryNo!==ref.deliveryNo||id(delivery.order.customerId)!==id(ref.customerId)||delivery.order.dispatchNo!==trip.dispatchNo)throw Error('POD source customer or dispatch drift')
const completed=await completeDelivery(context,delivery.order,id(ref.customerId),index+71)
if(completed?.order?.status!=='DELIVERED'||!completed.order.signedAt)throw Error('每个目的地真实保存签收资料后完成')
proof.receipts.push({customerId:id(ref.customerId),deliveryNo:ref.deliveryNo,signedAt:completed.order.signedAt})
}
}
proof.closure = await closeTrip(context, MODULE, trip)
check('本次车次货物结案及车辆司机释放完成', proof.closure?.status === 'CARGO_COMPLETED_AND_RESOURCES_RELEASED')
stage('HANDOVER_RESOURCE_CLEANUP')
await cleanup(context, MODULE, trip)
proof.status = 'HTTP_CHAIN_VERIFIED_WITH_ACK_GAP'; stage('FINISHED')
return proof
}
proof.closure=await closeTrip(context,MODULE,trip)
if(proof.closure?.status!=='CARGO_COMPLETED_AND_RESOURCES_RELEASED')throw Error('本次车次货物结案及车辆司机释放完成')
stage('HANDOVER_RESOURCE_CLEANUP');await cleanup(context,MODULE,trip)
proof.status='HTTP_CHAIN_VERIFIED_WITH_ACK_GAP';stage('FINISHED');return proof
}
export async function continueReviewedDeparture(context, { verify = verifyDepartedRecovery, prepareActors = prepareFleetCostActors,
costsAlreadyVerified = false, createdVehicle,
resolveAccess = resolvePickerAccess, makePicker = createPicker, closePicker = disablePicker,