优化
This commit is contained in:
@@ -49,7 +49,7 @@ export async function run(context) {
|
||||
| 订单创建、可计价主数据与审核异步收敛 | `orders.mjs` | `orders-pricing-fixture-contract.test.mjs`、`orders-approval-convergence-contract.test.mjs` |
|
||||
| 入库测量、首次打印冻结箱码与墨家扫码 | `inbound-measurement.mjs` | `inbound-measurement-box-code-contract.test.mjs` |
|
||||
| 入仓自动头程费用、派生详情、人工发布与 MANUAL 其他费用版本 | `inbound-fees.mjs`(前置:`merchant-qualification-fixture,orders,inbound-measurement`) | `inbound-fees-contract.test.mjs` |
|
||||
| 三期模拟柜关联、柜次概览、渠道隔离、装柜模型桥接、PDA、动态增量、四类货柜资料、报关与证据/出仓门禁 | `phase3-container-loading.mjs`、`ocean-booking-fixture.mjs`、`phase3-container-overview.mjs`、`phase3-loading.mjs`、`phase3-loading-model-bridge.mjs`、`phase3-pda-convergence.mjs`、`phase3-evidence-gate.mjs`、`phase3-loading-finalize.mjs`、`phase3-container-documents.mjs`、`phase3-customs.mjs`、`phase3-outbound-dispatch.mjs` | `container-simulation-association-contract.test.mjs`、`container-specification-fixture-contract.test.mjs`、`ocean-booking-channel-contract.test.mjs`、`phase3-container-regression-contract.test.mjs`、`phase3-loading-model-bridge-contract.test.mjs`、`phase3-loading-dynamic-final-contract.test.mjs`、`phase3-container-documents-contract.test.mjs`、`phase3-producer-chain-contract.test.mjs`、`manual-state-loading-pda-contract.test.mjs` |
|
||||
| 三期模拟柜关联、柜次概览、渠道隔离、装柜模型桥接、PDA、动态增量、四类货柜资料、报关与证据/出仓门禁 | `phase3-container-loading.mjs`、`ocean-booking-fixture.mjs`、`phase3-container-overview.mjs`、`phase3-loading.mjs`、`phase3-loading-model-bridge.mjs`、`phase3-pda-convergence.mjs`、`phase3-evidence-gate.mjs`、`phase3-loading-finalize.mjs`、`phase3-container-documents.mjs`、`phase3-customs.mjs`、`phase3-outbound-dispatch.mjs` | `container-simulation-association-contract.test.mjs`、`container-specification-fixture-contract.test.mjs`、`ocean-booking-channel-contract.test.mjs`、`phase3-container-regression-contract.test.mjs`、`phase3-loading-model-bridge-contract.test.mjs`、`phase3-loading-dynamic-final-contract.test.mjs`、`phase3-container-documents-contract.test.mjs`、`phase3-phase4-departure-pickup-chain-contract.test.mjs`、`phase3-producer-chain-contract.test.mjs`、`manual-state-loading-pda-contract.test.mjs` |
|
||||
| 分柜与父子入库单数量守恒 | `phase3-order-splitting.mjs` | `phase3-order-splitting-contract.test.mjs` |
|
||||
| 订舱共享主数据前置与订舱验收 | `ocean-booking-fixture.mjs`、`ocean-bookings.mjs`、`phase3-port-schedules.mjs`、`phase3-carrier-schedule-queries.mjs`(DRAFT 可编辑;RELEASED 冻结;船司查询只创建并写当前 marker 登记任务) | `ocean-booking-prototype-contract.test.mjs`、`ocean-booking-fixture-contract.test.mjs`、`phase3-carrier-schedule-queries.test.mjs` |
|
||||
| 独立拖车预约与可选履约关联 | `truck-appointments.mjs`(同 marker 客户 + ACTIVE TRUCKING 承运商;订舱/柜次/计划可空;DISPATCH 前需提单号和有效 `BILL_OF_LADING` 文件) | `truck-appointments-contract.test.mjs` |
|
||||
@@ -59,6 +59,7 @@ export async function run(context) {
|
||||
| 四期海外仓 HTTP 生命周期 | `phase4-overseas-warehouse.mjs` | `phase4-overseas-warehouse-contract.test.mjs` |
|
||||
| 四期海外 PDA 柜号收货与库内操作 HTTP 生命周期 | `phase4-overseas-pda.mjs`(按柜完成签收和实收,成功后仅以内存上下文交接操作员会话和已完成库存事实)、`phase4-overseas-pda-operations.mjs`(紧随收货执行 P2 盘点、P3 整托移库、P4 托盘拆装换绑、P5 换标复核;finally 停用双操作员并清空临时 Token;任何真实 fixture 缺失均 fail-closed) | `phase4-overseas-pda-contract.test.mjs`、`phase4-overseas-pda-operations-contract.test.mjs` |
|
||||
| 三四期跨区事件与安全门禁 | `phase4-cross-region-events.mjs`(双 Token Audience 互斥、canonical 权限 403、国内装柜完成到 Lastleg 建单、海外柜完成到 Firstleg 回写、轮询重试/幂等/诊断证据;缺本轮 marker 前置时阻断) | `phase4-cross-region-events-contract.test.mjs` |
|
||||
| 国内离仓至海外 PDA 可见人工验收节点 | `phase3-customs.mjs`(四类柜资料与整柜照片)、`phase3-outbound-dispatch.mjs`(柜级离仓)、`phase3-carrier-schedule-queries.mjs`、`phase4-carrier-pickup-ready.mjs`、`phase4-overseas-pda.mjs`(同柜候选) | `phase3-phase4-departure-pickup-chain-contract.test.mjs` |
|
||||
| 四期海外业务 HTTP 生命周期 | `phase4-overseas-business.mjs` | `phase4-overseas-business-contract.test.mjs` |
|
||||
| 四期海外主数据 HTTP 生命周期 | `phase4-overseas-master-data.mjs` | `phase4-overseas-master-data-contract.test.mjs` |
|
||||
| 四期船节点安全冒烟 | `phase4-carrier-nodes.mjs`(standalone 对历史任务只读;仅复用当前 marker 的 `phase3CarrierScheduleQuery` 时补录预计事实) | `phase4-carrier-nodes-contract.test.mjs` |
|
||||
|
||||
@@ -151,8 +151,6 @@ export function outboundAggregateFieldGaps(record) {
|
||||
containerNo: isNonBlank,
|
||||
loadingPlanId: isPositiveId,
|
||||
loadingPlanStatus: isNonBlank,
|
||||
pickTaskId: isPositiveId,
|
||||
pickTaskStatus: isNonBlank,
|
||||
oceanBookingId: isPositiveId,
|
||||
oceanBookingStatus: isNonBlank,
|
||||
dispatchStatus: isNonBlank,
|
||||
|
||||
@@ -12,15 +12,16 @@ export async function run(context) {
|
||||
'本轮未取得 phase3-outbound-dispatch 的 marker 隔离聚合,未使用历史资源', 'fixture')
|
||||
return
|
||||
}
|
||||
const selector = `inboundId=${encodeURIComponent(fixture.inboundId)}&containerBookingId=${encodeURIComponent(fixture.containerBookingId)}`
|
||||
const response = await context.request('GET', `${BASE}/check-gate?${selector}`)
|
||||
const gate = context.apiSuccess(MODULE, '复核国内出仓统一硬门禁', response, selector, OPTIONS)
|
||||
const items = Array.isArray(gate?.items) ? gate.items : []
|
||||
const response = await context.request('GET', `${BASE}/${encodeURIComponent(fixture.containerBookingId)}/gate`)
|
||||
const gate = context.apiSuccess(MODULE, '复核国内出仓柜级统一硬门禁', response,
|
||||
`containerBookingId=${fixture.containerBookingId}`, OPTIONS)
|
||||
const items = Array.isArray(gate?.inbounds)
|
||||
? gate.inbounds.flatMap((item) => Array.isArray(item?.gate?.items) ? item.gate.items : []) : []
|
||||
const required = ['INBOUND_LOADED', 'LOADING_PLAN_COMPLETED', 'PDA_PICK_COMPLETED',
|
||||
'OCEAN_BOOKING_RELEASED', 'TRUCK_DEPARTED', 'LOADING_EVIDENCE_COMPLETE',
|
||||
'CUSTOMS_CLEARANCE', 'SAILING_SCHEDULE']
|
||||
context.assert(MODULE, '八项业务门禁全部由服务端真实判定并通过', response,
|
||||
'OCEAN_BOOKING_RELEASED', 'LOADING_EVIDENCE_COMPLETE', 'CUSTOMS_CLEARANCE']
|
||||
context.assert(MODULE, '六项硬门禁全部由服务端真实判定并通过', response,
|
||||
gate?.passed === true && required.every((code) => items.some((item) => item.code === code
|
||||
&& item.passed === true && item.actual !== 'MODULE_UNAVAILABLE' && item.actual !== 'ASSESSMENT_ERROR')),
|
||||
&& item.passed === true && item.actual !== 'MODULE_UNAVAILABLE' && item.actual !== 'ASSESSMENT_ERROR'))
|
||||
&& items.every((item) => !['TRUCK_DEPARTED', 'SAILING_SCHEDULE'].includes(item.code)),
|
||||
`passed=${gate?.passed ?? 'missing'}, gates=${items.map((item) => `${item.code}:${item.passed}`).join(',')}`, OPTIONS)
|
||||
}
|
||||
|
||||
@@ -35,8 +35,9 @@ test('phase3 inventory mutation and downstream gates cannot pass as coverage gap
|
||||
assert.match(dispatch, /国内出仓六项硬门禁必须在同轮全部通过[\s\S]{0,300}false/)
|
||||
assert.match(dispatch, /拖车与独立船期仅作聚合参考且不参与出仓门禁/)
|
||||
assert.match(dispatch, /'CUSTOMS_CLEARANCE'/)
|
||||
assert.doesNotMatch(dispatch, /expectedCodes[\s\S]{0,240}'TRUCK_DEPARTED'|expectedCodes[\s\S]{0,240}'SAILING_SCHEDULE'/)
|
||||
assert.match(aggregate, /'CUSTOMS', 'SAILING_SCHEDULE'/)
|
||||
assert.match(dispatch, /const expectedCodes = \['INBOUND_LOADED', 'LOADING_PLAN_COMPLETED', 'PDA_PICK_COMPLETED',[\s\S]{0,120}'CUSTOMS_CLEARANCE'\]/)
|
||||
assert.match(aggregate, /'OCEAN_BOOKING', 'CUSTOMS', 'OUTBOUND_DISPATCH'/)
|
||||
assert.match(aggregate, /拖车和独立船期时间线只作参考/)
|
||||
assert.doesNotMatch(aggregate, /报关与船期历史并入统一时间线|当前服务端聚合未公开/)
|
||||
})
|
||||
|
||||
@@ -62,4 +63,5 @@ test('outbound lifecycle checks confirm replay, immutable departure reverse boun
|
||||
assert.match(waiting, /confirmations\.length === 1 && reversed\.length === 0/)
|
||||
assert.match(waiting, /实际离仓节点冻结后保持一次确认且零次成功撤销/)
|
||||
assert.match(waiting, /detail\?\.aggregate\?\.dispatchStatus === 'WAITING_FOR_LOADING'/)
|
||||
assert.match(waiting, /inbounds\?\.every/)
|
||||
})
|
||||
|
||||
@@ -23,29 +23,27 @@ export async function run(context) {
|
||||
'本轮未取得 phase3-outbound-dispatch 的 marker 隔离聚合,未使用历史资源', 'fixture')
|
||||
return
|
||||
}
|
||||
const selector = `inboundId=${encodeURIComponent(fixture.inboundId)}&containerBookingId=${encodeURIComponent(fixture.containerBookingId)}`
|
||||
const response = await context.request('GET', `${BASE}/detail?${selector}`)
|
||||
const detail = context.apiSuccess(MODULE, '复核出仓主聚合与统一时间线', response, selector, OPTIONS)
|
||||
const response = await context.request('GET', `${BASE}/${encodeURIComponent(fixture.containerBookingId)}`)
|
||||
const detail = context.apiSuccess(MODULE, '复核柜级出仓主聚合与统一时间线', response,
|
||||
`containerBookingId=${fixture.containerBookingId}`, OPTIONS)
|
||||
const aggregate = detail?.aggregate
|
||||
const inbound = aggregate?.inbounds?.find?.((item) => id(item?.inboundId) === id(fixture.inboundId))
|
||||
const timeline = Array.isArray(detail?.timeline) ? detail.timeline : []
|
||||
const expectedModules = ['INBOUND', 'LOADING_PLAN', 'PDA_PICK', 'LOADING_EVIDENCE',
|
||||
'OCEAN_BOOKING', 'TRUCK_APPOINTMENT', 'CUSTOMS', 'SAILING_SCHEDULE', 'OUTBOUND_DISPATCH']
|
||||
'OCEAN_BOOKING', 'CUSTOMS', 'OUTBOUND_DISPATCH']
|
||||
context.assert(MODULE, '聚合唯一关联本轮订单、入库单、柜次和客户', response,
|
||||
id(aggregate?.orderId) === id(fixture.orderId) && id(aggregate?.tenantId) === id(fixture.tenantId)
|
||||
&& id(aggregate?.inboundId) === id(fixture.inboundId)
|
||||
id(inbound?.orderId) === id(fixture.orderId) && id(inbound?.tenantId) === id(fixture.tenantId)
|
||||
&& id(inbound?.inboundId) === id(fixture.inboundId)
|
||||
&& id(aggregate?.containerBookingId) === id(fixture.containerBookingId),
|
||||
`orderId=${id(aggregate?.orderId) || 'missing'}, inboundId=${id(aggregate?.inboundId) || 'missing'}, containerBookingId=${id(aggregate?.containerBookingId) || 'missing'}`, OPTIONS)
|
||||
`orderId=${id(inbound?.orderId) || 'missing'}, inboundId=${id(inbound?.inboundId) || 'missing'}, containerBookingId=${id(aggregate?.containerBookingId) || 'missing'}`, OPTIONS)
|
||||
context.assert(MODULE, '统一时间线有序且各子流程包含审计字段', response,
|
||||
timeline.length > 0 && ordered(timeline)
|
||||
&& expectedModules.every((module) => timeline.some((item) => item.module === module))
|
||||
&& timeline.every((item) => Boolean(item.action) && Boolean(item.occurredAt)),
|
||||
`timeline=${timeline.length}, modules=${[...new Set(timeline.map((item) => item.module))].join(',')}`, OPTIONS)
|
||||
|
||||
const other = context.createdResources?.phase3ContainerLoading
|
||||
if (other?.inboundId && other?.containerBookingId
|
||||
&& (id(other.inboundId) !== id(fixture.inboundId) || id(other.containerBookingId) !== id(fixture.containerBookingId))) {
|
||||
const forged = await context.request('GET', `${BASE}/detail?inboundId=${encodeURIComponent(fixture.inboundId)}&containerBookingId=${encodeURIComponent(other.containerBookingId)}`)
|
||||
context.addStep(MODULE, '伪造跨资源入库单与柜次组合不可匹配', forged, rejected(forged),
|
||||
`HTTP=${forged.http}, code=${codeOf(forged)}`, OPTIONS)
|
||||
}
|
||||
context.assert(MODULE, '拖车和独立船期时间线只作参考', response,
|
||||
['TRUCK_APPOINTMENT', 'SAILING_SCHEDULE'].every((module) => timeline
|
||||
.filter((item) => item.module === module).every((item) => Boolean(item.action) && Boolean(item.occurredAt))),
|
||||
`referenceModules=${timeline.filter((item) => ['TRUCK_APPOINTMENT', 'SAILING_SCHEDULE'].includes(item.module)).length}`, OPTIONS)
|
||||
}
|
||||
|
||||
@@ -60,8 +60,7 @@ export async function run(context) {
|
||||
const detailResponse = await request(context, 'GET', `${BASE}/${encodeURIComponent(containerBookingId)}`)
|
||||
const detail = context.apiSuccess(MODULE, '查询出仓主聚合与整体时间线', detailResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '整体时间线返回稳定数组且聚合定位不漂移', detailResponse,
|
||||
Array.isArray(detail?.timeline) && id(detail?.aggregate?.inboundId) === inboundId
|
||||
&& id(detail?.aggregate?.containerBookingId) === containerBookingId
|
||||
Array.isArray(detail?.timeline) && id(detail?.aggregate?.containerBookingId) === containerBookingId
|
||||
&& detail?.aggregate?.inbounds?.some?.((item) => id(item?.inboundId) === inboundId),
|
||||
`timeline=${detail?.timeline?.length ?? 'n/a'}`, OPTIONS)
|
||||
|
||||
@@ -81,7 +80,6 @@ export async function run(context) {
|
||||
`truck=${aggregate?.truckAppointmentStatus || 'none'}, ocean=${aggregate?.oceanBookingStatus || 'none'}`, OPTIONS)
|
||||
|
||||
const expectedVersion = Number(aggregate.version)
|
||||
const actionBody = { inboundId, containerBookingId, expectedVersion, remark: context.marker }
|
||||
const confirmKey = `${context.marker}-outbound-confirm`.slice(0, 128)
|
||||
if (!gate?.passed) {
|
||||
const blocked = await request(context, 'POST', `${BASE}/${encodeURIComponent(containerBookingId)}/confirm`, { expectedVersion, remark: context.marker },
|
||||
@@ -105,9 +103,9 @@ export async function run(context) {
|
||||
const confirmed = context.apiSuccess(MODULE, '确认国内出仓进入等待装船', confirmResponse, '', OPTIONS)
|
||||
context.assert(MODULE, 'confirm-dispatch 推进 WAITING_FOR_LOADING 并递增版本', confirmResponse,
|
||||
confirmed?.aggregate?.dispatchStatus === 'WAITING_FOR_LOADING'
|
||||
&& confirmed?.aggregate?.inboundStatus === 'LOADED'
|
||||
&& confirmed?.aggregate?.inbounds?.every?.((item) => item?.inboundStatus === 'LOADED')
|
||||
&& Number(confirmed?.aggregate?.version) === expectedVersion + 1,
|
||||
`dispatch=${confirmed?.aggregate?.dispatchStatus || 'missing'}, inbound=${confirmed?.aggregate?.inboundStatus || 'missing'}, version=${confirmed?.aggregate?.version ?? 'missing'}`, OPTIONS)
|
||||
`dispatch=${confirmed?.aggregate?.dispatchStatus || 'missing'}, inbounds=${confirmed?.aggregate?.inbounds?.length ?? 'missing'}, version=${confirmed?.aggregate?.version ?? 'missing'}`, OPTIONS)
|
||||
if (!confirmed?.aggregate) return
|
||||
|
||||
const replayResponse = await request(context, 'POST', `${BASE}/${encodeURIComponent(containerBookingId)}/confirm`, confirmBody,
|
||||
@@ -145,22 +143,22 @@ export async function run(context) {
|
||||
const finalDetail = context.apiSuccess(MODULE, '负向与幂等验证后回查出仓聚合', finalDetailResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '出仓聚合保持 WAITING_FOR_LOADING、LOADED 且无成功撤销历史', finalDetailResponse,
|
||||
finalDetail?.aggregate?.dispatchStatus === 'WAITING_FOR_LOADING'
|
||||
&& finalDetail?.aggregate?.inboundStatus === 'LOADED'
|
||||
&& finalDetail?.aggregate?.inbounds?.every?.((item) => item?.inboundStatus === 'LOADED')
|
||||
&& Number(finalDetail?.aggregate?.version) === Number(confirmed?.aggregate?.version)
|
||||
&& finalDetail?.timeline?.filter?.((item) => item.module === 'OUTBOUND_DISPATCH'
|
||||
&& item.action === 'CONFIRM_DISPATCH').length === 1
|
||||
&& !finalDetail?.timeline?.some?.((item) => item.module === 'OUTBOUND_DISPATCH'
|
||||
&& item.action === 'REVERSE_DISPATCH'),
|
||||
`dispatch=${finalDetail?.aggregate?.dispatchStatus || 'missing'}, inbound=${finalDetail?.aggregate?.inboundStatus || 'missing'}, version=${finalDetail?.aggregate?.version ?? 'missing'}`, OPTIONS)
|
||||
`dispatch=${finalDetail?.aggregate?.dispatchStatus || 'missing'}, inbounds=${finalDetail?.aggregate?.inbounds?.length ?? 'missing'}, version=${finalDetail?.aggregate?.version ?? 'missing'}`, OPTIONS)
|
||||
if (confirmed?.aggregate) {
|
||||
context.createdResources.phase3OutboundDispatch = {
|
||||
marker: context.marker, inboundId, containerBookingId,
|
||||
orderId: id(inboundBinding?.orderId), tenantId: id(inboundBinding?.tenantId),
|
||||
confirmedStatus: confirmed.aggregate.dispatchStatus,
|
||||
confirmedInboundStatus: confirmed.aggregate.inboundStatus,
|
||||
confirmedInboundStatus: confirmed.aggregate.inbounds?.every?.((item) => item?.inboundStatus === 'LOADED') ? 'LOADED' : null,
|
||||
confirmedVersion: Number(confirmed.aggregate.version),
|
||||
finalStatus: confirmed.aggregate.dispatchStatus,
|
||||
finalInboundStatus: confirmed.aggregate.inboundStatus,
|
||||
finalInboundStatus: confirmed.aggregate.inbounds?.every?.((item) => item?.inboundStatus === 'LOADED') ? 'LOADED' : null,
|
||||
finalVersion: Number(confirmed.aggregate.version),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import test from 'node:test'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url))
|
||||
const read = (name) => readFileSync(resolve(here, name), 'utf8')
|
||||
|
||||
test('current-marker departure batch proves four documents photos customs and container-level confirm', () => {
|
||||
const customs = read('phase3-customs.mjs')
|
||||
const outbound = read('phase3-outbound-dispatch.mjs')
|
||||
for (const type of ['DESTINATION_CLEARANCE', 'CUSTOMS_DECLARATION', 'DELIVERY_ORDER', 'PICKUP_ORDER']) {
|
||||
assert.match(customs, new RegExp(type))
|
||||
}
|
||||
for (const evidence of ['首次上传', '回显', '下载', '更新', '完成']) assert.match(customs, new RegExp(evidence))
|
||||
assert.match(customs, /photos\.length >= 5/)
|
||||
assert.match(customs, /四类整柜资料必须完成后才允许进入出仓确认/)
|
||||
assert.match(outbound, /CUSTOMS_CLEARANCE/)
|
||||
assert.match(outbound, /拖车与独立船期仅作聚合参考且不参与出仓门禁/)
|
||||
assert.match(outbound, /\$\{BASE\}\/\$\{encodeURIComponent\(containerBookingId\)\}\/confirm/)
|
||||
assert.match(outbound, /confirm-dispatch 幂等重放/)
|
||||
assert.match(outbound, /相同 Idempotency-Key 不同请求拒绝/)
|
||||
assert.match(outbound, /实际DEPARTED_WAREHOUSE节点产生后撤销精确返回A0409/)
|
||||
assert.doesNotMatch(outbound, /\/confirm-dispatch|\/reverse-dispatch|\/check-gate\?/)
|
||||
})
|
||||
|
||||
test('outbound-created query supports automated or manual carrier then reaches PICKUP_READY PDA candidate', () => {
|
||||
const query = read('phase3-carrier-schedule-queries.mjs')
|
||||
const nodes = read('phase4-carrier-nodes.mjs')
|
||||
const pda = read('phase4-overseas-pda.mjs')
|
||||
assert.match(query, /出仓自动建任务兼容自动船司与未知船司人工处理/)
|
||||
assert.match(query, /manualCarrier && task\?\.status === 'MANUAL_REQUIRED'/)
|
||||
assert.match(nodes, /'DISCHARGED_FROM_VESSEL', 'PICKUP_READY'/)
|
||||
assert.match(nodes, /const pickupReadyIndex = STANDARD_NODES\.indexOf\('PICKUP_READY'\)/)
|
||||
assert.match(nodes, /createdResources\.phase4CarrierPickupReady/)
|
||||
assert.match(pda, /targetContainerNo[\s\S]*phase3Loading\?\.containerNo/)
|
||||
assert.match(pda, /id\(carrierQuery\?\.trackingNo\) === targetContainerNo/)
|
||||
assert.match(pda, /seen\.has\(targetContainerNo\)/)
|
||||
assert.match(pda, /targetCorrelated/)
|
||||
assert.match(pda, /deploymentBlocker: true/)
|
||||
assert.doesNotMatch(pda, /addCoverageGap\([^\n]*PICKUP_READY/)
|
||||
})
|
||||
@@ -12,9 +12,9 @@ export async function run(context) {
|
||||
'本轮未取得 phase3-outbound-dispatch 的 marker 隔离聚合,未使用历史资源', 'fixture')
|
||||
return
|
||||
}
|
||||
const selector = `inboundId=${encodeURIComponent(fixture.inboundId)}&containerBookingId=${encodeURIComponent(fixture.containerBookingId)}`
|
||||
const response = await context.request('GET', `${BASE}/detail?${selector}`)
|
||||
const detail = context.apiSuccess(MODULE, '回查 WAITING_FOR_LOADING 状态历史', response, selector, OPTIONS)
|
||||
const response = await context.request('GET', `${BASE}/${encodeURIComponent(fixture.containerBookingId)}`)
|
||||
const detail = context.apiSuccess(MODULE, '回查柜级 WAITING_FOR_LOADING 状态历史', response,
|
||||
`containerBookingId=${fixture.containerBookingId}`, OPTIONS)
|
||||
const history = Array.isArray(detail?.timeline)
|
||||
? detail.timeline.filter((item) => item.module === 'OUTBOUND_DISPATCH') : []
|
||||
const confirmations = history.filter((item) => item.action === 'CONFIRM_DISPATCH')
|
||||
@@ -27,7 +27,8 @@ export async function run(context) {
|
||||
&& Boolean(confirmed?.operatorId) && Boolean(confirmed?.operatorName) && Boolean(confirmed?.occurredAt),
|
||||
`confirmed=${confirmed?.fromStatus || 'missing'}->${confirmed?.toStatus || 'missing'}, version=${fixture.confirmedVersion ?? 'missing'}`, OPTIONS)
|
||||
context.assert(MODULE, '实际离仓节点冻结后保持一次确认且零次成功撤销', response,
|
||||
detail?.aggregate?.dispatchStatus === 'WAITING_FOR_LOADING' && detail?.aggregate?.inboundStatus === 'LOADED'
|
||||
detail?.aggregate?.dispatchStatus === 'WAITING_FOR_LOADING'
|
||||
&& detail?.aggregate?.inbounds?.every?.((item) => item?.inboundStatus === 'LOADED')
|
||||
&& confirmations.length === 1 && reversed.length === 0,
|
||||
`current=${detail?.aggregate?.dispatchStatus || 'missing'}, confirmations=${confirmations.length}, reversals=${reversed.length}`, OPTIONS)
|
||||
}
|
||||
|
||||
@@ -47,7 +47,9 @@ test('PDA module covers the container-number receiving contract and excludes out
|
||||
assert.match(source, /模式硬拒绝/)
|
||||
assert.match(source, /current返回404正常空态/)
|
||||
assert.match(source, /安全扫描返回A0400/)
|
||||
assert.match(source, /customerId: 0/)
|
||||
assert.match(source, /warehouse-admin\?tenantId=0&.*capabilityCode=RECEIVING&warehouseType=OVERSEAS&status=ACTIVE/)
|
||||
assert.match(source, /193000000000200003.*193000000000200004/)
|
||||
assert.doesNotMatch(source, /requestOverseas\('POST', '\/api\/platform\/v1\/warehouse-admin'/)
|
||||
assert.match(source, /overseasWarehouseId, password/)
|
||||
assert.match(source, /inbound\?\.overseasWarehouseId/)
|
||||
assert.doesNotMatch(source, /const warehouseId = id\(inbound\?\.destinationWarehouseId\)/)
|
||||
@@ -120,6 +122,10 @@ test('empty candidate response cannot become a zero-business-request pass', asyn
|
||||
if (method === 'GET' && requestPath.endsWith('/permissions')) return response([
|
||||
{ id: permissionId, permissionCode: 'lastleg:pda:operate' },
|
||||
], requestPath, method)
|
||||
if (method === 'GET' && requestPath.startsWith('/api/platform/v1/warehouse-admin?')) return response({
|
||||
records: [{ id: '193000000000200003', warehouseName: '洛杉矶仓', warehouseType: 'OVERSEAS', status: 'ACTIVE' }],
|
||||
page: 1, size: 200, total: 1, pages: 1,
|
||||
}, requestPath, method)
|
||||
if (method === 'POST' && requestPath.endsWith('/users')) return response({
|
||||
membershipId: String(9000 + userSequence), username: body.username, tenantId: '0', roleIds: [roleId],
|
||||
overseasWarehouseId: String(body.overseasWarehouseId), status: 'ACTIVE',
|
||||
@@ -130,7 +136,6 @@ test('empty candidate response cannot become a zero-business-request pass', asyn
|
||||
},
|
||||
async requestOverseas(method, requestPath, body, headers, token) {
|
||||
calls.push({ channel: 'overseas', method, requestPath })
|
||||
if (method === 'POST' && requestPath === '/api/platform/v1/warehouse-admin') return response('900', requestPath, method)
|
||||
if (requestPath.endsWith('/current')) return { method, path: requestPath, http: 200,
|
||||
body: { code: 'A0404', message: 'not found' }, requestId: 'req-current', traceId: 'trace-current' }
|
||||
return response([], requestPath, method)
|
||||
|
||||
@@ -559,9 +559,9 @@ export async function run(context) {
|
||||
const merchantToken = await loginMerchantForNegativeProbe(context)
|
||||
const pickerAccess = await resolvePickerAccess(context)
|
||||
if (!pickerAccess) return
|
||||
const sharedWarehouse = await createSharedWarehouse(context)
|
||||
const sharedWarehouse = await resolveSharedWarehouse(context)
|
||||
if (!sharedWarehouse) {
|
||||
blockerGap(context, '海外PDA共享实际收货仓', '无法通过平台正式API创建customerId=0共享物理仓')
|
||||
blockerGap(context, '海外PDA共享实际收货仓', '国内Warehouse主数据没有ACTIVE且启用RECEIVING的OVERSEAS固定作业仓')
|
||||
return
|
||||
}
|
||||
context.createdResources.phase4OverseasPda.overseasWarehouseId = sharedWarehouse.id
|
||||
@@ -623,21 +623,14 @@ export async function run(context) {
|
||||
}
|
||||
}
|
||||
|
||||
async function createSharedWarehouse(context) {
|
||||
const suffix = markerSuffix(context)
|
||||
const payload = {
|
||||
customerId: 0, warehouseCode: `PDA-${suffix}`.slice(0, 50),
|
||||
warehouseName: `PDA共享收货仓-${suffix}`.slice(0, 120), warehouseType: 'OVERSEAS',
|
||||
countryCode: 'US', timezone: 'America/Los_Angeles', contactName: 'PDA Acceptance',
|
||||
contactPhone: '+1-213-555-0198', contactEmail: `pda-${suffix.toLowerCase()}@example.com`,
|
||||
status: 'ACTIVE', remark: `${context.marker}-shared-receiving`,
|
||||
address: { addressType: 'WAREHOUSE', countryCode: 'US', stateProvince: 'CA', city: 'Los Angeles',
|
||||
district: 'Commerce', postalCode: '90040', addressLine1: '1 Acceptance Logistics Ave' },
|
||||
capabilities: [{ capabilityCode: 'RECEIVING', capabilityName: '收货', enabled: true }],
|
||||
}
|
||||
const created = await context.requestOverseas('POST', '/api/platform/v1/warehouse-admin', payload)
|
||||
const warehouseId = id(created?.body?.data)
|
||||
context.addStep(MODULE, '创建本轮PDA共享实际收货仓', created,
|
||||
success(created) && longId(warehouseId), `warehouseId=${warehouseId || 'missing'}; ${trace(created)}`, OPTIONS)
|
||||
return longId(warehouseId) ? { id: warehouseId, name: payload.warehouseName } : null
|
||||
async function resolveSharedWarehouse(context) {
|
||||
const path = '/api/platform/v1/warehouse-admin?tenantId=0&page=1&size=200&capabilityCode=RECEIVING&warehouseType=OVERSEAS&status=ACTIVE'
|
||||
const response = await context.request('GET', path)
|
||||
const rows = records(response?.body?.data)
|
||||
const fixedIds = new Set(['193000000000200003', '193000000000200004'])
|
||||
const warehouse = success(response) ? rows.find((item) => fixedIds.has(id(item?.id))) : null
|
||||
context.addStep(MODULE, '读取国内Warehouse权威海外PDA实际收货仓', response,
|
||||
Boolean(warehouse) && warehouse?.warehouseType === 'OVERSEAS' && warehouse?.status === 'ACTIVE',
|
||||
`warehouseId=${id(warehouse?.id) || 'missing'}, warehouseName=${warehouse?.warehouseName || warehouse?.name || 'missing'}; ${trace(response)}`, OPTIONS)
|
||||
return warehouse ? { id: id(warehouse.id), name: warehouse.warehouseName || warehouse.name } : null
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user