fix(file): allow customs broker receipt files

This commit is contained in:
2026-09-27 01:01:40 +08:00
parent 703b0f63ce
commit 9f025edc96
15 changed files with 51 additions and 12 deletions
+1
View File
@@ -168,6 +168,7 @@ export async function run(context) {
| 入仓自动头程费用、派生详情、人工发布与 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-customs.mjs`、`phase3-outbound-dispatch.mjs`、`phase3-container-documents.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-container-customs-fixture.mjs`、`phase3-container-customs.mjs`;定向入口通过正式订单、测量、同柜装载和最终完成 API 生成独立 `BUY_DECLARATION + TAX_REFUND` 混合柜,客户资料只覆盖退税票,买单行由下载中心 XLSX 验证;不复用历史柜、不执行 SQL | `container-customs-acceptance-contract.test.mjs`、`container-customs-acceptance-matrix.mjs` |
| 国内装柜验收作业员选择 | `domestic-pda-operator.mjs`;按本次 PDA 登录 Token 的字符串 subject 匹配目标仓库候选 userId,缺少匹配即停止,不使用首位候选或成员编号 | `domestic-pda-operator-contract.test.mjs` |
| 分柜与父子入库单数量守恒 | `phase3-order-splitting.mjs` | `phase3-order-splitting-contract.test.mjs` |
| 同柜且不可拆组 | `coload-groups.mjs`(可选;前置正式生成至少7票同线路 `WAIT_SPLIT`,另造不同线路反向票;覆盖创建/取消幂等、原子分配/移除、目标组隔离超容拒绝与零漂移、推送防绕过、PDA短拣完成拒绝且无余量子单、足量整组消费、普通单拆分不回归及已有父子单建组拒绝) | `coload-groups-contract.test.mjs` |
| 人工计划分柜准备与只读核验 | `manual-order-splitting-fixture.mjs`、`manual-order-splitting-verify.mjs`(可选,不进入默认全量) | `manual-order-splitting-contract.test.mjs` |
@@ -0,0 +1,16 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { selectSessionPdaOperator } from './domestic-pda-operator.mjs'
const subject = '2103886047128588290'
const token = `header.${Buffer.from(JSON.stringify({ sub: subject })).toString('base64url')}.signature`
test('task assignee matches authenticated user even when another operator is first', () => {
const current = { userId: subject, membershipId: '2103886047128588291' }
const operators = [{ userId: '194000000000000101' }, current]
assert.equal(selectSessionPdaOperator(operators, token), current)
assert.equal(selectSessionPdaOperator({ records: operators }, token), current)
})
test('missing warehouse candidate does not fall back to another operator or membership ID', () => {
assert.throws(() => selectSessionPdaOperator([{ userId: '2103886047128588291' }], token), /不在目标仓库候选/)
assert.throws(() => selectSessionPdaOperator([], ''), /有效登录身份/)
})
@@ -0,0 +1,14 @@
// Use the subject of the authenticated test session, never the first candidate
// or IAM membership ID. Server-side warehouse and assignment checks remain authoritative.
export function selectSessionPdaOperator(operators, token) {
let subject
try { subject = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString('utf8')).sub }
catch { throw new Error('国内 PDA 验收缺少有效登录身份') }
if (typeof subject !== 'string' || !/^[1-9]\d*$/.test(subject)) {
throw new Error('国内 PDA 登录身份必须包含字符串用户编号')
}
const rows = Array.isArray(operators) ? operators : operators?.records || []
const operator = rows.find((item) => String(item.userId) === subject)
if (!operator) throw new Error('当前登录 PDA 作业员不在目标仓库候选中,停止分配任务')
return operator
}
@@ -30,7 +30,7 @@ test('retains all clickable container, loading-list and PDA workflow states thro
assert.doesNotMatch(source, /push-preload/)
assert.match(source, /计划确认前不生成 PDA 任务/)
assert.match(source, /summary\?\.readiness\?\.canConfirm === true/)
assert.match(source, /Object\.freeze\(\{ expectedVersion: confirmableVersion \}\)/)
assert.match(source, /Object\.freeze\(\{ expectedVersion: confirmableVersion, assigneeId: id\(pdaOperator.userId\) \}\)/)
assert.match(source, /confirmationStatus === 'CONFIRMED'/)
assert.match(source, /readiness\?\.readyForPda === true/)
assert.match(source, /id\(task\?\.id\) !== confirmedTaskId/)
@@ -1,3 +1,4 @@
import { selectSessionPdaOperator } from './domestic-pda-operator.mjs'
const MODULE = 'manual-state-loading-pda'
const OPTIONS = { scenario: 'manual-loading-pda-state-matrix', deploymentBlocker: true }
const CONTAINERS = '/api/platform/v1/containers'
@@ -216,7 +217,7 @@ async function createPlanTask(context, label) {
const pdaOperatorsResponse = await context.request('GET',
`/api/platform/v1/iam/users/domestic-pda-operators?warehouseId=${encodeURIComponent(source.warehouseId)}`)
const pdaOperators = context.apiSuccess(MODULE, `${label} 查询国内头程 PDA 作业员候选`, pdaOperatorsResponse, '', OPTIONS)
const pdaOperator = (Array.isArray(pdaOperators) ? pdaOperators : pdaOperators?.records || [])[0]
const pdaOperator = selectSessionPdaOperator(pdaOperators, context.pdaToken)
context.assert(MODULE, `${label} 存在可用 PDA 作业员`, pdaOperatorsResponse,
Boolean(pdaOperator?.userId), `operatorId=${id(pdaOperator?.userId) || 'missing'}`, OPTIONS)
if (!pdaOperator?.userId) throw new Error(`${label} 缺少可用 PDA 作业员`)
@@ -83,7 +83,7 @@ function routes(context, merchantToken) {
undefined, customer.customerToken || merchantToken, { 'Idempotency-Key': idempotencyKey }),
merchantConfirmReceipt: (ctx, fixture, customer, request, idempotencyKey) => ctx.request('POST',
`${MERCHANT}/documents/${encodeURIComponent(id(request.documentId))}/receipt/${request.confirm === false ? 'reject' : 'confirm'}`,
{ expectedVersion: Number(request.expectedVersion), remark: request.remark || '' },
{ expectedVersion: Number(request.expectedVersion), confirm: request.confirm !== false, remark: request.remark || '' },
customer.customerToken || merchantToken, { 'Idempotency-Key': idempotencyKey }),
platformStart: (ctx, fixture, request, idempotencyKey) => ctx.request('POST',
`${PLATFORM}/${encodeURIComponent(id(fixture.loadingPlanId))}/start`, request, ctx.token,
@@ -1,3 +1,4 @@
import { selectSessionPdaOperator } from './domestic-pda-operator.mjs'
import { publishPlatformLogisticsChannels, resolveOceanBookingAgent } from './ocean-booking-fixture.mjs'
import { diffPushPreloadStability, projectPushPreloadStability } from './push-preload-stability.mjs'
import { containerFieldGaps, loadingListFieldGaps, preloadFieldGaps } from './fulfillment-list-field-contract.mjs'
@@ -563,7 +564,7 @@ export async function run(context) {
const pdaOperatorsResponse = await context.request('GET',
`/api/platform/v1/iam/users/domestic-pda-operators?warehouseId=${encodeURIComponent(warehouseId)}`)
const pdaOperators = context.apiSuccess(MODULE, '查询当前国内头程 PDA 作业员候选', pdaOperatorsResponse, '', OPTIONS)
const pdaOperator = (Array.isArray(pdaOperators) ? pdaOperators : records(pdaOperators))[0]
const pdaOperator = selectSessionPdaOperator(pdaOperators, context.pdaToken)
context.assert(MODULE, '存在可用于计划确认的国内头程 PDA 作业员', pdaOperatorsResponse,
Boolean(pdaOperator?.userId), `operatorId=${id(pdaOperator?.userId) || 'missing'}`, OPTIONS)
if (!pdaOperator?.userId) return
@@ -1,3 +1,4 @@
import { selectSessionPdaOperator } from './domestic-pda-operator.mjs'
import { diffPushPreloadStability, projectPushPreloadStability } from './push-preload-stability.mjs'
import { isPureStatusNoop, visibleLoadingHistories } from './loading-history-noop.mjs'
import { ensureMatsonCarrierPartner } from './carrier-partner-fixture.mjs'
@@ -672,7 +673,7 @@ export async function run(context) {
const pdaOperatorsResponse = await context.request('GET',
`/api/platform/v1/iam/users/domestic-pda-operators?warehouseId=${encodeURIComponent(warehouseId)}`)
const pdaOperators = context.apiSuccess(MODULE, '查询当前国内头程 PDA 作业员候选', pdaOperatorsResponse, '', OPTIONS)
const pdaOperator = (Array.isArray(pdaOperators) ? pdaOperators : pdaOperators?.records || [])[0]
const pdaOperator = selectSessionPdaOperator(pdaOperators, context.pdaToken)
context.assert(MODULE, '存在可用于计划确认的国内头程 PDA 作业员', pdaOperatorsResponse,
Boolean(pdaOperator?.userId), `operatorId=${id(pdaOperator?.userId) || 'missing'}`, OPTIONS)
if (!pdaOperator?.userId) return
@@ -52,7 +52,7 @@ test('PDA opens only after linking a released requirement and uses stable plan i
assert.match(source, /if \(!transportStable\) return null/)
assert.match(source, /计划确认前不生成 PDA 任务/)
assert.match(source, /summary\?\.readiness\?\.canConfirm === true/)
assert.match(source, /Object\.freeze\(\{ expectedVersion: confirmableVersion \}\)/)
assert.match(source, /Object\.freeze\(\{ expectedVersion: confirmableVersion, assigneeId: id\(pdaOperator.userId\) \}\)/)
assert.match(source, /confirmationStatus === 'CONFIRMED'/)
assert.match(source, /readiness\?\.readyForPda === true/)
assert.match(source, /taskId === confirmedTaskId/)
@@ -1,3 +1,4 @@
import { selectSessionPdaOperator } from './domestic-pda-operator.mjs'
const MODULE = 'phase3-order-splitting'
const SCENARIO = 'source-loaded-single-remainder-chain'
const OPTIONS = { scenario: SCENARIO, deploymentBlocker: true }
@@ -339,7 +340,7 @@ async function loadOneFragment(context, state) {
const pdaOperatorsResponse = await context.request('GET',
`/api/platform/v1/iam/users/domestic-pda-operators?warehouseId=${encodeURIComponent(warehouseId)}`)
const pdaOperators = context.apiSuccess(MODULE, `查询 ${sourceOrderNo} 国内头程 PDA 作业员候选`, pdaOperatorsResponse, '', OPTIONS)
const pdaOperator = (Array.isArray(pdaOperators) ? pdaOperators : pdaOperators?.records || [])[0]
const pdaOperator = selectSessionPdaOperator(pdaOperators, context.pdaToken)
context.assert(MODULE, `${sourceOrderNo} 存在可用 PDA 作业员`, pdaOperatorsResponse,
Boolean(pdaOperator?.userId), `operatorId=${id(pdaOperator?.userId) || 'missing'}`, OPTIONS)
if (!pdaOperator?.userId) return null