fix: link terminal drayage carrier events
This commit is contained in:
+1
-1
@@ -20,7 +20,7 @@
|
||||
| 入仓测量与整票库位块专项 | `admin-test-env-modules/inbound-measurement.mjs`、`admin-test-env-modules/README.md` | 仍从 Admin 总入口按 `orders,inbound-measurement` 依赖链执行;开启 `--verify-pallet-location-guide true` 后逐张确认回查块扩容只能追加,既有槽身份不得漂移,并验证删除图片后空槽保留与替代图片复用;会写目标环境,必须有对应环境授权 |
|
||||
| Finance 扩展 HTTP 模块 | `admin-test-env-modules/firstleg-container-fee-allocation.mjs`、`finance-direct-paid-isolation.mjs`、`phase4-overseas-finance-cross-region.mjs` | 分别验证柜费跨客户分摊、海外仓服务直接已付隔离、五类海外费用跨区入池;必须由 Admin 总入口按已登记依赖运行,使用本轮资源且禁止借历史数据,真实 HTTP 仍须当前轮授权 |
|
||||
| 订舱与拖车服务中心链路 | `admin-test-env-modules/service-center-flow.mjs` | 复用本轮 `RELEASED` 订舱,创建订舱级拖车并验证附件、完整状态链、历史和稳定关联;由 Admin 总入口按依赖运行,会写目标环境 |
|
||||
| 尾程单控制与目的港码头拖车 | `admin-test-env-modules/phase4-tail-order-controls.mjs`、`admin-test-env-modules/phase5-terminal-drayage.mjs`、`admin-test-env-modules/phase4-carrier-nodes.mjs` | PDA 前覆盖整柜接柜仓变更与恢复、单/批目的仓变更、单/批合托标记、CSV 导出、订单费用、单票暂扣/解除、八个人工节点和逐票人工计划;接柜仓仅复用公共主档且恢复原值。PDA 卸柜后经正式码头拖车接口完成选车队、预约、提柜、还柜,船节点模块只读核对最终回写,不直接伪造最后两个节点 |
|
||||
| 尾程单控制与目的港码头拖车 | `admin-test-env-modules/phase4-tail-order-controls.mjs`、`admin-test-env-modules/phase5-terminal-drayage.mjs`、`admin-test-env-modules/phase4-carrier-nodes.mjs` | PDA 前覆盖整柜接柜仓变更与恢复、单/批目的仓变更、单/批合托标记、CSV 导出、订单费用、单票暂扣/解除、八个人工节点和逐票人工计划;接柜仓仅复用公共主档且恢复原值。货柜达到 `PICKUP_READY` 后先经正式码头拖车接口完成选车队、预约和提柜,PDA 卸柜后再还柜;船节点模块只读核对最终回写,不直接伪造最后两个节点。人工船节点的 `observedAt` 使用运行机本地墙钟时间,避免无时区 `LocalDateTime` 被 UTC 偏移倒序 |
|
||||
| 定向 HTTP/诊断 | 以 `README.md` 的“仓库级入口清单”为准 | 必须明确是只读诊断还是会补证据、创建资源或推进状态,不能用诊断成功冒充全量通过 |
|
||||
| 本地质量与静态合同 | `backend-full-regression.ps1`、`frontend-full-regression.ps1`(含国内 PDA Node 全量测试)、`mapper-scan-contract.mjs`、`script-documentation-contract.test.mjs` 等 | 默认不访问开发或测试环境;数据库非 dry-run、Nacos 发布等有写入能力的入口必须按 README 单独识别 |
|
||||
| 部署与基础设施 | `../deploy/scripts/`、`../deploy/rocketmq/` | 靠近部署组件管理,不移动到根脚本目录;部署、初始化数据库或发布配置必须获得当前轮明确授权 |
|
||||
|
||||
@@ -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