fix: 支持多SKU共箱整托装柜
This commit is contained in:
@@ -89,6 +89,7 @@ Client 全量、Client 入库测量与计费、待审核订单和批量待分柜
|
||||
| 本地质量 | `java-quality-summary.mjs` | 聚合 Maven 拓扑、OpenAPI、JaCoCo 和 SpotBugs 已有报告,不启动服务 |
|
||||
| 本地质量 | `agents-index-contract.ps1` | 离线检查 AGENTS 快速索引与关键入口一致性 |
|
||||
| 契约数据 | `synchronous-dependency-baseline.mjs` | 同步服务依赖合同的数据源,只由相邻合同测试导入,不是 CLI |
|
||||
| SQL 合同辅助 | `sql-baseline-contract-helpers.mjs` | 只供离线 Node 合同读取统一增量 SQL、拆分表定义和种子数据;不连接数据库,也不是 CLI |
|
||||
| OpenAPI | `openapi-validation/validate-openapi.mjs` | 发现并解析各服务 OpenAPI,检查跨文件引用;只读源码 |
|
||||
| 本地环境 | `local-env.ps1` | 本地开发环境公共参数/辅助函数,由其他 PowerShell 脚本加载 |
|
||||
| 本地环境 | `start-gateway-overseas-local.ps1` | 使用本地 Maven 启动海外 Gateway,并将海外安全投影地址指向本机;会启动本地 Java 进程,不访问或修改共享环境 |
|
||||
|
||||
@@ -11,7 +11,7 @@ const OPTIONAL_MODULES = ['phase4-overseas-return-followups', 'manual-order-spli
|
||||
'manual-pda-ready-fixture', 'manual-pda-ready-verify',
|
||||
'manual-pda-batch-fixture', 'manual-pda-batch-verify',
|
||||
'manual-loading-complete-fixture', 'manual-loading-complete-verify',
|
||||
'coload-groups', 'dropship-pallet-matrix', 'dropship-browser-fixture', 'mixed-container-receiving', 'pda-loading-link-regression']
|
||||
'coload-groups', 'dropship-pallet-matrix', 'dropship-physical-carton-chain', 'dropship-browser-fixture', 'mixed-container-receiving', 'pda-loading-link-regression']
|
||||
const AVAILABLE_MODULES = new Set([...CONFIGURED_MODULES, ...OPTIONAL_MODULES])
|
||||
const A0_LOCATION_PAGE_SIZE = 200
|
||||
const A0_MAX_LOCATION_MUTATIONS = 8
|
||||
@@ -45,6 +45,7 @@ const MODULE_DEPENDENCIES = new Map([
|
||||
['pda-loading-link-regression', ['merchant-qualification-fixture']],
|
||||
['phase4-overseas-return-followups', ['phase4-overseas-return-receipt']],
|
||||
['dropship-pallet-matrix', ['merchant-qualification-fixture']],
|
||||
['dropship-physical-carton-chain', ['merchant-qualification-fixture']],
|
||||
['dropship-browser-fixture', ['merchant-qualification-fixture']],
|
||||
['mixed-container-receiving', ['merchant-qualification-fixture']],
|
||||
['suppliers', ['merchant-qualification-fixture']],
|
||||
|
||||
@@ -76,6 +76,8 @@ export async function run(context) {
|
||||
|
||||
`dropship-pallet-matrix.mjs` 验证下述四场景整托手工加入、整托移除、智能推荐、独立订舱与文件放舱、真实柜号关联、预装确认、PDA 整托拣货和五类证据上传;异步装柜完成必须等到任务 `COMPLETED` 并逐票回查 `LOADED`。智能装柜的托盘汇总与装柜详情逐票组成分别核对,不能把共托汇总行的首票编号当作全托归属。随后经正式报关、国内离仓及人工船节点接口推进至 `PICKUP_READY`,再按柜生成逐票海外入库计划,检查来源托盘完整绑定、共享托盘身份一致和同键重放。海外收货使用本轮创建并绑定接柜仓的 PDA 操作员,上传三类照片、签收、逐实物托盘收货、逐票箱数核平和完成入库,同键重放及同托新键扫码均不得重复入账。随后从本轮逐票库存候选创建一件代发专用出库,携带原始箱唛由 PDA 拣货、完成,核对终态、幂等和可用库存扣减;finally 停用本轮操作员。出库重放按业务身份和数据库毫秒时间精度核对。离线合同为 `dropship-pallet-matrix-contract.test.mjs`;脚本覆盖不等于真实 HTTP 已通过,须检查当轮报告和库存审计。
|
||||
|
||||
`dropship-physical-carton-chain.mjs` 复用同一条正式链路,专门覆盖多 SKU 共用物理箱:20 箱×2 SKU/2 托、5 箱×3 SKU/1 托、7 箱×2 SKU/预计 2 托后合为 1 托。每个场景从新订单、测量、箱唛冻结和 PDA 清点开始,按去重箱唛核对预装、装柜、尾程计划、海外整托收货、库存候选和一件代发出库;每箱必须保留全部 SKU 明细,但任何汇总不得按 SKU 行重复累计箱数。使用 `--modules dropship-physical-carton-chain`,会保留本轮业务数据和 JSON 报告,不自动清理。
|
||||
|
||||
- `dropship-pallet-matrix` 通过总入口 `--modules dropship-pallet-matrix` 执行,自动依赖客户资质准备。通过正式 HTTP 创建五票订单,覆盖单票多托、单票单托、单票移箱合托、多票共享开放托盘;会创建并保留测试数据,不自动清理。报告 `createdResources.dropshipPalletMatrix` 保存逐票箱唛、托盘及各阶段证据,失败即停止并保留阻断;重跑创建新批次,不借用历史资源。`fullFlowVerified=false`,不能代替海外收货及一件代发专用出库的完整验收。
|
||||
|
||||
- 一件代发任务 `Task.pallets` 只列实际参与绑箱的托盘;未使用的预生成托盘通过 `/api/platform/v1/first-leg/pda-dropship/inbound-orders/{inboundId}/pallets?accountId=...` 读取,客户范围取本轮任务。脚本同时核对 `generatedPalletCount/palletSetupReady`,不因空的任务托盘列表新增资源。
|
||||
|
||||
@@ -176,6 +176,18 @@ test('dropship matrix has a real customer prerequisite without generic dispatch
|
||||
assert.ok(!modules.includes('phase5-overseas-fulfillment'))
|
||||
})
|
||||
|
||||
test('physical-carton chain covers mixed SKU cartons without multiplying carton totals', () => {
|
||||
const modules = normalizeModuleOrder(['dropship-physical-carton-chain'])
|
||||
assert.deepEqual(modules, ['merchant-qualification-fixture', 'dropship-physical-carton-chain'])
|
||||
assert.match(source, /prepare\('PHYSICAL-20X2', 2[\s\S]*totalPackageCount: 20, skuCount: 2/)
|
||||
assert.match(source, /prepare\('PHYSICAL-5X3', 1[\s\S]*totalPackageCount: 5, skuCount: 3/)
|
||||
assert.match(source, /prepare\('PHYSICAL-7X2-MERGED', 2, \{ mergeToFirstPallet: true \}/)
|
||||
assert.match(source, /carton\.skuLines\.length === \(record\.expectedSkuCount \|\| 1\)/)
|
||||
assert.match(source, /packagesByPhysicalUnit/)
|
||||
assert.match(source, /new Set\(actual\.map\(\(item\) => item\.skuCode\)/)
|
||||
assert.match(source, /PHYSICAL_CARTON_OUTBOUND_COMPLETED/)
|
||||
})
|
||||
|
||||
test('all four physical pallet shapes are explicitly produced', () => {
|
||||
assert.match(source, /prepare\('SINGLE-MULTI', 2\)/)
|
||||
assert.match(source, /prepare\('SINGLE-SINGLE', 1\)/)
|
||||
@@ -190,7 +202,7 @@ test('allocation checks both physical summaries and uncollapsed per-inbound quan
|
||||
assert.match(source, /first-leg\/loading-plans\/\$\{plan.id\}/)
|
||||
assert.match(source, /const actual = detail.items.filter/)
|
||||
assert.match(source, /plan.items\?\.length === pallets.length/)
|
||||
assert.match(source, /actual.reduce\([\s\S]*?record.expectedCartonMarks.length/)
|
||||
assert.match(source, /packagesByPhysicalUnit\.values\(\)[\s\S]*?record.expectedCartonMarks.length/)
|
||||
assert.match(source, /await conserve\('手工加入后'\)/)
|
||||
assert.match(source, /await conserve\('智能推荐后'\)/)
|
||||
assert.match(source, /fullFlowVerified: false/)
|
||||
|
||||
@@ -47,7 +47,12 @@ export async function runDomesticLinkRegression(context) {
|
||||
return runWorkflow(context, false, false, true)
|
||||
}
|
||||
|
||||
async function runWorkflow(context, stopBeforeOverseasPda, mixedContainer = false, linkRegression = false) {
|
||||
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 }
|
||||
@@ -58,7 +63,7 @@ async function runWorkflow(context, stopBeforeOverseasPda, mixedContainer = fals
|
||||
context.assert(MODULE, name, response, passed, details, OPTIONS)
|
||||
if (!passed) throw new Error(name)
|
||||
}
|
||||
async function prepare(label, palletCount, mode = {}, destinationType = null) {
|
||||
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]
|
||||
@@ -67,7 +72,13 @@ async function runWorkflow(context, stopBeforeOverseasPda, mixedContainer = fals
|
||||
context.context.orderDestinationType = destinationType || 'OVERSEAS'
|
||||
context.context.orderFulfillmentType = destinationType ? 'STANDARD' : 'ONE_CLICK_DROPSHIP'
|
||||
context.context.orderProfile = { taxMode: 'BUY_DECLARATION', pickupMethod: 'SELF_EXPRESS', containerMode: 'LCL' }
|
||||
context.context.waitSplitVariant = { skuCount: 1, packageCountPerSku: 4, packageCounts: [4], palletCount }
|
||||
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)]
|
||||
}
|
||||
@@ -101,6 +112,7 @@ async function runWorkflow(context, stopBeforeOverseasPda, mixedContainer = fals
|
||||
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)
|
||||
@@ -116,6 +128,8 @@ async function runWorkflow(context, stopBeforeOverseasPda, mixedContainer = fals
|
||||
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
|
||||
@@ -177,9 +191,18 @@ async function runWorkflow(context, stopBeforeOverseasPda, mixedContainer = fals
|
||||
=== 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,
|
||||
actual.reduce((sum, item) => sum + Number(item.packageCount), 0) === record.expectedCartonMarks.length
|
||||
&& (!record.task || actual.every((item) => record.task.pallets.some((pallet) => String(pallet.palletId) === String(item.palletId)))))
|
||||
[...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) {
|
||||
@@ -603,6 +626,26 @@ async function runWorkflow(context, stopBeforeOverseasPda, mixedContainer = fals
|
||||
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'
|
||||
return
|
||||
}
|
||||
if (mixedContainer) {
|
||||
const dropship = await prepare('MIXED-DROPSHIP', 2)
|
||||
await verify(dropship, 2)
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { runPhysicalCartonRegression } from './dropship-pallet-matrix.mjs'
|
||||
|
||||
export async function run(context) {
|
||||
return runPhysicalCartonRegression(context)
|
||||
}
|
||||
Reference in New Issue
Block a user