补齐海外仓和私人地址正常及短少收货真实验收
This commit is contained in:
@@ -12,7 +12,7 @@ import { run as verifyCarrierQuery } from './phase3-carrier-schedule-queries.mjs
|
||||
import { runUntilPickupReady } from './phase4-carrier-nodes.mjs'
|
||||
import { resolvePickerAccess, createPicker, disablePicker, uploadPhotos } from './phase4-overseas-pda.mjs'
|
||||
import { receivingProgressMatches, outboundCandidateGuards, outboundCreationGuards, outboundTaskGuards, probePartialPick } from './dropship-outbound-edge-cases.mjs'
|
||||
import { receiveMixedContainer } from './mixed-container-receiving.mjs'
|
||||
import { receiveMixedContainer, receiveOverseasComplete, receiveOverseasShortage } from './mixed-container-receiving.mjs'
|
||||
import { exerciseOldCabinets } from './pda-loading-link-regression.mjs'
|
||||
import { selectSessionPdaOperator } from './domestic-pda-operator.mjs'
|
||||
|
||||
@@ -20,6 +20,14 @@ const MODULE = 'dropship-pallet-matrix'
|
||||
const BASE = '/api/platform/v1/first-leg/pda-dropship'
|
||||
const OPTIONS = { scenario: MODULE, deploymentBlocker: true }
|
||||
|
||||
export function loadingUnitTypesMatch(group, items) {
|
||||
const expected = new Set(group.map(record => record.order.fulfillmentType === 'ONE_CLICK_DROPSHIP'
|
||||
? 'SEALED_PALLET' : 'PACKAGE'))
|
||||
const actual = new Set(items.map(item => item.unitType))
|
||||
return items.length > 0 && actual.size === expected.size
|
||||
&& [...actual].every(type => expected.has(type))
|
||||
}
|
||||
|
||||
export function sameOutboundTask(left, right) {
|
||||
const databaseTime = (value) => {
|
||||
const match = typeof value === 'string' && value.match(/^(.*T\d{2}:\d{2}:\d{2})(?:\.(\d+))?$/)
|
||||
@@ -155,6 +163,25 @@ async function runWorkflow(context, stopBeforeOverseasPda, mixedContainer = fals
|
||||
const pallets = [...new Map(group.flatMap((item) => item.task?.pallets || [])
|
||||
.map((pallet) => [String(pallet.palletId), pallet])).values()]
|
||||
for (const record of group) {
|
||||
if (record.order.destinationType === 'PRIVATE') {
|
||||
const path = `/api/platform/v1/inbounds/${record.source.inboundId}/fees/manual-quote?tenantId=${encodeURIComponent(record.order.tenantId)}`
|
||||
const body = {
|
||||
selectedWeightKg: Number(record.summary.actualNetWeightKg || record.summary.actualGrossWeightKg),
|
||||
selectedVolumeCbm: Number(record.summary.actualVolumeCbm), unitPrice: 1,
|
||||
reason: `${marker}私人地址验收测试人工报价`,
|
||||
}
|
||||
check('私人地址人工报价采用实际测量重量和体积', null,
|
||||
body.selectedWeightKg > 0 && body.selectedVolumeCbm > 0)
|
||||
const headers = { 'Idempotency-Key': `${marker}-${record.label}-manual-quote` }
|
||||
const quoted = await command('私人地址人工头程报价', 'POST', path, body, headers)
|
||||
check('私人地址报价形成待分柜正式费用快照', null,
|
||||
quoted.rateCode === 'MANUAL_QUOTE' && quoted.feeStatus === 'CALCULATED'
|
||||
&& quoted.nextProcessStatus === 'WAIT_SPLIT' && Number(quoted.unitPrice) === body.unitPrice)
|
||||
const replay = await command('私人地址人工报价原键重放', 'POST', path, body, headers)
|
||||
check('私人地址报价重放保持同一费用版本', null, JSON.stringify(replay) === JSON.stringify(quoted))
|
||||
record.manualQuote = { feeId: quoted.feeId, feeVersion: quoted.feeVersion,
|
||||
unitPrice: body.unitPrice, purpose: 'TEST_ACCEPTANCE' }
|
||||
}
|
||||
const fee = await ensureInitialCalculated(context, { ...record.measurement,
|
||||
tenantId: String(record.order.tenantId), inboundId: record.source.inboundId,
|
||||
finalStatus: 'APPROVED', processStatus: record.summary.processStatus }, OPTIONS)
|
||||
@@ -296,10 +323,7 @@ async function runWorkflow(context, stopBeforeOverseasPda, mixedContainer = fals
|
||||
? { action: 'PICK_PALLET', palletNo: item.palletNo }
|
||||
: { action: 'PICK', pickedPackageCount: Number(item.plannedPackageCount) }) })),
|
||||
}
|
||||
check(`${label}PDA全部任务均为封托单元`, null,
|
||||
claimed.items.length > 0 && (mixedContainer
|
||||
? claimed.items.some(item => item.unitType === 'SEALED_PALLET') && claimed.items.some(item => item.unitType !== 'SEALED_PALLET')
|
||||
: claimed.items.every((item) => item.unitType === 'SEALED_PALLET')))
|
||||
check(`${label}PDA任务单元与实际履约类型一致`, null, loadingUnitTypesMatch(group, claimed.items))
|
||||
let picked = claimed
|
||||
if (linkRegression) {
|
||||
for (const palletNo of [...new Set(claimed.items.map(item => item.palletNo))]) {
|
||||
@@ -426,6 +450,16 @@ async function runWorkflow(context, stopBeforeOverseasPda, mixedContainer = fals
|
||||
summary.orders.some((order) => order.inboundCode === record.source.inboundCode
|
||||
&& String(order.customerId) === String(record.order.tenantId))),
|
||||
`containerNo=${loading.containerNo}, expectedOrders=${group.length}, actualOrders=${summary?.orders?.length ?? 'unavailable'}`)
|
||||
if (mixedContainer && group.length === 3) {
|
||||
const overseas = group.find(record => record.order.fulfillmentType === 'STANDARD' && record.order.destinationType === 'OVERSEAS')
|
||||
const order = summary.orders.find(item => item.inboundCode === overseas.source.inboundCode)
|
||||
const detail = await call('读取普通海外仓订单当前版本', 'GET', `${base}/orders/${order.orderId}?customerId=${encodeURIComponent(order.customerId)}`)
|
||||
check('普通海外仓人工计划采用当前订单版本', null, Number.isInteger(detail.order?.version))
|
||||
await call('普通海外仓四箱预留两托', 'POST', `${base}/inbound-plans/manual`, {
|
||||
customerId: String(order.customerId), orderId: String(order.orderId), expectedOrderVersion: detail.order.version,
|
||||
palletCount: 2, palletType: 'WOOD', assigned: false, mergeRequired: false, urgent: false, remark: marker,
|
||||
}, { 'Idempotency-Key': `${marker}-${label}-overseas-two-pallets` })
|
||||
}
|
||||
const preview = await call('入库计划预览', 'GET', `${base}/inbound-plans/container-preview?sourceContainerBookingId=${bookingId}`)
|
||||
check(`${label}全柜入库计划就绪`, null, preview.ready === true && Number(preview.totalOrderCount) === group.length)
|
||||
const body = { sourceContainerBookingId: bookingId, snapshotVersion: Number(preview.snapshotVersion),
|
||||
@@ -437,6 +471,13 @@ async function runWorkflow(context, stopBeforeOverseasPda, mixedContainer = fals
|
||||
const matching = plans.filter((plan) => plan.lines?.some((line) => line.inboundCode === record.source.inboundCode))
|
||||
check(`${record.label}计划精确关联来源入仓码`, null, matching.length === 1)
|
||||
const target = matching[0]
|
||||
if (record.order.destinationType === 'PRIVATE') {
|
||||
const source = summary.orders.find(order => order.inboundCode === record.source.inboundCode)
|
||||
const detail = await call('读取私人地址尾程收货类型', 'GET',
|
||||
`${base}/orders/${source.orderId}?customerId=${encodeURIComponent(source.customerId)}`)
|
||||
check(`${record.label}私人地址进入普通海外收货模式`, null,
|
||||
detail.order.destinationType === 'OVERSEAS' && detail.order.orderType === 'FIRST_LEG')
|
||||
}
|
||||
const expectedNos = record.task?.pallets.map((pallet) => pallet.palletNo).sort()
|
||||
const actualNos = (target.pallets || []).map((pallet) => pallet.palletNo).sort()
|
||||
check(`${record.label}来源托盘完整绑定海外真实资源`, null,
|
||||
@@ -459,6 +500,7 @@ async function runWorkflow(context, stopBeforeOverseasPda, mixedContainer = fals
|
||||
async function receiveOverseas(group) {
|
||||
const record = group[0]
|
||||
const label = record.label
|
||||
const sourceContainerBookingId = String(record.loading.cabinet.id)
|
||||
// The PDA account helper keeps the marker tail; retain the run identity there.
|
||||
context.marker = `${label}-RECEIVE-${marker}`
|
||||
const base = '/api/pda/v1/last-leg/receiving-containers'
|
||||
@@ -476,19 +518,32 @@ async function runWorkflow(context, stopBeforeOverseasPda, mixedContainer = fals
|
||||
let candidate
|
||||
for (let attempt = 0; attempt < 60; attempt++) {
|
||||
const response = await context.request('GET', `${base}/candidates`, undefined, picker.token, {})
|
||||
candidate = response?.body?.data?.find?.((item) => item.containerNo === record.loading.containerNo)
|
||||
candidate = response?.body?.data?.find?.((item) => item.containerNo === record.loading.containerNo
|
||||
&& String(item.sourceContainerBookingId) === sourceContainerBookingId)
|
||||
if (candidate?.operable && candidate?.allPlansReady) break
|
||||
if (attempt < 59) await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
}
|
||||
check(`${label}本柜船节点和入库计划已跨区收敛`, null,
|
||||
candidate?.operable === true && candidate?.allPlansReady === true)
|
||||
await call('准备拆柜会话', 'POST', `${base}/${record.loading.containerNo}/prepare`,
|
||||
await call('准备拆柜会话', 'POST', `${base}/${record.loading.containerNo}/prepare?sourceContainerBookingId=${encodeURIComponent(sourceContainerBookingId)}`,
|
||||
{ idempotencyKey: `${context.marker}-prepare` })
|
||||
const photos = await uploadPhotos(context, picker.token)
|
||||
check(`${label}三类卸柜照片齐全`, null, photos?.length === 3)
|
||||
const signed = await call('照片绑定并签收', 'POST', `${base}/current/sign`,
|
||||
{ photos, idempotencyKey: `${context.marker}-sign` })
|
||||
check(`${label}签收成功`, null, signed.status === 'SIGNED')
|
||||
const adminCall = async (name, method, path, body, headers) => {
|
||||
const response = await context.request(method, path, body, context.token, headers)
|
||||
const data = context.apiSuccess(MODULE, `${label}${name}`, response, '', OPTIONS)
|
||||
check(`${label}${name}成功`, response, response?.body?.code === '200' && data != null)
|
||||
return data
|
||||
}
|
||||
if (mixedContainer && group.length === 1) {
|
||||
const receive = record.label === 'PRIVATE-COMPLETE' ? receiveOverseasComplete : receiveOverseasShortage
|
||||
record.overseas.receiving = await receive({ record, call, adminCall, check, marker: context.marker,
|
||||
request: (method, path, body) => context.request(method, path, body, picker.token, {}) })
|
||||
return
|
||||
}
|
||||
if (mixedContainer) {
|
||||
const completed = await receiveMixedContainer({ group, call, check, marker: context.marker,
|
||||
request: (method, path, body) => context.request(method, path, body, picker.token, {}) })
|
||||
@@ -502,6 +557,18 @@ async function runWorkflow(context, stopBeforeOverseasPda, mixedContainer = fals
|
||||
&& row.locationId && item.overseas.plan.pallets.some(p => String(p.warehousePalletId) === String(row.palletId)))
|
||||
&& stock.items.reduce((sum, row) => sum + Number(row.availablePackages), 0) === item.expectedCartonMarks.length)
|
||||
item.overseas.receiving = { completed, stock, receivingVerified: true }
|
||||
if (item.order.fulfillmentType === 'STANDARD' && item.order.destinationType === 'OVERSEAS') {
|
||||
const plan = await adminCall('回查普通海外仓现场逐托实收', 'GET', `/api/platform/v1/last-leg/inbound-plans/${encodeURIComponent(item.overseas.plan.planNo)}?customerId=${encodeURIComponent(item.order.tenantId)}`)
|
||||
check('普通海外仓计划二加二可按三加一实收', null,
|
||||
JSON.stringify(plan.allocations.map(row => Number(row.plannedPackageCount)).sort()) === '[2,2]'
|
||||
&& JSON.stringify(plan.pallets.map(row => Number(row.actualPackageCount)).sort()) === '[1,3]')
|
||||
for (const pallet of plan.pallets) {
|
||||
const label = await context.requestBinary('GET', `/api/platform/v1/overseas-warehouse-resources/${item.overseas.warehouseId}/pallets/${pallet.warehousePalletId}/label`)
|
||||
check('普通海外仓预留托盘标签返回有效PDF', label,
|
||||
label.http === 200 && label.contentType.toLowerCase().includes('application/pdf')
|
||||
&& label.bytes.length > 1000 && new TextDecoder().decode(label.bytes.slice(0, 5)) === '%PDF-')
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -666,6 +733,16 @@ async function runWorkflow(context, stopBeforeOverseasPda, mixedContainer = fals
|
||||
await progressShipping(group)
|
||||
await convertOverseas(group)
|
||||
await receiveOverseas(group)
|
||||
const shortage = await prepare('PRIVATE-SHORTAGE', 1, {}, 'PRIVATE')
|
||||
await allocate([shortage])
|
||||
await progressShipping([shortage])
|
||||
await convertOverseas([shortage])
|
||||
await receiveOverseas([shortage])
|
||||
const complete = await prepare('PRIVATE-COMPLETE', 1, {}, 'PRIVATE')
|
||||
await allocate([complete])
|
||||
await progressShipping([complete])
|
||||
await convertOverseas([complete])
|
||||
await receiveOverseas([complete])
|
||||
context.createdResources.dropshipPalletMatrix.stage = 'MIXED_CONTAINER_INBOUND_COMPLETED'
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user