优化
This commit is contained in:
@@ -62,11 +62,11 @@ export async function run(context) {
|
||||
|
||||
默认 Admin 全量入口已注册并按同一 `context.marker` 串行执行货柜主链。冻结时序为:
|
||||
|
||||
`WAIT_SPLIT -> SIMULATION/ACTIVE 内部模拟柜 -> 智能排货 -> 无集装箱号 push-preload -> exact replay/冲突零漂移 -> 订舱逐柜登记 OCEAN_BOOKING/ACTIVE 真实柜 -> 按 realContainerBookingId 原地合并(来源 MERGED,模拟柜身份存活)-> RELEASED -> 计划级原子确认 -> 唯一 PDA task -> claim 后 LOADING -> 原子批量拣货 -> AWAITING_REVIEW -> 五类证据 -> LOADING 阶段增量入库单/旧 inventory token A0409/刷新后续拣 -> 拖车与报关清关门禁 -> PDA task complete 同事务完成 task/plan/inbound -> LOADED`。
|
||||
`WAIT_SPLIT -> SIMULATION/ACTIVE 内部模拟柜 -> 智能排货 -> 无集装箱号 push-preload -> exact replay/冲突零漂移 -> 订舱逐柜登记 OCEAN_BOOKING/ACTIVE 真实柜 -> 按 realContainerBookingId 原地合并(来源 MERGED,模拟柜身份存活)-> RELEASED -> 计划级原子确认 -> 唯一 PDA task -> claim 后 LOADING -> 原子批量拣货 -> AWAITING_REVIEW -> 五类证据 -> LOADING 阶段增量入库单/旧 inventory token A0409/刷新后续拣 -> PDA task complete 联动同柜有效拖车(如有)并同事务完成 task/plan/inbound -> LOADED`。
|
||||
|
||||
其中,无号 push 只冻结装柜计划、建立 bridge 并使预装可见,不得创建 PDA task;真实柜号由合并动作从未消费的订舱真实柜带入,合并前、订舱未 `RELEASED` 或整份计划未确认时,PDA 查询必须返回业务码 `A0404`。合并后正常列表只保留模拟柜原地转成的一个 ACTIVE 操作身份,PDA 禁止使用 MERGED 来源 ID。唯一确认入口为 `POST /api/platform/v1/first-leg/loading-plans/{loadingPlanId}/confirm`,请求体只含当前计划 `expectedVersion` 并携带 `Idempotency-Key`;成功必须返回 `confirmationStatus=CONFIRMED`、`readiness.readyForPda=true` 和唯一 `readiness.pdaTaskId`。验收脚本禁止调用旧入库单级 `/preload/confirming`、`/preload/confirm`。
|
||||
|
||||
计划在 `EDITING/LOADING` 可通过正式 assignments/PATCH/DELETE 端点增量;计划、任务和明细版本必须递增,PDA 四元身份不变,旧 `inventorySnapshotToken` 必须返回 `A0409 库存已变化,请刷新后重试`。已拣项不可删或减量,未拣项可删;增量会把全拣的 `AWAITING_REVIEW` 撤回为 `IN_PROGRESS`,刷新 task detail 与 inventory 后续拣才可再次等待最终确认。最终唯一完成入口为 `POST /api/platform/v1/first-leg/pda-loading/tasks/{taskId}/complete`:服务端预检同柜 `RELEASED` 订舱、`COMPLETED` 拖车、五类 ACTIVE 证据和 bridge,缺项返回 A0409 且 task/plan/inbound 零写入;全部满足后同一事务推进 `LOADED`。后续模块必须复用 `phase3Loading` 发布的本轮 `taskId + inboundId + loadingPlanId + containerBookingId`,不得回退历史资源。
|
||||
计划在 `EDITING/LOADING` 可通过正式 assignments/PATCH/DELETE 端点增量;计划、任务和明细版本必须递增,PDA 四元身份不变,旧 `inventorySnapshotToken` 必须返回 `A0409 库存已变化,请刷新后重试`。已拣项不可删或减量,未拣项可删;增量会把全拣的 `AWAITING_REVIEW` 撤回为 `IN_PROGRESS`,刷新 task detail 与 inventory 后续拣才可再次等待最终确认。最终唯一完成入口仍为 `POST /api/platform/v1/first-leg/pda-loading/tasks/{taskId}/complete`:保留同柜 `RELEASED` 订舱、bridge 与五类 ACTIVE 证据门禁;拖车缺失或未完成不得阻断,若稳定柜次有关联有效拖车,则该业务动作负责将其推进到“已完成装柜”。当前后端 OpenAPI 仍写“预先 COMPLETED 拖车”,验收 finalize 已 `BLOCKED_CONTRACT` 主动停止,待新响应/历史/回滚字段冻结后接线。后续模块必须复用 `phase3Loading` 发布的本轮 `taskId + inboundId + loadingPlanId + containerBookingId`,不得回退历史资源。
|
||||
|
||||
部署后需要定向验证该完整链时,模块顺序固定为:
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ export function selectCustomsFlow(createdResources) {
|
||||
return { fixture: null, container: null }
|
||||
}
|
||||
|
||||
async function ensureServiceCenterFlow(context, container) {
|
||||
async function ensureReleasedOceanBooking(context, container) {
|
||||
const containerBookingId = id(container?.containerBookingId)
|
||||
const loadingPlanId = id(container?.loadingPlanId)
|
||||
let ocean = context.createdResources?.oceanBooking
|
||||
@@ -104,22 +104,7 @@ async function ensureServiceCenterFlow(context, container) {
|
||||
return false
|
||||
}
|
||||
|
||||
let truck = context.createdResources?.serviceCenterFlow
|
||||
if (truck?.finalStatus !== 'COMPLETED'
|
||||
|| id(truck?.oceanBookingId) !== id(ocean.id)
|
||||
|| id(truck?.containerBookingId) !== containerBookingId
|
||||
|| id(truck?.loadingPlanId) !== loadingPlanId) {
|
||||
const serviceCenter = await import(new URL('./service-center-flow.mjs', import.meta.url))
|
||||
await serviceCenter.run(context)
|
||||
truck = context.createdResources?.serviceCenterFlow
|
||||
}
|
||||
const ready = truck?.finalStatus === 'COMPLETED'
|
||||
&& id(truck?.oceanBookingId) === id(ocean.id)
|
||||
&& id(truck?.containerBookingId) === containerBookingId
|
||||
&& id(truck?.loadingPlanId) === loadingPlanId
|
||||
context.addStep(MODULE, '报关前拖车已完成并绑定实际装柜柜次', null, ready,
|
||||
`truckStatus=${truck?.finalStatus || 'missing'}, truckContainer=${id(truck?.containerBookingId) || 'missing'}, containerBookingId=${containerBookingId || 'missing'}`, OPTIONS)
|
||||
return ready
|
||||
return true
|
||||
}
|
||||
|
||||
export async function run(context) {
|
||||
@@ -137,7 +122,7 @@ export async function run(context) {
|
||||
`tenantId=${tenantId || 'missing'}, parentOrderId=${parentOrderId || 'missing'}, inboundId=${inboundId || 'missing'}, containerBookingId=${containerBookingId || 'missing'}, loadingPlanId=${loadingPlanId || 'missing'}, containerStatus=${container?.finalStatus || 'missing'}`, OPTIONS)
|
||||
return
|
||||
}
|
||||
if (!await ensureServiceCenterFlow(context, container)) return
|
||||
if (!await ensureReleasedOceanBooking(context, container)) return
|
||||
|
||||
const slicesPath = `/api/platform/v1/orders/container-bookings/${encodeURIComponent(containerBookingId)}/fulfillment-slices?tenantId=${encodeURIComponent(tenantId)}`
|
||||
const slicesResponse = await context.request('GET', slicesPath)
|
||||
|
||||
@@ -73,32 +73,25 @@ test('HTTP chain refreshes after stale inventory and keeps one stable PDA identi
|
||||
assert.doesNotMatch(loading, /\/loading\/complete\?tenantId=/)
|
||||
})
|
||||
|
||||
test('downstream modules keep the task pending until the transactional PDA final complete', () => {
|
||||
test('downstream modules stop before HTTP until the new truck-coupled complete contract freezes', () => {
|
||||
assert.match(convergence, /summary\?\.status === 'LOADING'/)
|
||||
assert.match(convergence, /!operations\.some\(\(item\) => item\.actionType === 'COMPLETE_LOADING'\)/)
|
||||
assert.match(customs, /\['AWAITING_REVIEW', 'LOADED'\]/)
|
||||
assert.match(customs, /ensureReleasedOceanBooking/)
|
||||
assert.doesNotMatch(customs, /truck\?\.finalStatus !== 'COMPLETED'/)
|
||||
ordered(finalize, [
|
||||
'最终完成前拖车与报关清关门禁属于同一稳定柜次',
|
||||
'最终完成前四元身份稳定且任务等待人工确认',
|
||||
'读取最终完成并发前计划即时版本',
|
||||
'Admin 增量与 PDA final complete 并发恰好一个事务成功',
|
||||
'订舱拖车报关齐备后完成装柜',
|
||||
'统一硬门禁通过后 task 与 workflow 同事务完成且身份不漂移',
|
||||
'入库单终态 LOADED、版本递增且审计历史存在',
|
||||
'统一硬门禁通过后五类有效证据仍完整',
|
||||
'COMPLETED 后新增计划明细被拒绝',
|
||||
'COMPLETED 后调整计划明细被拒绝',
|
||||
'COMPLETED 后删除计划明细被拒绝',
|
||||
'最终装柜前保持 LOADING 且版本有效',
|
||||
'PDA complete 联动拖车并原子推进 LOADED',
|
||||
'return',
|
||||
'const taskPath =',
|
||||
])
|
||||
assert.match(finalize, /`\$\{taskPath\}\/complete`/)
|
||||
assert.match(finalize, /await Promise\.all\(/)
|
||||
assert.match(finalize, /completeWon !== adjustWon/)
|
||||
assert.match(finalize, /BLOCKED_CONTRACT:用户已确认拖车缺失或未完成不再阻断/)
|
||||
assert.doesNotMatch(finalize, /truck\?\.finalStatus === 'COMPLETED'/)
|
||||
assert.doesNotMatch(finalize, /\/loading\/complete\?tenantId=/)
|
||||
})
|
||||
|
||||
test('PDA final completion OpenAPI freezes zero-write gate semantics', () => {
|
||||
test('current PDA complete OpenAPI remains a blocker because it still requires pre-completed truck', () => {
|
||||
assert.match(pdaOpenApi, /\/api\/platform\/v1\/first-leg\/pda-loading\/tasks\/\{taskId\}\/complete:/)
|
||||
assert.match(pdaOpenApi, /先无写入校验全部关联入库单的RELEASED订舱、COMPLETED拖车和五类ACTIVE证据/)
|
||||
assert.match(pdaOpenApi, /任何阻断返回A0409且task、plan、inbound仍保持LOADING/)
|
||||
assert.match(pdaOpenApi, /全部满足后在同一事务内结算资源、完成task\/plan并推进inbound为LOADED/)
|
||||
assert.match(finalize, /BLOCKED_CONTRACT/)
|
||||
})
|
||||
|
||||
@@ -69,14 +69,11 @@ export async function run(context) {
|
||||
if (before?.summary?.processStatus !== 'LOADING' || !Number.isInteger(version)) return
|
||||
|
||||
const base = `/api/platform/v1/inbounds/${inboundId}`
|
||||
const truck = context.createdResources?.serviceCenterFlow
|
||||
const customs = context.createdResources?.phase3Customs
|
||||
context.assert(MODULE, '最终完成前拖车与报关清关门禁属于同一稳定柜次', beforeResponse,
|
||||
truck?.marker === context.marker && truck?.finalStatus === 'COMPLETED'
|
||||
&& id(truck?.containerBookingId) === containerBookingId
|
||||
&& customs?.marker === context.marker && customs?.finalStatus === 'CLEARANCE_COMPLETED'
|
||||
&& id(customs?.containerBookingId) === containerBookingId,
|
||||
`truck=${truck?.finalStatus || 'missing'}, customs=${customs?.finalStatus || 'missing'}, containerBookingId=${containerBookingId}`, OPTIONS)
|
||||
context.addCoverageGap(MODULE, OPTIONS.scenario, 'PDA complete 联动拖车并原子推进 LOADED',
|
||||
'BLOCKED_CONTRACT:用户已确认拖车缺失或未完成不再阻断;等待后端冻结 complete 响应中的拖车投影、LOADING→COMPLETED 历史动作、错柜隔离、重放与失败回滚字段后再执行写 HTTP。当前脚本主动停止,禁止沿用“拖车必须预先 COMPLETED”的旧门禁。',
|
||||
'contract')
|
||||
return
|
||||
|
||||
const taskPath = `/api/platform/v1/first-leg/pda-loading/tasks/${encodeURIComponent(taskId)}`
|
||||
const taskBeforeResponse = await context.request('GET', taskPath)
|
||||
const taskBefore = context.apiSuccess(MODULE, '读取最终完成前 PDA 任务即时版本', taskBeforeResponse, '', OPTIONS)
|
||||
|
||||
@@ -460,15 +460,16 @@ test('phase3 loading fixture forces A0 through reversible public location status
|
||||
assert.match(orders, /receivingWarehouseId: id\(master\.receiving\.id\)/)
|
||||
})
|
||||
|
||||
test('customs provisions released booking and completed truck for the selected loading container', () => {
|
||||
test('customs provisions released booking without pre-completing truck', () => {
|
||||
const value = source('./phase3-customs.mjs')
|
||||
assertOrder(value, [
|
||||
'selectCustomsFlow(context.createdResources)',
|
||||
'if (!await ensureServiceCenterFlow(context, container)) return',
|
||||
'if (!await ensureReleasedOceanBooking(context, container)) return',
|
||||
'const slicesPath =',
|
||||
])
|
||||
assert.match(value, /oceanBookings\.run\(context\)/)
|
||||
assert.match(value, /serviceCenter\.run\(context\)/)
|
||||
assert.doesNotMatch(value, /serviceCenter\.run\(context\)/)
|
||||
assert.doesNotMatch(value, /truck\?\.finalStatus !== 'COMPLETED'/)
|
||||
assert.match(value, /request\('POST', '\/api\/platform\/v1\/suppliers\?tenantId=0'/)
|
||||
assert.match(value, /category: 'CUSTOMS'/)
|
||||
assert.match(value, /context\.createdResources\.phase3CustomsSupplier/)
|
||||
|
||||
@@ -102,7 +102,7 @@ test('test-environment loading chains push without a number and open PDA only af
|
||||
assert.doesNotMatch(containerLoading, /\/preload\/confirming|\/preload\/confirm\?tenantId=/)
|
||||
})
|
||||
|
||||
test('default Admin HTTP entry carries one marker-isolated loading chain through evidence and LOADED', () => {
|
||||
test('default Admin HTTP entry carries evidence chain but fail-closes before obsolete truck completion semantics', () => {
|
||||
const focused = normalizeModuleOrder([
|
||||
'merchant-qualification-fixture', 'orders', 'inbound-measurement',
|
||||
'phase3-container-loading', 'phase3-loading', 'phase3-container-overview',
|
||||
@@ -130,9 +130,10 @@ test('default Admin HTTP entry carries one marker-isolated loading chain through
|
||||
assert.match(evidenceGate, /taskId[\s\S]*inboundId[\s\S]*loadingPlanId[\s\S]*containerBookingId/)
|
||||
assert.match(customs, /createdResources\?\.phase3Loading[\s\S]*container\?\.finalStatus/)
|
||||
assert.match(finalize, /fixture\?\.marker !== context\.marker/)
|
||||
assert.match(finalize, /统一硬门禁通过后 task 与 workflow 同事务完成且身份不漂移/)
|
||||
assert.match(finalize, /统一硬门禁通过后五类有效证据仍完整/)
|
||||
assert.match(finalize, /统一订单详情投影同一订单为 LOADED[\s\S]*统一订单列表投影同一订单为 LOADED/)
|
||||
assert.match(finalize, /BLOCKED_CONTRACT:用户已确认拖车缺失或未完成不再阻断/)
|
||||
assert.ok(finalize.indexOf('PDA complete 联动拖车并原子推进 LOADED')
|
||||
< finalize.indexOf('const taskPath ='))
|
||||
assert.doesNotMatch(finalize, /truck\?\.finalStatus === 'COMPLETED'/)
|
||||
})
|
||||
|
||||
test('Finance-only HTTP entry expands the completed-container producer prerequisites', () => {
|
||||
|
||||
Reference in New Issue
Block a user