feat: enforce canonical container identity across regions

This commit is contained in:
2026-08-31 12:13:20 +08:00
parent b386f35ab4
commit 942dbace19
78 changed files with 1203 additions and 730 deletions
@@ -2,6 +2,7 @@ import { publishPlatformLogisticsChannels } from './ocean-booking-fixture.mjs'
import { diffPushPreloadStability, projectPushPreloadStability } from './push-preload-stability.mjs'
import { containerFieldGaps, loadingListFieldGaps, preloadFieldGaps } from './fulfillment-list-field-contract.mjs'
import { ensureMatsonCarrierPartner } from './carrier-partner-fixture.mjs'
import { iso6346ContainerNumber } from './iso6346-container-number.mjs'
const MODULE = 'phase3-container-loading'
const OPTIONS = { scenario: 'phase3-container-smart-loading-pda', deploymentBlocker: true }
@@ -146,7 +147,7 @@ export async function run(context) {
if (!selectedChannel) return
const channelName = selectedChannel.channelName
const primaryRequirement = { containerType: '40HC', containerKind: 'FCL', capacityCbm: 76, maxWeightKg: 28000 }
const realContainerNo = `TST-${suffix}-01`.slice(0, 64)
const realContainerNo = iso6346ContainerNumber(`${context.marker}-initial`)
const wrongContainerNo = `TST-${suffix}-02`.slice(0, 64)
const agentResponse = await context.request('GET',
'/api/platform/v1/suppliers?tenantId=0&category=BOOKING&status=ACTIVE&page=1&size=20')
@@ -472,12 +473,6 @@ export async function run(context) {
context.addStep(MODULE, '已消费订舱需求拒绝再次关联', duplicateLinkResponse,
failure(duplicateLinkResponse), `HTTP=${duplicateLinkResponse.http}, code=${codeOf(duplicateLinkResponse)}`, OPTIONS)
const unlinkAfterLoadingResponse = await request(context, 'DELETE', `${CONTAINERS}/${containerId}/ocean-booking-link`, {
expectedVersion: Number(linkedContainer.version), reason: context.marker,
})
context.addStep(MODULE, '已有排货后拒绝解绑或换绑', unlinkAfterLoadingResponse,
failure(unlinkAfterLoadingResponse), `HTTP=${unlinkAfterLoadingResponse.http}, code=${codeOf(unlinkAfterLoadingResponse)}`, OPTIONS)
const preloadPageResponse = await context.request('GET',
`${FIRST_LEG}/loading-plans?warehouseId=${encodeURIComponent(warehouseId)}&keyword=${encodeURIComponent(realContainerNo)}&page=1&size=20`)
const preloadPage = context.apiSuccess(MODULE, '查询本轮预装清单', preloadPageResponse, '', OPTIONS)
@@ -576,22 +571,24 @@ export async function run(context) {
`${PDA}/tasks/${taskId}/inventory-snapshot?warehouseId=${encodeURIComponent(warehouseId)}`)
const beforeReplacementSnapshot = context.apiSuccess(MODULE, '换柜号前读取 PDA 稳定库存快照',
beforeReplacementSnapshotResponse, '', OPTIONS)
const replacementContainerNo = `TST-R-${suffix}`.slice(0, 64)
const replacementResponse = await request(context, 'PATCH', `${CONTAINERS}/${containerId}/transport`, {
containerNo: replacementContainerNo, voyageNo: `V-${suffix}`.slice(0, 64),
version: Number(releasedContainer?.version), reason: `${context.marker}-replace-container-no`,
})
const replacementContainerNo = iso6346ContainerNumber(`${context.marker}-replacement`)
const replacementResponse = await request(context, 'PATCH',
`${FIRST_LEG}/loading-plans/${planId}/transport?warehouseId=${encodeURIComponent(warehouseId)}`, {
containerNo: replacementContainerNo, voyageNo: `V-${suffix}`.slice(0, 64),
expectedVersion: Number(confirmedPlan?.version), operationReason: `${context.marker}-replace-container-no`,
}, { 'Idempotency-Key': `${context.marker}-replace-container-no`.slice(0, 128) })
const replaced = context.apiSuccess(MODULE, 'PDA 任务创建后替换真实柜号', replacementResponse, '', OPTIONS)
context.assert(MODULE, '任务创建后真实柜号替换成功', replacementResponse,
replaced?.containerNo === replacementContainerNo,
`containerNo=${replaced?.containerNo || 'missing'}`, OPTIONS)
replaced?.summary?.containerNo === replacementContainerNo,
`containerNo=${replaced?.summary?.containerNo || 'missing'}`, OPTIONS)
const clearContainerNoResponse = await request(context, 'PATCH', `${CONTAINERS}/${containerId}/transport`, {
containerNo: null, voyageNo: `V-${suffix}`.slice(0, 64),
version: Number(replaced?.version), reason: `${context.marker}-clear-container-no`,
})
const clearContainerNoResponse = await request(context, 'PATCH',
`${FIRST_LEG}/loading-plans/${planId}/transport?warehouseId=${encodeURIComponent(warehouseId)}`, {
containerNo: ' ', voyageNo: `V-${suffix}`.slice(0, 64),
expectedVersion: Number(replaced?.summary?.version), operationReason: `${context.marker}-clear-container-no`,
}, { 'Idempotency-Key': `${context.marker}-clear-container-no`.slice(0, 128) })
context.addStep(MODULE, 'PDA 任务创建后清空真实柜号失败', clearContainerNoResponse,
clearContainerNoResponse.http === 200 && ['A0409', '409'].includes(codeOf(clearContainerNoResponse)),
clearContainerNoResponse.http === 200 && ['A0400', '400'].includes(codeOf(clearContainerNoResponse)),
`HTTP=${clearContainerNoResponse.http}, code=${codeOf(clearContainerNoResponse)}`, OPTIONS)
const replacedTaskResponse = await context.request('GET',
@@ -644,6 +641,15 @@ export async function run(context) {
claimedPlan?.status === 'LOADING' && Number(claimedPlan?.version) > beforeClaimPlanVersion,
`status=${claimedPlan?.status || 'missing'}, version=${claimedPlan?.version ?? 'missing'}`, OPTIONS)
const claimedCorrectionResponse = await request(context, 'PATCH',
`${FIRST_LEG}/loading-plans/${planId}/transport?warehouseId=${encodeURIComponent(warehouseId)}`, {
containerNo: iso6346ContainerNumber(`${context.marker}-claimed-mutate`), voyageNo: 'CLAIMED',
expectedVersion: Number(claimedPlan?.version), operationReason: `${context.marker}-claimed-mutate`,
}, { 'Idempotency-Key': `${context.marker}-claimed-mutate`.slice(0, 128) })
context.addStep(MODULE, 'PDA任务领取后真实柜号纠错被拒绝', claimedCorrectionResponse,
claimedCorrectionResponse.http === 200 && ['A0409', '409'].includes(codeOf(claimedCorrectionResponse)),
`HTTP=${claimedCorrectionResponse.http}, code=${codeOf(claimedCorrectionResponse)}`, OPTIONS)
const inventoryResponse = await context.request('GET',
`${PDA}/tasks/${taskId}/inventory-snapshot?warehouseId=${encodeURIComponent(warehouseId)}`)
const inventory = context.apiSuccess(MODULE, '读取 PDA 提交前实时库存快照', inventoryResponse, '', OPTIONS)
@@ -810,7 +816,7 @@ export async function run(context) {
const immutableResponse = await request(context, 'PATCH',
`${FIRST_LEG}/loading-plans/${planId}/transport?warehouseId=${encodeURIComponent(warehouseId)}`,
{ containerNo: `MUTATE-${suffix}`.slice(0, 64), voyageNo: 'MUTATE', expectedVersion: Number(finalPlan?.version), operationReason: context.marker },
{ containerNo: iso6346ContainerNumber(`${context.marker}-completed-mutate`), voyageNo: 'MUTATE', expectedVersion: Number(finalPlan?.version), operationReason: context.marker },
{ 'Idempotency-Key': `${context.marker}-completed-mutate`.slice(0, 128) })
context.addStep(MODULE, '完成装柜后运输信息不可变', immutableResponse,
immutableResponse.http === 200 && ['A0409', '409'].includes(codeOf(immutableResponse)),