feat(dispatch): unify whole vehicle scheduling, source ownership and staged execution
This commit is contained in:
@@ -4,7 +4,7 @@ import { verifyPalletLabels } from './phase3-loading-finalize.mjs'
|
||||
import { stableLoadedCartonMarks } from './phase3-loading.mjs'
|
||||
import { awaitProducerConsumption, convertReceivingContainers, runReceiving,
|
||||
resolvePickerAccess, createPicker, disablePicker } from './phase4-overseas-pda.mjs'
|
||||
import { pickAll, verifyDepartureReadiness, saveReceiptAndComplete } from './phase5-overseas-fulfillment.mjs'
|
||||
import { pickAll, verifyDepartureReadiness, saveReceiptAndComplete, arrangeDispatchDelivery, fulfillmentEvidenceContext } from './phase5-overseas-fulfillment.mjs'
|
||||
import { isDeepStrictEqual } from 'node:util'
|
||||
import { assertTestDeliveryFaultProof } from '../test-delivery-fault-proof.mjs'
|
||||
import { prepareTerminalDrayage } from './phase5-terminal-drayage.mjs'
|
||||
@@ -15,7 +15,17 @@ const MODULE = 'phase5-root-fulfillment'
|
||||
const OPTIONS = { scenario: 'f40-real-root-and-remainder-signatures', deploymentBlocker: true }
|
||||
const DISPATCH = '/api/platform/v1/last-leg/dispatches'
|
||||
const PDA = '/api/pda/v1/last-leg/dispatches'
|
||||
const id = value => value == null ? '' : String(value)
|
||||
const id = value => {
|
||||
if (typeof value === 'number' && !Number.isSafeInteger(value)) throw new Error('F40业务ID精度无效')
|
||||
return value == null ? '' : String(value)
|
||||
}
|
||||
const validVersion = value => (typeof value === 'string' || typeof value === 'number' && Number.isSafeInteger(value))
|
||||
&& /^(0|[1-9]\d*)$/.test(String(value))
|
||||
const quantity = value => /^(0|[1-9]\d*)$/.test(String(value)) && Number.isSafeInteger(Number(value)) ? Number(value) : null
|
||||
const wireVersion = value => {
|
||||
if (!validVersion(value)) throw new Error('F40服务器版本缺失或精度无效')
|
||||
return value
|
||||
}
|
||||
const success = response => response?.http === 200 && String(response?.body?.code) === '200'
|
||||
const query = (path, values) => `${path}?${new URLSearchParams(values)}`
|
||||
|
||||
@@ -66,11 +76,12 @@ export function preserveReceivingEvidence(proof, scope) {
|
||||
proof.receivingCandidateEvidence = scope.createdResources?.phase4OverseasPda?.candidateWaitEvidence || []
|
||||
}
|
||||
export function validFreshReceiptSave(before, after) {
|
||||
const previousVersion = Number(before?.documents?.version)
|
||||
const nextVersion = Number(after?.documents?.version)
|
||||
if (!Number.isSafeInteger(previousVersion) || nextVersion !== previousVersion + 1
|
||||
|| Number(before?.delivery?.order?.version) !== previousVersion
|
||||
|| Number(after?.delivery?.order?.version) !== nextVersion
|
||||
if (![before?.documents?.version, after?.documents?.version, before?.delivery?.order?.version, after?.delivery?.order?.version].every(validVersion)) return false
|
||||
const previousVersion = BigInt(before.documents.version)
|
||||
const nextVersion = BigInt(after.documents.version)
|
||||
if (nextVersion !== previousVersion + 1n
|
||||
|| BigInt(before.delivery.order.version) !== previousVersion
|
||||
|| BigInt(after.delivery.order.version) !== nextVersion
|
||||
|| before?.delivery?.order?.status !== 'DELIVERED' || !before.delivery.order.signedAt) return false
|
||||
const same = (left,right) => JSON.stringify(left) === JSON.stringify(right)
|
||||
if (!same(before.documents,{...after.documents,version:before.documents.version})) return false
|
||||
@@ -87,9 +98,9 @@ export function validFreshReceiptSave(before, after) {
|
||||
operations:oldOperations})) return false
|
||||
if (!Array.isArray(before.revisions) || !Array.isArray(after.revisions)
|
||||
|| after.revisions.length !== before.revisions.length + 1) return false
|
||||
const addedRevisions = after.revisions.filter(row => Number(row.version) === nextVersion)
|
||||
const addedRevisions = after.revisions.filter(row => validVersion(row.version) && BigInt(row.version) === nextVersion)
|
||||
return addedRevisions.length === 1 && Boolean(addedRevisions[0].savedAt && addedRevisions[0].operatorId)
|
||||
&& same(before.revisions,after.revisions.filter(row => Number(row.version) !== nextVersion))
|
||||
&& same(before.revisions,after.revisions.filter(row => !validVersion(row.version) || BigInt(row.version) !== nextVersion))
|
||||
&& same(addedRevisions[0].snapshot,after.documents)
|
||||
}
|
||||
export function resolveF40MerchantCredentials(context) {
|
||||
@@ -102,18 +113,48 @@ export function resolveF40MerchantCredentials(context) {
|
||||
if (!fixture.username || !fixture.password || !id(fixture.tenantId)) return null
|
||||
return { username:fixture.username, password:fixture.password, customerId:id(fixture.tenantId) }
|
||||
}
|
||||
export function f40TransferBody(marker, index, vehicle, now = new Date()) {
|
||||
return {
|
||||
idempotencyKey:`${marker}-F40-TRANSFER-${index}`,
|
||||
plannedDeliveryAt:f40LocalDateTime(new Date(now.getTime()+172800000)),
|
||||
deliveryMethod:'TRUCK_PALLET', waybillNo:`${marker}-WAYBILL-${index}`.slice(0,64),
|
||||
carrierName:vehicle.carrierName, pickupMethod:'WAREHOUSE_PICKUP', loadingType:'PALLET',
|
||||
vehicleType:'TRUCK', vehicleSize:'15托', driverName:vehicle.driverName,
|
||||
driverPhone:vehicle.driverPhone, vehiclePlate:vehicle.vehiclePlate,
|
||||
appointmentAccount:'SELF', sellerName:null, remark:`${marker}-phase5-transfer`,
|
||||
fees:[{feeType:'DELIVERY_FEE',feeName:'派送费',direction:'RECEIVABLE',currency:'USD',amount:10,
|
||||
remark:`${marker}-acceptance`}],
|
||||
}
|
||||
/** 本片只接受正式候选的冻结选择器和FBA逐票来源,不补猜来源版本。 */
|
||||
export function f40CreateDispatchBody(marker, warehouseId, customerId, candidates) {
|
||||
if (!Array.isArray(candidates) || !candidates.length) throw new Error('F40缺少本片真实候选')
|
||||
const ids = new Set(), batchAllocations = []
|
||||
const positive = value => /^[1-9]\d*$/.test(id(value))
|
||||
const cargo = candidates.map(candidate => {
|
||||
const selection = candidate?.selection
|
||||
if (!selection || selection.sourceType !== 'INBOUND_PLAN' || !positive(selection.planPalletId)
|
||||
|| !positive(selection.warehousePalletId) || selection.returnReceiptId != null || selection.relabelNo != null
|
||||
|| candidate.sourceKey !== `PLAN:${id(selection.planPalletId)}` || ids.has(candidate.sourceKey)
|
||||
|| !/^[a-f\d]{64}$/i.test(selection.sourceVersion || '') || candidate.sourceVersion !== selection.sourceVersion
|
||||
|| id(candidate.customerId) !== id(customerId) || id(candidate.warehouseId) !== id(warehouseId)
|
||||
|| quantity(candidate.availablePackages) == null || quantity(candidate.availablePackages) <= 0
|
||||
|| candidate.destination?.type !== 'FBA' || !candidate.destination.key
|
||||
|| !Array.isArray(candidate.blockReasons) || candidate.blockReasons.length
|
||||
|| !Array.isArray(candidate.appointmentBatches)) throw new Error('F40本片候选身份、库存或冻结版本不完整')
|
||||
ids.add(candidate.sourceKey)
|
||||
if (candidate.appointmentBatches.length) {
|
||||
if (candidate.appointmentBatches.length !== 1) throw new Error('F40跨批次缺少本轮真实逐票分配,不推算')
|
||||
const batch = candidate.appointmentBatches[0]
|
||||
if (!/^FBA_APPOINTMENT:[1-9]\d*$/.test(batch.batchKey || '') || id(batch.customerId) !== id(customerId)
|
||||
|| !Array.isArray(batch.sources) || !batch.sources.length
|
||||
|| batch.sources.some(source => !/^(BOOKING:.+|ORDER:[1-9]\d*)$/.test(source.sourceKey || '')
|
||||
|| source.destinationKey !== candidate.destination.key || quantity(source.packageCount) == null || quantity(source.packageCount) <= 0)
|
||||
|| batch.sources.reduce((sum, source) => sum + quantity(source.packageCount), 0) !== quantity(candidate.availablePackages))
|
||||
throw new Error('F40预约批次与本片逐票实物不一致')
|
||||
for (const source of batch.sources) batchAllocations.push({ planPalletId: id(selection.planPalletId), batchKey: batch.batchKey,
|
||||
customerId: id(customerId), sourceKey: source.sourceKey, destinationKey: source.destinationKey,
|
||||
packageCount: quantity(source.packageCount), recordVersion: wireVersion(batch.recordVersion) })
|
||||
}
|
||||
return { sourceType: selection.sourceType, planPalletId: id(selection.planPalletId),
|
||||
warehousePalletId: id(selection.warehousePalletId), returnReceiptId: null, relabelNo: null, sourceVersion: selection.sourceVersion }
|
||||
})
|
||||
return { idempotencyKey: `${marker}-CREATE`.slice(0, 200), warehouseId: id(warehouseId), vehicle: null, driver: null,
|
||||
plannedDepartureAt: null, estimatedArrivalAt: null, remark: `${marker}-F40-native`, cargo, batchAllocations }
|
||||
}
|
||||
/** 调用方可提供只读实测读取器;不按箱数或计费重量构造物理实载值。 */
|
||||
export async function resolveF40LoadingMeasurements(context, dispatchNo, producer, warehouseId) {
|
||||
const source = context.context?.f40LoadingMeasurements
|
||||
if (typeof source === 'function') return source({ environment: 'TEST', marker: context.marker, dispatchNo,
|
||||
warehouseId: id(warehouseId), sourceInboundId: id(producer.sourceInboundId), inboundCode: producer.inboundCode })
|
||||
return source?.[dispatchNo]
|
||||
}
|
||||
export function validFragmentCabinetIdentity(workbench, fragment, task) {
|
||||
const container = workbench?.container
|
||||
@@ -129,7 +170,7 @@ export function fulfillmentHistorySnapshot(detail) {
|
||||
return {
|
||||
orderId: id(detail?.order?.id), rootOrderId: id(detail?.order?.rootOrderId),
|
||||
parentOrderId: id(detail?.order?.parentOrderId), role: detail?.order?.orderRole,
|
||||
status: detail?.order?.status, version: Number(detail?.order?.version),
|
||||
status: detail?.order?.status, version: detail?.order?.version,
|
||||
inboundCode: detail?.order?.inboundCode, orderNo: detail?.order?.orderNo,
|
||||
rawOrder: detail?.order,
|
||||
histories: (detail?.statusHistory || []).map(row => ({ ...row, id: id(row.id) })),
|
||||
@@ -443,61 +484,110 @@ async function fragment(context, source, rootOrderId, sequence) {
|
||||
shippingReady: true, carrierScheduleQueryId: pickup.queryId, carrierLatestActualAt: pickup.latestActualAt,
|
||||
palletLabelManifest, rootOrderId }
|
||||
}
|
||||
async function sendFragment(context, producer, picker, merchantToken, index) {
|
||||
const customerId = producer.tenantId
|
||||
const candidates = await api(context, 'GET', '读取该来源入仓码实收排车候选', `${DISPATCH}/candidates?sourceStage=RECEIVED`)
|
||||
const matches = (Array.isArray(candidates) ? candidates : []).filter(row => row.sourceStage === 'RECEIVED'
|
||||
&& row.inboundCode === producer.inboundCode && id(row.overseasWarehouseId) === id(picker.warehouseId))
|
||||
check(context, 'F40 只选择该根履约片的真实实收托盘', matches.length > 0
|
||||
&& matches.every(row => Number(row.packageCount) > 0), `inboundCode=${producer.inboundCode}, pallets=${matches.length}`)
|
||||
const dispatch = await api(context, 'POST', '该履约片创建独立真实排车', `${DISPATCH}/pre-arrange`, {
|
||||
planPalletIds: matches.map(row => id(row.planPalletId)),
|
||||
})
|
||||
const dispatchNo = dispatch.dispatchNo
|
||||
const vehicle = {
|
||||
vehiclePlate: `F40-${index}-${context.marker.slice(-6)}`.slice(0,64), driverName: 'F40验收司机',
|
||||
driverPhone: '19900000000', carrierName: 'F40测试车队',
|
||||
estimatedArrivalAt: f40LocalDateTime(new Date(Date.now()+86400000)),
|
||||
}
|
||||
await api(context, 'PUT', '补齐该片车辆司机', `${DISPATCH}/${dispatchNo}`, vehicle)
|
||||
const transfer = await api(context, 'POST', '客户正式创建该片派送单',
|
||||
query('/api/merchant/v1/last-leg/delivery-orders', { dispatchNo }),
|
||||
f40TransferBody(context.marker,index,vehicle), merchantToken)
|
||||
const deliveries = Array.isArray(transfer.deliveries) ? transfer.deliveries : []
|
||||
check(context, 'F40 同目的仓一片对应唯一派送单', deliveries.length === 1 && Boolean(deliveries[0]?.deliveryNo), `count=${deliveries.length}`)
|
||||
const deliveryPath = query(`/api/platform/v1/last-leg/delivery-orders/${deliveries[0].deliveryNo}`, { customerId })
|
||||
const detail = await api(context, 'GET', '核对派送实际 SKU 仅来自本片入仓码', deliveryPath)
|
||||
check(context, 'F40 派送实际物品来源和一箱数量严格匹配', detail.order?.customerId === customerId
|
||||
&& detail.skus?.length > 0 && detail.skus.every(row => row.inboundCode === producer.inboundCode)
|
||||
&& detail.skus.reduce((sum,row) => sum + Number(row.packageCount),0) === 1, `source=${producer.inboundCode}`)
|
||||
const F40_DISPATCH_DEPENDENCIES = { prepareFleetTrip, pickAll, confirmFleetLoading, arrangeDispatchDelivery,
|
||||
verifyDepartureReadiness, saveReceiptAndComplete, finishFleetTrip, fulfillmentEvidenceContext }
|
||||
|
||||
export async function sendFragment(context, producer, picker, merchantToken, index, overrides = {}) {
|
||||
const { prepareFleetTrip, pickAll, confirmFleetLoading, arrangeDispatchDelivery, verifyDepartureReadiness,
|
||||
saveReceiptAndComplete, finishFleetTrip, fulfillmentEvidenceContext } = { ...F40_DISPATCH_DEPENDENCIES, ...overrides }
|
||||
const customerId = id(producer.tenantId), warehouseId = id(picker.warehouseId)
|
||||
context.marker = `${context.context.f40BaseMarker}-SEND-${index}`
|
||||
const tripProof = await prepareFleetTrip(context, MODULE, dispatchNo, picker.warehouseId)
|
||||
check(context, 'F40 正式 PDA 拣货完成', await pickAll(context, picker.token, dispatchNo), `dispatch=${dispatchNo}`)
|
||||
const tripVersion = await confirmFleetLoading(context, MODULE, tripProof)
|
||||
check(context, 'F40 发车前置就绪', await verifyDepartureReadiness(context, picker.token, dispatchNo), `dispatch=${dispatchNo}`)
|
||||
const departed = await api(context, 'POST', 'F40 正式 PDA 发车', `${PDA}/${dispatchNo}/depart`,
|
||||
{ recordVersion: tripVersion, idempotencyKey: `${context.marker}-DEPART`.slice(0,128), remark: context.marker }, picker.token)
|
||||
check(context, 'F40 整车发车包含本片排车', departed.status === 'IN_TRANSIT' && departed.tripNo === tripProof.tripNo
|
||||
&& departed.dispatches?.some(item => item.dispatchNo === dispatchNo), `dispatch=${dispatchNo}`)
|
||||
const outbound = await api(context, 'GET', '回查实际来源出库完成',
|
||||
query(`/api/platform/v1/last-leg/outbound-orders/${transfer.outboundNo}`, { page:'1',size:'20' }))
|
||||
check(context,'F40 实际出库单 OUTBOUNDED 且来源入仓码匹配', outbound.outbound?.status === 'OUTBOUNDED'
|
||||
&& outbound.pallets?.length > 0 && outbound.pallets.every(row => row.inboundCode === producer.inboundCode),
|
||||
`outbound=${transfer.outboundNo}`)
|
||||
const beforeDelivery = await api(context, 'GET', '读取本片派送推进前版本', deliveryPath)
|
||||
const delivering = await api(context,'PUT','实际派送推进配送中',
|
||||
query(`/api/platform/v1/last-leg/delivery-orders/${deliveries[0].deliveryNo}/status`,{customerId}),
|
||||
{status:'DELIVERING',expectedVersion:Number(beforeDelivery.order.version),remark:context.marker})
|
||||
check(context,'F40 派送处于配送中后保存签收',delivering.deliveryStatus === 'DELIVERING',`delivery=${deliveries[0].deliveryNo}`)
|
||||
const completed = await withDeliveryEventRecovery(context, {
|
||||
marker: context.context.f40BaseMarker, customerId: id(customerId), deliveryId: id(beforeDelivery.order.id),
|
||||
sourceInboundId: id(producer.sourceInboundId), sourceOrderId: id(producer.sourceOrderId), rootOrderId: id(producer.rootOrderId),
|
||||
}, id(picker.warehouseId), index, () => saveReceiptAndComplete(context, deliveries[0], customerId, index))
|
||||
check(context, 'F40 实际签收资料驱动派送完成', completed?.order?.status === 'DELIVERED', `delivery=${deliveries[0].deliveryNo}`)
|
||||
check(context,'F40 签收完成记录真实签收时间', Boolean(completed.order.signedAt), `delivery=${deliveries[0].deliveryNo}`)
|
||||
const tripClosure = await finishFleetTrip(context, MODULE, tripProof)
|
||||
return { producer, dispatchNo, outboundNo: transfer.outboundNo, deliveryNo: deliveries[0].deliveryNo,
|
||||
receiptVersion: completed.order.version, signedAt: completed.order.signedAt, receiptSnapshot:completed, tripNo: tripProof.tripNo, tripClosure }
|
||||
const rootProof = context.context.f40RootFulfillmentProof
|
||||
check(context, 'F40 排车具有本轮根来源生产回执', Boolean(rootProof && producer.inboundCode && id(producer.sourceInboundId)
|
||||
&& rootProof.fragments?.some(row => id(row.sourceInboundId) === id(producer.sourceInboundId)
|
||||
&& row.inboundCode === producer.inboundCode && id(row.tenantId) === customerId
|
||||
&& id(row.containerBookingId) === id(producer.containerBookingId))), 'actual-root-source')
|
||||
const dispatchProof = { marker: context.marker, customerId, warehouseId, sourceInboundId: id(producer.sourceInboundId),
|
||||
inboundCode: producer.inboundCode, dispatchNo: null, stage: 'PREPARING' }
|
||||
;(rootProof.dispatches ||= []).push(dispatchProof)
|
||||
// 复用同一已验证的写前落盘/未知停写助手;证据对象也保留在根单证明中。
|
||||
context.createdResources.phase5OverseasFulfillment = dispatchProof
|
||||
context = await fulfillmentEvidenceContext(context)
|
||||
try {
|
||||
const candidates = await api(context, 'GET', '读取该来源入仓码原生实收排车候选',
|
||||
query(`${DISPATCH}/candidates`, { warehouseId }))
|
||||
check(context, 'F40 正式候选列表完整', candidates?.complete === true && Array.isArray(candidates.candidates), 'native-complete-candidate-list')
|
||||
const matches = candidates.candidates.filter(row => row.selection?.sourceType === 'INBOUND_PLAN'
|
||||
&& row.inboundCode === producer.inboundCode && id(row.customerId) === customerId && id(row.warehouseId) === warehouseId)
|
||||
check(context, 'F40 只选择本片完整真实实收候选且总量一箱', candidates?.complete === true && matches.length > 0
|
||||
&& matches.reduce((sum, row) => sum + Number(row.availablePackages), 0) === 1, `inboundCode=${producer.inboundCode}, pallets=${matches.length}`)
|
||||
dispatchProof.candidates = matches
|
||||
const createBody = f40CreateDispatchBody(context.marker, warehouseId, customerId, matches)
|
||||
const dispatch = await api(context, 'POST', '该履约片通过原生创建独立真实整车', DISPATCH, createBody)
|
||||
const dispatchNo = dispatch?.dispatchNo
|
||||
check(context, 'F40 创建返回原生整车业务身份及装车前阶段', Boolean(dispatchNo) && dispatch.status === 'ACTIVE'
|
||||
&& dispatch.stage === 'BEFORE_LOADING' && validVersion(dispatch.recordVersion), 'native-create-result')
|
||||
dispatchProof.dispatchNo = dispatchNo; dispatchProof.createResult = dispatch
|
||||
const createReplay = await api(context, 'POST', 'F40 原生建单原命令重放', DISPATCH, createBody)
|
||||
check(context, 'F40 建单重放只恢复原版本及业务结果', createReplay.replayed === true && createReplay.dispatchNo === dispatchNo
|
||||
&& String(createReplay.recordVersion) === String(dispatch.recordVersion) && JSON.stringify(createReplay.result) === JSON.stringify(dispatch.result), 'immutable-create-result')
|
||||
dispatchProof.action = 'PREPARE_AND_REVIEW'
|
||||
const tripProof = await prepareFleetTrip(context, MODULE, dispatchNo, warehouseId)
|
||||
dispatchProof.trip = tripProof
|
||||
dispatchProof.action = 'PDA_PICK'
|
||||
check(context, 'F40 正式 PDA 拣货核平仍待显式装车', await pickAll(context, picker.token, dispatchNo), `dispatch=${dispatchNo}`)
|
||||
dispatchProof.action = 'LOADING_COMPLETE'
|
||||
const actualMeasurements = await resolveF40LoadingMeasurements(context, dispatchNo, producer, warehouseId)
|
||||
await confirmFleetLoading(context, MODULE, tripProof, picker.token, actualMeasurements)
|
||||
dispatchProof.action = 'ARRANGE_DELIVERY'
|
||||
const transfer = await arrangeDispatchDelivery(context, dispatchNo)
|
||||
check(context, 'F40 平台装车后一次安排本片完整客户派送', Boolean(transfer), `dispatch=${dispatchNo}`)
|
||||
const deliveries = transfer.deliveries, outbounds = transfer.outbounds
|
||||
check(context, 'F40 同客户同目的仓一片对应唯一出库与派送', deliveries?.length === 1 && outbounds?.length === 1
|
||||
&& id(deliveries[0].customerId) === customerId && id(outbounds[0].customerId) === customerId
|
||||
&& deliveries[0].outboundNo === outbounds[0].outboundNo && Number(deliveries[0].packageCount) === 1
|
||||
&& deliveries[0].destination?.key === matches[0].destination.key, 'one-actual-customer-destination')
|
||||
const outboundNo = outbounds[0].outboundNo
|
||||
dispatchProof.arrangement = transfer.data?.result; dispatchProof.outboundNo = outboundNo
|
||||
dispatchProof.deliveryNo = deliveries[0].deliveryNo
|
||||
const deliveryPath = query(`/api/platform/v1/last-leg/delivery-orders/${deliveries[0].deliveryNo}`, { customerId })
|
||||
const detail = await api(context, 'GET', '核对派送实际 SKU 仅来自本片入仓码', deliveryPath)
|
||||
check(context, 'F40 派送实际物品来源和一箱数量严格匹配', id(detail.order?.customerId) === customerId
|
||||
&& detail.skus?.length > 0 && detail.skus.every(row => row.inboundCode === producer.inboundCode)
|
||||
&& detail.skus.reduce((sum,row) => sum + Number(row.packageCount),0) === 1, `source=${producer.inboundCode}`)
|
||||
dispatchProof.action = 'PDA_DEPART'
|
||||
const ready = await verifyDepartureReadiness(context, picker.token, dispatchNo)
|
||||
check(context, 'F40 双端当前公共版本发车门禁就绪', Boolean(ready), `dispatch=${dispatchNo}`)
|
||||
const departBody = { recordVersion: wireVersion(ready.data.recordVersion),
|
||||
idempotencyKey: `${context.marker}-DEPART`.slice(0,128), remark: context.marker }
|
||||
const departed = await api(context, 'POST', 'F40 正式 PDA 实际发车', `${PDA}/${dispatchNo}/depart`, departBody, picker.token)
|
||||
check(context, 'F40 本片原生发车进入DELIVERING并且真实出库一箱', departed.dispatchNo === dispatchNo
|
||||
&& departed.status === 'ACTIVE' && departed.stage === 'DELIVERING' && Number(departed.result?.shippedPackages) === 1
|
||||
&& departed.result?.deliveries?.length === 1 && departed.result.deliveries[0].deliveryNo === deliveries[0].deliveryNo, `dispatch=${dispatchNo}`)
|
||||
const departureReplay = await api(context, 'POST', 'F40 实际发车原命令重放', `${PDA}/${dispatchNo}/depart`, departBody, picker.token)
|
||||
check(context, 'F40 发车重放保留原公共版本及全部发运结果', departureReplay.replayed === true
|
||||
&& String(departureReplay.recordVersion) === String(departed.recordVersion)
|
||||
&& JSON.stringify(departureReplay.result) === JSON.stringify(departed.result), 'immutable-departure-result')
|
||||
dispatchProof.departure = departed; dispatchProof.departureReplay = departureReplay; dispatchProof.stage = 'DELIVERING'
|
||||
const outbound = await api(context, 'GET', '回查实际来源出库完成', `/api/platform/v1/last-leg/outbound-orders/${outboundNo}`)
|
||||
check(context,'F40 实际出库单 OUTBOUNDED 且来源入仓码匹配', id(outbound.outbound?.customerId) === customerId
|
||||
&& outbound.outbound?.outboundNo === outboundNo && outbound.outbound?.status === 'OUTBOUNDED'
|
||||
&& outbound.pallets?.length > 0 && outbound.pallets.every(row => row.inboundCode === producer.inboundCode), `outbound=${outboundNo}`)
|
||||
const beforeDelivery = await api(context, 'GET', '回查实际发车推进本片派送及其版本', deliveryPath)
|
||||
check(context, 'F40 实际发车已推进派送DELIVERING且身份未漂移', beforeDelivery.order?.status === 'DELIVERING'
|
||||
&& id(beforeDelivery.order.customerId) === customerId && beforeDelivery.order.outboundNo === outboundNo
|
||||
&& validVersion(beforeDelivery.order.version), 'actual-departure-delivery-facts')
|
||||
dispatchProof.action = 'SAVE_RECEIPT_AND_EVENT_RECOVERY'
|
||||
const completed = await withDeliveryEventRecovery(context, {
|
||||
marker: context.context.f40BaseMarker, customerId: id(customerId), deliveryId: id(beforeDelivery.order.id),
|
||||
sourceInboundId: id(producer.sourceInboundId), sourceOrderId: id(producer.sourceOrderId), rootOrderId: id(producer.rootOrderId),
|
||||
}, id(picker.warehouseId), index, () => saveReceiptAndComplete(context, deliveries[0], customerId, index))
|
||||
check(context, 'F40 实际签收资料驱动派送完成', completed?.order?.status === 'DELIVERED', `delivery=${deliveries[0].deliveryNo}`)
|
||||
check(context,'F40 签收完成记录真实签收时间', Boolean(completed.order.signedAt), `delivery=${deliveries[0].deliveryNo}`)
|
||||
const nativeCompletion = await waitState(() => api(context, 'GET', 'F40 回查本片全部正常签收汇总阶段', `${DISPATCH}/${dispatchNo}`),
|
||||
value => value?.status === 'ACTIVE' && value?.stage === 'DELIVERED')
|
||||
check(context, 'F40 正常签收自动汇总原生整车DELIVERED', nativeCompletion?.dispatchNo === dispatchNo
|
||||
&& nativeCompletion.status === 'ACTIVE' && nativeCompletion.stage === 'DELIVERED', 'actual-normal-signature-completion')
|
||||
dispatchProof.normalCompletion = nativeCompletion; dispatchProof.stage = 'DELIVERED'
|
||||
const tripClosure = await finishFleetTrip(context, MODULE, tripProof)
|
||||
dispatchProof.tripClosure = tripClosure
|
||||
await context.checkpoint()
|
||||
return { producer, dispatchNo, outboundNo, deliveryNo: deliveries[0].deliveryNo,
|
||||
receiptVersion: completed.order.version, signedAt: completed.order.signedAt, receiptSnapshot:completed, tripNo: tripProof.tripNo, tripClosure }
|
||||
} catch (error) {
|
||||
dispatchProof.stage = 'FAILED'; dispatchProof.failedAction = dispatchProof.action || 'CREATE'
|
||||
try { await context.checkpoint() } finally { throw error }
|
||||
}
|
||||
}
|
||||
|
||||
/** Only the first actual FBA signature is faulted; caller performs the normal receipt command. */
|
||||
@@ -763,7 +853,7 @@ async function runRootFulfillment(context) {
|
||||
revisions:await api(scope,'GET','F40 新键前冻结资料版本历史',historyPath)}
|
||||
check(scope,'F40 新键前原签收事实保持完整稳定',
|
||||
JSON.stringify(signed.receiptSnapshot) === JSON.stringify(beforeFresh.delivery),`delivery=${signed.deliveryNo}`)
|
||||
const saveBody = {expectedVersion:Number(current.version),idempotencyKey:`${scope.marker}-SAVE`,bolNo:current.bolNo || null,documents}
|
||||
const saveBody = {expectedVersion:wireVersion(current.version),idempotencyKey:`${scope.marker}-SAVE`,bolNo:current.bolNo || null,documents}
|
||||
const receiptProof = {deliveryNo:signed.deliveryNo,beforeFresh,saveBody,afterFresh:{},afterSameKey:{}}
|
||||
;(proof.receiptReplayProofs ||= []).push(receiptProof)
|
||||
const saved = await api(scope,'PUT','重复已完成派送文件保存不得重发签收事实',
|
||||
|
||||
Reference in New Issue
Block a user