feat(dispatch): unify whole vehicle scheduling, source ownership and staged execution
This commit is contained in:
@@ -2,6 +2,9 @@ import { createPicker, disablePicker, resolvePickerAccess } from './phase4-overs
|
||||
import { run as createMerchant } from './merchant-qualification-fixture.mjs'
|
||||
import { prepareFleetTrip, confirmFleetLoading, finishFleetTrip } from './fleet-trip-fulfillment.mjs'
|
||||
import { verifyFleetTripFinance } from './fleet-trip-finance-acceptance.mjs'
|
||||
import { mkdir, writeFile, rename, readFile } from 'node:fs/promises'
|
||||
import { resolve, dirname } from 'node:path'
|
||||
import { createHash } from 'node:crypto'
|
||||
|
||||
const MODULE = 'phase5-overseas-fulfillment'
|
||||
const SCENARIO = 'phase5-dispatch-outbound-delivery-exception-visit'
|
||||
@@ -11,7 +14,86 @@ const LASTLEG_BASE = '/api/platform/v1/last-leg'
|
||||
const PDA_DISPATCH_BASE = '/api/pda/v1/last-leg/dispatches'
|
||||
const MERCHANT_DELIVERY_BASE = '/api/merchant/v1/last-leg/delivery-orders'
|
||||
|
||||
function id(value) { return value == null ? '' : String(value) }
|
||||
function id(value) {
|
||||
if (typeof value === 'number' && !Number.isSafeInteger(value)) throw new Error('业务ID精度无效')
|
||||
return value == null ? '' : String(value)
|
||||
}
|
||||
function version(value) {
|
||||
if ((typeof value === 'number' && !Number.isSafeInteger(value)) || !/^(0|[1-9]\d*)$/.test(String(value)))
|
||||
throw new Error('服务端recordVersion缺失或精度无效')
|
||||
return value
|
||||
}
|
||||
|
||||
// 发送前保存原命令;未知写结果只保留证据,禁止本轮继续推进。
|
||||
export async function fulfillmentEvidenceContext(context) {
|
||||
const marker = String(context.marker)
|
||||
if (!/^[A-Za-z0-9_-]+$/.test(marker)) throw new Error('验收marker不能用于独占检查点')
|
||||
const path = resolve(dirname(context.options?.output || 'reports/admin-test-env.json'), 'phase5-checkpoints', `${marker}.json`)
|
||||
try { await readFile(path); throw new Error(`已存在本轮履约检查点,必须核对原命令结果后续验:${path}`) }
|
||||
catch (error) { if (error.code !== 'ENOENT') throw error }
|
||||
const redact = value => {
|
||||
if (Array.isArray(value)) return value.map(redact)
|
||||
if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value)
|
||||
.filter(([name]) => !/password|token|authorization|base64|^bytes$|^content$/i.test(name))
|
||||
.map(([name, nested]) => [name, redact(nested)]))
|
||||
return value
|
||||
}
|
||||
const evidence = { marker, status: 'PREPARED', requests: [] }
|
||||
const scoped = Object.create(context)
|
||||
let stopped = false
|
||||
scoped.checkpoint = async () => {
|
||||
await mkdir(dirname(path), { recursive: true })
|
||||
await writeFile(`${path}.tmp`, `${JSON.stringify(redact({ ...evidence,
|
||||
fulfillment: context.createdResources.phase5OverseasFulfillment,
|
||||
fleetTrips: context.createdResources.fleetTrips }), null, 2)}\n`, 'utf8')
|
||||
await rename(`${path}.tmp`, path)
|
||||
}
|
||||
scoped.request = async (method, requestPath, body, ...rest) => {
|
||||
if (method === 'GET' || /\/auth\//.test(requestPath)) return context.request(method, requestPath, body, ...rest)
|
||||
const identityCleanup = method === 'PUT' && /^\/api\/platform\/v1\/iam\/users\/[0-9]+\/status$/.test(requestPath) && body?.status === 'DISABLED'
|
||||
if (stopped && !identityCleanup) throw new Error('存在未知写结果,保留原命令并停止后续写入')
|
||||
const entry = { method, path: requestPath, body: redact(body), status: 'SENDING' }
|
||||
evidence.requests.push(entry)
|
||||
await scoped.checkpoint()
|
||||
let response
|
||||
try { response = await context.request(method, requestPath, body, ...rest) }
|
||||
catch (error) {
|
||||
stopped = true; entry.status = 'UNKNOWN'; evidence.status = 'UNKNOWN'; await scoped.checkpoint(); throw error
|
||||
}
|
||||
const nativeCommand = /^\/api\/(?:platform|pda)\/v1\/last-leg\/dispatches(?:\/|$)/.test(requestPath)
|
||||
const knownResponse = response?.http > 0 && response.http < 500 && response.body?.code != null
|
||||
&& !(nativeCommand && code(response) === '200' && response.body.data == null)
|
||||
Object.assign(entry, { status: knownResponse ? 'RESPONDED' : 'UNKNOWN',
|
||||
http: response?.http, code: code(response), requestId: response?.requestId, traceId: response?.traceId,
|
||||
result: redact(response?.body?.data) })
|
||||
if (entry.status === 'UNKNOWN') { stopped = true; evidence.status = 'UNKNOWN' }
|
||||
await scoped.checkpoint()
|
||||
if (stopped && !identityCleanup) throw new Error(`履约写入结果未知,停止并保留原命令;${trace(response)}`)
|
||||
return response
|
||||
}
|
||||
scoped.requestMultipart = async (method, requestPath, file, ...rest) => {
|
||||
const bytes = file?.content == null ? null : Buffer.from(file.content)
|
||||
const metadata = { name: file?.name, type: file?.type, size: bytes?.length,
|
||||
sha256: bytes ? createHash('sha256').update(bytes).digest('hex') : null }
|
||||
// multipart也必须在发送前落盘;证据只含文件摘要而不含文件字节。
|
||||
const entry = { method, path: requestPath, file: metadata, status: 'SENDING' }
|
||||
if (stopped) throw new Error('未知写结果后禁止继续上传')
|
||||
evidence.requests.push(entry); await scoped.checkpoint()
|
||||
let response
|
||||
try { response = await context.requestMultipart(method, requestPath, file, ...rest) }
|
||||
catch (error) { stopped = true; entry.status = 'UNKNOWN'; evidence.status = 'UNKNOWN'; await scoped.checkpoint(); throw error }
|
||||
Object.assign(entry, { status: response?.http > 0 && response.http < 500 ? 'RESPONDED' : 'UNKNOWN',
|
||||
http: response?.http, code: code(response), requestId: response?.requestId, traceId: response?.traceId,
|
||||
result: redact(response?.body?.data) })
|
||||
if (entry.status === 'UNKNOWN') { stopped = true; evidence.status = 'UNKNOWN' }
|
||||
await scoped.checkpoint()
|
||||
if (stopped) throw new Error(`上传结果未知,保留原文件摘要并停写;${trace(response)}`)
|
||||
return response
|
||||
}
|
||||
context.createdResources.phase5OverseasFulfillment.checkpointPath = path
|
||||
await scoped.checkpoint()
|
||||
return scoped
|
||||
}
|
||||
function nonBlank(value) { return typeof value === 'string' && value.trim() !== '' }
|
||||
function success(response) { return response?.http === 200 && String(response?.body?.code) === '200' }
|
||||
function code(response) { return String(response?.body?.code ?? '') }
|
||||
@@ -52,7 +134,7 @@ async function merchant(context, token, method, name, path, body = undefined) {
|
||||
}
|
||||
|
||||
function transferDeliveries(data) {
|
||||
return Array.isArray(data?.deliveries) ? data.deliveries.filter((value) => nonBlank(value?.deliveryNo)) : []
|
||||
return Array.isArray(data?.result?.deliveries) ? data.result.deliveries.filter((value) => nonBlank(value?.deliveryNo)) : []
|
||||
}
|
||||
|
||||
function deliveryNumbers(data) {
|
||||
@@ -63,21 +145,19 @@ export function dispatchDestinationConservation(detail, fixture, deliveries = nu
|
||||
const codes = values => values.map(value => id(value).trim().toUpperCase())
|
||||
const sameSet = (left,right) => left.length > 0 && right.length > 0 && left.every(nonBlank)
|
||||
&& right.every(nonBlank) && JSON.stringify([...new Set(left)].sort()) === JSON.stringify([...new Set(right)].sort())
|
||||
const items = detail?.items
|
||||
if (!Array.isArray(items) || !items.length || !Array.isArray(fixture?.planPalletIds)
|
||||
|| !Array.isArray(fixture?.destinationWarehouseCodes) || !nonBlank(detail?.route)) return false
|
||||
const actualIds = items.map(row => id(row.planPalletId)), expectedIds=fixture.planPalletIds.map(id)
|
||||
if (!actualIds.every(nonBlank) || actualIds.length !== expectedIds.length
|
||||
|| new Set(actualIds).size !== actualIds.length || !sameSet(actualIds,expectedIds)) return false
|
||||
if (items.some(row => nonBlank(row.fbaWarehouseCode) && nonBlank(row.destinationWarehouseCode)
|
||||
&& id(row.fbaWarehouseCode).trim().toUpperCase() !== id(row.destinationWarehouseCode).trim().toUpperCase())) return false
|
||||
const actual = codes(items.map(row => row.fbaWarehouseCode || row.destinationWarehouseCode))
|
||||
const frozen = codes(fixture.destinationWarehouseCodes), route = codes(detail.route.split(','))
|
||||
const items = detail?.orderDetails
|
||||
if (!Array.isArray(items) || !items.length || !Array.isArray(fixture?.sourceKeys)
|
||||
|| !Array.isArray(fixture?.destinationWarehouseCodes) || !nonBlank(detail?.dispatchInfo?.route)) return false
|
||||
const actualIds = items.map(row => id(row.sourceKey)), expectedIds=fixture.sourceKeys.map(id)
|
||||
if (!actualIds.every(nonBlank) || !sameSet(actualIds,expectedIds)) return false
|
||||
const actual = codes(items.map(row => row.destinationFbaWarehouse))
|
||||
const frozen = codes(fixture.destinationWarehouseCodes), route = codes(detail.dispatchInfo.route.split(','))
|
||||
if (!sameSet(actual,frozen) || !sameSet(route,actual) || route.length !== new Set(route).size) return false
|
||||
if (deliveries == null) return true
|
||||
if (!Array.isArray(deliveries) || !deliveries.length) return false
|
||||
const delivered=codes(deliveries.map(row => row.destinationWarehouseCode))
|
||||
return delivered.length === new Set(delivered).size && sameSet(delivered,actual)
|
||||
const delivered=codes(deliveries.map(row => row.destination?.warehouseCode))
|
||||
const groups = deliveries.map(row => `${id(row.customerId)}:${id(row.destination?.key)}`)
|
||||
return groups.every(nonBlank) && new Set(groups).size === groups.length && sameSet(delivered,actual)
|
||||
}
|
||||
|
||||
async function poll(action, predicate, attempts = 45, intervalMs = 1_000) {
|
||||
@@ -223,12 +303,12 @@ export function dispatchInventorySources(task, dispatchNo, warehouseId) {
|
||||
if (task?.dispatchNo !== dispatchNo || id(task.warehouseId) !== warehouseId
|
||||
|| !Array.isArray(task.items) || !task.items.length) return null
|
||||
const positions = task.items.flatMap(item => Array.isArray(item.positions) ? item.positions : [])
|
||||
if (!positions.length || positions.some(row => !/^[1-9][0-9]*$/.test(id(row.id)) || !/^[1-9][0-9]*$/.test(id(row.balanceId)))) return null
|
||||
if (!positions.length || positions.some(row => !/^[1-9][0-9]*$/.test(id(row.positionId)) || !/^[1-9][0-9]*$/.test(id(row.balanceId)))) return null
|
||||
const references = new Map()
|
||||
for (const row of positions) {
|
||||
const previous = references.get(id(row.id))
|
||||
const previous = references.get(id(row.positionId))
|
||||
if (previous && previous !== id(row.balanceId)) return null
|
||||
references.set(id(row.id), id(row.balanceId))
|
||||
references.set(id(row.positionId), id(row.balanceId))
|
||||
}
|
||||
return { sourceBalanceIds: [...new Set(positions.map(row => id(row.balanceId)))].sort(),
|
||||
sourcePositionIds: [...references.keys()].sort(),
|
||||
@@ -312,7 +392,7 @@ export function dispatchStockConservation(context, { customerId, warehouseId, di
|
||||
const proof = { sourcePositionBalances, totals, checks, ledgerRowChecks: rowChecks,
|
||||
snapshots: { beforePick: stockBefore, afterPick: stockAfter }, ledgers: { beforePick: ledgerBefore, afterPick: ledgerAfter } }
|
||||
if (context.createdResources?.phase5OverseasFulfillment) {
|
||||
Object.assign(context.createdResources.phase5OverseasFulfillment.inventoryProof, proof)
|
||||
Object.assign(context.createdResources.phase5OverseasFulfillment.inventoryProof ||= {}, proof)
|
||||
}
|
||||
for (const [name, passed] of Object.entries(checks)) {
|
||||
context.assert(MODULE, `本排车库存独立核对-${name}`, null, passed, JSON.stringify(totals), OPTIONS)
|
||||
@@ -517,14 +597,14 @@ export async function completeReturnedException(context, { customerId, warehouse
|
||||
id(pallet.data?.warehouseId) === warehouseId && pallet.data?.palletCode === palletCode
|
||||
&& Boolean(location?.locationCode), `targetPallet=${palletCode}, location=${location?.locationCode || 'missing'}`, OPTIONS)
|
||||
if (!location?.locationCode) return null
|
||||
let processingVersion = Number(order.version)
|
||||
let processingVersion = version(order.version)
|
||||
let evidenceFileId = continuing ? id(previousAttempt.evidenceFileId) : ''
|
||||
if (!continuing) {
|
||||
const startBody = { expectedVersion: Number(order.version), idempotencyKey: key(context, 'RETURN-START'), handlingAction: '退回海外仓实收' }
|
||||
const startBody = { expectedVersion: version(order.version), idempotencyKey: key(context, 'RETURN-START'), handlingAction: '退回海外仓实收' }
|
||||
const started = await platform(context, 'POST', '开始处理同一派送退回异常',
|
||||
query(`${LASTLEG_BASE}/exception-orders/${encodeURIComponent(exceptionNo)}/start-processing`, { customerId }), startBody)
|
||||
if (!success(started.response) || started.data?.status !== 'PROCESSING') return null
|
||||
processingVersion = Number(started.data.version)
|
||||
processingVersion = version(started.data.version)
|
||||
const evidence = await context.requestMultipart('POST',
|
||||
query(`${LASTLEG_BASE}/exception-orders/${encodeURIComponent(exceptionNo)}/evidence`, { customerId }),
|
||||
{ name: `${context.marker}-return-receipt.txt`, type: 'text/plain',
|
||||
@@ -535,7 +615,7 @@ export async function completeReturnedException(context, { customerId, warehouse
|
||||
}
|
||||
const completeIdempotencyKey = continuing && nonBlank(previousAttempt.completeIdempotencyKey)
|
||||
? previousAttempt.completeIdempotencyKey : key(context, 'RETURN-COMPLETE')
|
||||
if (!Number.isInteger(processingVersion) || processingVersion < 0) {
|
||||
if (processingVersion == null || !/^(0|[1-9]\d*)$/.test(String(processingVersion))) {
|
||||
blocker(context, '派送退回处理版本', '当前PROCESSING异常缺少有效服务端版本,不能以默认版本完成', before.response)
|
||||
return null
|
||||
}
|
||||
@@ -602,10 +682,12 @@ export async function saveReceiptAndComplete(context, delivery, customerId, inde
|
||||
const deliveryNo = id(delivery.deliveryNo)
|
||||
const documentsPath = query(`${LASTLEG_BASE}/delivery-orders/${encodeURIComponent(deliveryNo)}/documents`, { customerId })
|
||||
const before = await platform(context, 'GET', `读取第${index + 1}张派送单资料版本`, documentsPath)
|
||||
const version = Number(before.data?.version)
|
||||
const documentVersion = before.data?.version
|
||||
const validVersion = documentVersion != null && /^(0|[1-9]\d*)$/.test(String(documentVersion))
|
||||
&& (typeof documentVersion !== 'number' || Number.isSafeInteger(documentVersion))
|
||||
context.assert(MODULE, `第${index + 1}张派送单资料提供乐观锁版本`, before.response,
|
||||
Number.isInteger(version) && version >= 0, `version=${before.data?.version ?? 'missing'}; ${trace(before.response)}`, OPTIONS)
|
||||
if (!success(before.response) || !Number.isInteger(version) || version < 0) return null
|
||||
validVersion, `version=${documentVersion ?? 'missing'}; ${trace(before.response)}`, OPTIONS)
|
||||
if (!success(before.response) || !validVersion) return null
|
||||
|
||||
const uploadResponse = await context.requestMultipart('POST',
|
||||
query(`${LASTLEG_BASE}/delivery-orders/${encodeURIComponent(deliveryNo)}/documents/RECEIPT/files`, { customerId }), {
|
||||
@@ -620,7 +702,7 @@ export async function saveReceiptAndComplete(context, delivery, customerId, inde
|
||||
if (!success(uploadResponse) || !fileId) return null
|
||||
if (context.createdResources.phase5OverseasFulfillment) {
|
||||
const receipts = context.createdResources.phase5OverseasFulfillment.receiptFiles ||= []
|
||||
receipts.push({ deliveryNo, fileId,
|
||||
receipts.push({ deliveryNo, customerId, fileId,
|
||||
content: `${context.marker} receipt acceptance for ${deliveryNo}` })
|
||||
}
|
||||
|
||||
@@ -629,7 +711,7 @@ export async function saveReceiptAndComplete(context, delivery, customerId, inde
|
||||
fileIds: (category.files || []).map((file) => id(file.fileId)).filter((value) => /^\d+$/.test(value)),
|
||||
})).filter((binding) => binding.category && binding.category !== 'RECEIPT' && binding.fileIds.length > 0)
|
||||
const saveBody = {
|
||||
expectedVersion: version, idempotencyKey: key(context, 'SAVE-RECEIPT', String(index)),
|
||||
expectedVersion: version(documentVersion), idempotencyKey: key(context, 'SAVE-RECEIPT', deliveryNo),
|
||||
bolNo: before.data?.bolNo || null,
|
||||
documents: [...existing, { category: 'RECEIPT', fileIds: [fileId] }],
|
||||
}
|
||||
@@ -641,7 +723,7 @@ export async function saveReceiptAndComplete(context, delivery, customerId, inde
|
||||
|
||||
const savedReplay = await platform(context, 'PUT', `第${index + 1}张派送单签收资料同键重放`, documentsPath, saveBody)
|
||||
context.assert(MODULE, `第${index + 1}张派送单资料重放版本稳定`, savedReplay.response,
|
||||
savedReplay.data?.replayed === true && Number(savedReplay.data?.version) === Number(saved.data?.version),
|
||||
savedReplay.data?.replayed === true && String(savedReplay.data?.version) === String(saved.data?.version),
|
||||
trace(savedReplay.response), OPTIONS)
|
||||
const completed = await platform(context, 'GET', `回查第${index + 1}张派送单自动完成`,
|
||||
query(`${LASTLEG_BASE}/delivery-orders/${encodeURIComponent(deliveryNo)}`, { customerId }))
|
||||
@@ -768,18 +850,21 @@ export function relabelOutboundMarksMatch(detail, relabel) {
|
||||
&& Number(detail?.outbound?.packageCount) === expected.length
|
||||
}
|
||||
|
||||
async function verifyRelabelDispatchOutbound(context, relabel, transferBody) {
|
||||
async function verifyRelabelDispatchOutbound(context, relabel, token, warehouseId) {
|
||||
if (relabel?.marker !== context.marker || !nonBlank(relabel?.dispatchNo)) {
|
||||
blocker(context, '换标排车箱唛传递前置', '缺少本轮换标排车,不借用历史数据')
|
||||
return
|
||||
}
|
||||
const transferred = await platform(context, 'POST', '本轮换标排车转派送并生成出库快照',
|
||||
`${DISPATCH_BASE}/${encodeURIComponent(relabel.dispatchNo)}/transfer-delivery`,
|
||||
{ ...transferBody, idempotencyKey: key(context, 'RELABEL-TRANSFER') })
|
||||
const outboundNo = transferred.data?.outboundNo
|
||||
if (!success(transferred.response) || !nonBlank(outboundNo)) return
|
||||
const proof = await prepareFleetTrip(context, MODULE, relabel.dispatchNo, warehouseId)
|
||||
if (!await pickAll(context, token, relabel.dispatchNo)) return
|
||||
await confirmFleetLoading(context, MODULE, proof, token, relabel.loadingMeasurements)
|
||||
const transferred = await arrangeDispatchDelivery(context, relabel.dispatchNo)
|
||||
if (!transferred) return
|
||||
const customerId = id(relabel.customerId)
|
||||
const outboundNo = transferred.outbounds.find(row => id(row.customerId) === customerId)?.outboundNo
|
||||
if (!nonBlank(outboundNo)) { blocker(context, '换标排车客户出库', '全车安排结果缺少本轮换标客户出库单'); return }
|
||||
const detail = await platform(context, 'GET', '回查换标排车的真实出库箱唛',
|
||||
query(`${LASTLEG_BASE}/outbound-orders/${encodeURIComponent(outboundNo)}`, { page: 1, size: 200 }))
|
||||
`${LASTLEG_BASE}/outbound-orders/${encodeURIComponent(outboundNo)}`)
|
||||
context.assert(MODULE, '排车出库快照精确保留全部新箱唛且无原箱唛或重复箱', detail.response,
|
||||
success(detail.response) && detail.data?.outbound?.outboundNo === outboundNo
|
||||
&& detail.data?.outbound?.dispatchNo === relabel.dispatchNo
|
||||
@@ -816,80 +901,138 @@ async function loginMerchant(context, customerId) {
|
||||
function fixture(context) {
|
||||
const dispatch = context.createdResources?.phase4OverseasDispatch
|
||||
const receiving = context.createdResources?.phase4OverseasPda
|
||||
if (dispatch?.marker !== context.marker || dispatch?.finalStatus !== 'PENDING_DEPARTURE'
|
||||
if (dispatch?.marker !== context.marker || dispatch?.finalStatus !== 'ACTIVE' || dispatch?.stage !== 'BEFORE_LOADING'
|
||||
|| !nonBlank(dispatch?.dispatchNo)) return null
|
||||
if (receiving?.marker !== context.marker || receiving?.finalStatus !== 'COMPLETED'
|
||||
|| !id(receiving?.overseasWarehouseId) || !id(receiving?.customerId)) return null
|
||||
|| !id(receiving?.overseasWarehouseId) || !id(receiving?.customerId)
|
||||
|| id(dispatch.warehouseId) !== id(receiving.overseasWarehouseId)) return null
|
||||
return { dispatch, receiving }
|
||||
}
|
||||
|
||||
export async function verifyDepartureReadiness(context, token, dispatchNo) {
|
||||
const ready = await pda(context, token, 'GET', '发车前回查服务端canDepart门禁',
|
||||
const ready = await pda(context, token, 'GET', '发车前回查PDA公共动作门禁',
|
||||
`${PDA_DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}`)
|
||||
const dispatch = await platform(context, 'GET', '发车前独立回查实际排车状态',
|
||||
const dispatch = await platform(context, 'GET', '发车前独立回查实际排车阶段',
|
||||
`${DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}`)
|
||||
const passed = success(ready.response) && success(dispatch.response)
|
||||
&& ready.data?.dispatchNo === dispatchNo && ready.data?.status === 'COMPLETED'
|
||||
&& ready.data?.canDepart === true && dispatch.data?.dispatchNo === dispatchNo
|
||||
&& dispatch.data?.status === 'READY_TO_DEPART'
|
||||
context.assert(MODULE, '发车前PDA任务完成且服务端门禁和实际排车待发均通过', ready.response,
|
||||
passed, `taskStatus=${ready.data?.status || 'missing'}, canDepart=${ready.data?.canDepart}, dispatchStatus=${dispatch.data?.status || 'missing'}; ${trace(ready.response)}`, OPTIONS)
|
||||
&& [ready.data, dispatch.data].every(data => data?.dispatchNo === dispatchNo
|
||||
&& data.status === 'ACTIVE' && data.stage === 'AFTER_LOADING'
|
||||
&& data.pickingReconciled === true && data.actions?.DEPART?.allowed === true)
|
||||
&& String(ready.data.recordVersion) === String(dispatch.data.recordVersion)
|
||||
context.assert(MODULE, '双端同版本装车后且服务端发车门禁通过', ready.response,
|
||||
passed, `stage=${ready.data?.stage}, dispatchStage=${dispatch.data?.stage}; ${trace(ready.response)}`, OPTIONS)
|
||||
if (passed) version(ready.data.recordVersion)
|
||||
return passed ? ready : null
|
||||
}
|
||||
|
||||
export async function pickAll(context, token, dispatchNo) {
|
||||
let detail = await pda(context, token, 'GET', 'PDA读取本轮排车拣货任务',
|
||||
`${PDA_DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}`)
|
||||
const items = Array.isArray(detail.data?.items) ? detail.data.items : []
|
||||
if (!success(detail.response) || items.length === 0) {
|
||||
blocker(context, 'PDA排车拣货任务', '本轮排车未生成可操作PDA托盘任务', detail.response)
|
||||
if (!success(detail.response) || detail.data?.status !== 'ACTIVE' || detail.data?.stage !== 'LOADING'
|
||||
|| !Array.isArray(detail.data.items) || !detail.data.items.length) {
|
||||
blocker(context, 'PDA排车拣货任务', '本轮排车尚未通过两级审核进入LOADING', detail.response)
|
||||
return false
|
||||
}
|
||||
|
||||
for (const [itemIndex, item] of items.entries()) {
|
||||
let remaining = Number(item.scheduledPackages || 0) - Number(item.pickedPackages || 0)
|
||||
const positions = Array.isArray(item.positions) ? item.positions : []
|
||||
for (const [positionIndex, position] of positions.entries()) {
|
||||
const items = detail.data.items
|
||||
for (const [itemIndex, original] of items.entries()) {
|
||||
// 每条真实命令前重新读取当前公共版本和可用来源,不从上次响应推算版本。
|
||||
detail = await pda(context, token, 'GET', '扫码前刷新公共版本及真实来源', `${PDA_DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}`)
|
||||
let item = detail.data?.items?.find(row => id(row.dispatchItemId) === id(original.dispatchItemId))
|
||||
if (!success(detail.response) || !item) return false
|
||||
const sources = item.returnCartons?.length ? item.returnCartons.map(carton => ({ carton })) : (item.positions || []).map(position => ({ position }))
|
||||
for (const [sourceIndex, source] of sources.entries()) {
|
||||
const remaining = Number(item.scheduledPackages) - Number(item.pickedPackages)
|
||||
if (remaining <= 0) break
|
||||
const available = Number(position.availablePackages || 0)
|
||||
if (available <= 0) continue
|
||||
const available = source.carton ? 1 : Number(source.position.availablePackages)
|
||||
if (!Number.isSafeInteger(available) || available <= 0) continue
|
||||
const packages = Math.min(remaining, available)
|
||||
const body = {
|
||||
positionId: id(position.id), packages, productQuantity: packages,
|
||||
idempotencyKey: key(context, 'DISPATCH-PICK', `${itemIndex}-${positionIndex}`),
|
||||
remark: `${context.marker}-phase5-dispatch-pick`,
|
||||
}
|
||||
const picked = await pda(context, token, 'POST', `PDA拣货托盘${itemIndex + 1}库存位${positionIndex + 1}`,
|
||||
`${PDA_DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}/items/${encodeURIComponent(id(item.dispatchItemId))}/pick`, body)
|
||||
const body = { recordVersion: version(detail.data.recordVersion),
|
||||
positionId: source.position ? id(source.position.positionId) : null,
|
||||
cartonMark: source.carton?.cartonMark || null, packages,
|
||||
productQuantity: source.carton ? Number(source.carton.productQuantity) : packages,
|
||||
idempotencyKey: key(context, 'DISPATCH-PICK', `${dispatchNo}-${itemIndex}-${sourceIndex}`),
|
||||
remark: `${context.marker}-phase5-dispatch-pick` }
|
||||
const path = `${PDA_DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}/items/${encodeURIComponent(id(item.dispatchItemId))}/pick`
|
||||
const picked = await pda(context, token, 'POST', `PDA真实扫码托盘${itemIndex + 1}来源${sourceIndex + 1}`, path, body)
|
||||
if (!success(picked.response)) return false
|
||||
const replay = await pda(context, token, 'POST', `PDA拣货托盘${itemIndex + 1}同键重放`,
|
||||
`${PDA_DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}/items/${encodeURIComponent(id(item.dispatchItemId))}/pick`, body)
|
||||
context.assert(MODULE, `PDA拣货托盘${itemIndex + 1}重放数量稳定`, replay.response,
|
||||
success(replay.response) && Number(replay.data?.pickedPackages) === Number(picked.data?.pickedPackages),
|
||||
trace(replay.response), OPTIONS)
|
||||
remaining -= packages
|
||||
const replay = await pda(context, token, 'POST', 'PDA扫码原命令重放', path, body)
|
||||
const stable = success(replay.response) && replay.data?.replayed === true
|
||||
&& String(replay.data.recordVersion) === String(picked.data.recordVersion)
|
||||
&& JSON.stringify(replay.data.result) === JSON.stringify(picked.data.result)
|
||||
&& JSON.stringify(replay.data.counts) === JSON.stringify(picked.data.counts)
|
||||
context.assert(MODULE, 'PDA扫码重放保留原结果及版本数量', replay.response, stable, trace(replay.response), OPTIONS)
|
||||
if (!stable) return false
|
||||
if (itemIndex === 0 && sourceIndex === 0) {
|
||||
const stale = await context.request('POST', path, { ...body, idempotencyKey: `${body.idempotencyKey}-STALE` }, token, {})
|
||||
context.addStep(MODULE, '旧公共版本扫码冲突且不得再扣库存', stale, conflict(stale), trace(stale), OPTIONS)
|
||||
if (!conflict(stale)) return false
|
||||
}
|
||||
detail = await pda(context, token, 'GET', '扫码后重新读取公共版本与数量', `${PDA_DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}`)
|
||||
item = detail.data?.items?.find(row => id(row.dispatchItemId) === id(original.dispatchItemId))
|
||||
if (!success(detail.response) || !item) return false
|
||||
}
|
||||
if (remaining !== 0) {
|
||||
blocker(context, `PDA托盘${itemIndex + 1}足量库存`,
|
||||
`scheduled=${item.scheduledPackages}, picked=${item.pickedPackages}, remaining=${remaining}`, detail.response)
|
||||
if (Number(item.scheduledPackages) !== Number(item.pickedPackages)) {
|
||||
blocker(context, `PDA托盘${itemIndex + 1}足量库存`, '实际来源不足,保留本轮已扫证据', detail.response)
|
||||
return false
|
||||
}
|
||||
}
|
||||
const passed = detail.data?.stage === 'LOADING' && detail.data?.pickingReconciled === true
|
||||
&& Number(detail.data?.counts?.packageCount) > 0
|
||||
&& Number(detail.data.counts.packageCount) === Number(detail.data.counts.pickedPackageCount)
|
||||
context.assert(MODULE, '真实扫码核平仍停留LOADING等待显式装车完成', detail.response, passed, trace(detail.response), OPTIONS)
|
||||
return passed
|
||||
}
|
||||
|
||||
const completeBody = { idempotencyKey: key(context, 'DISPATCH-COMPLETE') }
|
||||
const completed = await pda(context, token, 'POST', 'PDA确认排车拣货全部完成',
|
||||
`${PDA_DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}/complete`, completeBody)
|
||||
context.assert(MODULE, '排车进入READY_TO_DEPART且数量守恒', completed.response,
|
||||
completed.data?.status === 'READY_TO_DEPART'
|
||||
&& Number(completed.data?.plannedPackages) === Number(completed.data?.pickedPackages)
|
||||
&& Number(completed.data?.remainingPackages) === 0,
|
||||
trace(completed.response), OPTIONS)
|
||||
if (!success(completed.response)) return false
|
||||
const replay = await pda(context, token, 'POST', 'PDA完成排车拣货同键重放',
|
||||
`${PDA_DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}/complete`, completeBody)
|
||||
context.assert(MODULE, 'PDA完成拣货重放明确标记且结果稳定', replay.response,
|
||||
replay.data?.replayed === true && replay.data?.status === 'READY_TO_DEPART', trace(replay.response), OPTIONS)
|
||||
return success(replay.response)
|
||||
export async function arrangeDispatchDelivery(context, dispatchNo) {
|
||||
const path = `${DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}`
|
||||
const preview = await platform(context, 'GET', '装车后读取全车真实派送分组与费用证据', `${path}/delivery-arrangement`)
|
||||
const value = preview.data
|
||||
if (!success(preview.response) || value?.dispatchNo !== dispatchNo || value.status !== 'ACTIVE'
|
||||
|| value.stage !== 'AFTER_LOADING' || value.arrange?.allowed !== true
|
||||
|| !nonBlank(value.cargoHash) || !nonBlank(value.loadingCompletionHash)
|
||||
|| !value.groups?.length || value.groups.some(group => !nonBlank(group.groupKey)
|
||||
|| !nonBlank(id(group.customerId)) || !nonBlank(group.destination?.key)
|
||||
|| !group.cargo?.length || !Array.isArray(group.blockReasons) || group.blockReasons.length)) {
|
||||
blocker(context, '全车派送安排', '装车后正式预览或完整客户目的地分组门禁未满足', preview.response)
|
||||
return null
|
||||
}
|
||||
const body = { recordVersion: version(value.recordVersion), idempotencyKey: key(context, 'TRANSFER', dispatchNo),
|
||||
cargoHash: value.cargoHash, loadingCompletionHash: value.loadingCompletionHash,
|
||||
groups: value.groups.map((group, index) => ({ groupKey: group.groupKey, groupCargoHash: group.groupCargoHash,
|
||||
plannedDeliveryAt: new Date(Date.now() + 172_800_000).toISOString(),
|
||||
deliveryMethod: 'TRUCK_PALLET', waybillNo: `${context.marker}-${index + 1}`.slice(0, 64),
|
||||
carrierName: '本轮TEST排车', pickupMethod: 'WAREHOUSE_PICKUP', loadingType: 'PALLET',
|
||||
vehicleType: 'TRUCK', vehicleSize: '按本轮真实实载', appointmentAccount: 'SELF', sellerName: null,
|
||||
remark: `${context.marker}-phase5-arrangement`, externalFreightHash: group.externalFreightHash,
|
||||
feeCoverageCargoHash: group.feeCoverageCargoHash, feeCoverage: [],
|
||||
fees: (group.requiredServices || []).map(service => ({ feeType: service, feeName: service,
|
||||
direction: 'RECEIVABLE', currency: 'USD', amount: 10, remark: `${context.marker}-controlled-test-fee` })) })) }
|
||||
const transferred = await platform(context, 'POST', '平台一次安排全车全部客户目的地派送', `${path}/transfer-delivery`, body)
|
||||
if (!success(transferred.response)) return null
|
||||
const result = transferred.data?.result
|
||||
const deliveries = transferDeliveries(transferred.data)
|
||||
const outbounds = result?.outbounds
|
||||
const customerGroups = value.groups.map(group => `${id(group.customerId)}:${group.destination.key}`).sort()
|
||||
const actualGroups = deliveries.map(row => `${id(row.customerId)}:${row.destination?.key}`).sort()
|
||||
const passed = transferred.data?.dispatchNo === dispatchNo && transferred.data?.status === 'ACTIVE'
|
||||
&& transferred.data?.stage === 'AFTER_LOADING' && result?.dispatchNo === dispatchNo
|
||||
&& result.cargoHash === body.cargoHash && deliveries.length === value.groups.length
|
||||
&& new Set(deliveries.map(row => row.deliveryNo)).size === deliveries.length
|
||||
&& JSON.stringify(customerGroups) === JSON.stringify(actualGroups)
|
||||
&& Array.isArray(outbounds) && outbounds.length === new Set(value.groups.map(group => id(group.customerId))).size
|
||||
&& outbounds.every(row => nonBlank(row.outboundNo) && row.status === 'WAIT_OUTBOUND'
|
||||
&& deliveries.filter(delivery => delivery.outboundNo === row.outboundNo).every(delivery => id(delivery.customerId) === id(row.customerId))
|
||||
&& deliveries.filter(delivery => delivery.outboundNo === row.outboundNo).reduce((sum, delivery) => sum + Number(delivery.packageCount), 0) === Number(row.packageCount))
|
||||
&& deliveries.every(row => row.status === 'WAIT_DELIVERY')
|
||||
context.assert(MODULE, '派送按客户出库及客户目的地完整分组并数量守恒', transferred.response, passed, trace(transferred.response), OPTIONS)
|
||||
if (!passed) return null
|
||||
const replay = await platform(context, 'POST', '全车安排派送原命令重放', `${path}/transfer-delivery`, body)
|
||||
const stable = success(replay.response) && replay.data?.replayed === true
|
||||
&& String(replay.data.recordVersion) === String(transferred.data.recordVersion)
|
||||
&& JSON.stringify(replay.data.result) === JSON.stringify(result)
|
||||
context.assert(MODULE, '安排重放返回原命令结果且不增单据', replay.response, stable, trace(replay.response), OPTIONS)
|
||||
if (!stable) return null
|
||||
return { ...transferred, preview: value, outbounds, deliveries }
|
||||
}
|
||||
|
||||
/** 只从本次派送的真实主单及其原出库来源创建退回请求,不要求普通入仓关联。 */
|
||||
@@ -947,120 +1090,23 @@ export async function run(context) {
|
||||
const merchantToken = await loginMerchant(context, customerId)
|
||||
if (!merchantToken) return
|
||||
|
||||
const beforeEdit = await platform(context, 'GET', '编辑车辆前读取服务端目的仓线路', `${DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}`)
|
||||
context = await fulfillmentEvidenceContext(context)
|
||||
const beforeEdit = await platform(context, 'GET', '读取本轮原生整车详情及线路', `${DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}`)
|
||||
if (!success(beforeEdit.response) || beforeEdit.data?.dispatchNo !== dispatchNo) return
|
||||
const routeProof = context.createdResources.phase5OverseasFulfillment.routeProof = {
|
||||
frozenPlanPalletIds:source.dispatch.planPalletIds,
|
||||
frozenDestinationWarehouseCodes:source.dispatch.destinationWarehouseCodes,
|
||||
beforeEdit:beforeEdit.data,
|
||||
}
|
||||
context.assert(MODULE,'排车线路目的地集合与实际托盘及本轮冻结候选完全一致',beforeEdit.response,
|
||||
dispatchDestinationConservation(beforeEdit.data,source.dispatch),trace(beforeEdit.response),OPTIONS)
|
||||
sourceKeys: source.dispatch.sourceKeys, frozenDestinationWarehouseCodes: source.dispatch.destinationWarehouseCodes,
|
||||
beforeEdit: beforeEdit.data }
|
||||
context.assert(MODULE, '排车来源和目的地与本轮生产者守恒', beforeEdit.response,
|
||||
dispatchDestinationConservation(beforeEdit.data,source.dispatch), trace(beforeEdit.response), OPTIONS)
|
||||
if (!dispatchDestinationConservation(beforeEdit.data,source.dispatch)) return
|
||||
const updateBody = {
|
||||
vehiclePlate: `DEV-${context.marker.slice(-8)}`.slice(0, 64),
|
||||
driverName: `五期验收司机-${context.marker.slice(-6)}`,
|
||||
driverPhone: '19900000000', carrierName: '五期开发验收车队',
|
||||
estimatedArrivalAt: new Date(Date.now() + 86_400_000).toISOString(),
|
||||
}
|
||||
const updated = await platform(context, 'PUT', '补齐本轮排车车辆司机与线路',
|
||||
`${DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}`, updateBody)
|
||||
context.assert(MODULE, '排车编辑字段完整回显', updated.response,
|
||||
updated.data?.dispatchNo === dispatchNo && updated.data?.vehiclePlate === updateBody.vehiclePlate
|
||||
&& updated.data?.driverName === updateBody.driverName && updated.data?.route === beforeEdit.data.route,
|
||||
trace(updated.response), OPTIONS)
|
||||
if (!success(updated.response)) return
|
||||
|
||||
const tripProof = await prepareFleetTrip(context, MODULE, dispatchNo, warehouseId)
|
||||
const bol = await context.requestBinary('GET', `${DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}/prints/bol`)
|
||||
context.addStep(MODULE, '排车BOL下载为真实PDF而非JSON错误', bol,
|
||||
bol.http === 200 && /application\/pdf/i.test(bol.contentType || '')
|
||||
&& new TextDecoder().decode(bol.bytes?.slice(0, 5)) === '%PDF-', trace(bol), OPTIONS)
|
||||
|
||||
const transferBody = {
|
||||
idempotencyKey: key(context, 'TRANSFER'), plannedDeliveryAt: new Date(Date.now() + 172_800_000).toISOString(),
|
||||
deliveryMethod: 'TRUCK_PALLET', waybillNo: `${context.marker}-WAYBILL`.slice(0, 64),
|
||||
carrierName: updateBody.carrierName, pickupMethod: 'WAREHOUSE_PICKUP', loadingType: 'PALLET',
|
||||
vehicleType: 'TRUCK', vehicleSize: '15托', driverName: updateBody.driverName,
|
||||
driverPhone: updateBody.driverPhone, vehiclePlate: updateBody.vehiclePlate,
|
||||
appointmentAccount: 'SELF', sellerName: null, remark: `${context.marker}-phase5-transfer`,
|
||||
fees: [{ feeType: 'DELIVERY_FEE', feeName: '派送费', direction: 'RECEIVABLE',
|
||||
currency: 'USD', amount: 10, remark: `${context.marker}-acceptance` }],
|
||||
}
|
||||
await verifyRelabelDispatchOutbound(context, context.createdResources?.phase4OverseasRelabel, transferBody)
|
||||
const transferred = await merchant(context, merchantToken, 'POST', '客户创建派送并原子转出库单',
|
||||
query(MERCHANT_DELIVERY_BASE, { dispatchNo }), transferBody)
|
||||
const outboundNo = id(transferred.data?.outboundNo)
|
||||
const deliveries = transferDeliveries(transferred.data)
|
||||
const deliveryNos = deliveryNumbers(transferred.data)
|
||||
const destinationKeys = deliveries.map((value) => id(value.destinationWarehouseCode || value.destinationWarehouseId))
|
||||
routeProof.deliveries = deliveries
|
||||
context.assert(MODULE,'转派送目的地集合与实际排车线路及本轮冻结候选守恒',transferred.response,
|
||||
dispatchDestinationConservation(beforeEdit.data,source.dispatch,deliveries),trace(transferred.response),OPTIONS)
|
||||
if (!dispatchDestinationConservation(beforeEdit.data,source.dispatch,deliveries)) return
|
||||
const deliveryNo = deliveryNos[0] || ''
|
||||
context.assert(MODULE, '转派送按目的FBA仓生成WAIT_DELIVERY单据数组', transferred.response,
|
||||
nonBlank(outboundNo) && deliveries.length > 0 && new Set(deliveryNos).size === deliveries.length
|
||||
&& new Set(destinationKeys).size === deliveries.length && destinationKeys.every(nonBlank)
|
||||
&& transferred.data?.outboundStatus === 'WAIT_OUTBOUND'
|
||||
&& deliveries.every((value) => value?.deliveryStatus === 'WAIT_DELIVERY')
|
||||
&& deliveries.reduce((sum, value) => sum + Number(value?.packageCount || 0), 0) === Number(transferred.data?.packageCount)
|
||||
&& Number(transferred.data?.packageCount) > 0,
|
||||
`outboundNo=${outboundNo || 'missing'}, deliveries=${deliveryNos.join(',') || 'missing'}, destinations=${destinationKeys.join(',') || 'missing'}; ${trace(transferred.response)}`, OPTIONS)
|
||||
if (!success(transferred.response) || !outboundNo || deliveries.length === 0) return
|
||||
Object.assign(context.createdResources.phase5OverseasFulfillment, { outboundNo, deliveryNo, deliveryNos })
|
||||
const adminReplay = await platform(context, 'POST', '后台转派送复用客户已创建的同一出库派送单',
|
||||
`${DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}/transfer-delivery`, transferBody)
|
||||
context.assert(MODULE, '后台与客户转派送共享同一业务事实', adminReplay.response,
|
||||
adminReplay.data?.replayed === true && id(adminReplay.data?.outboundNo) === outboundNo
|
||||
&& JSON.stringify(deliveryNumbers(adminReplay.data)) === JSON.stringify(deliveryNos), trace(adminReplay.response), OPTIONS)
|
||||
const lockedItem = updated.data?.items?.[0]
|
||||
if (lockedItem?.id) {
|
||||
const deniedRemove = await context.request('DELETE', `${DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}/items/${encodeURIComponent(id(lockedItem.id))}`)
|
||||
context.addStep(MODULE, '已生成出库单禁止移除排车托盘', deniedRemove, conflict(deniedRemove), trace(deniedRemove), OPTIONS)
|
||||
const stable = await platform(context, 'GET', '移除拒绝后回查排车托盘零漂移', `${DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}`)
|
||||
context.assert(MODULE, '移除失败不改变箱数和托盘身份', stable.response,
|
||||
Number(stable.data?.packageCount) === Number(updated.data?.packageCount)
|
||||
&& JSON.stringify(stable.data?.items?.map(item => id(item.id))) === JSON.stringify(updated.data?.items?.map(item => id(item.id))), trace(stable.response), OPTIONS)
|
||||
} else blocker(context, '转派送后移除门禁前置', '本轮排车缺少真实明细ID')
|
||||
const transferReplay = await merchant(context, merchantToken, 'POST', '客户创建派送同键重放',
|
||||
query(MERCHANT_DELIVERY_BASE, { dispatchNo }), transferBody)
|
||||
context.assert(MODULE, '转派送重放复用同一业务单据', transferReplay.response,
|
||||
transferReplay.data?.replayed === true && id(transferReplay.data?.outboundNo) === outboundNo
|
||||
&& JSON.stringify(deliveryNumbers(transferReplay.data)) === JSON.stringify(deliveryNos), trace(transferReplay.response), OPTIONS)
|
||||
const transferConflict = await context.request('POST', query(MERCHANT_DELIVERY_BASE, { dispatchNo }), { ...transferBody, idempotencyKey: key(context, 'TRANSFER-DIFFERENT') }, merchantToken, {})
|
||||
context.addStep(MODULE, '同一排车使用另一幂等键仍返回既有单据', transferConflict,
|
||||
success(transferConflict) && id(transferConflict.body?.data?.outboundNo) === outboundNo
|
||||
&& transferConflict.body?.data?.replayed === true
|
||||
&& JSON.stringify(deliveryNumbers(transferConflict.body?.data)) === JSON.stringify(deliveryNos), trace(transferConflict), OPTIONS)
|
||||
|
||||
const merchantPage = await merchant(context, merchantToken, 'GET', '客户分页回查自身派送单',
|
||||
query(MERCHANT_DELIVERY_BASE, { page: 1, size: 20, dispatchNo }))
|
||||
const merchantRows = Array.isArray(merchantPage.data?.records) ? merchantPage.data.records : []
|
||||
context.assert(MODULE, '客户派送列表包含本轮全部FBA派送单且不依赖customerId参数', merchantPage.response,
|
||||
deliveryNos.every((number) => merchantRows.some((value) => id(value?.deliveryNo) === number
|
||||
&& id(value?.dispatchNo) === dispatchNo)),
|
||||
`records=${merchantRows.length}; ${trace(merchantPage.response)}`, OPTIONS)
|
||||
const merchantDetail = await merchant(context, merchantToken, 'GET', '客户读取自身派送详情',
|
||||
`${MERCHANT_DELIVERY_BASE}/${encodeURIComponent(deliveryNo)}`)
|
||||
context.assert(MODULE, '客户派送详情返回订单、货物、费用与操作四段正式契约', merchantDetail.response,
|
||||
id(merchantDetail.data?.order?.deliveryNo) === deliveryNo
|
||||
&& id(merchantDetail.data?.order?.dispatchNo) === dispatchNo
|
||||
&& nonBlank(id(merchantDetail.data?.order?.destinationWarehouseCode
|
||||
|| merchantDetail.data?.order?.destinationWarehouseId))
|
||||
&& Array.isArray(merchantDetail.data?.skus) && merchantDetail.data.skus.length > 0
|
||||
&& Array.isArray(merchantDetail.data?.fees) && Array.isArray(merchantDetail.data?.operations),
|
||||
`skus=${merchantDetail.data?.skus?.length ?? 'missing'}, fees=${merchantDetail.data?.fees?.length ?? 'missing'}, operations=${merchantDetail.data?.operations?.length ?? 'missing'}; ${trace(merchantDetail.response)}`,
|
||||
OPTIONS)
|
||||
|
||||
const access = await resolvePickerAccess(context)
|
||||
const picker = access ? await createPicker(context, 5, warehouseId, access) : null
|
||||
const dispatchInboundCodes = [...new Set((updated.data?.items || []).map(item => item.inboundCode).filter(nonBlank))]
|
||||
let tripProof
|
||||
if (dispatchInboundCodes.length === 0) {
|
||||
blocker(context, '排车库存数量守恒', '实际排车详情未提供来源入仓码')
|
||||
await disablePicker(context, picker, 5)
|
||||
return
|
||||
}
|
||||
let deliveries, outbounds, transferred
|
||||
try {
|
||||
if (!nonBlank(picker?.token)) {
|
||||
blocker(context, '创建本轮五期PDA操作员', '正式IAM账号已创建但海外安全投影或登录未就绪')
|
||||
@@ -1068,90 +1114,117 @@ export async function run(context) {
|
||||
}
|
||||
const sourceTask = await pda(context, picker.token, 'GET', '扫码前冻结实际排车全部库存来源',
|
||||
`${PDA_DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}`)
|
||||
const sourceIds = dispatchInventorySources(sourceTask.data, dispatchNo, warehouseId)
|
||||
context.assert(MODULE, '真实排车任务冻结全部库存位置及余额ID', sourceTask.response,
|
||||
success(sourceTask.response) && Boolean(sourceIds), `dispatchNo=${dispatchNo}`, OPTIONS)
|
||||
if (!sourceIds) return
|
||||
const { sourceBalanceIds, sourcePositionIds, sourcePositionBalances } = sourceIds
|
||||
context.createdResources.phase5OverseasFulfillment.inventoryProof = { sourceBalanceIds, sourcePositionIds, sourcePositionBalances }
|
||||
const stockBeforePick = await inventorySnapshot(context, customerId, warehouseId, dispatchInboundCodes, '扫码前本排车来源库存', sourceBalanceIds)
|
||||
const ledgerBeforePick = await shipmentLedger(context, customerId, dispatchNo, '扫码前本排车SHIP流水', 'SHIP', dispatchNo)
|
||||
Object.assign(context.createdResources.phase5OverseasFulfillment.inventoryProof, { snapshots: { beforePick: stockBeforePick }, ledgers: { beforePick: ledgerBeforePick } })
|
||||
const sourcesBeforeValid = inventorySourcesPresent(stockBeforePick, sourcePositionBalances)
|
||||
context.assert(MODULE, '扫码前库存完整覆盖任务冻结的每个原位置关联', null, sourcesBeforeValid, `positions=${sourcePositionIds.length}`, OPTIONS)
|
||||
if (!sourcesBeforeValid || !ledgerBeforePick) return
|
||||
tripProof = await prepareFleetTrip(context, MODULE, dispatchNo, warehouseId)
|
||||
const pickedSuccessfully = await pickAll(context, picker.token, dispatchNo)
|
||||
const stockAfterPick = await inventorySnapshot(context, customerId, warehouseId, dispatchInboundCodes, '扫码及重放后库存', sourceBalanceIds)
|
||||
const ledgerAfterPick = await shipmentLedger(context, customerId, dispatchNo, '扫码及重放后SHIP流水', 'SHIP', dispatchNo)
|
||||
const expectedPackages = Number(transferred.data.packageCount)
|
||||
const conservation = dispatchStockConservation(context, { customerId, warehouseId, dispatchNo, expectedPackages,
|
||||
sourcePositionBalances, stockBefore: stockBeforePick, stockAfter: stockAfterPick,
|
||||
ledgerBefore: ledgerBeforePick, ledgerAfter: ledgerAfterPick })
|
||||
const shippedRows = conservation.newLedger
|
||||
if (!pickedSuccessfully) return
|
||||
const tripVersion = await confirmFleetLoading(context, MODULE, tripProof)
|
||||
if (!success(sourceTask.response) || !sourceTask.data?.items?.length) return
|
||||
const customerIds = [...new Set(sourceTask.data.items.map(item => id(item.customerId)))]
|
||||
const detailCustomers = [...new Set(beforeEdit.data.orderDetails.map(row => id(row.customerId)))].sort()
|
||||
if (!customerIds.every(nonBlank) || JSON.stringify([...customerIds].sort()) !== JSON.stringify(detailCustomers)) {
|
||||
blocker(context, '整车真实客户集合', 'PDA客户与本轮原生详情不一致'); return
|
||||
}
|
||||
const proofs = context.createdResources.phase5OverseasFulfillment.inventoryByCustomer = []
|
||||
for (const currentCustomer of customerIds) {
|
||||
const task = { ...sourceTask.data, items: sourceTask.data.items.filter(item => id(item.customerId) === currentCustomer) }
|
||||
const sources = dispatchInventorySources(task, dispatchNo, warehouseId)
|
||||
const inboundCodes = [...new Set(beforeEdit.data.orderDetails.filter(row => id(row.customerId) === currentCustomer)
|
||||
.map(row => row.inboundCode).filter(nonBlank))]
|
||||
if (!sources || !inboundCodes.length) { blocker(context, '整车客户库存生产证据', '缺少当前客户真实库存位置或入仓码'); return }
|
||||
const stockBefore = await inventorySnapshot(context, currentCustomer, warehouseId, inboundCodes, `客户${currentCustomer}扫码前库存`, sources.sourceBalanceIds)
|
||||
const ledgerBefore = await shipmentLedger(context, currentCustomer, dispatchNo, `客户${currentCustomer}扫码前SHIP`, 'SHIP', dispatchNo)
|
||||
if (!stockBefore || !ledgerBefore || !inventorySourcesPresent(stockBefore, sources.sourcePositionBalances)) return
|
||||
proofs.push({ customerId: currentCustomer, inboundCodes, ...sources, stockBefore, ledgerBefore,
|
||||
expectedPackages: task.items.reduce((sum, row) => sum + Number(row.scheduledPackages), 0) })
|
||||
}
|
||||
await context.checkpoint()
|
||||
if (!await pickAll(context, picker.token, dispatchNo)) return
|
||||
for (const proof of proofs) {
|
||||
proof.stockAfter = await inventorySnapshot(context, proof.customerId, warehouseId, proof.inboundCodes,
|
||||
`客户${proof.customerId}扫码后库存`, proof.sourceBalanceIds)
|
||||
proof.ledgerAfter = await shipmentLedger(context, proof.customerId, dispatchNo, `客户${proof.customerId}扫码后SHIP`, 'SHIP', dispatchNo)
|
||||
const conservation = dispatchStockConservation(context, { ...proof, warehouseId, dispatchNo })
|
||||
proof.conservation = conservation.proof
|
||||
if (!conservation.passed) return
|
||||
}
|
||||
const primaryInventory = proofs.find(proof => proof.customerId === customerId)
|
||||
if (primaryInventory) context.createdResources.phase5OverseasFulfillment.inventoryProof = { ...primaryInventory, ...primaryInventory.conservation }
|
||||
await confirmFleetLoading(context, MODULE, tripProof, picker.token, source.dispatch.loadingMeasurements)
|
||||
transferred = await arrangeDispatchDelivery(context, dispatchNo)
|
||||
if (!transferred) return
|
||||
deliveries = transferred.deliveries; outbounds = transferred.outbounds
|
||||
routeProof.deliveries = deliveries
|
||||
context.assert(MODULE, '转派送客户目的地与本轮冻结来源守恒', transferred.response,
|
||||
dispatchDestinationConservation(beforeEdit.data,source.dispatch,deliveries), trace(transferred.response), OPTIONS)
|
||||
if (!dispatchDestinationConservation(beforeEdit.data,source.dispatch,deliveries)) return
|
||||
const ready = await verifyDepartureReadiness(context, picker.token, dispatchNo)
|
||||
if (!ready) return
|
||||
const departBody = { recordVersion: tripVersion, idempotencyKey: key(context, 'DISPATCH-DEPART').slice(0, 128), remark: `${context.marker}-depart` }
|
||||
const departed = await pda(context, picker.token, 'POST', 'PDA确认车辆发车并完成真实出库',
|
||||
const departBody = { recordVersion: version(ready.data.recordVersion),
|
||||
idempotencyKey: key(context, 'DISPATCH-DEPART', dispatchNo).slice(0, 128), remark: `${context.marker}-depart` }
|
||||
const departed = await pda(context, picker.token, 'POST', 'PDA实际发车并完成真实出库',
|
||||
`${PDA_DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}/depart`, departBody)
|
||||
context.assert(MODULE, '发车推进排车DEPARTED并返回实发数量', departed.response,
|
||||
departed.data?.status === 'IN_TRANSIT' && departed.data?.tripNo === tripProof.tripNo
|
||||
&& departed.data?.dispatches?.some(item => item.dispatchNo === dispatchNo) && Number(departed.data?.shippedPackages) > 0,
|
||||
trace(departed.response), OPTIONS)
|
||||
if (!success(departed.response)) return
|
||||
const departReplay = await pda(context, picker.token, 'POST', 'PDA发车同键重放',
|
||||
const expectedPackages = proofs.reduce((sum, proof) => sum + proof.expectedPackages, 0)
|
||||
const departedCorrectly = success(departed.response) && departed.data?.dispatchNo === dispatchNo
|
||||
&& departed.data.status === 'ACTIVE' && departed.data.stage === 'DELIVERING'
|
||||
&& Number(departed.data.result?.shippedPackages) === expectedPackages
|
||||
&& JSON.stringify(deliveryNumbers(departed.data)) === JSON.stringify(deliveries.map(row => row.deliveryNo).sort())
|
||||
context.assert(MODULE, '实际发车进入DELIVERING且全部客户实发数量守恒', departed.response, departedCorrectly, trace(departed.response), OPTIONS)
|
||||
if (!departedCorrectly) return
|
||||
const departReplay = await pda(context, picker.token, 'POST', 'PDA发车原命令重放',
|
||||
`${PDA_DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}/depart`, departBody)
|
||||
context.assert(MODULE, 'PDA发车重放明确标记且发运数量稳定', departReplay.response,
|
||||
departReplay.data?.replayed === true
|
||||
&& Number(departReplay.data?.shippedPackages) === Number(departed.data?.shippedPackages),
|
||||
trace(departReplay.response), OPTIONS)
|
||||
const stockAfterDepart = await inventorySnapshot(context, customerId, warehouseId, dispatchInboundCodes, '发车及重放后库存', sourceBalanceIds)
|
||||
const ledgerAfterDepart = await shipmentLedger(context, customerId, dispatchNo, '发车及重放后SHIP流水', 'SHIP', dispatchNo)
|
||||
const inventoryProof = context.createdResources.phase5OverseasFulfillment.inventoryProof
|
||||
inventoryProof.snapshots.afterDepart = stockAfterDepart
|
||||
inventoryProof.ledgers.afterDepart = ledgerAfterDepart
|
||||
context.assert(MODULE, '发车及重放只推进履约且库存和SHIP流水零额外扣减', departReplay.response,
|
||||
inventorySourcesPresent(stockAfterDepart, sourcePositionBalances)
|
||||
&& sameSnapshot(stockAfterPick, stockAfterDepart) && sameSnapshot(ledgerAfterPick, ledgerAfterDepart), trace(departReplay.response), OPTIONS)
|
||||
Object.assign(context.createdResources.phase5OverseasFulfillment.inventoryProof, {
|
||||
sourceBalanceIds, sourcePositionIds, sourcePositionBalances, inboundCodes: dispatchInboundCodes, beforePackages: conservation.proof.totals.positionBefore,
|
||||
afterPackages: quantityTotal((stockAfterDepart || []).flatMap(row => row.positions.filter(position => sourcePositionBalances.some(reference => reference.positionId === position.id && reference.balanceId === position.balanceId)))), expectedPackages,
|
||||
shipmentLedgerIds: (shippedRows || []).map(row => row.id),
|
||||
canDepart: ready.data?.canDepart === true,
|
||||
})
|
||||
const replayStable = success(departReplay.response) && departReplay.data?.replayed === true
|
||||
&& String(departReplay.data.recordVersion) === String(departed.data.recordVersion)
|
||||
&& JSON.stringify(departReplay.data.result) === JSON.stringify(departed.data.result)
|
||||
context.assert(MODULE, 'PDA发车重放保持原结果及公共版本', departReplay.response, replayStable, trace(departReplay.response), OPTIONS)
|
||||
if (!replayStable) return
|
||||
for (const proof of proofs) {
|
||||
proof.stockAfterDepart = await inventorySnapshot(context, proof.customerId, warehouseId, proof.inboundCodes,
|
||||
`客户${proof.customerId}发车后库存`, proof.sourceBalanceIds)
|
||||
proof.ledgerAfterDepart = await shipmentLedger(context, proof.customerId, dispatchNo, `客户${proof.customerId}发车后SHIP`, 'SHIP', dispatchNo)
|
||||
const unchanged = inventorySourcesPresent(proof.stockAfterDepart, proof.sourcePositionBalances)
|
||||
&& sameSnapshot(proof.stockAfter, proof.stockAfterDepart) && sameSnapshot(proof.ledgerAfter, proof.ledgerAfterDepart)
|
||||
context.assert(MODULE, '发车及重放库存和SHIP零额外扣减', departReplay.response, unchanged, trace(departReplay.response), OPTIONS)
|
||||
if (!unchanged) return
|
||||
}
|
||||
// 换标副场景仍核对真实新箱唛快照,执行同一原生装车前置。
|
||||
await verifyRelabelDispatchOutbound(context, context.createdResources?.phase4OverseasRelabel, picker.token, warehouseId)
|
||||
} finally {
|
||||
await disablePicker(context, picker, 5)
|
||||
if (picker) picker.token = null
|
||||
try { await disablePicker(context, picker, 5) }
|
||||
finally { if (picker) picker.token = null; await context.checkpoint() }
|
||||
}
|
||||
const primaryDelivery = deliveries.find(row => id(row.customerId) === customerId)
|
||||
if (!primaryDelivery) { blocker(context, '客户签收资料前置', '本轮主客户不在实际派送结果中'); return }
|
||||
const deliveryNo = primaryDelivery.deliveryNo, outboundNo = primaryDelivery.outboundNo
|
||||
const deliveryNos = deliveries.map(row => row.deliveryNo).sort()
|
||||
Object.assign(context.createdResources.phase5OverseasFulfillment, { outboundNo, deliveryNo, deliveryNos, outbounds, deliveries })
|
||||
for (const outboundRef of outbounds) {
|
||||
const outbound = await platform(context, 'GET', '逐客户回查发车后真实出库状态', `${LASTLEG_BASE}/outbound-orders/${encodeURIComponent(outboundRef.outboundNo)}`)
|
||||
const passed = outbound.data?.outbound?.outboundNo === outboundRef.outboundNo
|
||||
&& id(outbound.data?.outbound?.customerId) === id(outboundRef.customerId) && outbound.data?.outbound?.status === 'OUTBOUNDED'
|
||||
context.assert(MODULE, '每客户出库单独立OUTBOUNDED且归属不漂移', outbound.response, passed, trace(outbound.response), OPTIONS)
|
||||
if (!passed) return
|
||||
}
|
||||
const customerDeliveries = deliveries.filter(row => id(row.customerId) === customerId)
|
||||
const merchantPage = await merchant(context, merchantToken, 'GET', '客户分页回查自身派送单', query(MERCHANT_DELIVERY_BASE, { page: 1, size: 200, dispatchNo }))
|
||||
const merchantRows = merchantPage.data?.records
|
||||
const merchantScoped = Array.isArray(merchantRows)
|
||||
&& customerDeliveries.every(delivery => merchantRows.some(row => row.deliveryNo === delivery.deliveryNo))
|
||||
&& merchantRows.every(row => id(row.customerId) === customerId)
|
||||
context.assert(MODULE, '客户只看本车自己的派送单', merchantPage.response, merchantScoped, trace(merchantPage.response), OPTIONS)
|
||||
if (!merchantScoped) return
|
||||
const merchantDetail = await merchant(context, merchantToken, 'GET', '客户读取自身派送详情', `${MERCHANT_DELIVERY_BASE}/${encodeURIComponent(deliveryNo)}`)
|
||||
context.assert(MODULE, '客户派送详情保留订单货物费用操作真实结构', merchantDetail.response,
|
||||
merchantDetail.data?.order?.deliveryNo === deliveryNo && Array.isArray(merchantDetail.data?.skus)
|
||||
&& Array.isArray(merchantDetail.data?.fees) && Array.isArray(merchantDetail.data?.operations), trace(merchantDetail.response), OPTIONS)
|
||||
|
||||
const outbound = await platform(context, 'GET', '回查发车后出库单状态',
|
||||
query(`${LASTLEG_BASE}/outbound-orders/${encodeURIComponent(outboundNo)}`, { page: 1, size: 20 }))
|
||||
context.assert(MODULE, '排车PDA发车同步出库单OUTBOUNDED', outbound.response,
|
||||
outbound.data?.outbound?.outboundNo === outboundNo && outbound.data?.outbound?.status === 'OUTBOUNDED',
|
||||
trace(outbound.response), OPTIONS)
|
||||
|
||||
for (const [index, delivery] of deliveries.entries()) {
|
||||
const currentNo = id(delivery.deliveryNo)
|
||||
const beforeStatus = await platform(context, 'GET', `读取第${index + 1}张派送单状态版本`,
|
||||
query(`${LASTLEG_BASE}/delivery-orders/${encodeURIComponent(currentNo)}`, { customerId }))
|
||||
const deliveryVersion = Number(beforeStatus.data?.order?.version)
|
||||
context.assert(MODULE, `第${index + 1}张派送单提供状态乐观锁版本`, beforeStatus.response,
|
||||
Number.isInteger(deliveryVersion) && deliveryVersion >= 0,
|
||||
`version=${beforeStatus.data?.order?.version ?? 'missing'}; ${trace(beforeStatus.response)}`, OPTIONS)
|
||||
if (!Number.isInteger(deliveryVersion) || deliveryVersion < 0) return
|
||||
const delivering = await platform(context, 'PUT', `第${index + 1}张派送单推进配送中`,
|
||||
query(`${LASTLEG_BASE}/delivery-orders/${encodeURIComponent(currentNo)}/status`, { customerId }),
|
||||
{ status: 'DELIVERING', expectedVersion: deliveryVersion, remark: `${context.marker}-delivering-${index + 1}` })
|
||||
context.assert(MODULE, `第${index + 1}张派送状态进入DELIVERING`, delivering.response,
|
||||
delivering.data?.deliveryStatus === 'DELIVERING', trace(delivering.response), OPTIONS)
|
||||
if (!success(delivering.response)) return
|
||||
for (const delivery of deliveries) {
|
||||
const current = await platform(context, 'GET', '逐客户核对发车后真实派送状态',
|
||||
query(`${LASTLEG_BASE}/delivery-orders/${encodeURIComponent(delivery.deliveryNo)}`, { customerId: id(delivery.customerId) }))
|
||||
const passed = current.data?.order?.status === 'DELIVERING'
|
||||
&& id(current.data.order.customerId) === id(delivery.customerId) && current.data.order.outboundNo === delivery.outboundNo
|
||||
context.assert(MODULE, '派送由实际发车进入DELIVERING并保留客户出库关联', current.response, passed, trace(current.response), OPTIONS)
|
||||
if (!passed) return
|
||||
}
|
||||
|
||||
const completedDeliveries = []
|
||||
for (const [index, delivery] of deliveries.entries()) {
|
||||
const completed = await saveReceiptAndComplete(context, delivery, customerId, index)
|
||||
const completed = await saveReceiptAndComplete(context, delivery, id(delivery.customerId), index)
|
||||
if (!completed) return
|
||||
completedDeliveries.push(completed)
|
||||
}
|
||||
@@ -1160,6 +1233,12 @@ export async function run(context) {
|
||||
&& completedDeliveries.every((value) => value?.order?.status === 'DELIVERED'),
|
||||
`deliveries=${deliveryNos.join(',')}, completed=${completedDeliveries.length}`, OPTIONS)
|
||||
|
||||
const normalCompletion = await poll(() => context.request('GET', `${DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}`),
|
||||
response => success(response) && response.body?.data?.status === 'ACTIVE' && response.body?.data?.stage === 'DELIVERED')
|
||||
context.assert(MODULE, '全部客户正常签收汇总整车DELIVERED', normalCompletion.result,
|
||||
success(normalCompletion.result) && normalCompletion.result.body?.data?.stage === 'DELIVERED', trace(normalCompletion.result), OPTIONS)
|
||||
if (normalCompletion.result?.body?.data?.stage !== 'DELIVERED') return
|
||||
context.createdResources.phase5OverseasFulfillment.normalCompletion = normalCompletion.result.body.data
|
||||
const removedComplete = await context.request('POST', query(`${LASTLEG_BASE}/delivery-orders/${encodeURIComponent(deliveryNo)}/complete`, { customerId }), { status: 'DELIVERED', expectedVersion: 0, remark: `${context.marker}-removed-complete-probe` })
|
||||
context.addStep(MODULE, '人工确认派送完成接口已移除', removedComplete,
|
||||
removedComplete?.http === 404 || removedComplete?.http === 405
|
||||
@@ -1201,11 +1280,12 @@ export async function run(context) {
|
||||
return
|
||||
}
|
||||
context.createdResources.phase5OverseasFulfillment.sourceOrderId = sourceOrderId
|
||||
await verifyFulfillmentRootStatus(context, { sourceOrderId, customerId, merchantToken })
|
||||
const rootCompletion = await verifyFulfillmentRootStatus(context, { sourceOrderId, customerId, merchantToken })
|
||||
if (!rootCompletion?.passed) return
|
||||
const foreignMerchantToken = context.context?.phase5TailVerification?.foreignMerchantToken
|
||||
|| await createSignedDocumentIsolationCustomer(context)
|
||||
await verifyCustomerSignedDocuments(context, merchantToken,
|
||||
context.createdResources.phase5OverseasFulfillment.receiptFiles, foreignMerchantToken)
|
||||
if (!await verifyCustomerSignedDocuments(context, merchantToken,
|
||||
context.createdResources.phase5OverseasFulfillment.receiptFiles?.filter(receipt => receipt.customerId === customerId), foreignMerchantToken)) return
|
||||
|
||||
const savedVisit = await platform(context, 'PUT', '保存无附件客户回访结果',
|
||||
query(`${LASTLEG_BASE}/customer-visits/${encodeURIComponent(visitId)}`, { customerId }), {
|
||||
@@ -1216,6 +1296,19 @@ export async function run(context) {
|
||||
savedVisit.data?.visit?.status === 'COMPLETED'
|
||||
&& savedVisit.data?.visit?.selectedInboundCode === inbound.inboundCode
|
||||
&& savedVisit.data?.visit?.visitResult === 'SATISFIED', trace(savedVisit.response), OPTIONS)
|
||||
if (!success(savedVisit.response) || savedVisit.data?.visit?.status !== 'COMPLETED'
|
||||
|| savedVisit.data.visit.selectedInboundCode !== inbound.inboundCode
|
||||
|| savedVisit.data.visit.visitResult !== 'SATISFIED') return
|
||||
|
||||
// 默认验收在正常签收与回访完成后结案。旧异常子场景仅接受调用方明确授权的独立开关。
|
||||
if (context.context?.phase5TailVerification?.runReturnScenario !== true) {
|
||||
context.createdResources.phase5OverseasFulfillment.tripClosure = await finishFleetTrip(context, MODULE, tripProof)
|
||||
await verifyFleetTripFinance(context, MODULE, { tripNo: tripProof.tripNo, dispatchNos: [dispatchNo],
|
||||
businesses: deliveries.map(row => ({ customerId: id(row.customerId), deliveryNo: row.deliveryNo })), receiptsVisible: true })
|
||||
context.createdResources.phase5OverseasFulfillment.finalStatus = 'DELIVERED_WITH_COMPLETED_VISIT'
|
||||
await context.checkpoint()
|
||||
return
|
||||
}
|
||||
|
||||
const manualExceptionBody = {
|
||||
customerId, inboundCode: inbound.inboundCode, lastMileOrderNo: inbound.lastMileOrderNo,
|
||||
@@ -1223,10 +1316,6 @@ export async function run(context) {
|
||||
}
|
||||
let manualException = await platform(context, 'POST', '为本轮尾程单创建人工异常',
|
||||
`${LASTLEG_BASE}/exception-orders`, manualExceptionBody)
|
||||
if (manualException.response.http === 0) {
|
||||
manualException = await platform(context, 'POST', '人工异常连接失败后使用同载荷重试',
|
||||
`${LASTLEG_BASE}/exception-orders`, manualExceptionBody)
|
||||
}
|
||||
const manualExceptionNo = id(manualException.data?.exceptionNo)
|
||||
context.assert(MODULE, '人工异常进入PENDING并返回业务单号', manualException.response,
|
||||
nonBlank(manualExceptionNo) && manualException.data?.status === 'PENDING', trace(manualException.response), OPTIONS)
|
||||
@@ -1264,7 +1353,7 @@ export async function run(context) {
|
||||
context.createdResources.phase5OverseasFulfillment.returnReceipt = returnReceipt
|
||||
context.createdResources.phase5OverseasFulfillment.tripClosure = await finishFleetTrip(context, MODULE, tripProof)
|
||||
await verifyFleetTripFinance(context, MODULE, { tripNo: tripProof.tripNo, dispatchNos: [dispatchNo],
|
||||
businesses: deliveryNos.map(deliveryNo => ({ customerId, deliveryNo })), receiptsVisible: true })
|
||||
businesses: deliveries.map(row => ({ customerId: id(row.customerId), deliveryNo: row.deliveryNo })), receiptsVisible: true })
|
||||
context.createdResources.phase5OverseasFulfillment.finalStatus = returnReceipt
|
||||
? 'RETURNED_WITH_COMPLETED_VISIT' : 'RETURN_PENDING_WITH_COMPLETED_VISIT'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user