This commit is contained in:
2026-08-26 15:24:48 +08:00
parent 3f9ee28d22
commit 4ae236fe11
18 changed files with 198 additions and 92 deletions
+2 -1
View File
@@ -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
}