fix(dropship): preserve carton quantities and shared pallet outbound ownership
This commit is contained in:
@@ -37,7 +37,7 @@ export async function run(context) {
|
||||
|
||||
## 可用上下文
|
||||
|
||||
`dropship-pallet-matrix.mjs` 验证下述四场景整托手工加入、整托移除、智能推荐、独立订舱与文件放舱、真实柜号关联、预装确认、PDA 整托拣货和五类证据上传;异步装柜完成必须等到任务 `COMPLETED` 并逐票回查 `LOADED`。智能装柜的托盘汇总与装柜详情逐票组成分别核对,不能把共托汇总行的首票编号当作全托归属。随后经正式报关、国内离仓及人工船节点接口推进至 `PICKUP_READY`,再按柜生成逐票海外入库计划,检查来源托盘完整绑定、共享托盘身份一致和同键重放。海外收货使用本轮创建并绑定接柜仓的 PDA 操作员,上传三类照片、签收、逐实物托盘收货、逐票箱数核平和完成入库,同键重放及同托新键扫码均不得重复入账;finally 停用本轮操作员。离线合同为 `dropship-pallet-matrix-contract.test.mjs`;尚未包含专用出库验收。
|
||||
`dropship-pallet-matrix.mjs` 验证下述四场景整托手工加入、整托移除、智能推荐、独立订舱与文件放舱、真实柜号关联、预装确认、PDA 整托拣货和五类证据上传;异步装柜完成必须等到任务 `COMPLETED` 并逐票回查 `LOADED`。智能装柜的托盘汇总与装柜详情逐票组成分别核对,不能把共托汇总行的首票编号当作全托归属。随后经正式报关、国内离仓及人工船节点接口推进至 `PICKUP_READY`,再按柜生成逐票海外入库计划,检查来源托盘完整绑定、共享托盘身份一致和同键重放。海外收货使用本轮创建并绑定接柜仓的 PDA 操作员,上传三类照片、签收、逐实物托盘收货、逐票箱数核平和完成入库,同键重放及同托新键扫码均不得重复入账。随后从本轮逐票库存候选创建一件代发专用出库,携带原始箱唛由 PDA 拣货、完成,核对终态、幂等和可用库存扣减;finally 停用本轮操作员。出库重放按业务身份和数据库毫秒时间精度核对。离线合同为 `dropship-pallet-matrix-contract.test.mjs`;脚本覆盖不等于真实 HTTP 已通过,须检查当轮报告和库存审计。
|
||||
|
||||
- `dropship-pallet-matrix` 通过总入口 `--modules dropship-pallet-matrix` 执行,自动依赖客户资质准备。通过正式 HTTP 创建五票订单,覆盖单票多托、单票单托、单票移箱合托、多票共享开放托盘;会创建并保留测试数据,不自动清理。报告 `createdResources.dropshipPalletMatrix` 保存逐票箱唛、托盘及各阶段证据,失败即停止并保留阻断;重跑创建新批次,不借用历史资源。`fullFlowVerified=false`,不能代替海外收货及一件代发专用出库的完整验收。
|
||||
|
||||
|
||||
@@ -2,6 +2,17 @@ import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { normalizeModuleOrder } from '../admin-test-env-acceptance.mjs'
|
||||
import { sameOutboundTask } from './dropship-pallet-matrix.mjs'
|
||||
|
||||
test('outbound replay preserves business identity and database millisecond timestamps', () => {
|
||||
const task = { id: '1', taskNo: 'PDA1', outboundNo: 'OD1', status: 'PENDING', pushedAt: '2026-09-14T20:24:45.920278604' }
|
||||
assert.equal(sameOutboundTask(task, { ...task, pushedAt: '2026-09-14T20:24:45.92' }), true)
|
||||
assert.equal(sameOutboundTask({ ...task, pushedAt: '2026-09-14T20:24:45.9999' },
|
||||
{ ...task, pushedAt: '2026-09-14T20:24:46' }), true)
|
||||
for (const key of ['id', 'taskNo', 'outboundNo', 'status', 'pushedAt']) {
|
||||
assert.equal(sameOutboundTask(task, { ...task, [key]: 'changed' }), false)
|
||||
}
|
||||
})
|
||||
|
||||
const source = readFileSync(new URL('./dropship-pallet-matrix.mjs', import.meta.url), 'utf8')
|
||||
|
||||
|
||||
@@ -16,6 +16,16 @@ 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) {
|
||||
@@ -415,12 +425,13 @@ export async function run(context) {
|
||||
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, JSON.stringify(replay) === JSON.stringify(task))
|
||||
check(`${label}专用出库重放不重复建单`, null, sameOutboundTask(replay, 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)
|
||||
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))
|
||||
@@ -429,16 +440,18 @@ export async function run(context) {
|
||||
cartonMarks: inputs[index].cartonMarks, idempotencyKey: `${context.marker}-outbound-pick-${index}` }
|
||||
const picked = await call('PDA专用出库拣货', 'POST', `${pdaPath}/pick`, pick, true)
|
||||
const repeated = await call('PDA出库拣货同键重放', 'POST', `${pdaPath}/pick`, pick, true)
|
||||
check(`${label}PDA出库拣货重放不重复扣减`, null, JSON.stringify(repeated) === JSON.stringify(picked))
|
||||
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' && JSON.stringify(repeated) === JSON.stringify(completed))
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user