720 lines
47 KiB
JavaScript
720 lines
47 KiB
JavaScript
import { run as createOrder } from './orders.mjs'
|
|
import { run as measure, completeDropshipPalletization } from './inbound-measurement.mjs'
|
|
import { channelGroup } from './smart-loading-channel-group.mjs'
|
|
import { ensureInitialCalculated } from './inbound-fees.mjs'
|
|
import { oceanBookingDraft, resolveOceanBookingAgent } from './ocean-booking-fixture.mjs'
|
|
import { ensureMatsonCarrierPartner } from './carrier-partner-fixture.mjs'
|
|
import { iso6346ContainerNumber } from './iso6346-container-number.mjs'
|
|
import { uploadEvidence, markerSuffix } from './phase3-loading.mjs'
|
|
import { waitForCompletion } from './phase3-loading-finalize.mjs'
|
|
import { run as completeCustoms } from './phase3-customs.mjs'
|
|
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 { exerciseOldCabinets } from './pda-loading-link-regression.mjs'
|
|
|
|
const MODULE = 'dropship-pallet-matrix'
|
|
const BASE = '/api/platform/v1/first-leg/pda-dropship'
|
|
const OPTIONS = { scenario: MODULE, deploymentBlocker: true }
|
|
|
|
export function sameOutboundTask(left, right) {
|
|
const databaseTime = (value) => {
|
|
const match = typeof value === 'string' && value.match(/^(.*T\d{2}:\d{2}:\d{2})(?:\.(\d+))?$/)
|
|
return match ? Date.parse(match[1]) + Math.round(Number(`0.${match[2] || '0'}`) * 1000) : NaN
|
|
}
|
|
return ['id', 'taskNo', 'outboundNo', 'status'].every((key) => left?.[key] === right?.[key])
|
|
&& ['pushedAt', 'completedAt'].every((key) => left?.[key] == null && right?.[key] == null
|
|
|| Number.isFinite(databaseTime(left?.[key])) && databaseTime(left[key]) === databaseTime(right?.[key]))
|
|
}
|
|
|
|
// Domestic producer stage: these retained loaded fixtures must subsequently be verified
|
|
// through overseas receipt and dedicated dropship outbound, not generic dispatch.
|
|
export async function run(context) {
|
|
return runWorkflow(context, false)
|
|
}
|
|
|
|
export async function prepareBrowserFixture(context) {
|
|
return runWorkflow(context, true)
|
|
}
|
|
|
|
export async function runMixedContainer(context) {
|
|
return runWorkflow(context, false, true)
|
|
}
|
|
|
|
export async function runDomesticLinkRegression(context) {
|
|
return runWorkflow(context, false, false, true)
|
|
}
|
|
|
|
export async function runPhysicalCartonRegression(context) {
|
|
return runWorkflow(context, false, false, false, true)
|
|
}
|
|
|
|
async function runWorkflow(context, stopBeforeOverseasPda, mixedContainer = false, linkRegression = false,
|
|
physicalCartonRegression = false) {
|
|
const marker = context.marker
|
|
const savedOptions = context.options.prepareDropshipManualCount
|
|
const savedContext = { ...context.context }
|
|
const records = []
|
|
context.createdResources.dropshipPalletMatrix = { marker, stage: 'DOMESTIC_PALLETIZATION', records,
|
|
fullFlowVerified: false }
|
|
const check = (name, response, passed, details = '') => {
|
|
context.assert(MODULE, name, response, passed, details, OPTIONS)
|
|
if (!passed) throw new Error(name)
|
|
}
|
|
async function prepare(label, palletCount, mode = {}, destinationType = null, variant = {}) {
|
|
context.marker = `${marker}-${label}`
|
|
for (const key of ['orders', 'inboundMeasurement', 'ocrInbound', 'testInbound', 'dropshipManualCount']) {
|
|
delete context.createdResources[key]
|
|
}
|
|
for (const key of ['inboundWeightOcr', 'ocrInbound', 'testInbound', 'createdInbound']) delete context.context[key]
|
|
context.context.orderDestinationType = destinationType || 'OVERSEAS'
|
|
context.context.orderFulfillmentType = destinationType ? 'STANDARD' : 'ONE_CLICK_DROPSHIP'
|
|
context.context.orderProfile = { taxMode: 'BUY_DECLARATION', pickupMethod: 'SELF_EXPRESS', containerMode: 'LCL' }
|
|
const totalPackageCount = Number(variant.totalPackageCount || 4)
|
|
const skuCount = Number(variant.skuCount || 1)
|
|
context.context.waitSplitVariant = {
|
|
skuCount, packageCountPerSku: totalPackageCount,
|
|
packageCounts: Array.from({ length: skuCount }, () => totalPackageCount),
|
|
totalPackageCount, palletCount,
|
|
}
|
|
if (label.startsWith('MULTI-MERGE-')) {
|
|
context.context.waitSplitVariant.skuCodes = [`SKU-SHARED-${marker}`.slice(0, 64)]
|
|
}
|
|
context.options.prepareDropshipManualCount = !destinationType
|
|
const start = context.steps.length
|
|
await context.executeModule(`${MODULE}-${label}-order`, () => createOrder(context))
|
|
await context.executeModule(`${MODULE}-${label}-measurement`, () => measure(context))
|
|
check(`${label}本轮订单和测量前置全部通过`, null,
|
|
!context.steps.slice(start).some((step) => !step.passed))
|
|
if (destinationType) {
|
|
const measurement = { ...context.createdResources.inboundMeasurement }
|
|
const order = { ...context.createdResources.orders }
|
|
const response = await context.request('GET',
|
|
`/api/platform/inbounds/${measurement.inboundId}?tenantId=${measurement.tenantId}`)
|
|
const detail = context.apiSuccess(MODULE, `${label}普通订单待分柜快照`, response, '', OPTIONS)
|
|
check(`${label}普通订单四箱待分柜且类型未漂移`, response,
|
|
detail?.summary?.processStatus === 'WAIT_SPLIT' && measurement.actualPackageCount === 4
|
|
&& order.fulfillmentType === 'STANDARD' && order.destinationType === destinationType)
|
|
const record = { label, order, measurement, summary: detail.summary, task: null,
|
|
source: { inboundId: String(measurement.inboundId), inboundCode: detail.summary.inboundCode },
|
|
expectedCartonMarks: Array.from({ length: 4 }, (_, index) => `${detail.summary.inboundCode}#4-${index + 1}`) }
|
|
records.push(record)
|
|
return record
|
|
}
|
|
const source = context.createdResources.dropshipManualCount
|
|
check(`${label}存在本轮未清点任务`, null, Boolean(source?.taskId))
|
|
const task = await completeDropshipPalletization(context, {
|
|
...source, measurement: { actualPackageCount: source.totalCartons, expectedPalletCount: palletCount }, ...mode,
|
|
})
|
|
check(`${label}托盘作业断言全部通过`, null,
|
|
Boolean(task) && !context.steps.slice(start).some((step) => !step.passed))
|
|
const record = { label, order: { ...context.createdResources.orders },
|
|
measurement: { ...context.createdResources.inboundMeasurement }, source: { ...source }, task,
|
|
expectedSkuCount: skuCount,
|
|
expectedCartonMarks: Array.from({ length: source.totalCartons }, (_, index) =>
|
|
`${source.inboundCode}#${source.totalCartons}-${index + 1}`) }
|
|
records.push(record)
|
|
return record
|
|
}
|
|
async function verify(record, expectedPallets) {
|
|
const { taskId, warehouseId, inboundId } = record.source
|
|
const response = await context.request('GET', `${BASE}/tasks/${taskId}?warehouseId=${warehouseId}`)
|
|
const task = context.apiSuccess(MODULE, `${record.label}回查权威任务`, response, '', OPTIONS)
|
|
const cartons = (task?.pallets || []).flatMap((pallet) => pallet.cartons || [])
|
|
.filter((carton) => String(carton.inboundId) === String(inboundId))
|
|
check(`${record.label}逐票箱唛归属和数量守恒`, response,
|
|
task?.taskStatus === 'COMPLETED' && task.pallets?.length === expectedPallets
|
|
&& task.pallets.every((pallet) => pallet.status === 'SEALED')
|
|
&& cartons.length === record.expectedCartonMarks.length
|
|
&& cartons.every((carton) => Array.isArray(carton.skuLines)
|
|
&& carton.skuLines.length === (record.expectedSkuCount || 1))
|
|
&& JSON.stringify(cartons.map((carton) => carton.boxCode).sort())
|
|
=== JSON.stringify([...record.expectedCartonMarks].sort()))
|
|
record.task = task
|
|
const inboundResponse = await context.request('GET',
|
|
`/api/platform/inbounds/${inboundId}?tenantId=${task.tenantId}`)
|
|
const inbound = context.apiSuccess(MODULE, `${record.label}封托后回查入仓状态`, inboundResponse, '', OPTIONS)
|
|
check(`${record.label}实际进入待分柜状态`, inboundResponse,
|
|
inbound?.summary?.processStatus === 'WAIT_SPLIT')
|
|
record.summary = inbound.summary
|
|
record.domesticVerified = true
|
|
}
|
|
async function allocate(group, regression = {}) {
|
|
const label = group.map((item) => item.label).join('+')
|
|
context.marker = `${marker}-${group[0].label}-LOAD`
|
|
const smart = '/api/platform/v1/first-leg/smart-loading'
|
|
async function command(name, method, path, body, headers = {}) {
|
|
const response = await context.request(method, path, body, undefined, headers)
|
|
const value = context.apiSuccess(MODULE, `${label}${name}`, response, '', OPTIONS)
|
|
check(`${label}${name}成功`, response, response?.body?.code === '200' && Boolean(value))
|
|
return value
|
|
}
|
|
const pallets = [...new Map(group.flatMap((item) => item.task?.pallets || [])
|
|
.map((pallet) => [String(pallet.palletId), pallet])).values()]
|
|
for (const record of group) {
|
|
const fee = await ensureInitialCalculated(context, { ...record.measurement,
|
|
tenantId: String(record.order.tenantId), inboundId: record.source.inboundId,
|
|
finalStatus: 'APPROVED', processStatus: record.summary.processStatus }, OPTIONS)
|
|
check(`${record.label}初始费用已计算`, null, Boolean(fee))
|
|
}
|
|
const summary = group[0].summary
|
|
if (regression.rounds) await exerciseOldCabinets({ context, group, pallets, command, check, rounds: regression.rounds })
|
|
let cabinet = await command('创建本场景模拟柜', 'POST', '/api/platform/v1/containers', {
|
|
channelGroupCode: channelGroup(summary.channelCode), warehouseId: String(summary.warehouseId),
|
|
containerType: '40HQ', capacityCbm: 68, maxWeightKg: 19500,
|
|
})
|
|
// Arrival is frozen by the loading event; prepare it while the cabinet is editable.
|
|
const estimatedWarehouseArrivalAt = `${new Date(Date.now() + 86400000).toISOString().slice(0, 10)}T10:00:00`
|
|
cabinet = await command('装柜前保存柜级预计到仓时间', 'PUT', `/api/platform/v1/containers/${cabinet.id}`, {
|
|
version: Number(cabinet.version), data: {
|
|
warehouseId: String(summary.warehouseId), containerType: '40HQ',
|
|
capacityCbm: 68, maxWeightKg: 19500, estimatedWarehouseArrivalAt,
|
|
},
|
|
})
|
|
check(`${label}预计到仓时间已写入正式柜资料`, null,
|
|
cabinet.estimatedWarehouseArrivalAt?.slice(0, 16) === estimatedWarehouseArrivalAt.slice(0, 16))
|
|
let plan = await command('读取自动生成装柜计划', 'GET', `${smart}/plans/by-booking/${cabinet.id}`)
|
|
const path = `${smart}/plans/${plan.id}`
|
|
async function conserve(name) {
|
|
// Smart-loading items are physical pallet summaries. The loading detail is the
|
|
// authoritative per-inbound composition; shared pallets must satisfy both views.
|
|
const detail = await command(`${name}逐票装柜组成`, 'GET',
|
|
`/api/platform/v1/first-leg/loading-plans/${plan.id}?warehouseId=${summary.warehouseId}`)
|
|
const expectedIds = new Set(group.map((record) => String(record.source.inboundId)))
|
|
check(`${label}${name}没有混入其他订单`, null,
|
|
detail.items?.every((item) => expectedIds.has(String(item.inboundId))))
|
|
check(`${label}${name}整托汇总不重复计算箱数`, null,
|
|
(mixedContainer || plan.items?.length === pallets.length)
|
|
&& plan.items.reduce((sum, item) => sum + Number(item.packageCount), 0)
|
|
=== group.reduce((sum, record) => sum + record.expectedCartonMarks.length, 0))
|
|
for (const record of group) {
|
|
const actual = detail.items.filter((item) => String(item.inboundId) === record.source.inboundId)
|
|
const packagesByPhysicalUnit = new Map()
|
|
for (const item of actual) {
|
|
const key = item.palletId == null ? `ITEM:${item.id}` : `PALLET:${item.palletId}`
|
|
packagesByPhysicalUnit.set(key, Math.max(packagesByPhysicalUnit.get(key) || 0,
|
|
Number(item.packageCount)))
|
|
}
|
|
check(`${record.label}${name}分配箱数与托盘身份正确`, null,
|
|
[...packagesByPhysicalUnit.values()].reduce((sum, count) => sum + count, 0)
|
|
=== record.expectedCartonMarks.length
|
|
&& new Set(actual.map((item) => item.skuCode).filter(Boolean)).size === (record.expectedSkuCount || 1)
|
|
&& (!record.task || actual.every((item) => record.task.pallets.some((pallet) =>
|
|
String(pallet.palletId) === String(item.palletId)))))
|
|
}
|
|
}
|
|
for (const pallet of pallets) {
|
|
plan = await command('手工整托加入', 'POST', `${path}/pallet-assignments`, {
|
|
palletId: String(pallet.palletId), expectedVersion: Number(plan.version),
|
|
operationId: `${context.marker}-add-${pallet.palletId}`,
|
|
})
|
|
}
|
|
if (mixedContainer) {
|
|
for (const record of group.filter(item => !item.task)) {
|
|
plan = await command(`${record.label}普通订单按整票箱数加入`, 'POST', `${path}/inbound-assignments`, {
|
|
inboundId: record.source.inboundId, packageCount: record.expectedCartonMarks.length,
|
|
expectedVersion: Number(plan.version), operationId: `${context.marker}-add-${record.label}`,
|
|
})
|
|
}
|
|
}
|
|
await conserve('手工加入后')
|
|
if (!mixedContainer) {
|
|
for (const pallet of pallets) {
|
|
plan = await command('按物理托盘完整移除', 'DELETE', `${path}/pallet-assignments/${pallet.palletId}`, {
|
|
expectedPlanVersion: Number(plan.version), operationId: `${context.marker}-remove-${pallet.palletId}`,
|
|
})
|
|
}
|
|
check(`${label}整托移除不残留其他票明细`, null, plan.items?.length === 0)
|
|
plan = await command('智能推荐整托加入', 'POST', `${path}/recommend`, {
|
|
expectedVersion: Number(plan.version), operationId: `${context.marker}-recommend`,
|
|
customerId: String(group[0].task.tenantId), inboundCode: group[0].source.inboundCode,
|
|
})
|
|
await conserve('智能推荐后')
|
|
}
|
|
for (const record of group) record.loading = { cabinet, plan, allocationVerified: true }
|
|
if (regression.activeUnstarted) {
|
|
const pallet = pallets.at(-1)
|
|
plan = await command('分配另一实托到未开始柜前移除', 'DELETE', `${path}/pallet-assignments/${pallet.palletId}`, {
|
|
expectedPlanVersion: Number(plan.version), operationId: `${context.marker}-split-remove`,
|
|
})
|
|
await exerciseOldCabinets({ context, group, pallets: [pallet], command, check, rounds: 1, active: true })
|
|
}
|
|
const carrier = await ensureMatsonCarrierPartner(context, MODULE, OPTIONS)
|
|
const agent = await resolveOceanBookingAgent(context, MODULE, 'MATSON', OPTIONS)
|
|
check(`${label}真实船司和订舱代理齐全`, null, Boolean(carrier?.id && agent?.id))
|
|
const suffix = markerSuffix(context.marker)
|
|
const draft = oceanBookingDraft(context, suffix, {
|
|
customerId: String(group[0].order.tenantId), customerName: summary.customerName,
|
|
warehouseId: String(summary.warehouseId), channelCode: summary.channelCode,
|
|
}, { carrierCode: 'MATSON', shippingCompanyId: String(carrier.id), shippingCompanyName: carrier.name,
|
|
bookingAgentId: String(agent.id), bookingAgentName: agent.name })
|
|
const booking = await command('创建独立订舱', 'POST', '/api/platform/v1/ocean-bookings',
|
|
{ expectedVersion: 0, data: draft }, { 'Idempotency-Key': `${context.marker}-booking` })
|
|
const bookingId = String(booking.oceanBooking.id)
|
|
const form = new FormData()
|
|
form.append('expectedVersion', String(booking.oceanBooking.version))
|
|
form.append('documentType', 'BOOKING_CONFIRMATION')
|
|
form.append('file', new Blob([`HTTP acceptance ${context.marker}`], { type: 'text/plain' }), 'booking-confirmation.txt')
|
|
const docResponse = await context.requestMultipart('POST',
|
|
`/api/platform/v1/ocean-bookings/${bookingId}/documents/upload`, form, context.token,
|
|
{ 'Idempotency-Key': `${context.marker}-booking-document` })
|
|
const doc = context.apiSuccess(MODULE, `${label}上传订舱确认文件`, docResponse, '', OPTIONS)
|
|
check(`${label}订舱文件已持久化`, docResponse, Boolean(doc?.document?.id))
|
|
const released = await command('放舱', 'POST', `/api/platform/v1/ocean-bookings/${bookingId}/release`,
|
|
{ expectedVersion: Number(doc.bookingVersion), reason: context.marker },
|
|
{ 'Idempotency-Key': `${context.marker}-release` })
|
|
const latest = await command('回查模拟柜版本', 'GET', `/api/platform/v1/containers/domestic/${cabinet.id}`)
|
|
const containerNo = iso6346ContainerNumber(context.marker)
|
|
const linked = await command('关联订舱并补真实柜号', 'POST', `/api/platform/v1/containers/${cabinet.id}/ocean-booking-link`, {
|
|
oceanBookingId: bookingId, requirementId: String(released.containerRequirements[0].id),
|
|
containerNo, expectedVersion: Number(latest.container.version),
|
|
})
|
|
const loadingDetail = await command('读取整份预装确认门禁', 'GET',
|
|
`/api/platform/v1/first-leg/loading-plans/${plan.id}?warehouseId=${summary.warehouseId}`)
|
|
check(`${label}全部成员可以确认预装`, null, loadingDetail.summary?.readiness?.canConfirm === true)
|
|
const confirmed = await command('确认整份预装', 'POST', `/api/platform/v1/first-leg/loading-plans/${plan.id}/confirm`,
|
|
{ expectedVersion: Number(loadingDetail.summary.version) }, { 'Idempotency-Key': `${context.marker}-confirm` })
|
|
const taskId = String(confirmed.readiness.pdaTaskId)
|
|
const pda = `/api/platform/v1/first-leg/pda-loading/tasks/${taskId}`
|
|
const task = await command('读取装柜PDA任务', 'GET', `${pda}?warehouseId=${summary.warehouseId}`)
|
|
const claimed = await command('领取装柜任务', 'POST', `${pda}/claim`, {
|
|
expectedAssignmentVersion: Number(task.assignment?.version || 0), operationId: `${context.marker}-claim`,
|
|
})
|
|
const inventory = await command('读取整托库存快照', 'GET', `${pda}/inventory-snapshot?warehouseId=${summary.warehouseId}`)
|
|
const batch = {
|
|
warehouseId: String(summary.warehouseId), loadingPlanId: String(plan.id), containerBookingId: String(cabinet.id),
|
|
expectedVersion: Number(claimed.version), inventorySnapshotToken: inventory.snapshotToken,
|
|
items: claimed.items.map((item) => ({ itemId: String(item.id), version: Number(item.version),
|
|
...(item.unitType === 'SEALED_PALLET'
|
|
? { 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')))
|
|
let picked = claimed
|
|
if (linkRegression) {
|
|
for (const palletNo of [...new Set(claimed.items.map(item => item.palletNo))]) {
|
|
const priorVersion = Number(picked.version)
|
|
const payload = { warehouseId: String(summary.warehouseId), palletNo,
|
|
expectedTaskVersion: priorVersion, operationId: `${context.marker}-pick-${palletNo}` }
|
|
picked = await command('逐托即时提交', 'POST', `${pda}/pallets/pick`, payload)
|
|
check('即时提交返回最新任务版本', null, Number(picked.version) > priorVersion)
|
|
const replay = await command('逐托提交同键重放', 'POST', `${pda}/pallets/pick`, payload)
|
|
check('重放不重复累加数量或版本', null,
|
|
replay.version === picked.version && JSON.stringify(replay.items) === JSON.stringify(picked.items))
|
|
}
|
|
} else {
|
|
picked = await command('整托拣货', 'POST', `${pda}/batch`, batch,
|
|
{ 'Idempotency-Key': `${context.marker}-pick` })
|
|
}
|
|
check(`${label}整托拣货等待最终确认`, null, picked.workflowStatus === 'AWAITING_REVIEW')
|
|
for (const evidenceType of ['PHOTO_STEP_1', 'PHOTO_STEP_2', 'PHOTO_STEP_3', 'PHOTO_STEP_4', 'SEAL']) {
|
|
const response = await uploadEvidence(context, taskId, String(summary.warehouseId), evidenceType, 0,
|
|
evidenceType, { sealNo: evidenceType === 'SEAL' ? `SEAL-${suffix}` : null })
|
|
const evidence = context.apiSuccess(MODULE, `${label}上传${evidenceType}装柜证据`, response, '', OPTIONS)
|
|
check(`${label}${evidenceType}已绑定本柜`, response,
|
|
String(evidence?.containerBookingId) === String(cabinet.id)
|
|
&& evidence?.evidences?.some((item) => item.evidenceType === evidenceType && item.status !== 'VOID'))
|
|
}
|
|
const current = await command('读取最终任务版本', 'GET', `${pda}?warehouseId=${summary.warehouseId}`)
|
|
const completed = await command('最终完成装柜', 'POST', `${pda}/complete`, {
|
|
expectedTaskVersion: Number(current.version), operationId: `${context.marker}-complete`, remark: context.marker,
|
|
}, { 'Idempotency-Key': `${context.marker}-complete` })
|
|
const terminal = await waitForCompletion(context, `${pda}?warehouseId=${summary.warehouseId}`, `${pda}/operations`)
|
|
const finalTask = terminal.task
|
|
if (regression.activeUnstarted) {
|
|
check('有实际货物的未开始柜次阻止PDA完成', terminal.taskResponse,
|
|
finalTask?.taskStatus === 'COMPLETION_FAILED'
|
|
&& finalTask.completionCommand?.failureStage === 'OWNER_READY'
|
|
&& finalTask.completionCommand?.failureCode === 'A0409')
|
|
const inbound = await command('失败后回查入仓状态未完成', 'GET',
|
|
`/api/platform/inbounds/${group[0].source.inboundId}?tenantId=${group[0].order.tenantId}`)
|
|
check('负向完成未推进LOADED', null, inbound.summary?.processStatus === 'LOADING')
|
|
context.createdResources.pdaLoadingLinkRegression.scenarios.push({ name: label, taskId,
|
|
outcome: 'ACTIVE_UNSTARTED_BLOCKED', task: finalTask })
|
|
return
|
|
}
|
|
check(`${label}任务真实完成`, terminal.taskResponse, finalTask?.taskStatus === 'COMPLETED',
|
|
`status=${finalTask?.taskStatus}, attempts=${terminal.attempts}`)
|
|
for (const record of group) {
|
|
const inbound = await command(`${record.label}回查入仓单实装状态`, 'GET',
|
|
`/api/platform/inbounds/${record.source.inboundId}?tenantId=${record.order.tenantId}`)
|
|
check(`${record.label}装柜完成回写`, null, inbound.summary?.processStatus === 'LOADED')
|
|
record.loading = { cabinet: linked, plan, task: finalTask, completed, booking: released,
|
|
allocationVerified: true, loadingVerified: true, containerNo }
|
|
}
|
|
if (linkRegression) context.createdResources.pdaLoadingLinkRegression.scenarios.push({
|
|
name: label, taskId, outcome: 'COMPLETED', task: finalTask })
|
|
}
|
|
async function progressShipping(group) {
|
|
const record = group[0]
|
|
context.marker = `${marker}-${record.label}-SHIPPING`
|
|
context.createdResources.orders = { ...record.order, tenantId: String(record.order.tenantId),
|
|
inboundId: record.source.inboundId, warehouseId: String(record.summary.warehouseId) }
|
|
context.createdResources.phase3Loading = {
|
|
marker: context.marker, tenantId: String(record.order.tenantId), orderId: record.order.orderId,
|
|
inboundId: record.source.inboundId, warehouseId: String(record.summary.warehouseId),
|
|
containerBookingId: String(record.loading.cabinet.id), loadingPlanId: String(record.loading.plan.id),
|
|
oceanBookingId: String(record.loading.booking.oceanBooking.id),
|
|
containerNo: record.loading.containerNo, finalStatus: 'LOADED',
|
|
}
|
|
for (const key of ['phase3Customs', 'phase3OutboundDispatch', 'phase3CarrierScheduleQuery', 'phase4CarrierPickupReady']) {
|
|
delete context.createdResources[key]
|
|
}
|
|
async function confirmDomesticDeparture() {
|
|
const bookingId = String(record.loading.cabinet.id)
|
|
const response = await context.request('GET', `/api/platform/v1/first-leg/outbound-dispatches/${bookingId}`)
|
|
const detail = context.apiSuccess(MODULE, `${record.label}回查报关后的实际离仓事实`, response, '', OPTIONS)
|
|
const aggregate = detail?.aggregate
|
|
check(`${record.label}报关完成自动离仓且全柜逐票已装`, response,
|
|
String(aggregate?.containerBookingId) === bookingId
|
|
&& aggregate?.dispatchStatus === 'WAITING_FOR_LOADING'
|
|
&& aggregate.inbounds?.length === group.length
|
|
&& group.every((item) => aggregate.inbounds.some((inbound) =>
|
|
String(inbound.inboundId) === String(item.source.inboundId) && inbound.inboundStatus === 'LOADED'))
|
|
&& detail.timeline?.some((item) => item.module === 'OUTBOUND_DISPATCH' && item.action === 'CONFIRM_DISPATCH'))
|
|
context.createdResources.phase3OutboundDispatch = { marker: context.marker, containerBookingId: bookingId,
|
|
tenantId: String(record.order.tenantId), confirmedStatus: aggregate.dispatchStatus }
|
|
}
|
|
for (const [name, action] of [['customs', completeCustoms], ['departure', confirmDomesticDeparture],
|
|
['carrier-query', verifyCarrierQuery], ['pickup-ready', runUntilPickupReady]]) {
|
|
const start = context.steps.length
|
|
await context.executeModule(`${MODULE}-${record.label}-${name}`, () => action(context))
|
|
check(`${record.label}${name}阶段全部通过`, null,
|
|
!context.steps.slice(start).some((step) => !step.passed))
|
|
}
|
|
check(`${record.label}已完成国内离仓并达到可提柜节点`, null,
|
|
context.createdResources.phase3OutboundDispatch?.confirmedStatus === 'WAITING_FOR_LOADING'
|
|
&& context.createdResources.phase4CarrierPickupReady?.ready === true)
|
|
for (const item of group) item.shipping = {
|
|
departure: context.createdResources.phase3OutboundDispatch,
|
|
carrier: context.createdResources.phase3CarrierScheduleQuery,
|
|
pickupReady: context.createdResources.phase4CarrierPickupReady, shippingVerified: true,
|
|
}
|
|
}
|
|
async function convertOverseas(group) {
|
|
const label = group[0].label
|
|
const loading = group[0].loading
|
|
const bookingId = String(loading.cabinet.id)
|
|
const base = '/api/platform/v1/last-leg'
|
|
async function call(name, method, path, body, headers) {
|
|
const response = await context.requestOverseas(method, path, body, headers)
|
|
const data = context.apiSuccess(MODULE, `${label}${name}`, response, '', OPTIONS)
|
|
check(`${label}${name}成功`, response, response?.body?.code === '200' && data != null)
|
|
return data
|
|
}
|
|
let summary
|
|
let summaryResponse
|
|
for (let attempt = 0; attempt < 60; attempt++) {
|
|
const response = await context.requestOverseas('GET', `${base}/orders/container-summaries/${bookingId}`)
|
|
summaryResponse = response
|
|
summary = response?.body?.code === '200' ? response.body.data : null
|
|
if (summary?.orders?.length === group.length) break
|
|
if (attempt < 59) await new Promise((resolve) => setTimeout(resolve, 1000))
|
|
}
|
|
check(`${label}跨区事件逐票建单完整`, summaryResponse,
|
|
summary?.orders?.length === group.length && group.every((record) =>
|
|
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'}`)
|
|
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),
|
|
snapshotHash: preview.snapshotHash }
|
|
const headers = { 'Idempotency-Key': `${marker}-${label}-convert` }
|
|
const plans = await call('按柜生成真实入库计划', 'POST', `${base}/inbound-plans/convert-container`, body, headers)
|
|
check(`${label}每票独立计划`, null, Array.isArray(plans) && plans.length === group.length)
|
|
for (const record of group) {
|
|
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]
|
|
const expectedNos = record.task?.pallets.map((pallet) => pallet.palletNo).sort()
|
|
const actualNos = (target.pallets || []).map((pallet) => pallet.palletNo).sort()
|
|
check(`${record.label}来源托盘完整绑定海外真实资源`, null,
|
|
(expectedNos ? JSON.stringify(expectedNos) === JSON.stringify(actualNos) : actualNos.length > 0)
|
|
&& target.pallets.every((pallet) => pallet.warehousePalletId && pallet.warehouseLocationId))
|
|
record.overseas = { plan: target, warehouseId: String(preview.overseasWarehouseId),
|
|
summary: summary.orders.find((order) => order.inboundCode === record.source.inboundCode),
|
|
planVerified: true }
|
|
}
|
|
const byPallet = new Map()
|
|
for (const plan of plans) for (const pallet of plan.pallets) {
|
|
const identity = `${pallet.warehousePalletId}:${pallet.warehouseLocationId}`
|
|
check(`${label}同一来源托盘只绑定一个海外实体`, null,
|
|
!byPallet.has(pallet.palletNo) || byPallet.get(pallet.palletNo) === identity)
|
|
byPallet.set(pallet.palletNo, identity)
|
|
}
|
|
const replay = await call('计划转换同键重放', 'POST', `${base}/inbound-plans/convert-container`, body, headers)
|
|
check(`${label}计划重放不新增或改变实物资源`, null, JSON.stringify(replay) === JSON.stringify(plans))
|
|
}
|
|
async function receiveOverseas(group) {
|
|
const record = group[0]
|
|
const label = record.label
|
|
// 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'
|
|
const access = await resolvePickerAccess(context)
|
|
check(`${label}海外PDA角色可用`, null, Boolean(access))
|
|
const picker = await createPicker(context, records.indexOf(record) + 1, record.overseas.warehouseId, access)
|
|
try {
|
|
check(`${label}海外PDA账号和安全投影可用`, null, Boolean(picker?.token))
|
|
async function call(name, method, path, body) {
|
|
const response = await context.requestOverseas(method, path, body, {}, picker.token)
|
|
const data = context.apiSuccess(MODULE, `${label}${name}`, response, '', OPTIONS)
|
|
check(`${label}${name}成功`, response, response?.body?.code === '200' && data != null)
|
|
return data
|
|
}
|
|
let candidate
|
|
for (let attempt = 0; attempt < 60; attempt++) {
|
|
const response = await context.requestOverseas('GET', `${base}/candidates`, undefined, {}, picker.token)
|
|
candidate = response?.body?.data?.find?.((item) => item.containerNo === record.loading.containerNo)
|
|
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`,
|
|
{ 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')
|
|
if (mixedContainer) {
|
|
const completed = await receiveMixedContainer({ group, call, check, marker: context.marker,
|
|
request: (method, path, body) => context.requestOverseas(method, path, body, {}, picker.token) })
|
|
for (const item of group) {
|
|
const stock = await call(`${item.label}混柜完成后按入仓码查询库存`, 'POST',
|
|
'/api/pda/v1/last-leg/inventory/resolve', { code: item.source.inboundCode })
|
|
check(`${item.label}库存归属和真实托盘绑定正确且箱数核平`, null,
|
|
stock.queryCode === item.source.inboundCode && Array.isArray(stock.items) && stock.items.length > 0
|
|
&& stock.items.every(row => row.inboundCode === item.source.inboundCode
|
|
&& String(row.customerId) === String(item.order.tenantId)
|
|
&& 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 }
|
|
}
|
|
return
|
|
}
|
|
const sourceNos = [...new Set(group.flatMap((item) => item.task.pallets.map((pallet) => pallet.palletNo)))]
|
|
const receivedPallets = new Set()
|
|
let accepted = 0
|
|
for (const [index, sourcePalletNo] of sourceNos.entries()) {
|
|
const before = await call('未齐收货前回查柜级事实', 'GET', `${base}/current`)
|
|
check(`${label}剩余托盘待收箱数准确`, null,
|
|
Number(before.pendingPackages) === group.reduce((sum, item) => sum + item.expectedCartonMarks.length, 0) - accepted)
|
|
const premature = await context.requestOverseas('POST', `${base}/current/complete`,
|
|
{ idempotencyKey: `${context.marker}-premature-${index}`, allowDifference: false }, {}, picker.token)
|
|
check(`${label}未收齐不得按正常完成提交`, premature, premature.body?.code === 'A0409')
|
|
const unchanged = await call('未齐完成被拒后回查柜级事实', 'GET', `${base}/current`)
|
|
check(`${label}未齐完成拒绝后事实不变且可继续收货`, null,
|
|
JSON.stringify(unchanged) === JSON.stringify(before))
|
|
const body = { sourcePalletNo, idempotencyKey: `${context.marker}-pallet-${index}` }
|
|
const received = await call('按来源实物托盘整托收货', 'POST', `${base}/current/receipts/pallet`, body)
|
|
accepted += Number(received.acceptedPackages)
|
|
const replay = await call('整托收货同键重放', 'POST', `${base}/current/receipts/pallet`, body)
|
|
check(`${label}实收重放结果一致`, null, JSON.stringify(replay) === JSON.stringify(received))
|
|
const duplicate = await call('同托新键重复扫描', 'POST', `${base}/current/receipts/pallet`,
|
|
{ sourcePalletNo, idempotencyKey: `${context.marker}-duplicate-${index}` })
|
|
check(`${label}同托重复扫描不重复收货`, null,
|
|
duplicate.result === 'ALREADY_RECEIVED' && Number(duplicate.acceptedPackages) === 0)
|
|
receivedPallets.add(sourcePalletNo)
|
|
const progress = await call('每托收货及重扫后回查逐票进度', 'GET', `${base}/current`)
|
|
const expected = group.map(record => ({ inboundCode: record.source.inboundCode,
|
|
total: record.expectedCartonMarks.length,
|
|
received: new Set(record.task.pallets.filter(p => receivedPallets.has(p.palletNo))
|
|
.flatMap(p => p.cartons || []).filter(c => String(c.inboundId) === String(record.source.inboundId))
|
|
.map(c => c.boxCode)).size }))
|
|
check(`${label}每托实收不串票且柜级与逐票剩余同步`, null, receivingProgressMatches(progress, expected))
|
|
}
|
|
check(`${label}整柜实收总箱数守恒`, null,
|
|
accepted === group.reduce((sum, item) => sum + item.expectedCartonMarks.length, 0))
|
|
const current = await call('逐票回查实收', 'GET', `${base}/current`)
|
|
check(`${label}逐票实收核平且没有遗漏`, null, Number(current.pendingPackages) === 0
|
|
&& current.inbounds?.length === group.length
|
|
&& group.every((item) => current.inbounds.some((inbound) => inbound.inboundCode === item.source.inboundCode
|
|
&& Number(inbound.receivedPackages) === item.expectedCartonMarks.length)))
|
|
const body = { idempotencyKey: `${context.marker}-complete` }
|
|
const completed = await call('完成海外拆柜入库', 'POST', `${base}/current/complete`, body)
|
|
const replay = await call('完成入库同键重放', 'POST', `${base}/current/complete`, body)
|
|
check(`${label}海外入库完成且重放稳定`, null, completed.status === 'COMPLETED'
|
|
&& JSON.stringify(replay) === JSON.stringify(completed))
|
|
for (const item of group) item.overseas.receiving = { completed, current, receivingVerified: true }
|
|
await outboundOverseas(group, picker.token)
|
|
} finally {
|
|
await disablePicker(context, picker, records.indexOf(record) + 1)
|
|
}
|
|
}
|
|
async function outboundOverseas(group, token) {
|
|
const label = group[0].label
|
|
const base = '/api/platform/v1/last-leg/outbound-orders'
|
|
const customerId = String(group[0].task.tenantId)
|
|
const warehouseId = group[0].overseas.warehouseId
|
|
async function call(name, method, path, body, pda = false) {
|
|
const response = await context.requestOverseas(method, path, body, {}, pda ? token : context.overseasToken)
|
|
const value = context.apiSuccess(MODULE, `${label}${name}`, response, '', OPTIONS)
|
|
check(`${label}${name}成功`, response, response?.body?.code === '200' && value != null)
|
|
return value
|
|
}
|
|
const candidatePath = `${base}/one-click-dropship/candidates?customerId=${customerId}&warehouseId=${warehouseId}`
|
|
await outboundCandidateGuards({ context, group, customerId, warehouseId, label, check, call })
|
|
const all = await call('读取专用出库可用库存', 'GET', candidatePath)
|
|
const selected = all.filter((candidate) => group.some((record) => record.source.inboundCode === candidate.inboundCode))
|
|
const inputs = selected.map((candidate) => {
|
|
const record = group.find((item) => item.source.inboundCode === candidate.inboundCode)
|
|
const pallet = record.task.pallets.find((item) => item.palletNo === candidate.palletNo)
|
|
const cartonMarks = (pallet?.cartons || []).filter((carton) =>
|
|
String(carton.inboundId) === String(record.source.inboundId)).map((carton) => carton.boxCode)
|
|
check(`${label}专用出库候选逐票逐托箱数准确`, null,
|
|
cartonMarks.length > 0 && Number(candidate.availablePackages) === cartonMarks.length
|
|
&& String(candidate.customerId) === customerId && String(candidate.overseasWarehouseId) === warehouseId)
|
|
return { inventoryPositionId: candidate.inventoryPositionId, packages: cartonMarks.length,
|
|
sku: candidate.skuCode, cartonMarks }
|
|
})
|
|
check(`${label}专用出库候选覆盖全部票和箱`, null, group.every((record) =>
|
|
selected.filter((item) => item.inboundCode === record.source.inboundCode)
|
|
.reduce((sum, item) => sum + Number(item.availablePackages), 0) === record.expectedCartonMarks.length))
|
|
const body = { recipient: `HTTP acceptance ${context.marker}`, items: inputs,
|
|
remark: context.marker, idempotencyKey: `${context.marker}-outbound-create` }
|
|
const edge = { context, group, token, customerId, warehouseId, candidatePath, body, label, check, call }
|
|
await outboundCreationGuards(edge)
|
|
const task = await call('创建一件代发专用出库', 'POST', `${base}/one-click-dropship?customerId=${customerId}`, body)
|
|
for (const record of group) record.outbound = { task, selected, completed: false }
|
|
const replay = await call('专用出库创建同键重放', 'POST', `${base}/one-click-dropship?customerId=${customerId}`, body)
|
|
check(`${label}专用出库重放不重复建单`, null, sameOutboundTask(replay, task))
|
|
await outboundTaskGuards({ ...edge, task })
|
|
const pdaPath = `/api/pda/v1/last-leg/outbound-tasks/${task.taskNo}`
|
|
const detail = await call('PDA读取专用出库任务', 'GET', pdaPath, undefined, true)
|
|
check(`${label}PDA出库明细完整`, null, detail.items?.length === inputs.length)
|
|
for (const [index, candidate] of selected.entries()) {
|
|
const item = detail.items.find((row) => row.palletNo === candidate.palletNo && row.skuCode === candidate.skuCode
|
|
&& row.inboundCode === candidate.inboundCode)
|
|
check(`${label}PDA拣货来源仓位和入仓码一致`, null, Boolean(item)
|
|
&& item.positions?.some((position) => String(position.inventoryPositionId) === String(candidate.inventoryPositionId)
|
|
&& position.inboundCode === candidate.inboundCode))
|
|
const pick = { outboundPalletItemId: item.outboundPalletItemId,
|
|
inventoryPositionId: candidate.inventoryPositionId, packages: inputs[index].packages,
|
|
cartonMarks: inputs[index].cartonMarks, idempotencyKey: `${context.marker}-outbound-pick-${index}` }
|
|
if (label === 'SINGLE-SINGLE' && index === 0) {
|
|
const count = await probePartialPick({ ...edge, task, pick })
|
|
pick.packages -= count
|
|
pick.cartonMarks = pick.cartonMarks.slice(count)
|
|
}
|
|
const picked = await call('PDA专用出库拣货', 'POST', `${pdaPath}/pick`, pick, true)
|
|
const repeated = await call('PDA出库拣货同键重放', 'POST', `${pdaPath}/pick`, pick, true)
|
|
check(`${label}PDA出库拣货重放不重复扣减`, null, sameOutboundTask(repeated, picked))
|
|
}
|
|
const completion = { idempotencyKey: `${context.marker}-outbound-complete` }
|
|
const completed = await call('PDA完成专用出库', 'POST', `${pdaPath}/complete`, completion, true)
|
|
const repeated = await call('PDA专用出库完成重放', 'POST', `${pdaPath}/complete`, completion, true)
|
|
check(`${label}专用出库任务完成且重放稳定`, null,
|
|
completed.status === 'COMPLETED' && sameOutboundTask(repeated, completed),
|
|
JSON.stringify({ completed, repeated }))
|
|
const outbound = await call('回查专用出库单终态', 'GET', `${base}/${task.outboundNo}`)
|
|
check(`${label}专用出库单真实完成且箱数核平`, null,
|
|
outbound.outbound?.status === 'OUTBOUNDED' && outbound.outbound?.outboundType === 'ONE_CLICK_DROPSHIP'
|
|
&& Number(outbound.outbound.palletCount) === new Set(selected.map((item) => item.palletNo)).size
|
|
&& Number(outbound.outbound.packageCount) === inputs.reduce((sum, item) => sum + item.packages, 0)
|
|
&& outbound.pallets.every((item) => Number(item.packageCount) === Number(item.pickedPackageCount)))
|
|
const remaining = await call('回查专用出库可用库存扣减', 'GET', candidatePath)
|
|
check(`${label}本轮每票库存全部扣减`, null,
|
|
!remaining.some((candidate) => group.some((record) => record.source.inboundCode === candidate.inboundCode)))
|
|
for (const record of group) record.outbound = { task: completed, selected, detail: outbound, completed: true }
|
|
}
|
|
try {
|
|
if (physicalCartonRegression) {
|
|
const scenarios = [
|
|
await prepare('PHYSICAL-20X2', 2, {}, null, { totalPackageCount: 20, skuCount: 2 }),
|
|
await prepare('PHYSICAL-5X3', 1, {}, null, { totalPackageCount: 5, skuCount: 3 }),
|
|
await prepare('PHYSICAL-7X2-MERGED', 2, { mergeToFirstPallet: true }, null,
|
|
{ totalPackageCount: 7, skuCount: 2 }),
|
|
]
|
|
for (const [index, record] of scenarios.entries()) await verify(record, index === 2 ? 1 : index === 0 ? 2 : 1)
|
|
const groups = scenarios.map((record) => [record])
|
|
for (const group of groups) await allocate(group)
|
|
context.createdResources.dropshipPalletMatrix.stage = 'PHYSICAL_CARTON_LOADING_COMPLETED'
|
|
for (const group of groups) await progressShipping(group)
|
|
context.createdResources.dropshipPalletMatrix.stage = 'PHYSICAL_CARTON_PICKUP_READY'
|
|
for (const group of groups) {
|
|
await convertOverseas(group)
|
|
await receiveOverseas(group)
|
|
}
|
|
context.createdResources.dropshipPalletMatrix.stage = 'PHYSICAL_CARTON_OUTBOUND_COMPLETED'
|
|
context.createdResources.dropshipPalletMatrix.fullFlowVerified = true
|
|
return
|
|
}
|
|
if (mixedContainer) {
|
|
const dropship = await prepare('MIXED-DROPSHIP', 2)
|
|
await verify(dropship, 2)
|
|
context.context.waitSplitRoute = { originWarehouseName: dropship.order.receivingWarehouseName,
|
|
channelCode: dropship.order.channelCode }
|
|
const fba = await prepare('MIXED-FBA', 1, {}, 'FBA')
|
|
const overseas = await prepare('MIXED-OVERSEAS', 1, {}, 'OVERSEAS')
|
|
const group = [dropship, fba, overseas]
|
|
check('混柜三票属于同一起运仓和渠道组', null, group.every(r =>
|
|
String(r.summary.warehouseId) === String(dropship.summary.warehouseId)
|
|
&& channelGroup(r.summary.channelCode) === channelGroup(dropship.summary.channelCode)))
|
|
await allocate(group)
|
|
await progressShipping(group)
|
|
await convertOverseas(group)
|
|
await receiveOverseas(group)
|
|
context.createdResources.dropshipPalletMatrix.stage = 'MIXED_CONTAINER_INBOUND_COMPLETED'
|
|
return
|
|
}
|
|
if (linkRegression) {
|
|
const single = await prepare('LINK-SINGLE', 2)
|
|
await verify(single, 2)
|
|
await allocate([single], { rounds: 1 })
|
|
const first = await prepare('MULTI-MERGE-LINK-A', 1, { deferSeal: true })
|
|
const second = await prepare('MULTI-MERGE-LINK-B', 1, { targetPalletNo: first.task.pallets[0].palletNo })
|
|
await verify(first, 1)
|
|
await verify(second, 1)
|
|
await allocate([first, second], { rounds: 3 })
|
|
const negative = await prepare('LINK-NEGATIVE', 2)
|
|
await verify(negative, 2)
|
|
await allocate([negative], { activeUnstarted: true })
|
|
context.createdResources.dropshipPalletMatrix.stage = 'DOMESTIC_LINK_REGRESSION_VERIFIED'
|
|
return
|
|
}
|
|
if (stopBeforeOverseasPda) {
|
|
const record = await prepare('BROWSER-MULTI', 2)
|
|
await verify(record, 2)
|
|
await allocate([record])
|
|
await progressShipping([record])
|
|
await convertOverseas([record])
|
|
context.createdResources.dropshipPalletMatrix.stage = 'READY_FOR_BROWSER_RECEIVING'
|
|
return
|
|
}
|
|
await verify(await prepare('SINGLE-MULTI', 2), 2)
|
|
await verify(await prepare('SINGLE-SINGLE', 1), 1)
|
|
await verify(await prepare('SINGLE-MERGE', 2, { mergeToFirstPallet: true }), 1)
|
|
const first = await prepare('MULTI-MERGE-A', 1, { deferSeal: true })
|
|
const targetPalletNo = first.task.pallets[0].palletNo
|
|
const second = await prepare('MULTI-MERGE-B', 1, { targetPalletNo })
|
|
await verify(first, 1)
|
|
await verify(second, 1)
|
|
check('多票共托物理身份相同且两票保持独立归属', null,
|
|
first.task.pallets[0].palletNo === second.task.pallets[0].palletNo
|
|
&& first.source.inboundCode !== second.source.inboundCode
|
|
&& first.task.pallets[0].cartons.length === first.expectedCartonMarks.length + second.expectedCartonMarks.length)
|
|
const groups = [[records[0]], [records[1]], [records[2]], [first, second]]
|
|
for (const group of groups) await allocate(group)
|
|
context.createdResources.dropshipPalletMatrix.stage = 'DOMESTIC_LOADING_COMPLETED'
|
|
for (const group of groups) await progressShipping(group)
|
|
context.createdResources.dropshipPalletMatrix.stage = 'SHIPPING_PICKUP_READY'
|
|
for (const group of groups) {
|
|
await convertOverseas(group)
|
|
await receiveOverseas(group)
|
|
}
|
|
context.createdResources.dropshipPalletMatrix.stage = 'DEDICATED_OUTBOUND_COMPLETED'
|
|
} finally {
|
|
context.marker = marker
|
|
context.options.prepareDropshipManualCount = savedOptions
|
|
context.context = savedContext
|
|
}
|
|
}
|