fix: link terminal drayage carrier events
This commit is contained in:
@@ -110,7 +110,7 @@ export async function run(context) {
|
||||
|
||||
节点全链定向入口为 `--modules phase4-overseas-pda`。依赖展开固定先为本轮 `LOADED` 柜次完成国内 `CUSTOMS_DECLARATION` 与 PDA 装柜证据,再确认国内出仓;离仓后才维护 `DESTINATION_CLEARANCE`、`DELIVERY_ORDER` 两类目的港文件,PU 使用柜级数字码而不是文件,随后核对自动创建的 `OUTBOUND_CONFIRM` 船节点任务、人工补录至实际 `PICKUP_READY`,最后验证海外 PDA 候选可见。资料模块只写本轮 marker 柜次,覆盖上传/完成重放、同键异载荷 `A0409`、冲突后零漂移和聚合回读;任一 fixture 或 Trace 缺失均为 deployment blocker。
|
||||
|
||||
尾程单公网控制与目的港码头拖车定向入口为 `--modules phase4-carrier-nodes`。依赖会在海外 PDA 前插入 `phase4-tail-order-controls`,先对本轮柜验证接柜仓变更/恢复、目的仓变更、合托标记、CSV 导出、订单费用、暂扣阻断、解除和八个人工节点,再通过柜级预览与确认自动生成逐票计划和实体托盘;PDA 必须消费这些计划托盘,不得临时另建脱离计划的托盘。货柜到达 `PICKUP_READY` 后先复用公共 `ACTIVE + TRUCKING` 供应商完成选车队、预约和实际提柜,PDA 卸柜完成后再确认实际还柜。最后的船节点模块仅回读两次业务动作同步生成的 `PICKED_UP/EMPTY_RETURNED`,不得直接补录这两个节点。
|
||||
尾程单公网控制与目的港码头拖车定向入口为 `--modules phase4-carrier-nodes`。依赖会在海外 PDA 前插入 `phase4-tail-order-controls`,先对本轮柜验证接柜仓变更/恢复、目的仓变更、合托标记、CSV 导出、订单费用、暂扣阻断、解除和八个人工节点,再通过柜级预览与确认自动生成逐票计划和实体托盘;PDA 必须消费这些计划托盘,不得临时另建脱离计划的托盘。货柜到达 `PICKUP_READY` 后先复用公共 `ACTIVE + TRUCKING` 供应商完成选车队、预约和实际提柜;PDA 卸柜完成后,有界等待跨区卸柜事件回写国内聚合为 `UNLOADED`,再确认实际还柜。最后的船节点模块仅回读两次业务动作同步生成的 `PICKED_UP/EMPTY_RETURNED`,不得直接补录这两个节点。
|
||||
|
||||
```powershell
|
||||
node --env-file="$env:USERPROFILE\.codex\chongchong-global-test-acceptance.env" scripts/admin-test-env-acceptance.mjs --modules phase4-carrier-nodes
|
||||
|
||||
@@ -71,6 +71,8 @@ test('canonical states, ten ordered nodes, exception codes and complete payload
|
||||
test('actual node timestamps advance from the frozen outbound-confirm fact instead of the runner timezone', () => {
|
||||
assert.match(source, /Date\.parse\(`\$\{frozenLatestActual\}Z`\)/)
|
||||
assert.match(source, /frozenLatestActualMs\) \? Date\.now\(\) : frozenLatestActualMs/)
|
||||
assert.equal((source.match(/observedAt: localWallIso\(new Date\(\)\)/g) || []).length, 3)
|
||||
assert.match(source, /const observedAt = localWallIso\(new Date\(\)\)/)
|
||||
})
|
||||
|
||||
test('module exposes no scheduler, attempt, internal or arbitrary stop endpoint', () => {
|
||||
|
||||
@@ -38,6 +38,11 @@ function monotonicRejected(response) {
|
||||
&& Boolean(response?.requestId) && Boolean(response?.traceId)
|
||||
}
|
||||
function localIso(value) { return value.toISOString().slice(0, 19) }
|
||||
function localWallIso(value) {
|
||||
const pad = (part) => String(part).padStart(2, '0')
|
||||
return `${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(value.getDate())}`
|
||||
+ `T${pad(value.getHours())}:${pad(value.getMinutes())}:${pad(value.getSeconds())}`
|
||||
}
|
||||
|
||||
export function markerOwnedCarrierQuery(context) {
|
||||
const resource = context?.createdResources?.phase3CarrierScheduleQuery
|
||||
@@ -167,7 +172,7 @@ export async function runUntilPickupReady(context) {
|
||||
ownershipMatches, `registered=${queryId}, actual=${id(initialTask?.id) || 'missing'}`, OPTIONS)
|
||||
if (!ownershipMatches) return
|
||||
|
||||
const observedAt = localIso(new Date())
|
||||
const observedAt = localWallIso(new Date())
|
||||
const estimatedAt = localIso(new Date(Date.now() + 30 * 24 * 60 * 60_000))
|
||||
const estimatedBody = manualNodePayload({ expectedVersion: Number(initialTask.version),
|
||||
standardNode: 'ARRIVED_DESTINATION_PORT', observedAt, estimatedAt, marker: context.marker })
|
||||
@@ -226,7 +231,7 @@ export async function runUntilPickupReady(context) {
|
||||
|
||||
const sameNodeSnapshot = actualProgressSnapshot(detail)
|
||||
const sameNodeBody = manualNodePayload({ expectedVersion: version,
|
||||
standardNode: detail.query.query.latestActualNode, observedAt: localIso(new Date()),
|
||||
standardNode: detail.query.query.latestActualNode, observedAt: localWallIso(new Date()),
|
||||
actualAt: localIso(new Date(baseActual)), marker: context.marker, index: frozenLatestNodeIndex + 1 })
|
||||
const sameNodeResponse = await context.request('POST', `${BASE}/${queryId}/manual-nodes`, sameNodeBody,
|
||||
context.token, { 'Idempotency-Key': `${context.marker}-carrier-same-actual`.slice(0, 128) })
|
||||
@@ -250,7 +255,7 @@ export async function runUntilPickupReady(context) {
|
||||
const standardNode = STANDARD_NODES[index]
|
||||
const actualAt = localIso(new Date(baseActual + index * 60_000))
|
||||
const body = manualNodePayload({ expectedVersion: version, standardNode,
|
||||
observedAt: localIso(new Date()), actualAt, marker: context.marker, index: index + 1 })
|
||||
observedAt: localWallIso(new Date()), actualAt, marker: context.marker, index: index + 1 })
|
||||
const key = `${context.marker}-carrier-actual-${String(index + 1).padStart(2, '0')}`.slice(0, 128)
|
||||
const response = await context.request('POST', `${BASE}/${queryId}/manual-nodes`, body,
|
||||
context.token, { 'Idempotency-Key': key })
|
||||
@@ -275,7 +280,7 @@ export async function runUntilPickupReady(context) {
|
||||
const progressedSnapshot = actualProgressSnapshot(detail)
|
||||
const earlierNode = STANDARD_NODES[index - 1]
|
||||
const earlierBody = manualNodePayload({ expectedVersion: version, standardNode: earlierNode,
|
||||
observedAt: localIso(new Date()), actualAt: localIso(new Date(baseActual + (index + 1) * 60_000)),
|
||||
observedAt: localWallIso(new Date()), actualAt: localIso(new Date(baseActual + (index + 1) * 60_000)),
|
||||
marker: context.marker, index })
|
||||
const earlierResponse = await context.request('POST', `${BASE}/${queryId}/manual-nodes`, earlierBody,
|
||||
context.token, { 'Idempotency-Key': `${context.marker}-carrier-earlier-actual`.slice(0, 128) })
|
||||
|
||||
@@ -84,6 +84,8 @@ test('terminal drayage prepares pickup before unloading and finalizes return bef
|
||||
const finalCarrier = modules.indexOf('phase4-carrier-nodes')
|
||||
assert.ok(pickup >= 0 && pickup < pda)
|
||||
assert.ok(pda < drayage && drayage < finalCarrier)
|
||||
assert.match(source, /attempt <= 30/)
|
||||
assert.match(source, /initialData\?\.status === 'UNLOADED'/)
|
||||
})
|
||||
|
||||
test('module uses only public business endpoints and reuses public trucking master data', () => {
|
||||
|
||||
@@ -130,7 +130,22 @@ export async function run(context) {
|
||||
return
|
||||
}
|
||||
const path = `${BASE}/${encodeURIComponent(target.containerBookingId)}`
|
||||
const initial = await api(context, 'GET', '卸柜后读取目的港码头拖车聚合', path)
|
||||
let initialResponse = null
|
||||
let initialData = null
|
||||
let pollRequests = 0
|
||||
for (let attempt = 1; attempt <= 30; attempt += 1) {
|
||||
pollRequests = attempt
|
||||
initialResponse = await context.request('GET', path, undefined, context.token)
|
||||
initialData = success(initialResponse) ? initialResponse.body?.data : null
|
||||
if (initialData?.status === 'UNLOADED'
|
||||
&& initialData?.allowedActions?.includes('RECORD_EMPTY_RETURN')) break
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
}
|
||||
const initial = {
|
||||
response: initialResponse,
|
||||
data: context.apiSuccess(MODULE, '卸柜后等待跨区事件并读取目的港码头拖车聚合', initialResponse,
|
||||
`pollRequests=${pollRequests}; ${trace(initialResponse)}`, OPTIONS),
|
||||
}
|
||||
context.assert(MODULE, '卸柜聚合保留提柜事实并开放还柜', initial.response,
|
||||
id(initial.data?.containerBookingId) === target.containerBookingId
|
||||
&& id(initial.data?.containerNo) === id(target.completed.containerNo)
|
||||
@@ -138,7 +153,7 @@ export async function run(context) {
|
||||
&& initial.data?.status === 'UNLOADED'
|
||||
&& initial.data?.actualPickupAt === target.pickupAt
|
||||
&& initial.data?.allowedActions?.includes('RECORD_EMPTY_RETURN'),
|
||||
`status=${initial.data?.status || 'missing'}, actions=${initial.data?.allowedActions?.join(',') || 'missing'}; ${trace(initial.response)}`, OPTIONS)
|
||||
`pollRequests=${pollRequests}, status=${initial.data?.status || 'missing'}, actions=${initial.data?.allowedActions?.join(',') || 'missing'}; ${trace(initial.response)}`, OPTIONS)
|
||||
if (!success(initial.response)) return
|
||||
|
||||
const unloadingAt = Date.parse(`${initial.data?.unloadingCompletedAt}Z`)
|
||||
|
||||
Reference in New Issue
Block a user