fix: restore phase five development acceptance chain

This commit is contained in:
2026-09-01 21:55:27 +08:00
parent 04e0111756
commit ad5e0812bf
35 changed files with 623 additions and 313 deletions
@@ -685,6 +685,33 @@ CREATE TABLE IF NOT EXISTS ord_order_party (
CONSTRAINT fk_party_order FOREIGN KEY (order_id) REFERENCES ord_order (id)
) ENGINE=InnoDB COMMENT='订单收发货方快照表:固化下单时发货人和收货人信息,避免主数据变化影响历史订单';
CREATE TABLE IF NOT EXISTS ord_customer_address (
id BIGINT UNSIGNED NOT NULL COMMENT '客户地址ID',
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
address_type VARCHAR(32) NOT NULL COMMENT 'PICKUP取货地址或PRIVATE_DESTINATION私人目的仓',
address_name VARCHAR(64) NOT NULL COMMENT '客户自定义地址名称',
company_name VARCHAR(128) NOT NULL COMMENT '公司名称',
contact_name VARCHAR(64) NOT NULL COMMENT '联系人',
phone VARCHAR(32) NOT NULL COMMENT '联系电话',
email VARCHAR(128) NULL COMMENT '联系邮箱',
country_code CHAR(2) NOT NULL COMMENT '国家二字码',
province VARCHAR(64) NULL COMMENT '省州',
city VARCHAR(64) NOT NULL COMMENT '城市',
district VARCHAR(64) NULL COMMENT '区县',
address_line VARCHAR(255) NOT NULL COMMENT '详细地址',
postal_code VARCHAR(20) NULL COMMENT '邮政编码',
default_address TINYINT(1) NOT NULL DEFAULT 0 COMMENT '同类型默认地址',
active TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否可选',
created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改人ID',
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记',
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
PRIMARY KEY (id),
KEY idx_customer_address_scope (tenant_id, address_type, active, updated_at)
) ENGINE=InnoDB COMMENT='客户自有下单地址簿;不承载平台仓库主档';
CREATE TABLE IF NOT EXISTS ord_order_cargo (
id BIGINT UNSIGNED NOT NULL COMMENT '货物明细ID',
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
@@ -721,6 +748,7 @@ CREATE TABLE IF NOT EXISTS ord_order_cargo (
shipment_id VARCHAR(64) NULL COMMENT '本货物明细对应的Amazon Shipment ID',
reference_id VARCHAR(64) NULL COMMENT '本货物明细对应的Amazon Reference ID',
product_image_file_id BIGINT UNSIGNED NULL COMMENT 'File服务中的产品图片逻辑文件ID',
wooden_product TINYINT(1) NOT NULL DEFAULT 0 COMMENT '木制品权威判定快照;1时订单必须商检',
created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改人ID',
@@ -1733,6 +1761,7 @@ CALL phase1_add_column('ord_order_cargo', 'product_height_cm', "DECIMAL(12,2) NU
CALL phase1_add_column('ord_order_cargo', 'shipment_id', "VARCHAR(64) NULL COMMENT 'Cargo Amazon Shipment ID'");
CALL phase1_add_column('ord_order_cargo', 'reference_id', "VARCHAR(64) NULL COMMENT 'Cargo Amazon Reference ID'");
CALL phase1_add_column('ord_order_cargo', 'product_image_file_id', "BIGINT UNSIGNED NULL COMMENT 'Product image logical file ID'");
CALL phase1_add_column('ord_order_cargo', 'wooden_product', "TINYINT(1) NOT NULL DEFAULT 0 COMMENT '木制品权威判定快照;1时订单必须商检'");
UPDATE ord_order_cargo cargo
JOIN ord_order orders ON orders.id = cargo.order_id AND orders.tenant_id = cargo.tenant_id
@@ -4275,6 +4304,7 @@ DELIMITER ;
CALL unified_assert_table('baiyue_auth', 'auth_account');
CALL unified_assert_table('baiyue_iam', 'iam_permission');
CALL unified_assert_table('baiyue_order', 'ord_order');
CALL unified_assert_table('baiyue_order', 'ord_customer_address');
CALL unified_assert_table('baiyue_warehouse', 'wh_warehouse');
CALL unified_assert_table('baiyue_warehouse', 'wh_pallet');
CALL unified_assert_table('baiyue_warehouse', 'wh_location');
@@ -4290,6 +4320,7 @@ CALL unified_assert_column('baiyue_order', 'ord_order', 'carrier_name');
CALL unified_assert_column('baiyue_order', 'ord_order', 'pricing_tax_mode');
CALL unified_assert_column('baiyue_order', 'ord_order', 'inbound_code');
CALL unified_assert_column('baiyue_order', 'ord_order', 'fulfillment_type');
CALL unified_assert_column('baiyue_order', 'ord_order_cargo', 'wooden_product');
CALL unified_assert_column('baiyue_firstleg', 'fl_inbound_pallet_allocation', 'warehouse_resource_tenant_id');
CALL unified_assert_column('baiyue_firstleg', 'fl_inbound_order', 'pricing_tax_mode');
CALL unified_assert_column('baiyue_firstleg', 'fl_inbound_order', 'fulfillment_type');
@@ -12383,6 +12414,7 @@ CALL queue_table_comment('baiyue_iam', 'iam_user', 'IAM用户资料表:连接
CALL queue_table_comment('baiyue_iam', 'sys_audit_outbox', '可靠审计事件本地发送箱');
CALL queue_table_comment('baiyue_order', 'ord_idempotency_record', '订单写接口持久化幂等记录');
CALL queue_table_comment('baiyue_order', 'ord_order', '头程订单主表:保存商户委托、目的仓、运输方式和订单当前状态');
CALL queue_table_comment('baiyue_order', 'ord_customer_address', '客户自有下单地址簿;不承载平台仓库主档');
CALL queue_table_comment('baiyue_order', 'ord_order_cargo', '订单货物明细表:记录件数、重量、体积、申报价值和危险品属性');
CALL queue_table_comment('baiyue_order', 'ord_order_fulfillment_split', '父订单→子订单→柜次数量台账');
CALL queue_table_comment('baiyue_order', 'ord_order_internal_note', '订单内部备注表:保存平台运营或履约人员的内部协作记录,不向商户客户展示');
@@ -84,7 +84,7 @@ public interface LoadingCompletedEventMapper {
@Select("""
SELECT carton.tenant_id AS account_id,carton.inbound_id AS source_inbound_id,
pallet.id AS pallet_id,1 AS actual_binding,
pallet.pallet_code,NULL AS allocation_no,
pallet.pallet_no AS pallet_code,NULL AS allocation_no,
COUNT(*) OVER (PARTITION BY pallet.id,carton.tenant_id,carton.inbound_id) AS allocated_package_count,
carton.inbound_detail_id AS source_inbound_detail_id,carton.sku_code,
item.product_name_zh,item.package_count,item.product_quantity,carton.box_code
@@ -183,7 +183,7 @@ class Phase3FinalUnifiedSqlContractTest {
.contains("存在缺失中文注释的业务表")
.contains("存在缺失中文注释的业务字段")
.doesNotContain("CREATE PROCEDURE upgrade_column_comment");
assertThat(count(sql, "CALL queue_table_comment(")).isEqualTo(144);
assertThat(count(sql, "CALL queue_table_comment(")).isEqualTo(145);
assertThat(count(sql, "CALL queue_column_comment(")).isGreaterThanOrEqualTo(2374);
assertThat(sql)
.contains("CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'supplier_id'")
@@ -239,6 +239,9 @@ class LoadingCompletedOutboxServiceTest {
.getAnnotation(Select.class).value()).replaceAll("\\s+", " ");
String palletsSql = String.join(" ", LoadingCompletedEventMapper.class.getMethod("palletRows", long.class)
.getAnnotation(Select.class).value()).replaceAll("\\s+", " ");
String actualPalletsSql = String.join(" ", LoadingCompletedEventMapper.class
.getMethod("actualPalletRows", long.class).getAnnotation(Select.class).value())
.replaceAll("\\s+", " ");
assertThat(rowsSql).contains("p.plan_status = 'COMPLETED'")
.contains("o.process_status = 'LOADED'")
@@ -265,6 +268,9 @@ class LoadingCompletedOutboxServiceTest {
.contains("label.inbound_detail_id = i.inbound_detail_id")
.contains("label.batch_status = 'ACTIVE'")
.doesNotContain("label.inbound_id = i.inbound_id");
assertThat(actualPalletsSql)
.contains("pallet.pallet_no AS pallet_code")
.doesNotContain("pallet.pallet_code");
}
@Test void publishesDropshipPalletSkuAndCartonSnapshots() throws Exception {
+6 -6
View File
@@ -255,12 +255,12 @@ CREATE TABLE IF NOT EXISTS ord_customer_address (
postal_code VARCHAR(20) NULL COMMENT '邮政编码',
default_address TINYINT(1) NOT NULL DEFAULT 0 COMMENT '同类型默认地址',
active TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否可选',
created_by BIGINT UNSIGNED NOT NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_by BIGINT UNSIGNED NOT NULL,
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
deleted TINYINT(1) NOT NULL DEFAULT 0,
version INT NOT NULL DEFAULT 0,
created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改人ID',
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记',
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
PRIMARY KEY (id),
KEY idx_customer_address_scope (tenant_id, address_type, active, updated_at)
) ENGINE=InnoDB COMMENT='客户自有下单地址簿;不承载平台仓库主档';
+3 -1
View File
@@ -87,6 +87,8 @@ Client 全量、Client 二期、待审核订单和批量待分柜均复用这套
| OpenAPI | `openapi-validation/validate-openapi.mjs` | 发现并解析各服务 OpenAPI,检查跨文件引用;只读源码 |
| 本地环境 | `local-env.ps1` | 本地开发环境公共参数/辅助函数,由其他 PowerShell 脚本加载 |
| 本地环境 | `publish-local-nacos-config.ps1` | 将 `config/nacos/local/*.yml` 发布到指定本地 Nacos,会修改 Nacos 配置 |
| Nacos 环境选择 | `use-nacos-environment.ps1` | 按显式环境与区域设置当前 PowerShell 进程的 Nacos、Spring Profile 和运行区域变量;开发/测试密码必须由参数或环境变量提供,不发布配置 |
| Nacos 配置发布 | `publish-nacos-config.ps1` | 将 `config/nacos/{local|cn|overseas}/*.yml` 发布到显式指定的 Nacos;非本地环境强制要求非 public namespace,会修改目标 Nacos 配置,执行前须取得对应环境授权 |
| 密钥配置 | `generate-overseas-jwk.mjs` | 生成 3072 位海外 JWT RSA JWK 并写入指定 `deploy/env/.env.*`;已有 JWK 时拒绝覆盖,密钥文件不得提交 |
| 迁移辅助 | `migrate-local-qualification-file.ps1` | 本地资质文件迁移辅助,会修改本地文件/数据;运行前核对脚本参数和备份策略 |
| Jenkins 管理 | `provision-development-service-jobs.groovy` | Jenkins Script Console 批量准备开发服务 Job;默认 `applyChanges=false` 仅预览,改为 true 会创建 Job |
@@ -99,7 +101,7 @@ Client 全量、Client 二期、待审核订单和批量待分柜均复用这套
| 验收目标 | 唯一入口 | 数据副作用 | 覆盖范围 |
|---|---|---|---|
| 平台后台全模块 HTTP 验收 | `scripts/admin-test-env-acceptance.mjs` | 会创建、更新或最终停用测试数据 | `--modules` 默认列表中的平台客户、仓储、订单、入库、分柜、装柜、船期、拖车、报关、财务、通知、统计等模块 |
| 平台后台全模块 HTTP 验收 | `scripts/admin-test-env-acceptance.mjs` | 会创建、更新或最终停用测试数据 | `--modules` 默认列表中的平台客户、仓储、订单、入库、分柜、装柜、船期、拖车、报关、海外排车/出库/派送/异常/回访、财务、通知、统计等模块 |
| 客户工作台完整 HTTP 链路验收 | `scripts/client-test-env-acceptance.mjs` | 会创建测试企业、订单、入库、权限和站内消息数据 | 注册/审核、客户隔离、订单、入库计费、文件、财务、客户侧权限,以及我的消息未读/已读/全部已读链路 |
| 固定客户批量创建待审核订单 | `scripts/create-pending-review-orders.mjs` | 只使用 `MERCHANT_TEST_USERNAME/PASSWORD` 配置的客户账号创建指定数量订单 | 使用 `ADMIN_TEST_GATEWAY`,选择真实可计价仓库/渠道,创建并提交订单,停在待审核,不创建客户、不审核订单 |
| Client 二期专项 HTTP 验收 | `scripts/client-phase2-test-env-acceptance.mjs` | 会写入测试环境业务数据 | 二期客户入库、测量、费用和权限专项场景;不是替代 Client 全量入口 |
+6 -4
View File
@@ -5,7 +5,7 @@ import { dirname, resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
import process from 'node:process'
const CONFIGURED_MODULES = ['merchant-qualification-fixture', 'customers', 'overseas-storage-fees', 'system', 'notifications', 'master-data', 'suppliers', 'domestic-container', 'orders', 'fee-items', 'pricing', 'downloads', 'inbound-weight-ocr', 'inbound-measurement', 'inbound-fees', 'phase3-container-loading', 'phase3-container-overview', 'phase3-loading', 'phase3-loading-model-bridge', 'phase3-pda-convergence', 'phase3-evidence-gate', 'supplemental', 'supplemental-boundaries', 'ocean-bookings', 'truck-appointments', 'service-center-flow', 'phase3-loading-finalize', 'phase3-container-documents', 'phase3-customs', 'phase3-container-customs', 'phase3-port-schedules', 'phase3-carrier-schedule-queries', 'phase3-outbound-dispatch', 'phase3-outbound-aggregate', 'phase3-dispatch-gate', 'phase3-waiting-for-loading', 'phase3-warehouse-rbac', 'phase3-order-splitting', 'manual-state-matrix', 'phase4-overseas-warehouse', 'phase4-carrier-pickup-ready', 'phase4-overseas-pda', 'phase4-overseas-pda-operations', 'phase4-overseas-inbound-operations', 'phase4-overseas-client-services', 'phase4-overseas-dispatch', 'phase4-cross-region-events', 'phase4-overseas-business', 'phase4-overseas-master-data', 'phase4-carrier-nodes', 'finance', 'finance-full-fixtures', 'finance-reconciliation-full', 'finance-settlement-full', 'finance-downloads', 'customer-operating-statistics']
const CONFIGURED_MODULES = ['merchant-qualification-fixture', 'customers', 'overseas-storage-fees', 'system', 'notifications', 'master-data', 'suppliers', 'domestic-container', 'orders', 'fee-items', 'pricing', 'downloads', 'inbound-weight-ocr', 'inbound-measurement', 'inbound-fees', 'phase3-container-loading', 'phase3-container-overview', 'phase3-loading', 'phase3-loading-model-bridge', 'phase3-pda-convergence', 'phase3-evidence-gate', 'supplemental', 'supplemental-boundaries', 'ocean-bookings', 'truck-appointments', 'service-center-flow', 'phase3-loading-finalize', 'phase3-container-documents', 'phase3-customs', 'phase3-container-customs', 'phase3-port-schedules', 'phase3-carrier-schedule-queries', 'phase3-outbound-dispatch', 'phase3-outbound-aggregate', 'phase3-dispatch-gate', 'phase3-waiting-for-loading', 'phase3-warehouse-rbac', 'phase3-order-splitting', 'manual-state-matrix', 'phase4-overseas-warehouse', 'phase4-carrier-pickup-ready', 'phase4-overseas-pda', 'phase4-overseas-pda-operations', 'phase4-overseas-inbound-operations', 'phase4-overseas-client-services', 'phase4-overseas-dispatch', 'phase5-overseas-fulfillment', 'phase4-cross-region-events', 'phase4-overseas-business', 'phase4-overseas-master-data', 'phase4-carrier-nodes', 'finance', 'finance-full-fixtures', 'finance-reconciliation-full', 'finance-settlement-full', 'finance-downloads', 'customer-operating-statistics']
const OPTIONAL_MODULES = ['manual-order-splitting-fixture', 'manual-order-splitting-verify',
'manual-preload-push-fixture', 'manual-preload-push-verify',
'manual-pda-ready-fixture', 'manual-pda-ready-verify',
@@ -75,6 +75,7 @@ const MODULE_DEPENDENCIES = new Map([
['phase4-overseas-inbound-operations', ['phase4-overseas-pda-operations']],
['phase4-overseas-client-services', ['phase4-overseas-pda-operations']],
['phase4-overseas-dispatch', ['phase4-overseas-pda-operations']],
['phase5-overseas-fulfillment', ['phase4-overseas-dispatch']],
['phase4-cross-region-events', ['system', 'phase3-customs', 'phase4-overseas-pda-operations']],
['phase4-carrier-nodes', ['phase4-overseas-pda-operations']],
])
@@ -1565,7 +1566,7 @@ export class AcceptanceRun {
|| this.options.orderProfileMode === 'randomized' || this.options.routeMode === 'random') {
this.applyWaitSplitVariant(this.options.waitSplitSequence || 1)
}
const loginResponse = await this.request('POST', '/api/auth/login', { username: this.options.username, password: this.options.password }, '')
const loginResponse = await this.request('POST', '/api/auth/login', { loginId: this.options.username, password: this.options.password }, '')
const login = this.apiSuccess('authentication', '平台管理员登录', loginResponse)
if (!login?.accessToken) this.fatal = '平台管理员登录失败,未执行业务模块'
else {
@@ -1574,7 +1575,8 @@ export class AcceptanceRun {
this.overseasToken = login.overseasAccessToken || ''
this.overseasExpiresAt = Number(login.overseasExpiresIn) > 0
? Date.now() + Number(login.overseasExpiresIn) * 1000 : 0
const requiresOverseas = this.options.modules.some((module) => module.startsWith('phase4-overseas-'))
const requiresOverseas = this.options.modules.some((module) =>
module.startsWith('phase4-overseas-') || module.startsWith('phase5-overseas-'))
if (requiresOverseas) {
this.assert('authentication', '登录响应包含有效海外访问令牌', loginResponse,
Boolean(this.overseasToken) && Number(login.overseasExpiresIn) > 0,
@@ -1584,7 +1586,7 @@ export class AcceptanceRun {
const handlers = { customers: () => this.runCustomers() }
for (let moduleIndex = 0; moduleIndex < this.options.modules.length; moduleIndex += 1) {
const module = this.options.modules[moduleIndex]
if (module.startsWith('phase4-overseas-')
if ((module.startsWith('phase4-overseas-') || module.startsWith('phase5-overseas-'))
&& this.overseasExpiresAt > 0 && Date.now() + 120_000 >= this.overseasExpiresAt) {
await this.refreshSession(`海外模块 ${module} 执行前Token临近到期`)
}
+7
View File
@@ -84,6 +84,7 @@ export async function run(context) {
| 四期海外业务 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` |
| 五期海外排车、出库、派送、异常与回访 HTTP 生命周期 | `phase5-overseas-fulfillment.mjs`(复用本轮四期已收货托盘和预排单,正式创建临时 PDA 操作员并在 `finally` 停用;覆盖排车编辑、转出库/派送、PDA 全量拣货/发车、派送完成、客户回访、人工异常及派送退回) | `phase5-overseas-fulfillment-contract.test.mjs` |
| 初始化基线(仅离线) | — | `initialization-baseline-contract.test.mjs` |
表中的 `*-fixture.mjs` 是被业务模块复用的测试数据前置,不是新增的独立全量入口。执行 HTTP 时仍从 `../admin-test-env-acceptance.mjs` 进入;`*contract.test.mjs` 只做离线脚本结构和数据归属回归。
@@ -102,6 +103,12 @@ node scripts/admin-test-env-acceptance.mjs --gateway <development-gateway> --ove
只有已登记在 `../admin-test-env-acceptance.mjs` 的 `CONFIGURED_MODULES` 中的名称才能用于 `--modules`;目录中存在同名 `.mjs` 不代表总入口已注册。新增或改名 HTTP 模块时必须同步总入口、依赖展开、离线入口合同和本表;尚未注册的实验脚本不得写成可执行验收入口。
五期履约链定向入口为 `--modules phase5-overseas-fulfillment`。依赖展开固定从本轮三期装柜完成、四期跨区接柜和海外 PDA 实收入库生成真实库存,再由四期智能预排创建 marker 专属 `PENDING_DEPARTURE` 排车。五期模块只消费该本轮排车,先补齐车辆与司机,原子转为 `WAIT_OUTBOUND + WAIT_DELIVERY`,随后使用绑定同一海外仓的临时 PDA 身份执行全量拣货和发车,使出库单进入 `OUTBOUNDED`;最后覆盖派送完成、回访、人工异常与退回海外仓。所有幂等重放都核对业务单号或数量不漂移,真实前置不足必须形成 deployment blocker,临时 PDA 身份无论成功失败都由正式 IAM 状态接口停用。
```powershell
node --env-file="$env:USERPROFILE\.codex\chongchong-global-dev-acceptance.env" scripts/admin-test-env-acceptance.mjs --modules phase5-overseas-fulfillment
```
## WAIT_SPLIT 到 LOADED 冻结链
默认 Admin 全量入口已注册并按同一 `context.marker` 串行执行货柜主链。冻结时序为:
@@ -145,7 +145,7 @@ async function verifyPermissionDenial(context, marker, warehouseId, containerNo)
const membershipId = id(user?.membershipId)
if (!membershipId) throw new Error('无法创建权限拒绝验收用户')
try {
const loginResponse = await context.request('POST', '/api/auth/login', { username, password }, '')
const loginResponse = await context.request('POST', '/api/auth/login', { loginId: username, password }, '')
const token = context.apiSuccess(MODULE, '无整柜权限用户登录', loginResponse)?.accessToken
if (!token) throw new Error('权限拒绝验收用户登录失败')
const denied = await context.request('GET', `${BASE}/inventories?page=1&size=20&warehouseId=${warehouseId}&containerNo=${containerNo}`, undefined, token)
+1 -1
View File
@@ -146,7 +146,7 @@ export async function run(context) {
`total=${activePage?.total ?? 'n/a'}`, OPTIONS)
const merchantLoginResponse = await context.request('POST', '/api/auth/login', {
username: MERCHANT_USERNAME, password: MERCHANT_PASSWORD,
loginId: MERCHANT_USERNAME, password: MERCHANT_PASSWORD,
}, '')
const merchantLogin = success(merchantLoginResponse) ? merchantLoginResponse.body?.data : null
const merchantToken = merchantLogin?.accessToken
@@ -173,7 +173,7 @@ async function noPermission(context) {
context.addCoverageGap(MODULE, 'finance-download-center', '无导出权限拒绝', '本轮商户验收账号前置未生成', 'prerequisite')
return
}
const login = await context.request('POST', '/api/auth/login', { username, password }, '')
const login = await context.request('POST', '/api/auth/login', { loginId: username, password }, '')
const token = login.http === 200 && String(login.body?.code) === '200' ? login.body.data?.accessToken : ''
if (!token) {
context.addStep(MODULE, '无权限账号登录', login, false, '商户 Token 未生成', OPTIONS); return
@@ -130,7 +130,7 @@ async function confirmIssuedStatementAsClient(context, fixture, statement) {
`username=${username ? 'present' : 'missing'}, password=${password ? 'present' : 'missing'}, statementId=${targetId || 'missing'}`, OPTIONS)
return null
}
const loginResponse = await context.request('POST', '/api/auth/login', { username, password }, '')
const loginResponse = await context.request('POST', '/api/auth/login', { loginId: username, password }, '')
const login = context.apiSuccess(MODULE, 'Client 客户登录用于确认对账单', loginResponse,
`username=${username}`, OPTIONS)
const clientToken = login?.accessToken
@@ -42,7 +42,7 @@ export async function run(context) {
const master = await resolveOceanBookingMaster(context, MODULE)
if (!master) return
const suffix = context.marker.replace(/\D/g, '').slice(-10) || 'BOOKING'
const agentResponse = await request(context, 'GET', '/api/platform/v1/suppliers?tenantId=0&category=BOOKING&status=ACTIVE&page=1&size=20')
const agentResponse = await request(context, 'GET', '/api/platform/v1/suppliers?tenantId=0&category=BOOKING&status=ACTIVE&carrierCode=MATSON&page=1&size=20')
const agentPage = payload(agentResponse)
const agent = agentPage?.records?.find?.((item) => item?.category === 'BOOKING' && item?.status === 'ACTIVE')
step(context, '使用 ACTIVE BOOKING 订舱代理事实源', agentResponse, Boolean(agent?.id && agent?.name),
@@ -225,7 +225,7 @@ export async function run(context, matrix = context.createdResources.manualState
}
matrix.pages ||= {}
const loginResponse = await context.request('POST', '/api/auth/login',
{ username: MERCHANT_USERNAME, password: MERCHANT_PASSWORD }, '')
{ loginId: MERCHANT_USERNAME, password: MERCHANT_PASSWORD }, '')
const token = context.apiSuccess(MODULE, 'IT 测试客户管理员登录', loginResponse)?.accessToken
if (!token) throw new Error('客户管理员登录未返回 accessToken')
const fixture = await loadFixtureInputs(context, token)
+38 -4
View File
@@ -339,7 +339,34 @@ async function loadMasterData(context, merchantToken) {
pricingTaxMode: priced.pricingTaxMode, destinationType, partner }
}
function buildPayload(context, profile, master, productImageFileId) {
async function ensureCustomerAddress(context, merchantToken, profile, addressType) {
const path = `/api/merchant/v1/orders/first-leg/addresses?addressType=${encodeURIComponent(addressType)}`
const listResponse = await request(context, 'GET', path, undefined, merchantToken)
const existing = context.apiSuccess(MODULE, `Client 查询${addressType}地址簿`, listResponse)
const existingId = id(Array.isArray(existing) ? existing[0]?.id : null)
if (existingId) return existingId
const isPickup = addressType === 'PICKUP'
const createResponse = await request(context, 'POST', '/api/merchant/v1/orders/first-leg/addresses', {
addressType,
addressName: isPickup ? '开发联调取货地址' : '开发联调私人目的仓',
companyName: profile.tenantName || `订单验收客户-${context.marker}`,
contactName: isPickup ? '订单验收发货人' : '订单验收收货人',
phone: isPickup ? '13800138000' : '12025550188',
email: isPickup ? 'order-acceptance@example.com' : 'private-receiver@example.com',
countryCode: isPickup ? 'CN' : 'US',
province: isPickup ? '广东省' : 'New York',
city: isPickup ? '深圳市' : 'New York',
district: isPickup ? '南山区' : 'Queens',
addressLine: `${context.marker} ${isPickup ? '开发联调取货地址' : '88 Randomized Test Ave'}`,
postalCode: isPickup ? '518000' : '11354',
defaultAddress: true,
}, merchantToken)
return id(context.apiSuccess(MODULE, `Client 创建${addressType}地址`, createResponse,
`addressType=${addressType}`, CONTRACT_OPTIONS))
}
function buildPayload(context, profile, master, productImageFileId, customerAddresses) {
const shipmentId = `SHIP-${context.marker}`.slice(0, 64)
const referenceId = `REF-${context.marker}`.slice(0, 64)
const companyName = profile.tenantName || `订单验收客户-${context.marker}`
@@ -371,6 +398,8 @@ function buildPayload(context, profile, master, productImageFileId) {
expectedShipDate: tomorrow(), customerRemark: `${context.marker} Client 创建隔离订单`,
pickupMethod, requestedPickupAt: pickupMethod === 'PICKUP'
? new Date(Date.now() + 7 * 86_400_000).toISOString().slice(0, 19) : null,
senderAddressId: pickupMethod === 'PICKUP' ? customerAddresses.pickupAddressId : null,
privateDestinationAddressId: isPrivate ? customerAddresses.privateDestinationAddressId : null,
receivingWarehouseId: master.receiving.id,
receivingWarehouseName: master.receiving.warehouseName,
channelCode: master.channel.channelCode, channelName: master.channel.channelName,
@@ -405,7 +434,7 @@ function buildPayload(context, profile, master, productImageFileId) {
shipmentId: master.destinationType === 'FBA' ? shipmentId : null,
referenceId: master.destinationType === 'FBA' ? referenceId : null,
upcNo: `${Date.now()}${index}`.slice(-12),
productImageFileId, dangerousGoods: false,
productImageFileId, dangerousGoods: false, woodenProduct: false,
cargoRemark: `${context.marker} boxType=${box.boxType}`,
}
}),
@@ -482,7 +511,7 @@ async function waitForInbound(context, tenantId, orderId) {
export async function run(context) {
const loginResponse = await context.request('POST', '/api/auth/login', {
username: MERCHANT_USERNAME, password: MERCHANT_PASSWORD,
loginId: MERCHANT_USERNAME, password: MERCHANT_PASSWORD,
}, '')
const login = context.apiSuccess(MODULE, 'Client 测试账号登录', loginResponse,
`username=${MERCHANT_USERNAME}`)
@@ -509,7 +538,12 @@ export async function run(context) {
if (!productImageFileId) throw new Error('产品图片新契约未部署,无法创建带图片的隔离订单')
await assertProductImageDownload(context, merchantToken, productImageFileId)
const payload = buildPayload(context, profile, master, productImageFileId)
const customerAddresses = {
pickupAddressId: await ensureCustomerAddress(context, merchantToken, profile, 'PICKUP'),
privateDestinationAddressId: master.destinationType === 'PRIVATE'
? await ensureCustomerAddress(context, merchantToken, profile, 'PRIVATE_DESTINATION') : null,
}
const payload = buildPayload(context, profile, master, productImageFileId, customerAddresses)
const createPath = '/api/merchant/v1/orders/first-leg'
const createResponse = await request(context, 'POST', createPath, payload, merchantToken,
{ 'Idempotency-Key': key(context, 'client-create') })
@@ -41,7 +41,7 @@ async function loginMerchant(context) {
const account = context.context?.merchantQualificationFixture
if (!account?.username || !account?.password) throw new Error('缺少本轮客户登录凭据')
const response = await context.request('POST', '/api/auth/login', {
username: account.username, password: account.password,
loginId: account.username, password: account.password,
}, '')
const token = success(response) ? response.body?.data?.accessToken : null
if (!token) throw new Error(`本轮客户登录失败: requestId=${response?.requestId || 'missing'}, traceId=${response?.traceId || 'missing'}`)
@@ -1,5 +1,4 @@
import { publishPlatformLogisticsChannels } from './ocean-booking-fixture.mjs'
import { diffPushPreloadStability, projectPushPreloadStability } from './push-preload-stability.mjs'
import { containerFieldGaps, loadingListFieldGaps, preloadFieldGaps } from './fulfillment-list-field-contract.mjs'
import { ensureMatsonCarrierPartner } from './carrier-partner-fixture.mjs'
import { iso6346ContainerNumber } from './iso6346-container-number.mjs'
@@ -163,7 +162,7 @@ export async function run(context) {
const realContainerNo = iso6346ContainerNumber(`${context.marker}-initial`)
const wrongContainerNo = `TST-${suffix}-02`.slice(0, 64)
const agentResponse = await context.request('GET',
'/api/platform/v1/suppliers?tenantId=0&category=BOOKING&status=ACTIVE&page=1&size=20')
'/api/platform/v1/suppliers?tenantId=0&category=BOOKING&status=ACTIVE&carrierCode=MATSON&page=1&size=20')
const agentPage = context.apiSuccess(MODULE, '查询放舱所需 ACTIVE 订舱代理', agentResponse, '', OPTIONS)
const bookingAgent = records(agentPage).find((item) => item?.category === 'BOOKING' && item?.status === 'ACTIVE')
context.assert(MODULE, '放舱订舱使用真实启用供应商', agentResponse,
@@ -259,8 +258,8 @@ export async function run(context) {
`planStatus=${containerDetail?.planStatus || 'missing'}, channel=${containerDetail?.channelCode || 'missing'}`, OPTIONS)
const containerNo = id(containerDetail?.containerNo || containerDetail?.cabinetNo)
const planResponse = await request(context, 'POST', `${SMART}/plans`, { bookingId: containerId, remark: context.marker })
const plan = context.apiSuccess(MODULE, '创建智能装柜计划', planResponse, '', OPTIONS)
const planResponse = await context.request('GET', `${SMART}/plans/by-booking/${encodeURIComponent(containerId)}`)
const plan = context.apiSuccess(MODULE, '读取柜次创建时原子生成的智能装柜计划', planResponse, '', OPTIONS)
const planId = id(plan?.id)
context.assert(MODULE, '装柜计划与货柜关联且处于 EDITING', planResponse,
Boolean(planId) && id(plan?.bookingId) === containerId && plan?.status === 'EDITING'
@@ -268,9 +267,9 @@ export async function run(context) {
`planId=${planId || 'missing'}, status=${plan?.status || 'missing'}`, OPTIONS)
if (!planId) return
const planReplayResponse = await request(context, 'POST', `${SMART}/plans`, { bookingId: containerId, remark: 'replay' })
const planReplay = context.apiSuccess(MODULE, '重放货柜装柜计划创建', planReplayResponse, '', OPTIONS)
context.assert(MODULE, '同一货柜重放返回同一计划', planReplayResponse,
const planReplayResponse = await context.request('GET', `${SMART}/plans/by-booking/${encodeURIComponent(containerId)}`)
const planReplay = context.apiSuccess(MODULE, '重复读取货柜唯一装柜计划', planReplayResponse, '', OPTIONS)
context.assert(MODULE, '同一货柜始终返回同一计划', planReplayResponse,
id(planReplay?.id) === planId, `first=${planId}, replay=${id(planReplay?.id) || 'missing'}`, OPTIONS)
const assignPayload = {
@@ -299,95 +298,28 @@ export async function run(context) {
staleAssignResponse.http === 200 && ['A0409', '409'].includes(codeOf(staleAssignResponse)),
`HTTP=${staleAssignResponse.http}, code=${codeOf(staleAssignResponse)}`, OPTIONS)
const pushKey = `${context.marker}-container-push`.slice(0, 128)
const pushRequest = Object.freeze({
path: `${SMART}/plans/${planId}/push-preload`,
body: Object.freeze({ expectedVersion: Number(assigned.version) }),
headers: Object.freeze({ 'Idempotency-Key': pushKey }),
})
const pushResponse = await request(context, 'POST', pushRequest.path, pushRequest.body, pushRequest.headers)
const pushed = context.apiSuccess(MODULE, '无真实柜号推送预装计划成功', pushResponse, '', OPTIONS)
context.assert(MODULE, '无号 push 仅冻结 EDITING 计划且版本递增', pushResponse,
id(pushed?.id) === planId && pushed?.status === 'EDITING' && pushed?.containerNo == null
&& Number(pushed?.version) > Number(assigned.version),
`status=${pushed?.status || 'missing'}, version=${pushed?.version ?? 'missing'}`, OPTIONS)
if (pushed?.status !== 'EDITING') return
const pushedDetailResponse = await context.request('GET',
const synchronizedDetailResponse = await context.request('GET',
`${FIRST_LEG}/loading-plans/${planId}?warehouseId=${encodeURIComponent(warehouseId)}`)
const pushedDetail = context.apiSuccess(MODULE, '无号 push 后预装清单可见', pushedDetailResponse, '', OPTIONS)
const pushedLinksResponse = await context.request('GET', `${SMART}/plans/${planId}/preloads`)
const pushedLinks = context.apiSuccess(MODULE, '无号 push 自动建立 bridge', pushedLinksResponse, '', OPTIONS)
const pushedLink = Array.isArray(pushedLinks)
? pushedLinks.find((item) => id(item.inboundId) === inboundId && id(item.loadingPlanId) === planId) : null
context.assert(MODULE, 'push 自动 bridge 保持预装和柜次稳定归属', pushedLinksResponse,
Boolean(pushedLink?.id) && Number(pushedLink?.allocatedPackageCount) === 1,
`linkId=${id(pushedLink?.id) || 'missing'}, preloadPlanId=${id(pushedLink?.preloadPlanId) || 'missing'}`, OPTIONS)
if (!pushedLink?.id) return
const postPushPlanResponse = await context.request('GET', `${SMART}/plans/${planId}`)
const postPushPlan = context.apiSuccess(MODULE, '首次 push 后读取事务完成态柜次计划', postPushPlanResponse, '', OPTIONS)
context.assert(MODULE, '事务完成态计划版本与 bridge 一致且保持 EDITING', postPushPlanResponse,
postPushPlan?.status === 'EDITING' && Number(postPushPlan?.version) === Number(pushedLink?.loadingPlanVersion)
&& Number(postPushPlan?.version) > Number(assigned.version),
`pushResponseVersion=${pushed?.version ?? 'missing'}, persistedVersion=${postPushPlan?.version ?? 'missing'}, bridgeVersion=${pushedLink?.loadingPlanVersion ?? 'missing'}`, OPTIONS)
if (postPushPlan?.status !== 'EDITING' || !Number.isInteger(Number(postPushPlan?.version))) return
const pushedTaskResponse = await context.request('GET',
context.apiSuccess(MODULE, '分配后预装清单可见', synchronizedDetailResponse, '', OPTIONS)
const synchronizedLinksResponse = await context.request('GET', `${SMART}/plans/${planId}/preloads`)
const synchronizedLinks = context.apiSuccess(MODULE, '分配同步建立 bridge', synchronizedLinksResponse, '', OPTIONS)
const synchronizedLink = Array.isArray(synchronizedLinks)
? synchronizedLinks.find((item) => id(item.inboundId) === inboundId && id(item.loadingPlanId) === planId) : null
context.assert(MODULE, '分配自动 bridge 保持预装和柜次稳定归属', synchronizedLinksResponse,
Boolean(synchronizedLink?.id) && Number(synchronizedLink?.allocatedPackageCount) === 1,
`linkId=${id(synchronizedLink?.id) || 'missing'}, preloadPlanId=${id(synchronizedLink?.preloadPlanId) || 'missing'}`, OPTIONS)
if (!synchronizedLink?.id) return
const synchronizedPlanResponse = await context.request('GET', `${SMART}/plans/${planId}`)
const synchronizedPlan = context.apiSuccess(MODULE, '读取分配事务完成态柜次计划', synchronizedPlanResponse, '', OPTIONS)
context.assert(MODULE, '事务完成态计划版本与 bridge 一致且保持 EDITING', synchronizedPlanResponse,
synchronizedPlan?.status === 'EDITING' && Number(synchronizedPlan?.version) === Number(synchronizedLink?.loadingPlanVersion),
`persistedVersion=${synchronizedPlan?.version ?? 'missing'}, bridgeVersion=${synchronizedLink?.loadingPlanVersion ?? 'missing'}`, OPTIONS)
if (synchronizedPlan?.status !== 'EDITING' || !Number.isInteger(Number(synchronizedPlan?.version))) return
const synchronizedTaskResponse = await context.request('GET',
`${PDA}/tasks?loadingPlanId=${encodeURIComponent(planId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
context.addStep(MODULE, '无号 push 成功后 PDA 查询 A0404', pushedTaskResponse,
pushedTaskResponse.http === 200 && codeOf(pushedTaskResponse) === 'A0404',
responseEvidence(pushedTaskResponse), OPTIONS)
const pushBaseline = projectPushPreloadStability({
plan: postPushPlan, detail: pushedDetail, links: pushedLinks,
task: { http: pushedTaskResponse.http, code: codeOf(pushedTaskResponse) },
})
const pushReplayResponse = await request(context, 'POST', pushRequest.path, pushRequest.body, pushRequest.headers)
context.apiSuccess(MODULE, '推送预装 exact replay', pushReplayResponse, '', OPTIONS)
const replayPlanResponse = await context.request('GET', `${SMART}/plans/${planId}`)
const replayPlan = context.apiSuccess(MODULE, '重放后回查柜次计划', replayPlanResponse, '', OPTIONS)
const replayDetailResponse = await context.request('GET',
`${FIRST_LEG}/loading-plans/${planId}?warehouseId=${encodeURIComponent(warehouseId)}`)
const replayDetail = context.apiSuccess(MODULE, '重放后回查预装历史', replayDetailResponse, '', OPTIONS)
const replayLinksResponse = await context.request('GET', `${SMART}/plans/${planId}/preloads`)
const replayLinks = context.apiSuccess(MODULE, '重放后回查 bridge', replayLinksResponse, '', OPTIONS)
const replayTaskResponse = await context.request('GET',
`${PDA}/tasks?loadingPlanId=${encodeURIComponent(planId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
const pushReplaySnapshot = projectPushPreloadStability({
plan: replayPlan, detail: replayDetail, links: replayLinks,
task: { http: replayTaskResponse.http, code: codeOf(replayTaskResponse) },
})
const pushReplayDiff = diffPushPreloadStability(pushBaseline, pushReplaySnapshot)
context.assert(MODULE, 'push exact replay 对计划版本历史关联任务零漂移', pushReplayResponse,
pushReplayDiff.length === 0 && codeOf(replayTaskResponse) === 'A0404',
`changed=${pushReplayDiff.join('|') || 'none'}; replayTask=${responseEvidence(replayTaskResponse)}`, OPTIONS)
const pushSameKeyDifferentBody = await request(context, 'POST', pushRequest.path,
{ expectedVersion: Number(postPushPlan.version) }, pushRequest.headers)
context.addStep(MODULE, 'push 同 key 异参拒绝', pushSameKeyDifferentBody,
failure(pushSameKeyDifferentBody), responseEvidence(pushSameKeyDifferentBody), OPTIONS)
const pushNewKeyDuplicate = await request(context, 'POST', pushRequest.path, pushRequest.body,
{ 'Idempotency-Key': `${context.marker}-container-push-duplicate`.slice(0, 128) })
context.addStep(MODULE, '已推送计划使用新 key 重复 push 拒绝', pushNewKeyDuplicate,
failure(pushNewKeyDuplicate), responseEvidence(pushNewKeyDuplicate), OPTIONS)
const rejectedPlanResponse = await context.request('GET', `${SMART}/plans/${planId}`)
const rejectedPlan = context.apiSuccess(MODULE, '非法 push 后回查柜次计划', rejectedPlanResponse, '', OPTIONS)
const rejectedDetailResponse = await context.request('GET',
`${FIRST_LEG}/loading-plans/${planId}?warehouseId=${encodeURIComponent(warehouseId)}`)
const rejectedDetail = context.apiSuccess(MODULE, '非法 push 后回查预装历史', rejectedDetailResponse, '', OPTIONS)
const rejectedLinksResponse = await context.request('GET', `${SMART}/plans/${planId}/preloads`)
const rejectedLinks = context.apiSuccess(MODULE, '非法 push 后回查 bridge', rejectedLinksResponse, '', OPTIONS)
const rejectedTaskResponse = await context.request('GET',
`${PDA}/tasks?loadingPlanId=${encodeURIComponent(planId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
const rejectedSnapshot = projectPushPreloadStability({
plan: rejectedPlan, detail: rejectedDetail, links: rejectedLinks,
task: { http: rejectedTaskResponse.http, code: codeOf(rejectedTaskResponse) },
})
const rejectedDiff = diffPushPreloadStability(pushBaseline, rejectedSnapshot)
context.assert(MODULE, '两类非法 push 后计划版本历史关联任务仍零漂移', rejectedTaskResponse,
failure(pushSameKeyDifferentBody) && failure(pushNewKeyDuplicate)
&& rejectedDiff.length === 0 && codeOf(rejectedTaskResponse) === 'A0404',
`changed=${rejectedDiff.join('|') || 'none'}; sameKey=${responseEvidence(pushSameKeyDifferentBody)}; newKey=${responseEvidence(pushNewKeyDuplicate)}; task=${responseEvidence(rejectedTaskResponse)}`, OPTIONS)
if (rejectedDiff.length !== 0 || codeOf(rejectedTaskResponse) !== 'A0404') return
context.addStep(MODULE, '无真实柜号时 PDA 查询 A0404', synchronizedTaskResponse,
synchronizedTaskResponse.http === 200 && codeOf(synchronizedTaskResponse) === 'A0404',
responseEvidence(synchronizedTaskResponse), OPTIONS)
const unlinkedTaskResponse = await context.request('GET',
`${PDA}/tasks?loadingPlanId=${encodeURIComponent(planId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
@@ -253,7 +253,7 @@ export async function run(context) {
|| !Number.isInteger(Number(carryingSlice?.packageCount)) || Number(carryingSlice.packageCount) <= 0) return
const loginResponse = await request(context, 'POST', '/api/auth/login', {
username: MERCHANT_USERNAME, password: MERCHANT_PASSWORD,
loginId: MERCHANT_USERNAME, password: MERCHANT_PASSWORD,
}, '')
const merchantLogin = context.apiSuccess(MODULE, '客户账号登录用于真实报关文件协同', loginResponse,
`username=${MERCHANT_USERNAME}`, OPTIONS)
+21 -106
View File
@@ -1,5 +1,3 @@
import { diffPushPreloadStability, projectPushPreloadStability } from './push-preload-stability.mjs'
import { isPureStatusNoop, visibleLoadingHistories } from './loading-history-noop.mjs'
import { ensureMatsonCarrierPartner } from './carrier-partner-fixture.mjs'
import { createHash } from 'node:crypto'
@@ -7,7 +5,7 @@ const MODULE = 'phase3-loading'
const OPTIONS = { scenario: 'phase3-split-preload-loading', deploymentBlocker: true }
const MERCHANT_USERNAME = process.env.MERCHANT_TEST_USERNAME || ''
const MERCHANT_PASSWORD = process.env.MERCHANT_TEST_PASSWORD || ''
const MATSON_SUPPLIER_NAME = 'MATSON 开发联调船司'
const MATSON_SUPPLIER_NAME = 'MATSON 开发联调订舱代理'
const LATE_CONTAINER_GATE_ONLY = process.env.LATE_CONTAINER_GATE_ONLY === '1'
function id(value) { return value == null ? '' : String(value) }
@@ -168,7 +166,7 @@ export async function run(context) {
const smartLoadingBase = '/api/platform/v1/first-leg/smart-loading'
const pdaBase = '/api/platform/v1/first-leg/pda-loading'
const supplierResponse = await context.request('GET',
'/api/platform/v1/suppliers?tenantId=0&category=BOOKING&page=1&size=200')
'/api/platform/v1/suppliers?tenantId=0&category=BOOKING&status=ACTIVE&carrierCode=MATSON&page=1&size=200')
const supplierPage = context.apiSuccess(MODULE, '读取订舱代理与船司供应商事实源', supplierResponse, '', OPTIONS)
const supplierRecords = Array.isArray(supplierPage?.records) ? supplierPage.records : []
const supplier = await ensureMatsonSupplier(context, supplierRecords)
@@ -223,10 +221,9 @@ export async function run(context) {
if (!candidate?.inboundDetailId || !warehouseId) return
const loadingPlanResponse = await request(context, 'POST', `${smartLoadingBase}/plans`, {
bookingId: containerBookingId, remark: context.marker,
})
const loadingPlan = context.apiSuccess(MODULE, '创建柜次级智能装柜计划', loadingPlanResponse, '', OPTIONS)
const loadingPlanResponse = await context.request('GET',
`${smartLoadingBase}/plans/by-booking/${encodeURIComponent(containerBookingId)}`)
const loadingPlan = context.apiSuccess(MODULE, '读取柜次创建时原子生成的智能装柜计划', loadingPlanResponse, '', OPTIONS)
const loadingPlanId = id(loadingPlan?.id)
if (!loadingPlanId) return
const assignmentResponse = await request(context, 'POST', `${smartLoadingBase}/plans/${loadingPlanId}/assignments`, {
@@ -310,119 +307,37 @@ export async function run(context) {
const pendingPreloadResponse = await context.request('GET', `${base}/preload?tenantId=${encodeURIComponent(tenantId)}`)
const pendingPreload = context.apiSuccess(MODULE, '智能排货自动建立 PENDING 预装聚合', pendingPreloadResponse, '', OPTIONS)
const planId = id(pendingPreload?.id)
context.assert(MODULE, '推送前入库单保持 WAIT_SPLIT 且预装聚合为 PENDING', pendingPreloadResponse,
context.assert(MODULE, '分配后入库单保持 WAIT_SPLIT 且预装聚合为 PENDING', pendingPreloadResponse,
Boolean(planId) && pendingPreload?.status === 'PENDING' && Number.isInteger(Number(pendingPreload?.version)),
`preloadPlanId=${planId || 'missing'}, status=${pendingPreload?.status || 'missing'}`, OPTIONS)
if (!planId) return
const pushRequest = Object.freeze({
path: `${smartLoadingBase}/plans/${loadingPlanId}/push-preload`,
body: Object.freeze({ expectedVersion: Number(persistedRemarkPlan.version) }),
headers: Object.freeze({ 'Idempotency-Key': `${context.marker}-bridge-push`.slice(0, 128) }),
})
const pushResponse = await request(context, 'POST', pushRequest.path, pushRequest.body, pushRequest.headers)
const pushedLoadingPlan = context.apiSuccess(MODULE, '无真实柜号推送预装成功并冻结柜次明细', pushResponse, '', OPTIONS)
context.assert(MODULE, '无号推送保持 EDITING、身份稳定且版本递增', pushResponse,
id(pushedLoadingPlan?.id) === loadingPlanId && pushedLoadingPlan?.status === 'EDITING'
&& pushedLoadingPlan?.containerNo == null
&& Number(pushedLoadingPlan?.version) > Number(assignedLoadingPlan.version),
`status=${pushedLoadingPlan?.status || 'missing'}, version=${pushedLoadingPlan?.version ?? 'missing'}`, OPTIONS)
if (pushedLoadingPlan?.status !== 'EDITING') return
const pushedDetailResponse = await context.request('GET',
const synchronizedDetailResponse = await context.request('GET',
`/api/platform/v1/first-leg/loading-plans/${loadingPlanId}?warehouseId=${encodeURIComponent(warehouseId)}`)
const pushedDetail = context.apiSuccess(MODULE, '无号推送后预装清单正式可见', pushedDetailResponse, '', OPTIONS)
const pushedHistories = visibleLoadingHistories(pushedDetail?.histories)
context.assert(MODULE, '首次 push 只保留一条 PUSH_PRELOAD 且无纯状态 no-op', pushedDetailResponse,
pushedHistories.filter((item) => item.actionType === 'PUSH_PRELOAD').length === 1
&& !(pushedDetail?.histories || []).some(isPureStatusNoop),
`raw=${pushedDetail?.histories?.length ?? 'invalid'}, effective=${pushedHistories.length}, push=${pushedHistories.filter((item) => item.actionType === 'PUSH_PRELOAD').length}`,
OPTIONS)
const pushedLinksResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}/preloads`)
const pushedLinks = context.apiSuccess(MODULE, '无号推送自动建立 bridge 关联', pushedLinksResponse, '', OPTIONS)
const link = Array.isArray(pushedLinks) ? pushedLinks.find((item) => id(item.inboundId) === inboundId) : null
context.assert(MODULE, 'push 自动建立唯一预装关联且分配身份稳定', pushedLinksResponse,
const synchronizedDetail = context.apiSuccess(MODULE, '分配后预装清单正式可见', synchronizedDetailResponse, '', OPTIONS)
const synchronizedLinksResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}/preloads`)
const synchronizedLinks = context.apiSuccess(MODULE, '分配同步建立 bridge 关联', synchronizedLinksResponse, '', OPTIONS)
const link = Array.isArray(synchronizedLinks) ? synchronizedLinks.find((item) => id(item.inboundId) === inboundId) : null
context.assert(MODULE, '分配自动建立唯一预装关联且身份稳定', synchronizedLinksResponse,
Boolean(link?.id) && id(link?.loadingPlanId) === loadingPlanId && id(link?.preloadPlanId) === planId
&& Number(link?.allocatedPackageCount) === 1,
`linkId=${id(link?.id) || 'missing'}, preloadPlanId=${id(link?.preloadPlanId) || 'missing'}`, OPTIONS)
if (!link?.id) return
const postPushPlanResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`)
const postPushPlan = context.apiSuccess(MODULE, '首次 push 后读取事务完成态柜次计划', postPushPlanResponse, '', OPTIONS)
context.assert(MODULE, '事务完成态计划版本与 bridge 一致且保持 EDITING', postPushPlanResponse,
postPushPlan?.status === 'EDITING' && Number(postPushPlan?.version) === Number(link?.loadingPlanVersion)
&& Number(postPushPlan?.version) > Number(assignedLoadingPlan.version),
`pushResponseVersion=${pushedLoadingPlan?.version ?? 'missing'}, persistedVersion=${postPushPlan?.version ?? 'missing'}, bridgeVersion=${link?.loadingPlanVersion ?? 'missing'}`, OPTIONS)
if (postPushPlan?.status !== 'EDITING' || !Number.isInteger(Number(postPushPlan?.version))) return
const synchronizedPlanResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`)
const synchronizedPlan = context.apiSuccess(MODULE, '读取分配事务完成态柜次计划', synchronizedPlanResponse, '', OPTIONS)
context.assert(MODULE, '分配事务完成态计划与 bridge 一致且保持 EDITING', synchronizedPlanResponse,
synchronizedPlan?.status === 'EDITING' && Number(synchronizedPlan?.version) === Number(link?.loadingPlanVersion)
&& Number(synchronizedPlan?.version) === Number(persistedRemarkPlan.version),
`persistedVersion=${synchronizedPlan?.version ?? 'missing'}, bridgeVersion=${link?.loadingPlanVersion ?? 'missing'}`, OPTIONS)
if (synchronizedPlan?.status !== 'EDITING' || !Number.isInteger(Number(synchronizedPlan?.version))) return
const missingContainerTaskResponse = await context.request('GET',
`${pdaBase}/tasks?loadingPlanId=${encodeURIComponent(loadingPlanId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
const missingContainerTaskAbsent = missingContainerTaskResponse.http === 200 && codeOf(missingContainerTaskResponse) === 'A0404'
context.addStep(MODULE, '无号 push 成功后 PDA 查询明确 A0404', missingContainerTaskResponse,
context.addStep(MODULE, '无真实柜号时 PDA 查询明确 A0404', missingContainerTaskResponse,
missingContainerTaskAbsent,
`HTTP=${missingContainerTaskResponse.http}, code=${codeOf(missingContainerTaskResponse)}, requestId=${missingContainerTaskResponse.requestId || 'missing'}, traceId=${missingContainerTaskResponse.traceId || 'missing'}`, OPTIONS)
if (!missingContainerTaskAbsent) return
const replayBaseline = projectPushPreloadStability({
plan: postPushPlan, detail: pushedDetail, links: pushedLinks,
task: { http: missingContainerTaskResponse.http, code: codeOf(missingContainerTaskResponse) },
})
const pushReplayResponse = await request(context, 'POST', pushRequest.path, pushRequest.body, pushRequest.headers)
context.apiSuccess(MODULE, 'push exact replay 返回首次结果', pushReplayResponse, '', OPTIONS)
const replayPlanResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`)
const replayPlan = context.apiSuccess(MODULE, '重放后回查柜次计划', replayPlanResponse, '', OPTIONS)
const replayDetailResponse = await context.request('GET',
`/api/platform/v1/first-leg/loading-plans/${loadingPlanId}?warehouseId=${encodeURIComponent(warehouseId)}`)
const replayDetail = context.apiSuccess(MODULE, '重放后回查预装历史', replayDetailResponse, '', OPTIONS)
const replayHistories = visibleLoadingHistories(replayDetail?.histories)
context.assert(MODULE, 'push 同幂等请求重放历史零新增', replayDetailResponse,
replayHistories.length === pushedHistories.length
&& replayHistories.filter((item) => item.actionType === 'PUSH_PRELOAD').length === 1,
`before=${pushedHistories.length}, after=${replayHistories.length}, push=${replayHistories.filter((item) => item.actionType === 'PUSH_PRELOAD').length}`,
OPTIONS)
const replayLinksResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}/preloads`)
const replayLinks = context.apiSuccess(MODULE, '重放后回查 bridge', replayLinksResponse, '', OPTIONS)
const replayTaskResponse = await context.request('GET',
`${pdaBase}/tasks?loadingPlanId=${encodeURIComponent(loadingPlanId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
const replaySnapshot = projectPushPreloadStability({
plan: replayPlan, detail: replayDetail, links: replayLinks,
task: { http: replayTaskResponse.http, code: codeOf(replayTaskResponse) },
})
const replayDiff = diffPushPreloadStability(replayBaseline, replaySnapshot)
context.assert(MODULE, 'push exact replay 对计划版本历史关联和任务零漂移', pushReplayResponse,
replayDiff.length === 0 && codeOf(replayTaskResponse) === 'A0404',
`changed=${replayDiff.join('|') || 'none'}; replayTask=${responseEvidence(replayTaskResponse)}`, OPTIONS)
const sameKeyDifferentBody = await request(context, 'POST', pushRequest.path,
{ expectedVersion: Number(postPushPlan.version) }, pushRequest.headers)
context.addStep(MODULE, 'push 同 key 异参返回冲突', sameKeyDifferentBody,
failure(sameKeyDifferentBody), `HTTP=${sameKeyDifferentBody.http}, code=${codeOf(sameKeyDifferentBody)}`, OPTIONS)
const newKeyDuplicate = await request(context, 'POST', pushRequest.path, pushRequest.body,
{ 'Idempotency-Key': `${context.marker}-bridge-push-duplicate`.slice(0, 128) })
context.addStep(MODULE, '已推送计划使用新 key 重复 push 返回冲突', newKeyDuplicate,
failure(newKeyDuplicate), `HTTP=${newKeyDuplicate.http}, code=${codeOf(newKeyDuplicate)}`, OPTIONS)
const rejectedPlanResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`)
const rejectedPlan = context.apiSuccess(MODULE, '非法 push 后回查柜次计划', rejectedPlanResponse, '', OPTIONS)
const rejectedDetailResponse = await context.request('GET',
`/api/platform/v1/first-leg/loading-plans/${loadingPlanId}?warehouseId=${encodeURIComponent(warehouseId)}`)
const rejectedDetail = context.apiSuccess(MODULE, '非法 push 后回查预装历史', rejectedDetailResponse, '', OPTIONS)
const rejectedHistories = visibleLoadingHistories(rejectedDetail?.histories)
const rejectedLinksResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}/preloads`)
const rejectedLinks = context.apiSuccess(MODULE, '非法 push 后回查 bridge', rejectedLinksResponse, '', OPTIONS)
const rejectedTaskResponse = await context.request('GET',
`${pdaBase}/tasks?loadingPlanId=${encodeURIComponent(loadingPlanId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
const rejectedSnapshot = projectPushPreloadStability({
plan: rejectedPlan, detail: rejectedDetail, links: rejectedLinks,
task: { http: rejectedTaskResponse.http, code: codeOf(rejectedTaskResponse) },
})
const rejectedDiff = diffPushPreloadStability(replayBaseline, rejectedSnapshot)
context.assert(MODULE, '两类非法 push 后计划版本历史关联和任务仍零漂移', rejectedTaskResponse,
failure(sameKeyDifferentBody) && failure(newKeyDuplicate)
&& rejectedDiff.length === 0 && codeOf(rejectedTaskResponse) === 'A0404'
&& rejectedHistories.length === pushedHistories.length
&& rejectedHistories.filter((item) => item.actionType === 'PUSH_PRELOAD').length === 1,
`changed=${rejectedDiff.join('|') || 'none'}; sameKey=${responseEvidence(sameKeyDifferentBody)}; newKey=${responseEvidence(newKeyDuplicate)}; task=${responseEvidence(rejectedTaskResponse)}`, OPTIONS)
if (rejectedDiff.length !== 0 || codeOf(rejectedTaskResponse) !== 'A0404') return
const unlinkedTaskResponse = await context.request('GET',
`${pdaBase}/tasks?loadingPlanId=${encodeURIComponent(loadingPlanId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
context.addStep(MODULE, '无号且未关联订舱仍无 PDA 任务', unlinkedTaskResponse,
@@ -695,7 +610,7 @@ export async function run(context) {
`taskVersion=${unchangedAfterInvalid?.version ?? 'missing'}, items=${taskItemsSignature(unchangedAfterInvalid)}, operations=${afterInvalidOperations?.length ?? 'missing'}`, OPTIONS)
const merchantLogin = await context.request('POST', '/api/auth/login', {
username: MERCHANT_USERNAME, password: MERCHANT_PASSWORD,
loginId: MERCHANT_USERNAME, password: MERCHANT_PASSWORD,
}, '')
const merchantToken = merchantLogin.http === 200 && codeOf(merchantLogin) === '200'
? merchantLogin.body?.data?.accessToken : null
@@ -180,7 +180,7 @@ async function loadOneFragment(context, state) {
const { tenantId, rootOrderId, sourceOrderId, sourceOrderNo, warehouseId, sourcePackageCount, sequence } = state
const operationBase = `${context.marker}-chain-${sequence}`.slice(0, 100)
const agentResponse = await context.request('GET',
'/api/platform/v1/suppliers?tenantId=0&category=BOOKING&status=ACTIVE&page=1&size=20')
'/api/platform/v1/suppliers?tenantId=0&category=BOOKING&status=ACTIVE&carrierCode=MATSON&page=1&size=20')
const agentPage = context.apiSuccess(MODULE, `为 ${sourceOrderNo} 读取 ACTIVE 订舱代理`, agentResponse, '', OPTIONS)
const agent = agentPage?.records?.find?.((item) => item?.id && item?.name)
if (!agent) return null
@@ -494,7 +494,7 @@ export async function run(context) {
`loadedOrderId=${id(firstLoaded?.orderId) || 'missing'}, sourceOrderId=${id(firstLoaded?.sourceOrderId) || 'missing'}`, OPTIONS)
const merchantLogin = await context.request('POST', '/api/auth/login', {
username: MERCHANT_USERNAME, password: MERCHANT_PASSWORD,
loginId: MERCHANT_USERNAME, password: MERCHANT_PASSWORD,
}, '')
const merchantAuth = context.apiSuccess(MODULE, '客户账号登录验证隔离与权限', merchantLogin, '', OPTIONS)
const merchantToken = merchantAuth?.accessToken
@@ -68,11 +68,11 @@ test('records LOADED and clears the reason only after a real LOADED reread', ()
assert.equal(result.notCleanedReason, null)
})
test('container loading follows push, claim, real pick, completed sequence', () => {
test('container loading follows allocation sync, claim, real pick, completed sequence', () => {
const value = source('./phase3-container-loading.mjs')
assert.match(value, /\['EDITING', 'WAIT_LOADING'\]\.includes\(containerDetail\?\.planStatus\)/)
assertOrder(value, [
"pushed?.status === 'EDITING'",
"synchronizedPlan?.status === 'EDITING'",
"claimedPlan?.status === 'LOADING'",
"const pickResponse = await request",
"const completeResponse = await request",
@@ -87,9 +87,9 @@ test('container loading follows push, claim, real pick, completed sequence', ()
test('bridge loading confirms preload before creating and claiming the PDA task', () => {
const value = source('./phase3-loading.mjs')
assertOrder(value, [
'无真实柜号推送预装成功并冻结柜次明细',
'无号 push 成功后 PDA 查询明确 A0404',
'push exact replay 对计划版本历史关联和任务零漂移',
'读取柜次创建时原子生成的智能装柜计划',
'分配同步建立 bridge 关联',
'无真实柜号时 PDA 查询明确 A0404',
'无号且未关联订舱仍无 PDA 任务',
'三期订舱 RELEASED 冻结运输快照',
'模拟柜原地关联三期 RELEASED 订舱需求并补真实柜号',
@@ -110,10 +110,10 @@ test('bridge loading confirms preload before creating and claiming the PDA task'
assert.match(value, /计划级确认同 key 异参返回冲突/)
assert.match(value, /已确认计划使用新 key 重复确认返回冲突/)
assert.doesNotMatch(value, /\/preload\/confirming|\/preload\/confirm\?tenantId=/)
assert.doesNotMatch(value.slice(value.indexOf('无真实柜号推送预装成功并冻结柜次明细'), value.indexOf('const linkResponse')),
assert.doesNotMatch(value.slice(value.indexOf('分配同步建立 bridge 关联'), value.indexOf('const linkResponse')),
/\/transport/)
assert.match(value, /missingContainerTaskResponse\.http === 200 && codeOf\(missingContainerTaskResponse\) === 'A0404'/)
assert.doesNotMatch(value, /无真实柜号拒绝推送柜次计划|PDA complete后补确认/)
assert.doesNotMatch(value, /push-preload|无真实柜号拒绝推送柜次计划|PDA complete后补确认/)
assert.match(value, /billOfLadingNo:[\s\S]*vesselName:[\s\S]*voyageNo:[\s\S]*estimatedSailingAt:[\s\S]*estimatedArrivalAt:/)
assert.doesNotMatch(value, /P3-SHIPPING|P3-AGENT/)
assert.match(value, /const frozenTransport = Object\.fromEntries\(transportFields\.map/)
@@ -158,24 +158,13 @@ test('PDA completion production and HTTP chain never call or gate on truck appoi
assert.doesNotMatch(loading, /\/truck-appointments|completeLoadingFromPda/)
})
test('push replay and both conflict modes preserve the complete pre-PDA snapshot', () => {
for (const file of ['./phase3-loading.mjs', './phase3-container-loading.mjs']) {
const value = source(file)
assert.match(value, /const pushRequest = Object\.freeze\([\s\S]*body: Object\.freeze\(\{ expectedVersion:/)
assert.match(value, /push exact replay[\s\S]*零漂移/)
assert.match(value, /同 key 异参[\s\S]*新 key 重复 push/)
assert.match(value, /非法 push 后回查柜次计划[\s\S]*非法 push 后回查预装历史[\s\S]*非法 push 后回查 bridge/)
assert.match(value, /两类非法 push 后计划版本历史关联(?:和)?任务仍?零漂移/)
assert.match(value, /codeOf\(rejectedTaskResponse\) === 'A0404'/)
assert.match(value, /projectPushPreloadStability/)
assert.match(value, /diffPushPreloadStability/)
assert.match(value, /首次 push 后读取事务完成态柜次计划/)
assert.match(value, /事务完成态计划版本与 bridge 一致且保持 EDITING/)
assert.match(value, /plan: postPushPlan, detail:/)
assert.match(value, /expectedVersion: Number\(postPushPlan\.version\)/)
assert.match(value, /changed=\$\{(?:pushReplayDiff|replayDiff|rejectedDiff)\.join/)
assert.doesNotMatch(value, /baselineBytes=|replayBytes=|rejectedBytes=/)
}
test('allocation synchronizes the complete pre-PDA snapshot without a push protocol', () => {
const value = source('./phase3-loading.mjs')
assert.match(value, /plans\/by-booking\/\$\{encodeURIComponent\(containerBookingId\)\}/)
assert.match(value, /分配同步建立 bridge 关联/)
assert.match(value, /分配事务完成态计划与 bridge 一致且保持 EDITING/)
assert.match(value, /Number\(synchronizedPlan\?\.version\) === Number\(link\?\.loadingPlanVersion\)/)
assert.doesNotMatch(value, /push-preload|pushRequest|projectPushPreloadStability|diffPushPreloadStability/)
})
test('push stability projection sorts unordered facts and reports persisted field drift', () => {
@@ -214,15 +203,13 @@ test('push stability projection sorts unordered facts and reports persisted fiel
])
})
test('push orchestration is transactional and a bridge failure cannot reach PDA creation', () => {
test('allocation orchestration is transactional and binds the preload before PDA creation', () => {
const service = source('../../firstleg/src/main/java/com/baiyue/firstleg/service/SmartLoadingService.java')
const serviceTest = source('../../firstleg/src/test/java/com/baiyue/firstleg/service/SmartLoadingServiceTest.java')
const push = service.slice(service.indexOf('public Plan pushPreload'), service.indexOf('private void requirePushReplay'))
assert.match(service, /@Transactional[\s\S]*public Plan pushPreload/)
assert.match(push, /repository\.push[\s\S]*history\([\s\S]*loadingModelBridge\.bindAllocatedBeforeLoading/)
assert.doesNotMatch(push, /loadingPickService\.beginIfReady/)
assert.match(serviceTest, /bindingFailureStopsBeforePdaAndPropagatesForTransactionRollback/)
assert.match(serviceTest, /verify\(loadingPickService, never\(\)\)\.beginIfReady/)
const assign = service.slice(service.indexOf('private Plan assign('), service.indexOf('public Plan assignInbound'))
assert.match(service, /@Transactional[\s\S]*public Plan assign/)
assert.match(assign, /ensurePendingForLoadingAllocation[\s\S]*loadingModelBridge\.bindIncrementalAllocation/)
assert.match(assign, /loadingModelBridge\.bindIncrementalAllocation[\s\S]*loadingPickService\.sync/)
assert.doesNotMatch(source('../../firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformSmartLoadingController.java'), /push-preload/)
})
test('finalize rechecks the same unified order detail and list as LOADED', () => {
@@ -276,9 +263,9 @@ test('container loading arranges a simulation before linking a released booking
assertOrder(value, [
"const simulationResponse = await request(context, 'POST', CONTAINERS",
"const candidateResponse = await context.request('GET', `${SMART}/candidates?bookingId=",
"const planResponse = await request(context, 'POST', `${SMART}/plans`",
"const planResponse = await context.request('GET', `${SMART}/plans/by-booking/",
"const assignResponse = await request",
'无真实柜号推送预装计划成功',
'分配同步建立 bridge',
"const bookingCreate = await request",
"const linkResponse = await request(context, 'POST', `${CONTAINERS}/${containerId}/ocean-booking-link`",
])
@@ -306,10 +293,10 @@ test('container loading arranges a simulation before linking a released booking
assert.match(value, /bookingId=\$\{encodeURIComponent\(containerId\)\}&warehouseId=/)
assert.match(value, /tasks\?loadingPlanId=\$\{encodeURIComponent\(planId\)\}&warehouseId=/)
assert.match(value, /loadingPlanId: planId, containerBookingId: containerId/)
assert.doesNotMatch(value.slice(value.indexOf('无真实柜号推送预装计划成功'), value.indexOf('const linkResponse')),
assert.doesNotMatch(value.slice(value.indexOf('分配同步建立 bridge'), value.indexOf('const linkResponse')),
/\/transport/)
assert.match(value, /订舱放舱为 RELEASED 且不预生成真实货柜/)
assert.match(value, /无真实柜号推送预装计划成功/)
assert.match(value, /分配同步建立 bridge/)
assert.match(value, /订舱 RELEASED 但预装未 CONFIRMED 不生成 PDA 任务/)
assert.match(value, /计划级原子确认整份预装并触发唯一 PDA 任务/)
assert.match(value, /confirmationStatus === 'CONFIRMED'/)
@@ -389,8 +376,8 @@ test('container loading enforces channel isolation and stable PDA identity acros
'新增真实异渠道模拟柜',
'两个真实有效渠道之间拒绝跨渠道装柜',
'人工分配本轮候选货物',
'无真实柜号推送预装计划成功',
'无号 push 成功后 PDA 查询 A0404',
'分配同步建立 bridge',
'无真实柜号时 PDA 查询 A0404',
'无号且未关联订舱不生成 PDA 任务',
'订舱放舱为 RELEASED 且不预生成真实货柜',
'首次关联 RELEASED 订舱需求并补真实柜号',
@@ -422,20 +409,21 @@ test('container loading enforces channel isolation and stable PDA identity acros
assert.doesNotMatch(batch, /containerNo:/)
})
test('preload and loading versions use immediate operation baselines and exact push replay', () => {
test('preload and loading versions use immediate allocation and claim baselines', () => {
const container = source('./phase3-container-loading.mjs')
assert.match(container, /const pushRequest = Object\.freeze\(\{[\s\S]*path:[\s\S]*body: Object\.freeze[\s\S]*headers: Object\.freeze/)
assert.equal((container.match(/request\(context, 'POST', pushRequest\.path, pushRequest\.body, pushRequest\.headers\)/g) || []).length, 2)
assert.match(container, /plans\/by-booking\/\$\{encodeURIComponent\(containerId\)\}/)
assert.match(container, /分配同步建立 bridge/)
assert.doesNotMatch(container, /push-preload|pushRequest/)
assert.match(container, /const beforeClaimPlanVersion = Number\(beforeClaimPlan\?\.version\)/)
assert.match(container, /Number\(claimedPlan\?\.version\) > beforeClaimPlanVersion/)
const loading = source('./phase3-loading.mjs')
assert.match(loading, /const pushRequest = Object\.freeze\(\{[\s\S]*body: Object\.freeze[\s\S]*headers: Object\.freeze/)
assert.equal((loading.match(/request\(context, 'POST', pushRequest\.path, pushRequest\.body, pushRequest\.headers\)/g) || []).length, 2)
assert.match(loading, /push exact replay 对计划版本历史关联和任务零漂移/)
assert.match(loading, /plans\/by-booking\/\$\{encodeURIComponent\(containerBookingId\)\}/)
assert.match(loading, /分配事务完成态计划与 bridge 一致且保持 EDITING/)
assert.doesNotMatch(loading, /push-preload|pushRequest/)
assert.match(loading, /Number\(claimedPlan\?\.version\) > beforeClaimPlanVersion/)
assert.match(loading, /MATSON_SUPPLIER_NAME = 'MATSON 开发联调船司'/)
assert.match(loading, /category=BOOKING&page=1&size=200/)
assert.match(loading, /MATSON_SUPPLIER_NAME = 'MATSON 开发联调订舱代理'/)
assert.match(loading, /category=BOOKING&status=ACTIVE&carrierCode=MATSON&page=1&size=200/)
assert.match(loading, /ensureMatsonSupplier\(context, supplierRecords\)/)
assert.match(loading, /shippingCompanyId: id\(shippingCarrier\.id\), shippingCompanyName: shippingCarrier\.name/)
assert.match(loading, /bookingAgentId: id\(supplier\.id\), bookingAgentName: supplier\.name/)
@@ -94,7 +94,7 @@ async function loginMerchant(context, activeCustomer) {
return blocker(context, '采购客户登录前置完整', null,
`缺少 marker=${context.marker} 的客户账号凭据,禁止使用历史客户或降级为 coverageGap`)
}
const login = await context.request('POST', '/api/auth/login', { username: fixture.username, password: fixture.password }, '')
const login = await context.request('POST', '/api/auth/login', { loginId: fixture.username, password: fixture.password }, '')
const auth = context.apiSuccess(MODULE, '采购客户账号登录', login, trace(login), OPTIONS)
const overseasToken = auth?.overseasAccessToken
context.assert(MODULE, '采购客户登录返回海外Token', login, nonEmpty(overseasToken),
@@ -46,7 +46,7 @@ async function loginMerchant(context) {
return null
}
const response = await context.request('POST', '/api/auth/login', {
username: fixture.username, password: fixture.password,
loginId: fixture.username, password: fixture.password,
}, '')
const auth = context.apiSuccess(MODULE, '客户工作台账号登录', response, trace(response), OPTIONS)
context.assert(MODULE, '客户登录返回海外访问Token', response, nonBlank(auth?.overseasAccessToken),
@@ -390,7 +390,7 @@ async function verifyCanonicalPermissionDenial(context, resource) {
resource.permissionProbe.membershipId = membershipId
resource.permissionProbe.userStatus = 'ACTIVE'
const loginResponse = await context.request('POST', '/api/auth/login', { username, password }, '')
const loginResponse = await context.request('POST', '/api/auth/login', { loginId: username, password }, '')
const login = requireData(context, '无主数据权限用户登录', loginResponse)
if (!login?.overseasAccessToken) throw new Error('无主数据权限用户未签发海外Token')
const denied = await context.requestOverseas('GET',
@@ -79,7 +79,7 @@ async function captureCompletedOrder(context, containerNo, expectedCustomerId) {
async function loginMerchantForNegativeProbe(context) {
const fixture = context.context?.merchantQualificationFixture
if (!fixture?.username || !fixture?.password) return null
const response = await context.request('POST', '/api/auth/login', { username: fixture.username, password: fixture.password }, '')
const response = await context.request('POST', '/api/auth/login', { loginId: fixture.username, password: fixture.password }, '')
const data = success(response) ? response.body?.data : null
context.addStep(MODULE, '本轮客户登录取得海外令牌用于PDA越权反证', response,
success(response) && nonBlank(data?.overseasAccessToken),
@@ -87,7 +87,7 @@ async function loginMerchantForNegativeProbe(context) {
return nonBlank(data?.overseasAccessToken) ? data.overseasAccessToken : null
}
async function resolvePickerAccess(context) {
export async function resolvePickerAccess(context) {
const rolesResponse = await context.request('GET', '/api/platform/v1/iam/roles')
const roles = context.apiSuccess(MODULE, '查询海外PDA仓库操作员角色', rolesResponse, '', OPTIONS)
const role = (Array.isArray(roles) ? roles : []).find((item) => item?.roleCode === 'OVERSEAS_WAREHOUSE_PDA_OPERATOR' && item?.status === 'ACTIVE')
@@ -104,7 +104,7 @@ async function resolvePickerAccess(context) {
return { role, operate }
}
async function createPicker(context, index, overseasWarehouseId, access) {
export async function createPicker(context, index, overseasWarehouseId, access) {
const role = access.role
const suffix = markerSuffix(context)
const username = `pda_overseas_${suffix.toLowerCase()}_${index}`.slice(0, 64)
@@ -122,7 +122,7 @@ async function createPicker(context, index, overseasWarehouseId, access) {
&& (created?.roleIds || []).map(id).includes(id(role.id)),
`membershipId=${membershipId || 'missing'}, tenantId=${id(created?.tenantId) || 'missing'}, overseasWarehouseId=${id(created?.overseasWarehouseId) || 'missing'}`, OPTIONS)
if (!longId(membershipId)) return null
const login = await context.request('POST', '/api/auth/login', { username, password }, '')
const login = await context.request('POST', '/api/auth/login', { loginId: username, password }, '')
const token = success(login) ? login.body?.data?.overseasAccessToken : null
context.addStep(MODULE, `海外PDA操作员${index}登录取得双区域海外令牌`, login, nonBlank(token),
`overseasTokenPresent=${nonBlank(token)}; ${trace(login)}`, OPTIONS)
@@ -143,7 +143,7 @@ async function createPicker(context, index, overseasWarehouseId, access) {
return { membershipId, username, token: projectionReady ? token : null }
}
async function disablePicker(context, picker, index) {
export async function disablePicker(context, picker, index) {
if (!longId(picker?.membershipId)) return
const response = await context.request('PUT', `/api/platform/v1/iam/users/${picker.membershipId}/status`, { status: 'DISABLED' })
const disabled = success(response) && response.body?.data?.status === 'DISABLED'
@@ -0,0 +1,64 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import { fileURLToPath } from 'node:url'
import { parseArguments } from '../admin-test-env-acceptance.mjs'
const source = await readFile(fileURLToPath(new URL('./phase5-overseas-fulfillment.mjs', import.meta.url)), 'utf8')
const entry = await readFile(fileURLToPath(new URL('../admin-test-env-acceptance.mjs', import.meta.url)), 'utf8')
test('phase-five fulfillment is a registered default HTTP module after the real phase-four dispatch fixture', () => {
assert.match(entry, /CONFIGURED_MODULES[^\n]*'phase4-overseas-dispatch', 'phase5-overseas-fulfillment'/)
assert.match(entry, /'phase5-overseas-fulfillment', \['phase4-overseas-dispatch'\]/)
const modules = parseArguments(['--modules', 'phase5-overseas-fulfillment']).modules
for (const prerequisite of ['merchant-qualification-fixture', 'phase3-loading', 'phase3-loading-finalize',
'phase4-overseas-pda', 'phase4-overseas-pda-operations', 'phase4-overseas-dispatch']) {
assert.ok(modules.indexOf(prerequisite) >= 0
&& modules.indexOf(prerequisite) < modules.indexOf('phase5-overseas-fulfillment'), prerequisite)
}
})
test('phase-five module covers dispatch edit, transfer, PDA pick/depart and delivery state', () => {
for (const token of [
"const DISPATCH_BASE = '/api/platform/v1/last-leg/dispatches'",
"const PDA_DISPATCH_BASE = '/api/pda/v1/last-leg/dispatches'",
'/transfer-delivery', '/items/${encodeURIComponent(id(item.dispatchItemId))}/pick',
'/complete`', '/depart`', '/outbound-orders/${encodeURIComponent(outboundNo)}',
'/delivery-orders/${encodeURIComponent(deliveryNo)}/status',
]) assert.ok(source.includes(token), token)
assert.match(source, /WAIT_OUTBOUND/)
assert.match(source, /WAIT_DELIVERY/)
assert.match(source, /READY_TO_DEPART/)
assert.match(source, /OUTBOUNDED/)
assert.match(source, /DELIVERING/)
})
test('phase-five module proves completion, customer visit, manual exception and delivery return', () => {
for (const token of [
'/delivery-orders/${encodeURIComponent(deliveryNo)}/complete',
'/customer-visits/${encodeURIComponent(visitId)}',
"visitResult: 'SATISFIED'", '/exception-orders',
'/return-inbound-options', '/mark-return', "returnDirection: 'OVERSEAS_WAREHOUSE'",
'RETURN_PENDING_WITH_COMPLETED_VISIT',
]) assert.ok(source.includes(token), token)
assert.match(source, /manualExceptionNo/)
assert.match(source, /returnExceptionNo/)
})
test('phase-five module uses marker-owned fixtures and always disables its temporary PDA identity', () => {
assert.match(source, /dispatch\?\.marker !== context\.marker/)
assert.match(source, /receiving\?\.marker !== context\.marker/)
assert.match(source, /resolvePickerAccess\(context\)/)
assert.match(source, /createPicker\(context, 5, warehouseId, access\)/)
assert.match(source, /finally \{[\s\S]*disablePicker\(context, picker, 5\)[\s\S]*picker\.token = null/)
assert.doesNotMatch(source, /8\.163\.|password\s*:/)
})
test('phase-five writes carry idempotency and record deployment blockers instead of passing empty fixtures', () => {
assert.match(source, /idempotencyKey: key\(context, 'TRANSFER'\)/)
assert.match(source, /idempotencyKey: key\(context, 'DISPATCH-PICK'/)
assert.match(source, /idempotencyKey: key\(context, 'DISPATCH-COMPLETE'\)/)
assert.match(source, /idempotencyKey: key\(context, 'DISPATCH-DEPART'\)/)
assert.match(source, /addCoverageGap\(MODULE, SCENARIO/)
assert.match(source, /deploymentBlocker: true/)
})
@@ -0,0 +1,282 @@
import { createPicker, disablePicker, resolvePickerAccess } from './phase4-overseas-pda.mjs'
const MODULE = 'phase5-overseas-fulfillment'
const SCENARIO = 'phase5-dispatch-outbound-delivery-exception-visit'
const OPTIONS = { scenario: SCENARIO, deploymentBlocker: true }
const DISPATCH_BASE = '/api/platform/v1/last-leg/dispatches'
const LASTLEG_BASE = '/api/platform/v1/last-leg'
const PDA_DISPATCH_BASE = '/api/pda/v1/last-leg/dispatches'
function id(value) { return value == null ? '' : String(value) }
function nonBlank(value) { return typeof value === 'string' && value.trim() !== '' }
function success(response) { return response?.http === 200 && String(response?.body?.code) === '200' }
function code(response) { return String(response?.body?.code ?? '') }
function conflict(response) {
return response?.http === 409 || (response?.http === 200 && ['409', 'A0409'].includes(code(response)))
}
function trace(response) {
return `HTTP=${response?.http ?? 'missing'}, code=${code(response) || 'missing'}, requestId=${response?.requestId || 'missing'}, traceId=${response?.traceId || 'missing'}`
}
function query(path, values) {
const params = new URLSearchParams()
for (const [name, value] of Object.entries(values || {})) {
if (value !== undefined && value !== null && String(value) !== '') params.set(name, String(value))
}
return params.size ? `${path}?${params}` : path
}
function key(context, action, suffix = '') {
return `${context.marker}-P5-${action}-${suffix}`.replace(/[^A-Za-z0-9:_-]/g, '').slice(0, 200)
}
function blocker(context, name, reason, response = null) {
context.addCoverageGap(MODULE, SCENARIO, name, reason, 'fixture-unavailable', { deploymentBlocker: true })
context.assert(MODULE, `${name}覆盖前置`, response, false, `${reason}; ${trace(response)}`, OPTIONS)
}
async function platform(context, method, name, path, body = undefined) {
const response = await context.requestOverseas(method, path, body)
const data = context.apiSuccess(MODULE, name, response, trace(response), OPTIONS)
return { response, data }
}
async function pda(context, token, method, name, path, body = undefined) {
const response = await context.requestOverseas(method, path, body, {}, token)
context.addStep(MODULE, name, response, success(response), trace(response), OPTIONS)
return { response, data: success(response) ? response.body?.data : null }
}
function fixture(context) {
const dispatch = context.createdResources?.phase4OverseasDispatch
const receiving = context.createdResources?.phase4OverseasPda
if (dispatch?.marker !== context.marker || dispatch?.finalStatus !== 'PENDING_DEPARTURE'
|| !nonBlank(dispatch?.dispatchNo)) return null
if (receiving?.marker !== context.marker || receiving?.finalStatus !== 'COMPLETED'
|| !id(receiving?.overseasWarehouseId) || !id(receiving?.customerId)) return null
return { dispatch, receiving }
}
async function pickAll(context, token, dispatchNo) {
let detail = await pda(context, token, 'GET', 'PDA读取本轮排车拣货任务',
`${PDA_DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}`)
const items = Array.isArray(detail.data?.items) ? detail.data.items : []
if (!success(detail.response) || items.length === 0) {
blocker(context, 'PDA排车拣货任务', '本轮排车未生成可操作PDA托盘任务', detail.response)
return false
}
for (const [itemIndex, item] of items.entries()) {
let remaining = Number(item.scheduledPackages || 0) - Number(item.pickedPackages || 0)
const positions = Array.isArray(item.positions) ? item.positions : []
for (const [positionIndex, position] of positions.entries()) {
if (remaining <= 0) break
const available = Number(position.availablePackages || 0)
if (available <= 0) continue
const packages = Math.min(remaining, available)
const body = {
positionId: id(position.id), packages, productQuantity: packages,
idempotencyKey: key(context, 'DISPATCH-PICK', `${itemIndex}-${positionIndex}`),
remark: `${context.marker}-phase5-dispatch-pick`,
}
const picked = await pda(context, token, 'POST', `PDA拣货托盘${itemIndex + 1}库存位${positionIndex + 1}`,
`${PDA_DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}/items/${encodeURIComponent(id(item.dispatchItemId))}/pick`, body)
if (!success(picked.response)) return false
const replay = await pda(context, token, 'POST', `PDA拣货托盘${itemIndex + 1}同键重放`,
`${PDA_DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}/items/${encodeURIComponent(id(item.dispatchItemId))}/pick`, body)
context.assert(MODULE, `PDA拣货托盘${itemIndex + 1}重放数量稳定`, replay.response,
success(replay.response) && Number(replay.data?.pickedPackages) === Number(picked.data?.pickedPackages),
trace(replay.response), OPTIONS)
remaining -= packages
}
if (remaining !== 0) {
blocker(context, `PDA托盘${itemIndex + 1}足量库存`,
`scheduled=${item.scheduledPackages}, picked=${item.pickedPackages}, remaining=${remaining}`, detail.response)
return false
}
}
const completeBody = { idempotencyKey: key(context, 'DISPATCH-COMPLETE') }
const completed = await pda(context, token, 'POST', 'PDA确认排车拣货全部完成',
`${PDA_DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}/complete`, completeBody)
context.assert(MODULE, '排车进入READY_TO_DEPART且数量守恒', completed.response,
completed.data?.status === 'READY_TO_DEPART'
&& Number(completed.data?.plannedPackages) === Number(completed.data?.pickedPackages)
&& Number(completed.data?.remainingPackages) === 0,
trace(completed.response), OPTIONS)
if (!success(completed.response)) return false
const replay = await pda(context, token, 'POST', 'PDA完成排车拣货同键重放',
`${PDA_DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}/complete`, completeBody)
context.assert(MODULE, 'PDA完成拣货重放明确标记且结果稳定', replay.response,
replay.data?.replayed === true && replay.data?.status === 'READY_TO_DEPART', trace(replay.response), OPTIONS)
return success(replay.response)
}
export async function run(context) {
context.createdResources.phase5OverseasFulfillment = {
marker: context.marker, dispatchNo: null, outboundNo: null, deliveryNo: null,
visitId: null, manualExceptionNo: null, returnExceptionNo: null, finalStatus: null,
cleanup: 'marker-owned completed delivery, customer visit and return exception are retained as acceptance evidence',
}
const source = fixture(context)
if (!source) {
blocker(context, '取得本轮四期已收货排车前置',
`dispatchMarker=${context.createdResources?.phase4OverseasDispatch?.marker || 'missing'}, dispatchStatus=${context.createdResources?.phase4OverseasDispatch?.finalStatus || 'missing'}, receivingStatus=${context.createdResources?.phase4OverseasPda?.finalStatus || 'missing'}`)
return
}
const dispatchNo = source.dispatch.dispatchNo
const customerId = id(source.receiving.customerId)
const warehouseId = id(source.receiving.overseasWarehouseId)
context.createdResources.phase5OverseasFulfillment.dispatchNo = dispatchNo
const updateBody = {
vehiclePlate: `DEV-${context.marker.slice(-8)}`.slice(0, 64),
driverName: `五期验收司机-${context.marker.slice(-6)}`,
driverPhone: '19900000000', carrierName: '五期开发验收车队',
route: `${context.marker}-PHASE5-ROUTE`, estimatedArrivalAt: new Date(Date.now() + 86_400_000).toISOString(),
}
const updated = await platform(context, 'PUT', '补齐本轮排车车辆司机与线路',
`${DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}`, updateBody)
context.assert(MODULE, '排车编辑字段完整回显', updated.response,
updated.data?.dispatchNo === dispatchNo && updated.data?.vehiclePlate === updateBody.vehiclePlate
&& updated.data?.driverName === updateBody.driverName && updated.data?.route === updateBody.route,
trace(updated.response), OPTIONS)
if (!success(updated.response)) return
const transferBody = {
idempotencyKey: key(context, 'TRANSFER'), plannedDeliveryAt: new Date(Date.now() + 172_800_000).toISOString(),
deliveryMethod: 'SELF_DELIVERY', waybillNo: `${context.marker}-WAYBILL`.slice(0, 64),
carrierName: updateBody.carrierName, pickupMethod: 'WAREHOUSE_PICKUP', loadingType: 'PALLET',
vehicleType: 'TRUCK', vehicleSize: 'MEDIUM', driverName: updateBody.driverName,
driverPhone: updateBody.driverPhone, vehiclePlate: updateBody.vehiclePlate,
appointmentAccount: 'SELF', sellerName: null, remark: `${context.marker}-phase5-transfer`,
fees: [{ feeType: 'DELIVERY_FEE', currency: 'USD', amount: 0, remark: `${context.marker}-acceptance` }],
}
const transferred = await platform(context, 'POST', '排车原子转出库与派送单',
`${LASTLEG_BASE}/dispatches/${encodeURIComponent(dispatchNo)}/transfer-delivery`, transferBody)
const outboundNo = id(transferred.data?.outboundNo)
const deliveryNo = id(transferred.data?.deliveryNo)
context.assert(MODULE, '转派送同时生成WAIT_OUTBOUND与WAIT_DELIVERY单据', transferred.response,
nonBlank(outboundNo) && nonBlank(deliveryNo) && transferred.data?.outboundStatus === 'WAIT_OUTBOUND'
&& transferred.data?.deliveryStatus === 'WAIT_DELIVERY' && Number(transferred.data?.packageCount) > 0,
`outboundNo=${outboundNo || 'missing'}, deliveryNo=${deliveryNo || 'missing'}; ${trace(transferred.response)}`, OPTIONS)
if (!success(transferred.response) || !outboundNo || !deliveryNo) return
Object.assign(context.createdResources.phase5OverseasFulfillment, { outboundNo, deliveryNo })
const transferReplay = await platform(context, 'POST', '排车转派送同键重放',
`${LASTLEG_BASE}/dispatches/${encodeURIComponent(dispatchNo)}/transfer-delivery`, transferBody)
context.assert(MODULE, '转派送重放复用同一业务单据', transferReplay.response,
transferReplay.data?.replayed === true && id(transferReplay.data?.outboundNo) === outboundNo
&& id(transferReplay.data?.deliveryNo) === deliveryNo, trace(transferReplay.response), OPTIONS)
const transferConflict = await context.requestOverseas('POST',
`${LASTLEG_BASE}/dispatches/${encodeURIComponent(dispatchNo)}/transfer-delivery`,
{ ...transferBody, idempotencyKey: key(context, 'TRANSFER-DIFFERENT') })
context.addStep(MODULE, '同一排车使用另一幂等键仍返回既有单据', transferConflict,
success(transferConflict) && id(transferConflict.body?.data?.outboundNo) === outboundNo
&& transferConflict.body?.data?.replayed === true, trace(transferConflict), OPTIONS)
const access = await resolvePickerAccess(context)
const picker = access ? await createPicker(context, 5, warehouseId, access) : null
try {
if (!nonBlank(picker?.token)) {
blocker(context, '创建本轮五期PDA操作员', '正式IAM账号已创建但海外安全投影或登录未就绪')
return
}
if (!await pickAll(context, picker.token, dispatchNo)) return
const departBody = { idempotencyKey: key(context, 'DISPATCH-DEPART'), remark: `${context.marker}-depart` }
const departed = await pda(context, picker.token, 'POST', 'PDA确认车辆发车并完成真实出库',
`${PDA_DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}/depart`, departBody)
context.assert(MODULE, '发车推进排车DEPARTED并扣减实物库存一次', departed.response,
departed.data?.status === 'DEPARTED' && Number(departed.data?.shippedPackages) > 0,
trace(departed.response), OPTIONS)
if (!success(departed.response)) return
const departReplay = await pda(context, picker.token, 'POST', 'PDA发车同键重放',
`${PDA_DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}/depart`, departBody)
context.assert(MODULE, 'PDA发车重放明确标记且发运数量稳定', departReplay.response,
departReplay.data?.replayed === true
&& Number(departReplay.data?.shippedPackages) === Number(departed.data?.shippedPackages),
trace(departReplay.response), OPTIONS)
} finally {
await disablePicker(context, picker, 5)
if (picker) picker.token = null
}
const outbound = await platform(context, 'GET', '回查发车后出库单状态',
query(`${LASTLEG_BASE}/outbound-orders/${encodeURIComponent(outboundNo)}`, { page: 1, size: 20 }))
context.assert(MODULE, '排车PDA发车同步出库单OUTBOUNDED', outbound.response,
outbound.data?.outbound?.outboundNo === outboundNo && outbound.data?.outbound?.status === 'OUTBOUNDED',
trace(outbound.response), OPTIONS)
const delivering = await platform(context, 'PUT', '派送单推进配送中',
query(`${LASTLEG_BASE}/delivery-orders/${encodeURIComponent(deliveryNo)}/status`, { customerId }),
{ status: 'DELIVERING', remark: `${context.marker}-delivering` })
context.assert(MODULE, '派送状态进入DELIVERING', delivering.response,
delivering.data?.deliveryStatus === 'DELIVERING', trace(delivering.response), OPTIONS)
const completed = await platform(context, 'POST', '确认派送完成并生成客户回访',
query(`${LASTLEG_BASE}/delivery-orders/${encodeURIComponent(deliveryNo)}/complete`, { customerId }))
const visitId = id(completed.data?.visitId)
context.assert(MODULE, '派送完成生成PENDING回访记录', completed.response,
completed.data?.deliveryStatus === 'DELIVERED' && completed.data?.visitStatus === 'PENDING'
&& /^\d+$/.test(visitId), `visitIdPresent=${/^\d+$/.test(visitId)}; ${trace(completed.response)}`, OPTIONS)
if (!success(completed.response) || !visitId) return
context.createdResources.phase5OverseasFulfillment.visitId = visitId
const completeReplay = await platform(context, 'POST', '确认派送完成重复调用',
query(`${LASTLEG_BASE}/delivery-orders/${encodeURIComponent(deliveryNo)}/complete`, { customerId }))
context.assert(MODULE, '派送完成重放复用同一回访', completeReplay.response,
completeReplay.data?.replayed === true && id(completeReplay.data?.visitId) === visitId,
trace(completeReplay.response), OPTIONS)
const visitDetail = await platform(context, 'GET', '读取本轮客户回访详情',
query(`${LASTLEG_BASE}/customer-visits/${encodeURIComponent(visitId)}`, { customerId }))
const inbound = Array.isArray(visitDetail.data?.inboundOrders)
? visitDetail.data.inboundOrders.find((value) => id(value?.inboundOrderId) && nonBlank(value?.lastMileOrderNo)) : null
context.assert(MODULE, '回访详情包含派送关联的真实入仓单与尾程单', visitDetail.response,
Boolean(inbound), `inboundOption=${Boolean(inbound)}; ${trace(visitDetail.response)}`, OPTIONS)
if (!inbound) return
const savedVisit = await platform(context, 'PUT', '保存无附件客户回访结果',
query(`${LASTLEG_BASE}/customer-visits/${encodeURIComponent(visitId)}`, { customerId }), {
selectedInboundOrderId: id(inbound.inboundOrderId), visitResult: 'SATISFIED',
customerFeedback: `${context.marker}-customer-feedback`, remark: `${context.marker}-visit-completed`, attachments: [],
})
context.assert(MODULE, '客户回访完成并冻结所选业务入仓码', savedVisit.response,
savedVisit.data?.visit?.status === 'COMPLETED'
&& savedVisit.data?.visit?.selectedInboundCode === inbound.inboundCode
&& savedVisit.data?.visit?.visitResult === 'SATISFIED', trace(savedVisit.response), OPTIONS)
const manualException = await platform(context, 'POST', '为本轮尾程单创建人工异常',
`${LASTLEG_BASE}/exception-orders`, {
customerId, inboundCode: inbound.inboundCode, lastMileOrderNo: inbound.lastMileOrderNo,
exceptionType: 'DAMAGED', reason: `${context.marker}-manual-exception`, packageCount: 1, skuCode: null,
})
const manualExceptionNo = id(manualException.data?.exceptionNo)
context.assert(MODULE, '人工异常进入PENDING并返回业务单号', manualException.response,
nonBlank(manualExceptionNo) && manualException.data?.status === 'PENDING', trace(manualException.response), OPTIONS)
context.createdResources.phase5OverseasFulfillment.manualExceptionNo = manualExceptionNo || null
const returnOptions = await platform(context, 'GET', '查询派送退回可选入仓单',
query(`${LASTLEG_BASE}/delivery-orders/${encodeURIComponent(deliveryNo)}/return-inbound-options`, { customerId }))
const returnInbound = Array.isArray(returnOptions.data)
? returnOptions.data.find((value) => id(value?.inboundOrderId) === id(inbound.inboundOrderId)) : null
context.assert(MODULE, '退回候选严格属于当前派送单', returnOptions.response,
Boolean(returnInbound), `candidate=${Boolean(returnInbound)}; ${trace(returnOptions.response)}`, OPTIONS)
if (!returnInbound) return
const returned = await platform(context, 'POST', '标记派送退回海外仓并创建退货异常',
query(`${LASTLEG_BASE}/delivery-orders/${encodeURIComponent(deliveryNo)}/mark-return`, { customerId }), {
inboundOrderId: id(returnInbound.inboundOrderId), reason: `${context.marker}-delivery-return`,
returnDirection: 'OVERSEAS_WAREHOUSE', returnLogisticsNo: `${context.marker}-RETURN`.slice(0, 128),
packageCount: 1, skuCode: null,
})
const returnExceptionNo = id(returned.data?.exceptionNo)
context.assert(MODULE, '派送退回生成独立PENDING异常单', returned.response,
nonBlank(returnExceptionNo) && returned.data?.status === 'PENDING'
&& returnExceptionNo !== manualExceptionNo, trace(returned.response), OPTIONS)
context.createdResources.phase5OverseasFulfillment.returnExceptionNo = returnExceptionNo || null
const delivery = await platform(context, 'GET', '回查派送退回终态',
query(`${LASTLEG_BASE}/delivery-orders/${encodeURIComponent(deliveryNo)}`, { customerId }))
context.assert(MODULE, '派送退回后单据状态为RETURN_PENDING且保留出库关联', delivery.response,
delivery.data?.order?.status === 'RETURN_PENDING' && delivery.data?.order?.outboundNo === outboundNo,
trace(delivery.response), OPTIONS)
const exceptionPage = await platform(context, 'GET', '分页回查本轮两类异常',
query(`${LASTLEG_BASE}/exception-orders`, { page: 1, size: 20, customerId, keyword: context.marker }))
const exceptionRows = Array.isArray(exceptionPage.data?.records) ? exceptionPage.data.records : []
context.assert(MODULE, '异常列表同时包含人工异常与派送退回异常', exceptionPage.response,
exceptionRows.some((value) => id(value?.exceptionNo) === manualExceptionNo)
&& exceptionRows.some((value) => id(value?.exceptionNo) === returnExceptionNo),
`records=${exceptionRows.length}; ${trace(exceptionPage.response)}`, OPTIONS)
context.createdResources.phase5OverseasFulfillment.finalStatus = 'RETURN_PENDING_WITH_COMPLETED_VISIT'
}
+1 -1
View File
@@ -42,7 +42,7 @@ export async function run(context) {
const nonBookingCarrierResponse=await context.request('POST',`${BASE}?tenantId=0`,payload(`${marker}NB`,{name:`非订舱供应商非法船司-${marker}`,category:'CUSTOMS',carrierCodes:['MATSON']}))
context.assert(MODULE,'非BOOKING携带carrierCodes返回A0400并保留链路ID',nonBookingCarrierResponse,code(nonBookingCarrierResponse,'A0400')&&traced(nonBookingCarrierResponse),`code=${nonBookingCarrierResponse.body?.code}, requestId=${nonBookingCarrierResponse.requestId||'missing'}, traceId=${nonBookingCarrierResponse.traceId||'missing'}`,OPTIONS)
const merchantLogin=await context.request('POST','/api/auth/login',{username:MERCHANT_USERNAME,password:MERCHANT_PASSWORD},'')
const merchantLogin=await context.request('POST','/api/auth/login',{loginId:MERCHANT_USERNAME,password:MERCHANT_PASSWORD},'')
const merchantToken=ok(merchantLogin)?merchantLogin.body?.data?.accessToken:''
const forbidden=merchantToken?await context.request('GET',query({tenantId:0,page:1,size:20}),undefined,merchantToken):merchantLogin
context.assert(MODULE,'客户账号调用平台供应商接口被拒绝',forbidden,Boolean(merchantToken)&&(forbidden.http===403||code(forbidden,'A0403'))&&traced(forbidden),`HTTP=${forbidden.http}, code=${forbidden.body?.code??'missing'}, requestId=${forbidden.requestId||'missing'}, traceId=${forbidden.traceId||'missing'}`,OPTIONS)
+3 -3
View File
@@ -137,13 +137,13 @@ export async function run(context) {
updatedUser?.displayName === updateUserPayload.displayName && updatedUser?.email === updateUserPayload.email,
`membershipId=${membershipId}`)
const loginResponse = await context.request('POST', '/api/auth/login', { username, password: initialPassword }, '')
const loginResponse = await context.request('POST', '/api/auth/login', { loginId: username, password: initialPassword }, '')
const loginPassed = loginResponse.http === 200 && String(loginResponse.body?.code) === '200' && Boolean(loginResponse.body?.data?.accessToken)
context.addStep(module, '新建内部用户可使用初始密码登录', loginResponse, loginPassed, `username=${username}`)
const resetResponse = await context.request('PUT', `/api/platform/v1/iam/users/${membershipId}/password`, { newPassword: resetPassword })
context.apiSuccess(module, '重置平台内部用户密码', resetResponse)
const resetLoginResponse = await context.request('POST', '/api/auth/login', { username, password: resetPassword }, '')
const resetLoginResponse = await context.request('POST', '/api/auth/login', { loginId: username, password: resetPassword }, '')
const resetLoginPassed = resetLoginResponse.http === 200 && String(resetLoginResponse.body?.code) === '200' && Boolean(resetLoginResponse.body?.data?.accessToken)
context.addStep(module, '内部用户使用重置密码登录', resetLoginResponse, resetLoginPassed, `username=${username}`)
if (context.options.modules.includes('phase4-cross-region-events')) {
@@ -165,7 +165,7 @@ export async function run(context) {
const disableUserResponse = await context.request('PUT', `/api/platform/v1/iam/users/${membershipId}/status`, { status: 'DISABLED' })
const disabledUser = context.apiSuccess(module, '停用平台内部用户', disableUserResponse)
context.assert(module, '内部用户停用状态正确', disableUserResponse, disabledUser?.status === 'DISABLED', `status=${disabledUser?.status ?? 'missing'}`)
const disabledLoginResponse = await context.request('POST', '/api/auth/login', { username, password: resetPassword }, '')
const disabledLoginResponse = await context.request('POST', '/api/auth/login', { loginId: username, password: resetPassword }, '')
context.addStep(module, '停用用户禁止登录', disabledLoginResponse,
disabledLoginResponse.http === 200 && String(disabledLoginResponse.body?.code) !== '200',
`HTTP=${disabledLoginResponse.http}, code=${disabledLoginResponse.body?.code ?? 'missing'}`)
@@ -75,12 +75,16 @@ export function truckAppointmentPayload(context, suffix, identity, version = 0,
}
async function activeSupplier(context, category, suffix) {
const list = await request(context, 'GET', `/api/platform/v1/suppliers?tenantId=0&category=${category}&status=ACTIVE&page=1&size=100`)
const existing = records(data(list)).find((item) => item?.category === category && item?.status === 'ACTIVE')
const carrierFilter = category === 'BOOKING' ? '&carrierCode=MATSON' : ''
const list = await request(context, 'GET', `/api/platform/v1/suppliers?tenantId=0&category=${category}&status=ACTIVE${carrierFilter}&page=1&size=100`)
const existing = records(data(list)).find((item) => item?.category === category && item?.status === 'ACTIVE'
&& (category !== 'BOOKING' || item?.carrierCodes?.includes?.('MATSON')))
if (existing) return { supplierId: id(existing.id), supplierName: existing.name }
const created = await request(context, 'POST', '/api/platform/v1/suppliers?tenantId=0', {
name: `${category}-验收-${suffix}`, category, carrierCodes: category === 'BOOKING' ? ['MATSON'] : [],
region: '深圳', contactName: '验收联系人', phone: '13800000000', status: 'ACTIVE', remark: context.marker,
region: '深圳', legalRepresentative: '验收法人', contactName: '验收联系人', phone: '13800000000',
email: 'truck-acceptance@example.com', address: '深圳市验收地址', businessLicenseNo: `TRUCK-${suffix}`.slice(0, 64),
licenseFileId: null, status: 'ACTIVE', settlementPeriod: 'MONTHLY_30', remark: context.marker,
})
const value = data(created); const supplierId = id(value?.id || value)
context.apiSuccess(MODULE, `创建 ACTIVE ${category} 供应商`, created, '', OPTIONS)
@@ -0,0 +1,42 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import { resolve } from 'node:path'
const callers = [
'scripts/admin-test-env-acceptance.mjs',
'scripts/client-test-env-acceptance.mjs',
'scripts/client-phase2-test-env-acceptance.mjs',
'scripts/create-pending-review-orders.mjs',
'scripts/pda-final-complete-diagnostic.mjs',
'scripts/admin-test-env-modules/domestic-container.mjs',
'scripts/admin-test-env-modules/fee-items.mjs',
'scripts/admin-test-env-modules/finance-downloads.mjs',
'scripts/admin-test-env-modules/finance-reconciliation-full.mjs',
'scripts/admin-test-env-modules/order-inbound-status-fixtures.mjs',
'scripts/admin-test-env-modules/orders.mjs',
'scripts/admin-test-env-modules/phase3-container-customs-fixture.mjs',
'scripts/admin-test-env-modules/phase3-customs.mjs',
'scripts/admin-test-env-modules/phase3-loading.mjs',
'scripts/admin-test-env-modules/phase3-order-splitting.mjs',
'scripts/admin-test-env-modules/phase4-overseas-business.mjs',
'scripts/admin-test-env-modules/phase4-overseas-client-services.mjs',
'scripts/admin-test-env-modules/phase4-overseas-master-data.mjs',
'scripts/admin-test-env-modules/phase4-overseas-pda.mjs',
'scripts/admin-test-env-modules/suppliers.mjs',
'scripts/admin-test-env-modules/system.mjs',
]
test('all acceptance login callers use the canonical loginId request field', async () => {
let loginCount = 0
for (const file of callers) {
const source = await readFile(resolve(file), 'utf8')
const canonical = source.match(/\/api\/auth\/login[\s\S]{0,180}loginId\s*:/g) || []
assert.ok(canonical.length > 0, `${file} must send loginId`)
loginCount += canonical.length
assert.doesNotMatch(source,
/\/api\/auth\/login[\s\S]{0,180}(?:\busername\s*:|\{\s*username\s*,)/,
`${file} must not send removed username`)
}
assert.ok(loginCount >= 20, `expected broad login coverage, got ${loginCount}`)
})
@@ -101,7 +101,7 @@ process.exitCode = report.success ? 0 : 1
async function sessionModule() {
const clientLogin = await apiStep('phase2-session', 'client login', {
method: 'POST', path: '/api/auth/login',
body: { username: clientUsername, password: clientPassword },
body: { loginId: clientUsername, password: clientPassword },
}, ({ json }) => {
requireSuccess(json)
assertPresent(json.data?.accessToken, 'client accessToken')
@@ -126,7 +126,7 @@ async function sessionModule() {
const platformLogin = await apiStep('phase2-fixture', 'platform fixture account login', {
method: 'POST', path: '/api/auth/login',
body: { username: platformUsername, password: platformPassword },
body: { loginId: platformUsername, password: platformPassword },
}, ({ json }) => {
requireSuccess(json)
assertPresent(json.data?.accessToken, 'platform accessToken')
+19 -19
View File
@@ -148,7 +148,7 @@ async function registrationModule() {
})
const login = await apiStep('registration', 'login before approval', {
method: 'POST', path: '/api/auth/login', body: { username, password: clientPassword },
method: 'POST', path: '/api/auth/login', body: { loginId: username, password: clientPassword },
}, ({ json }) => {
requireSuccess(json)
assertPresent(json.data?.accessToken, 'accessToken')
@@ -286,7 +286,7 @@ async function qualificationAndReviewModule() {
}, ({ json }) => ({ tenantId: json.data.id, status: json.data.status }))
const platformLogin = await apiStep('qualification-review', 'platform reviewer login', {
method: 'POST', path: '/api/auth/login', body: { username: platformUsername, password: platformPassword },
method: 'POST', path: '/api/auth/login', body: { loginId: platformUsername, password: platformPassword },
}, ({ json }) => {
requireSuccess(json)
assertPresent(json.data?.accessToken, 'platform accessToken')
@@ -345,7 +345,7 @@ async function qualificationAndReviewModule() {
async function merchantAfterApprovalModule() {
const login = await apiStep('merchant-session', 'client re-login after approval', {
method: 'POST', path: '/api/auth/login', body: { username, password: clientPassword },
method: 'POST', path: '/api/auth/login', body: { loginId: username, password: clientPassword },
}, ({ json }) => {
requireSuccess(json)
assertPresent(json.data?.accessToken, 'accessToken')
@@ -502,7 +502,7 @@ async function profileManagementModule() {
}, ({ json }) => ({ userId: json.data.userId, username: json.data.username, displayName: json.data.displayName }))
const relogin = await apiStep('profile-management', 're-login after personal profile token revocation', {
method: 'POST', path: '/api/auth/login', body: { username, password: clientPassword },
method: 'POST', path: '/api/auth/login', body: { loginId: username, password: clientPassword },
}, ({ json }) => {
requireSuccess(json)
assertPresent(json.data?.accessToken, 'accessToken')
@@ -692,7 +692,7 @@ async function rbacAndMemberModule() {
}, ({ json }) => ({ membershipId: report.resources.membershipId, rejectionCode: json.code }))
const memberLogin = await apiStep('rbac-member', 'new enterprise member can login', {
method: 'POST', path: '/api/auth/login', body: { username: memberUsername, password: memberPassword },
method: 'POST', path: '/api/auth/login', body: { loginId: memberUsername, password: memberPassword },
}, ({ json }) => {
requireSuccess(json)
assertPresent(json.data?.accessToken, 'member accessToken')
@@ -742,7 +742,7 @@ async function rbacAndMemberModule() {
}, ({ json }) => ({ rejectionCode: json?.code, message: json?.message }))
const disabledRoleLogin = await apiStep('menu-permissions', 'member login succeeds while role is disabled', {
method: 'POST', path: '/api/auth/login', body: { username: memberUsername, password: memberPassword },
method: 'POST', path: '/api/auth/login', body: { loginId: memberUsername, password: memberPassword },
}, ({ json }) => {
requireSuccess(json)
assertPresent(json.data?.accessToken, 'member accessToken')
@@ -769,7 +769,7 @@ async function rbacAndMemberModule() {
assertEqual(json.data?.status, 'ACTIVE', 'role status')
}, ({ json }) => ({ roleId: json.data.id, status: json.data.status }))
const enabledRoleLogin = await apiStep('menu-permissions', 'member re-login after role is enabled', {
method: 'POST', path: '/api/auth/login', body: { username: memberUsername, password: memberPassword },
method: 'POST', path: '/api/auth/login', body: { loginId: memberUsername, password: memberPassword },
}, ({ json }) => {
requireSuccess(json)
assertPresent(json.data?.accessToken, 'member accessToken')
@@ -800,7 +800,7 @@ async function rbacAndMemberModule() {
}, ({ json }) => ({ rejectionCode: json?.code, message: json?.message }))
const reducedPermissionLogin = await apiStep('menu-permissions', 'member re-login after permission removal', {
method: 'POST', path: '/api/auth/login', body: { username: memberUsername, password: memberPassword },
method: 'POST', path: '/api/auth/login', body: { loginId: memberUsername, password: memberPassword },
}, ({ json }) => {
requireSuccess(json)
assertPresent(json.data?.accessToken, 'member accessToken')
@@ -824,7 +824,7 @@ async function rbacAndMemberModule() {
body: { roleId, permissionIds },
}, ({ json }) => requireSuccess(json), () => ({ roleId, permissionCodes: desiredCodes }))
const restoredPermissionLogin = await apiStep('menu-permissions', 'member re-login after permission restoration', {
method: 'POST', path: '/api/auth/login', body: { username: memberUsername, password: memberPassword },
method: 'POST', path: '/api/auth/login', body: { loginId: memberUsername, password: memberPassword },
}, ({ json }) => {
requireSuccess(json)
assertPresent(json.data?.accessToken, 'member accessToken')
@@ -860,13 +860,13 @@ async function rbacAndMemberModule() {
body: { newPassword: resetMemberPassword },
}, ({ json }) => requireSuccess(json), () => ({ membershipId }))
await apiStep('rbac-member', 'member old password is rejected after reset', {
method: 'POST', path: '/api/auth/login', body: { username: memberUsername, password: memberPassword },
method: 'POST', path: '/api/auth/login', body: { loginId: memberUsername, password: memberPassword },
}, ({ json, status }) => {
assertEqual(status, 200, 'HTTP status')
assertEqual(String(json?.code), 'A0401', 'business code')
}, ({ json }) => ({ username: memberUsername, rejectionCode: json.code }))
const resetLogin = await apiStep('rbac-member', 'member can login with reset password', {
method: 'POST', path: '/api/auth/login', body: { username: memberUsername, password: resetMemberPassword },
method: 'POST', path: '/api/auth/login', body: { loginId: memberUsername, password: resetMemberPassword },
}, ({ json }) => {
requireSuccess(json)
assertPresent(json.data?.accessToken, 'member accessToken')
@@ -886,7 +886,7 @@ async function rbacAndMemberModule() {
assertTrue(['A0401', 'A0403'].includes(String(json?.code)), `expected A0401/A0403, actual=${json?.code}`)
}, ({ json }) => ({ membershipId, rejectionCode: json?.code }))
await apiStep('rbac-member', 'disabled member login is rejected', {
method: 'POST', path: '/api/auth/login', body: { username: memberUsername, password: resetMemberPassword },
method: 'POST', path: '/api/auth/login', body: { loginId: memberUsername, password: resetMemberPassword },
}, ({ json, status }) => {
assertEqual(status, 200, 'HTTP status')
assertTrue(['A0401', 'A0403'].includes(String(json?.code)), `expected A0401/A0403, actual=${json?.code}`)
@@ -899,7 +899,7 @@ async function rbacAndMemberModule() {
}, ({ json }) => ({ membershipId: json.data.membershipId, status: json.data.status }))
const enabledMemberLogin = await apiStep('rbac-member', 'member re-login after account is enabled', {
method: 'POST', path: '/api/auth/login', body: { username: memberUsername, password: resetMemberPassword },
method: 'POST', path: '/api/auth/login', body: { loginId: memberUsername, password: resetMemberPassword },
}, ({ json }) => {
requireSuccess(json)
assertPresent(json.data?.accessToken, 'member accessToken')
@@ -927,7 +927,7 @@ async function rbacAndMemberModule() {
assertEqual(String(json?.code), 'A0401', 'business code')
}, ({ json }) => ({ rejectionCode: json.code }))
const noRoleLogin = await apiStep('rbac-member', 'member re-login after role clearing', {
method: 'POST', path: '/api/auth/login', body: { username: memberUsername, password: resetMemberPassword },
method: 'POST', path: '/api/auth/login', body: { loginId: memberUsername, password: resetMemberPassword },
}, ({ json }) => {
requireSuccess(json)
assertPresent(json.data?.accessToken, 'member accessToken')
@@ -1294,7 +1294,7 @@ async function isolationModule() {
return
}
const login = await apiStep('isolation', 'existing client login', {
method: 'POST', path: '/api/auth/login', body: { username: existingClientUsername, password: existingClientPassword },
method: 'POST', path: '/api/auth/login', body: { loginId: existingClientUsername, password: existingClientPassword },
}, ({ json }) => {
requireSuccess(json)
assertPresent(json.data?.accessToken, 'accessToken')
@@ -1603,14 +1603,14 @@ async function accountSecurityModule() {
ownerPasswordNeedsRestore = true
await apiStep('account-security', 'original password is rejected after change', {
method: 'POST', path: '/api/auth/login', body: { username, password: clientPassword },
method: 'POST', path: '/api/auth/login', body: { loginId: username, password: clientPassword },
}, ({ json, status }) => {
assertEqual(status, 200, 'HTTP status')
assertEqual(String(json?.code), 'A0401', 'business code')
}, ({ json }) => ({ username, rejectionCode: json.code }))
const temporaryLogin = await apiStep('account-security', 'temporary password login succeeds', {
method: 'POST', path: '/api/auth/login', body: { username, password: temporaryOwnerPassword },
method: 'POST', path: '/api/auth/login', body: { loginId: username, password: temporaryOwnerPassword },
}, ({ json }) => {
requireSuccess(json)
assertPresent(json.data?.accessToken, 'accessToken')
@@ -1625,7 +1625,7 @@ async function accountSecurityModule() {
temporaryOwnerPassword = undefined
const restoredLogin = await apiStep('account-security', 'restored owner password login succeeds', {
method: 'POST', path: '/api/auth/login', body: { username, password: clientPassword },
method: 'POST', path: '/api/auth/login', body: { loginId: username, password: clientPassword },
}, ({ json }) => {
requireSuccess(json)
assertPresent(json.data?.accessToken, 'accessToken')
@@ -1655,7 +1655,7 @@ async function accountSecurityModule() {
async function recoverOwnerPassword() {
try {
const login = await apiStep('account-security-recovery', 'login with temporary owner password for recovery', {
method: 'POST', path: '/api/auth/login', body: { username, password: temporaryOwnerPassword },
method: 'POST', path: '/api/auth/login', body: { loginId: username, password: temporaryOwnerPassword },
}, ({ json }) => {
requireSuccess(json)
assertPresent(json.data?.accessToken, 'recovery accessToken')
+1 -1
View File
@@ -368,7 +368,7 @@ export async function main(argv = process.argv.slice(2)) {
const orders = []
const loginResponse = await request(gateway, timeoutMs, 'POST', '/api/auth/login', {
body: { username, password },
body: { loginId: username, password },
})
const login = requireSuccess(loginResponse, 'Merchant login')
if (!login?.accessToken) throw new Error('Merchant login did not return accessToken')
+2 -2
View File
@@ -89,8 +89,8 @@ async function uploadEvidence(tenantId, inboundId, evidenceType) {
const ok = (response) => response?.http === 200 && response?.code === '200'
const id = (input) => input == null ? '' : String(input)
const login = await call('POST', '/api/auth/login', { username, password })
login.body = { username, password: '[REDACTED]' }
const login = await call('POST', '/api/auth/login', { loginId: username, password })
login.body = { loginId: username, password: '[REDACTED]' }
if (!ok(login) || !login.data?.accessToken) {
login.data = null
const failedReport = { marker, startedAt: new Date().toISOString(), gateway, taskIds, stopped: true,