diff --git a/AGENTS.md b/AGENTS.md index c7935c8aa..63a126216 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -49,13 +49,13 @@ - 通用配置在 `config/nacos/{local,cn}/`;服务日志、上传文件、`target/`、`reports/` 不属于源码。 - 全量验证:`mvn -DskipTests validate`、`mvn test`;单服务优先 `mvn -pl -am test`。不要在本地启动 Java/Nacos/数据库,除非用户明确要求。 -## 全量测试脚本路由 +## 全量 HTTP 验收脚本路由 -- 本地后端静态/单元回归:在 `baiyue-firstleg/` 执行 `pwsh scripts/backend-full-regression.ps1 -DryRun` 先检查覆盖文件,再按需去掉 `-DryRun` 执行 `order + firstleg` Maven 测试;脚本不访问数据库、Gateway 或测试环境。 -- 本地前端回归:在 `baiyue-firstleg/` 执行 `pwsh scripts/frontend-full-regression.ps1 -DryRun` 先检查入口,再按需执行 Admin/Client 的 `pnpm test` 和 `pnpm typecheck`;脚本不启动开发服务器、不访问 Gateway。 -- 测试环境 API 全量验收:`scripts/admin-test-env-acceptance.mjs`、`scripts/client-test-env-acceptance.mjs`、`scripts/phase2-test-env-acceptance.ps1` 及 `scripts/admin-test-env-modules/` 属于有数据写入的场景脚本,会创建测试客户、订单或业务记录;只有用户在当前对话明确授权测试环境验收时才执行,并使用报告目录留存结果。 -- `scripts/local-db-full-acceptance.ps1` 仅允许 `127.0.0.1`/`localhost` 数据库,先用 `-DryRun` 校验统一 SQL 快照和注释门禁;不得改参数连接远程或测试数据库。 -- 验收脚本按“先本地静态回归,再测试环境 API 验收,最后人工浏览器样式验收”顺序使用;脚本通过不等于页面视觉、权限和真实数据链路全部通过。 +- 全量验收唯一指向测试环境 HTTP:平台端执行 `scripts/admin-test-env-acceptance.mjs`,客户端执行 `scripts/client-test-env-acceptance.mjs`;二期兼容场景才单独执行 `scripts/client-phase2-test-env-acceptance.mjs`。 +- Admin 总入口默认按模块列表覆盖平台全链路;需要定位单个业务时使用 `--modules `,模块名称和静态合同先查 `scripts/admin-test-env-modules/README.md`。 +- 这些脚本会创建、更新或最终停用测试数据,必须在当前对话明确授权测试环境验收后执行,并保存 JSON 报告;未授权不得请求 Gateway。 +- 后端 Maven、前端 `pnpm test/typecheck`、PDA `node --test` 和数据库 Dry-run 都是本地质量检查,不属于全量 HTTP 验收。 +- HTTP 全量通过后,才进入 Admin/Client 真实页面调用、浏览器样式和 PDA 页面/设备验收;自动化 HTTP 通过不代表视觉验收完成。 ## 阶段文档入口 diff --git a/deploy/sql/头程数据库统一增量-MySQL8.sql b/deploy/sql/头程数据库统一增量-MySQL8.sql index 3b88ddc85..346c5cb1a 100644 --- a/deploy/sql/头程数据库统一增量-MySQL8.sql +++ b/deploy/sql/头程数据库统一增量-MySQL8.sql @@ -2401,7 +2401,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_order ( order_id BIGINT UNSIGNED NOT NULL COMMENT '订单服务订单ID,不建立跨库外键', order_no VARCHAR(32) NOT NULL COMMENT '订单号快照', inbound_code VARCHAR(40) NOT NULL COMMENT '客户内唯一入仓码', - process_status VARCHAR(24) NOT NULL DEFAULT 'WAIT_MEASURE' COMMENT '入库流程状态:WAIT_MEASURE待测量、MEASURING测量中、MEASURE_PENDING待审核、MEASURE_REJECTED测量驳回、WAIT_PRICING待计费、FEE_CONFIRMING费用确认中、INBOUNDING入库收口中、INBOUNDED已入库、WAIT_SPLIT历史待分柜', + process_status VARCHAR(24) NOT NULL DEFAULT 'WAIT_MEASURE' COMMENT '入库流程状态:WAIT_MEASURE待测量、MEASURING测量中、MEASURE_PENDING待审核、MEASURE_REJECTED测量驳回、WAIT_PRICING待计费、FEE_CONFIRMING费用确认中、INBOUNDING入库收口中、WAIT_SPLIT待分柜', order_status_snapshot VARCHAR(24) NOT NULL COMMENT '订单状态快照', measure_review_status VARCHAR(24) NOT NULL DEFAULT 'NOT_SUBMITTED' COMMENT '测量审核状态:NOT_SUBMITTED未提交、PENDING待审核、APPROVED已通过、REJECTED已驳回', fee_status VARCHAR(24) NOT NULL DEFAULT 'UNPRICED' COMMENT '费用状态:UNPRICED未计费、CALCULATED已计算、CONFIRMED已确认、SENT已发送、VOIDED已作废', @@ -12162,7 +12162,7 @@ CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'package_count' CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'pallet_count', '已分配总托盘数快照'); CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'parent_inbound_id', '直属主入仓单ID;主单为空'); CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'pricing_tax_mode', '报价包税维度:TAX_INCLUDED/TAX_EXCLUDED'); -CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'process_status', '入库流程状态:WAIT_MEASURE待测量、MEASURING测量中、MEASURE_PENDING待审核、MEASURE_REJECTED测量驳回、WAIT_PRICING待计费、FEE_CONFIRMING费用确认中、INBOUNDING入库收口中、INBOUNDED已入库、WAIT_SPLIT历史待分柜'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'process_status', '入库流程状态:WAIT_MEASURE待测量、MEASURING测量中、MEASURE_PENDING待审核、MEASURE_REJECTED测量驳回、WAIT_PRICING待计费、FEE_CONFIRMING费用确认中、INBOUNDING入库收口中、WAIT_SPLIT待分柜'); CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'public_remark', '客户可见备注'); CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'root_inbound_id', '根主入仓单ID;存量主单可为空,按自身ID解释'); CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'split_purpose', '履约分片用途:MASTER、LOADED、REMAINDER'); @@ -13889,7 +13889,6 @@ INSERT INTO baiyue_iam.iam_permission (id, tenant_id, app_code, permission_type, parent_id, permission_code, permission_name, route_path, http_method, sort_no, status, deleted) VALUES (5105000, 0, 'PLATFORM', 'MENU', 0, 'platform:finance', '财务管理', '/finance', NULL, 60, 'ACTIVE', 0), - (5105010, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:bill:view', '查看账单', NULL, 'GET', 10, 'ACTIVE', 0), (5105011, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:bill:create', '创建账单', NULL, 'POST', 20, 'ACTIVE', 0), (5105012, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:bill:update', '修改草稿账单', NULL, 'PUT', 30, 'ACTIVE', 0), (5105013, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:bill:issue', '出账', NULL, 'POST', 40, 'ACTIVE', 0), @@ -13925,8 +13924,6 @@ BEGIN OR (id = 5105030 AND permission_code <> 'platform:finance:reconciliation:void') OR (id = 5105031 AND permission_code <> 'platform:finance:settlement-ar:create') OR (id = 5105032 AND permission_code <> 'platform:finance:settlement-ap:create') - OR (id = 5105033 AND permission_code <> 'platform:finance:settlement-ar:view') - OR (id = 5105034 AND permission_code <> 'platform:finance:settlement-ap:view') OR (id = 5105035 AND permission_code <> 'platform:finance:payment:create') OR (id = 5105036 AND permission_code <> 'platform:finance:settlement:void') OR (permission_code = 'platform:finance:account:view' AND id <> 5105022) @@ -13940,8 +13937,6 @@ BEGIN OR (permission_code = 'platform:finance:reconciliation:void' AND id <> 5105030) OR (permission_code = 'platform:finance:settlement-ar:create' AND id <> 5105031) OR (permission_code = 'platform:finance:settlement-ap:create' AND id <> 5105032) - OR (permission_code = 'platform:finance:settlement-ar:view' AND id <> 5105033) - OR (permission_code = 'platform:finance:settlement-ap:view' AND id <> 5105034) OR (permission_code = 'platform:finance:payment:create' AND id <> 5105035) OR (permission_code = 'platform:finance:settlement:void' AND id <> 5105036) OR (id BETWEEN 5105022 AND 5105036 @@ -13970,8 +13965,6 @@ BEGIN (5105030, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:reconciliation:void', '作废或重开客户对账单', NULL, 'POST', 210, 'ACTIVE', 1, 0), (5105031, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:settlement-ar:create', '生成应收结算单', NULL, 'POST', 220, 'ACTIVE', 1, 0), (5105032, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:settlement-ap:create', '生成应付结算单', NULL, 'POST', 230, 'ACTIVE', 1, 0), - (5105033, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:settlement-ar:view', '查看应收结算单', NULL, 'GET', 240, 'ACTIVE', 1, 0), - (5105034, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:settlement-ap:view', '查看应付结算单', NULL, 'GET', 250, 'ACTIVE', 1, 0), (5105035, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:payment:create', '登记应付付款', NULL, 'POST', 260, 'ACTIVE', 1, 0), (5105036, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:settlement:void', '作废或重开结算单', NULL, 'POST', 270, 'ACTIVE', 1, 0) ON DUPLICATE KEY UPDATE @@ -14001,10 +13994,8 @@ BEGIN 'platform:finance:reconciliation:void', 'platform:finance:settlement-ar:create', 'platform:finance:settlement-ap:create', - 'platform:finance:settlement-ar:view', - 'platform:finance:settlement-ap:view', 'platform:finance:payment:create', - 'platform:finance:settlement:void')) <> 15 THEN + 'platform:finance:settlement:void')) <> 13 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'finance G1-G4 permission catalog is incomplete'; END IF; @@ -14017,7 +14008,7 @@ INSERT INTO baiyue_iam.iam_role_permission (role_id, permission_id, tenant_id) SELECT 4000001, id, 0 FROM baiyue_iam.iam_permission WHERE tenant_id=0 AND app_code='PLATFORM' AND permission_code IN ( 'platform:finance', - 'platform:finance:bill:view', 'platform:finance:bill:create', + 'platform:finance:bill:create', 'platform:finance:bill:update', 'platform:finance:bill:issue', 'platform:finance:bill:adjust', 'platform:finance:bill:void', 'platform:finance:receipt:view', 'platform:finance:receipt:create', @@ -14032,8 +14023,7 @@ WHERE tenant_id=0 AND app_code='PLATFORM' AND permission_code IN ( 'platform:finance:reconciliation:create', 'platform:finance:reconciliation:review', 'platform:finance:reconciliation:issue', 'platform:finance:reconciliation:adjust', 'platform:finance:reconciliation:void', 'platform:finance:settlement-ar:create', - 'platform:finance:settlement-ap:create', 'platform:finance:settlement-ar:view', - 'platform:finance:settlement-ap:view', 'platform:finance:payment:create', + 'platform:finance:settlement-ap:create', 'platform:finance:payment:create', 'platform:finance:settlement:void') ON DUPLICATE KEY UPDATE tenant_id=VALUES(tenant_id); INSERT INTO baiyue_iam.iam_role_permission (role_id, permission_id, tenant_id) @@ -14045,13 +14035,13 @@ ON DUPLICATE KEY UPDATE tenant_id=VALUES(tenant_id); DELIMITER $$ CREATE PROCEDURE assert_finance_permissions() BEGIN - IF (SELECT COUNT(*) FROM baiyue_iam.iam_permission WHERE tenant_id=0 AND app_code='PLATFORM' AND permission_code LIKE 'platform:finance:%' AND status='ACTIVE' AND deleted=0) <> 27 THEN + IF (SELECT COUNT(*) FROM baiyue_iam.iam_permission WHERE tenant_id=0 AND app_code='PLATFORM' AND permission_code LIKE 'platform:finance:%' AND status='ACTIVE' AND deleted=0) <> 24 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='统一迁移失败:平台 Finance 权限不完整'; END IF; IF (SELECT COUNT(*) FROM baiyue_iam.iam_permission WHERE tenant_id=10001 AND app_code='MERCHANT' AND permission_code IN ('merchant:finance:statement:list','merchant:finance:statement:view') AND status='ACTIVE' AND deleted=0) <> 2 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='统一迁移失败:客户 Finance 只读权限不完整'; END IF; - IF (SELECT COUNT(*) FROM baiyue_iam.iam_role_permission rp JOIN baiyue_iam.iam_permission p ON p.id=rp.permission_id AND p.tenant_id=rp.tenant_id WHERE rp.role_id=4000001 AND rp.tenant_id=0 AND (p.permission_code='platform:finance' OR p.permission_code LIKE 'platform:finance:%')) <> 27 THEN + IF (SELECT COUNT(*) FROM baiyue_iam.iam_role_permission rp JOIN baiyue_iam.iam_permission p ON p.id=rp.permission_id AND p.tenant_id=rp.tenant_id WHERE rp.role_id=4000001 AND rp.tenant_id=0 AND (p.permission_code='platform:finance' OR p.permission_code LIKE 'platform:finance:%')) <> 25 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='统一迁移失败:平台管理员 Finance 授权不完整'; END IF; IF (SELECT COUNT(*) FROM baiyue_iam.iam_role_permission rp JOIN baiyue_iam.iam_permission p ON p.id=rp.permission_id AND p.tenant_id=rp.tenant_id WHERE rp.role_id=4000101 AND rp.tenant_id=10001 AND (p.permission_code='merchant:finance:statement' OR p.permission_code LIKE 'merchant:finance:%')) <> 3 THEN @@ -14431,7 +14421,7 @@ BEGIN END$$ DELIMITER ; --- 二、三期对外统一使用 INBOUNDED;历史 WAIT_SPLIT 仅由查询适配为 INBOUNDED,统一迁移不改写主表或历史状态。 +-- 二、三期入库完成后的唯一状态为 WAIT_SPLIT。 USE baiyue_firstleg; CREATE TABLE IF NOT EXISTS fl_container_exception_history ( id BIGINT UNSIGNED NOT NULL COMMENT '异常历史ID', @@ -14491,7 +14481,14 @@ UPDATE iam_permission SET status='INACTIVE', deleted=1 WHERE tenant_id=0 AND app INSERT IGNORE INTO iam_role_permission (tenant_id, role_id, permission_id, created_at) SELECT 0, r.id, p.id, CURRENT_TIMESTAMP(3) FROM iam_role r JOIN iam_permission p ON p.tenant_id=0 AND p.app_code='PLATFORM' WHERE r.tenant_id=0 AND r.app_code='PLATFORM' AND r.role_code='PLATFORM_ADMIN' AND p.permission_code IN ('platform:finance:customer-account:list','platform:finance:customer-arap:list','platform:finance:customer-statement:list','platform:finance:settlement-ar:list','platform:finance:settlement-ap:list'); -UPDATE iam_permission p JOIN (SELECT 'platform:finance:account:view' code,5105040 parent_id UNION ALL SELECT 'platform:finance:fee-source:view',5105041 UNION ALL SELECT 'platform:finance:fee-source:review',5105041 UNION ALL SELECT 'platform:finance:fee-source:settlement',5105041 UNION ALL SELECT 'platform:finance:reconciliation:create',5105042 UNION ALL SELECT 'platform:finance:reconciliation:review',5105042 UNION ALL SELECT 'platform:finance:reconciliation:issue',5105042 UNION ALL SELECT 'platform:finance:reconciliation:adjust',5105042 UNION ALL SELECT 'platform:finance:reconciliation:void',5105042 UNION ALL SELECT 'platform:finance:settlement-ar:create',5105043 UNION ALL SELECT 'platform:finance:settlement-ar:view',5105043 UNION ALL SELECT 'platform:finance:settlement-ap:create',5105044 UNION ALL SELECT 'platform:finance:settlement-ap:view',5105044 UNION ALL SELECT 'platform:finance:payment:create',5105044) mapping ON mapping.code=p.permission_code SET p.parent_id=mapping.parent_id WHERE p.tenant_id=0 AND p.app_code='PLATFORM' AND p.permission_type='BUTTON'; +UPDATE iam_permission p JOIN (SELECT 'platform:finance:account:view' code,5105040 parent_id UNION ALL SELECT 'platform:finance:fee-source:view',5105041 UNION ALL SELECT 'platform:finance:fee-source:review',5105041 UNION ALL SELECT 'platform:finance:fee-source:settlement',5105041 UNION ALL SELECT 'platform:finance:reconciliation:create',5105042 UNION ALL SELECT 'platform:finance:reconciliation:review',5105042 UNION ALL SELECT 'platform:finance:reconciliation:issue',5105042 UNION ALL SELECT 'platform:finance:reconciliation:adjust',5105042 UNION ALL SELECT 'platform:finance:reconciliation:void',5105042 UNION ALL SELECT 'platform:finance:settlement-ar:create',5105043 UNION ALL SELECT 'platform:finance:settlement-ap:create',5105044 UNION ALL SELECT 'platform:finance:payment:create',5105044) mapping ON mapping.code=p.permission_code SET p.parent_id=mapping.parent_id WHERE p.tenant_id=0 AND p.app_code='PLATFORM' AND p.permission_type='BUTTON'; + +DELETE rp FROM iam_role_permission rp JOIN iam_permission p ON p.id=rp.permission_id AND p.tenant_id=rp.tenant_id +WHERE p.tenant_id=0 AND p.app_code='PLATFORM' + AND p.permission_code IN('platform:finance:bill:view','platform:finance:settlement-ar:view','platform:finance:settlement-ap:view'); +UPDATE iam_permission SET status='INACTIVE',deleted=1 +WHERE tenant_id=0 AND app_code='PLATFORM' + AND permission_code IN('platform:finance:bill:view','platform:finance:settlement-ar:view','platform:finance:settlement-ap:view'); INSERT INTO baiyue_iam.iam_permission (id, tenant_id, app_code, permission_type, parent_id, permission_code, permission_name, route_path, http_method, sort_no, status, deleted) @@ -14701,6 +14698,6 @@ WHERE p.permission_code IN('platform:firstleg:container:view','platform:firstleg UPDATE iam_permission SET status='INACTIVE',deleted=1 WHERE permission_code IN('platform:firstleg:container:view','platform:firstleg:preload:view','platform:firstleg:loading-list:view','platform:supplier:view'); --- 新完成入库记录使用 WAIT_SPLIT;存量 INBOUNDED 继续作为可分柜兼容状态,统一迁移不改写主表或历史状态。 +-- 入库完成后的唯一状态为 WAIT_SPLIT。 SELECT 'PASS' AS migration_status, '统一增量全部执行完成;父子订单、履约分片、装柜门禁、订舱拖车、报关船期、船司查询、仓库角色、费用、Finance人工财务、公共资源及可靠审计结构校验通过' AS message; diff --git a/docs/系统状态枚举说明.md b/docs/系统状态枚举说明.md index c1c8f478d..795924650 100644 --- a/docs/系统状态枚举说明.md +++ b/docs/系统状态枚举说明.md @@ -54,7 +54,7 @@ | `PROCESSING` | 履约处理中。 | | `COMPLETED` | 订单完成。 | -统一订单列表的主状态是展示分组,不等于订单表的内部状态:`PENDING_REVIEW`、`WAIT_MEASURE`、`INBOUNDED`、`LOADING`、`LOADED`、`DELIVERED`。 +统一订单列表的主状态是展示分组,不等于订单表的内部状态:`PENDING_REVIEW`、`WAIT_MEASURE`、`WAIT_SPLIT`、`LOADING`、`LOADED`、`DELIVERED`。 ## 仓库与库位 @@ -89,13 +89,13 @@ | `WAIT_PRICING` | 测量通过或异常放行,等待计费。 | | `FEE_CONFIRMING` | 已生成费用,等待费用确认和发送。 | | `INBOUNDING` | 费用已确认,正在占用托盘、库位并完成入库。 | -| `INBOUNDED` | 入库完成,可进入智能装柜或预装确认;费用状态独立维护。 | +| `WAIT_SPLIT` | 入库完成,可进入智能装柜或预装确认;费用状态独立维护。 | | `LOADING` | 已进入装柜作业,PDA 可执行拣货。 | | `LOADED` | 装柜完成,等待派送完成。 | | `DELIVERED` | 已完成派送。 | | `VOIDED` | 已作废,不允许继续处理。 | -主链路为:`WAIT_MEASURE` -> `MEASURING` -> `MEASURE_PENDING` -> `WAIT_PRICING` -> `FEE_CONFIRMING` -> `INBOUNDING` -> `INBOUNDED` -> `LOADING` -> `LOADED` -> `DELIVERED`。测量驳回走 `MEASURE_PENDING` -> `MEASURE_REJECTED`;异常放行后回到 `WAIT_PRICING`。 +主链路为:`WAIT_MEASURE` -> `MEASURING` -> `MEASURE_PENDING` -> `WAIT_PRICING` -> `FEE_CONFIRMING` -> `INBOUNDING` -> `WAIT_SPLIT` -> `LOADING` -> `LOADED` -> `DELIVERED`。测量驳回走 `MEASURE_PENDING` -> `MEASURE_REJECTED`;异常放行后回到 `WAIT_PRICING`。 ### 测量、重量和费用状态 diff --git a/finance/src/main/java/com/baiyue/finance/controller/FinancePhase1OperationController.java b/finance/src/main/java/com/baiyue/finance/controller/FinancePhase1OperationController.java index bfd3616e7..a9fd3597f 100644 --- a/finance/src/main/java/com/baiyue/finance/controller/FinancePhase1OperationController.java +++ b/finance/src/main/java/com/baiyue/finance/controller/FinancePhase1OperationController.java @@ -27,7 +27,7 @@ public class FinancePhase1OperationController { public FinancePhase1OperationController(FinancePhase1OperationService service) { this.service = service; } @GetMapping("/accounts") - @PreAuthorize("hasAnyAuthority('platform:finance:customer-account:list','platform:finance:account:view')") + @PreAuthorize("hasAuthority('platform:finance:customer-account:list')") public Object listDerivedCustomerAccounts(@RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "20") int pageSize, @RequestParam(required = false) String keyword, @@ -36,7 +36,7 @@ public class FinancePhase1OperationController { } @GetMapping("/accounts/{customerId}/ledger") - @PreAuthorize("hasAnyAuthority('platform:finance:customer-account:list','platform:finance:account:view')") + @PreAuthorize("hasAuthority('platform:finance:customer-account:list')") public Object listDerivedCustomerAccountLedger(@PathVariable long customerId, @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "20") int pageSize, @@ -47,7 +47,7 @@ public class FinancePhase1OperationController { } @GetMapping("/fee-sources/{feeSourceId}") - @PreAuthorize("hasAnyAuthority('platform:finance:customer-arap:list','platform:finance:fee-source:view')") + @PreAuthorize("hasAuthority('platform:finance:customer-arap:list')") public ApiResponse getFinanceFeeSource(@PathVariable long feeSourceId) { return FinanceResponses.success(service.getFinanceFeeSource(feeSourceId)); } diff --git a/finance/src/main/java/com/baiyue/finance/controller/FinanceSettlementController.java b/finance/src/main/java/com/baiyue/finance/controller/FinanceSettlementController.java index 54fdb92af..fd335a729 100644 --- a/finance/src/main/java/com/baiyue/finance/controller/FinanceSettlementController.java +++ b/finance/src/main/java/com/baiyue/finance/controller/FinanceSettlementController.java @@ -41,7 +41,7 @@ public class FinanceSettlementController { } @GetMapping("/receivable-settlements") - @PreAuthorize("hasAnyAuthority('platform:finance:settlement-ar:list','platform:finance:settlement-ar:view')") + @PreAuthorize("hasAuthority('platform:finance:settlement-ar:list')") @Operation(summary = "查询正式应收结算单") public SettlementListEnvelope receivableSettlements( @Parameter(description = "可选客户筛选;省略时查询全部客户") @@ -50,7 +50,7 @@ public class FinanceSettlementController { } @GetMapping("/payable-settlements") - @PreAuthorize("hasAnyAuthority('platform:finance:settlement-ap:list','platform:finance:settlement-ap:view')") + @PreAuthorize("hasAuthority('platform:finance:settlement-ap:list')") @Operation(summary = "查询正式应付结算单") public SettlementListEnvelope payableSettlements( @Parameter(description = "可选客户筛选;省略时查询全部客户") @@ -77,17 +77,14 @@ public class FinanceSettlementController { } @GetMapping("/settlements/{settlementId}") - @PreAuthorize("hasAnyAuthority('platform:finance:settlement-ar:list','platform:finance:settlement-ar:view','platform:finance:settlement-ap:list','platform:finance:settlement-ap:view')") + @PreAuthorize("hasAnyAuthority('platform:finance:settlement-ar:list','platform:finance:settlement-ap:list')") public SettlementDetailEnvelope getSettlement(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable long settlementId) { SettlementDetail detail = service.getSettlement(settlementId); String direction = detail.summary().direction(); String listPermission = "RECEIVABLE".equals(direction) ? "platform:finance:settlement-ar:list" : "platform:finance:settlement-ap:list"; - String legacyViewPermission = "RECEIVABLE".equals(direction) - ? "platform:finance:settlement-ar:view" : "platform:finance:settlement-ap:view"; - if (user == null || (!user.permissions().contains(listPermission) - && !user.permissions().contains(legacyViewPermission))) { + if (user == null || !user.permissions().contains(listPermission)) { throw new AccessDeniedException("无权查看该方向结算单"); } return success(detail); diff --git a/finance/src/main/java/com/baiyue/finance/controller/PlatformFinanceController.java b/finance/src/main/java/com/baiyue/finance/controller/PlatformFinanceController.java index 520d80be3..9c9580fea 100644 --- a/finance/src/main/java/com/baiyue/finance/controller/PlatformFinanceController.java +++ b/finance/src/main/java/com/baiyue/finance/controller/PlatformFinanceController.java @@ -45,7 +45,7 @@ public class PlatformFinanceController { } @GetMapping("/customer-accounts/{customerId}") - @PreAuthorize("hasAnyAuthority('platform:finance:customer-account:list','platform:finance:account:view','platform:finance:bill:view')") + @PreAuthorize("hasAuthority('platform:finance:customer-account:list')") @Operation(summary = "客户人工应收账户及流水") public ApiResponse customerAccount(@PathVariable long customerId) { return FinanceResponses.success(queryService.customerAccount(customerId)); @@ -59,7 +59,7 @@ public class PlatformFinanceController { } @GetMapping("/fee-pool") - @PreAuthorize("hasAnyAuthority('platform:finance:customer-arap:list','platform:finance:bill:view')") + @PreAuthorize("hasAuthority('platform:finance:customer-arap:list')") @Operation(summary = "客户应收应付费用池") public ApiResponse feePool( @Parameter(description = "可选客户筛选;省略时查询全部客户") @@ -69,7 +69,7 @@ public class PlatformFinanceController { } @GetMapping("/statements") - @PreAuthorize("hasAnyAuthority('platform:finance:customer-statement:list','platform:finance:bill:view')") + @PreAuthorize("hasAuthority('platform:finance:customer-statement:list')") @Operation(summary = "客户对账单") public ApiResponse statements( @Parameter(description = "可选客户筛选;省略时查询全部客户") @@ -94,7 +94,7 @@ public class PlatformFinanceController { public ApiResponse terms(@PathVariable long tenantId) { return FinanceResponses.success(termsView(tenantId, service.customerTerms(tenantId))); } @GetMapping("/bills") - @PreAuthorize("hasAnyAuthority('platform:finance:customer-statement:list','platform:finance:bill:view')") + @PreAuthorize("hasAuthority('platform:finance:customer-statement:list')") @Operation(summary = "查询平台账单") public ApiResponse bills(@RequestParam long customerId) { return FinanceResponses.success(queryService.statements(customerId, false)); } @@ -122,11 +122,11 @@ public class PlatformFinanceController { } @GetMapping("/bills/{billId}") - @PreAuthorize("hasAnyAuthority('platform:finance:customer-statement:list','platform:finance:bill:view')") + @PreAuthorize("hasAuthority('platform:finance:customer-statement:list')") public ApiResponse bill(@PathVariable long billId) { return FinanceResponses.success(queryService.platformStatement(billId)); } @GetMapping("/bills/{billId}/revisions") - @PreAuthorize("hasAnyAuthority('platform:finance:customer-statement:list','platform:finance:bill:view')") + @PreAuthorize("hasAuthority('platform:finance:customer-statement:list')") public ApiResponse revisions(@PathVariable long billId, @RequestParam long customerId) { return FinanceResponses.success(queryService.billRevisions(customerId, billId)); } diff --git a/finance/src/main/java/com/baiyue/finance/controller/ReconciliationStatementController.java b/finance/src/main/java/com/baiyue/finance/controller/ReconciliationStatementController.java index 8280e06fe..a799e61ea 100644 --- a/finance/src/main/java/com/baiyue/finance/controller/ReconciliationStatementController.java +++ b/finance/src/main/java/com/baiyue/finance/controller/ReconciliationStatementController.java @@ -36,7 +36,7 @@ public class ReconciliationStatementController { } @GetMapping("/reconciliation-statements/{statementId}") - @PreAuthorize("hasAnyAuthority('platform:finance:customer-statement:list','platform:finance:bill:view')") + @PreAuthorize("hasAuthority('platform:finance:customer-statement:list')") public ApiResponse getReconciliationStatement(@PathVariable long statementId) { return FinanceResponses.success(service.detailForPlatform(statementId)); } diff --git a/finance/src/test/java/com/baiyue/finance/FinancePhase1PermissionContractTest.java b/finance/src/test/java/com/baiyue/finance/FinancePhase1PermissionContractTest.java index b85ef3c7c..7d974366c 100644 --- a/finance/src/test/java/com/baiyue/finance/FinancePhase1PermissionContractTest.java +++ b/finance/src/test/java/com/baiyue/finance/FinancePhase1PermissionContractTest.java @@ -15,20 +15,20 @@ class FinancePhase1PermissionContractTest { @Test void freezesG1AndG2ControllerPermissionsAndHttpActions() throws Exception { assertEndpoint("listDerivedCustomerAccounts", GetMapping.class, - "platform:finance:customer-account:list", "platform:finance:account:view", + "platform:finance:customer-account:list", int.class, int.class, String.class, String.class); assertEndpoint("listDerivedCustomerAccountLedger", GetMapping.class, - "platform:finance:customer-account:list", "platform:finance:account:view", + "platform:finance:customer-account:list", long.class, int.class, int.class, String.class, java.time.LocalDate.class, java.time.LocalDate.class); assertEndpoint("getFinanceFeeSource", GetMapping.class, - "platform:finance:customer-arap:list", "platform:finance:fee-source:view", long.class); + "platform:finance:customer-arap:list", long.class); assertEndpoint("reviewFinanceFeeSource", PostMapping.class, - "platform:finance:fee-source:review", null, + "platform:finance:fee-source:review", com.baiyue.common.security.AuthenticatedUser.class, long.class, String.class, com.baiyue.finance.dto.FinancePhase1Dtos.ReviewRequest.class); assertEndpoint("updateFeeSourceSettlementInclusion", PutMapping.class, - "platform:finance:fee-source:settlement", null, + "platform:finance:fee-source:settlement", com.baiyue.common.security.AuthenticatedUser.class, long.class, String.class, com.baiyue.finance.dto.FinancePhase1Dtos.SettlementInclusionRequest.class); } @@ -36,13 +36,10 @@ class FinancePhase1PermissionContractTest { private static void assertEndpoint(String methodName, Class mapping, String authority, - String compatibleAuthority, Class... parameterTypes) throws Exception { Method method = FinancePhase1OperationController.class.getMethod(methodName, parameterTypes); assertThat(method.isAnnotationPresent(mapping)).isTrue(); assertThat(method.getAnnotation(PreAuthorize.class).value()).contains(authority); - if (compatibleAuthority != null) { - assertThat(method.getAnnotation(PreAuthorize.class).value()).contains(compatibleAuthority); - } + assertThat(method.getAnnotation(PreAuthorize.class).value()).doesNotContain(":view"); } } diff --git a/finance/src/test/java/com/baiyue/finance/FinancePlatformListContractTest.java b/finance/src/test/java/com/baiyue/finance/FinancePlatformListContractTest.java index 3da8cea2d..2a4680bd0 100644 --- a/finance/src/test/java/com/baiyue/finance/FinancePlatformListContractTest.java +++ b/finance/src/test/java/com/baiyue/finance/FinancePlatformListContractTest.java @@ -2,6 +2,8 @@ package com.baiyue.finance; import com.baiyue.finance.controller.PlatformFinanceController; import com.baiyue.finance.controller.FinanceSettlementController; +import com.baiyue.finance.controller.FinancePhase1OperationController; +import com.baiyue.finance.controller.ReconciliationStatementController; import io.swagger.v3.oas.annotations.Parameter; import org.junit.jupiter.api.Test; import org.springframework.security.access.prepost.PreAuthorize; @@ -30,10 +32,35 @@ class FinancePlatformListContractTest { Map expected = Map.of( "feePool", "platform:finance:customer-arap:list", "statements", "platform:finance:customer-statement:list", - "receivableSettlements", "platform:finance:settlement-ar:view", - "payableSettlements", "platform:finance:settlement-ap:view"); + "receivableSettlements", "platform:finance:settlement-ar:list", + "payableSettlements", "platform:finance:settlement-ap:list"); for (var entry : expected.entrySet()) { - assertThat(find(entry.getKey()).getAnnotation(PreAuthorize.class).value()).contains(entry.getValue()); + assertThat(find(entry.getKey()).getAnnotation(PreAuthorize.class).value()) + .contains(entry.getValue()) + .doesNotContain(":view"); + } + } + + @Test + void platformReadEndpointsDoNotAcceptLegacyViewAuthorities() { + String[] legacyAuthorities = { + "platform:finance:account:view", + "platform:finance:fee-source:view", + "platform:finance:bill:view", + "platform:finance:settlement-ar:view", + "platform:finance:settlement-ap:view" + }; + for (Class controller : new Class[]{ + PlatformFinanceController.class, + FinancePhase1OperationController.class, + ReconciliationStatementController.class, + FinanceSettlementController.class + }) { + for (Method method : controller.getDeclaredMethods()) { + PreAuthorize permission = method.getAnnotation(PreAuthorize.class); + if (permission == null) continue; + assertThat(permission.value()).doesNotContain(legacyAuthorities); + } } } diff --git a/finance/src/test/java/com/baiyue/finance/FinanceSettlementBehaviorTest.java b/finance/src/test/java/com/baiyue/finance/FinanceSettlementBehaviorTest.java index 26f185f47..ef9417176 100644 --- a/finance/src/test/java/com/baiyue/finance/FinanceSettlementBehaviorTest.java +++ b/finance/src/test/java/com/baiyue/finance/FinanceSettlementBehaviorTest.java @@ -54,7 +54,7 @@ class FinanceSettlementBehaviorTest { FinanceSettlementController controller = new FinanceSettlementController(controllerService); SettlementDetail payable = detail("PAYABLE", "ISSUED", BigDecimal.ZERO, new BigDecimal("100.00"), 0); when(controllerService.getSettlement(88L)).thenReturn(payable); - AuthenticatedUser apViewer = user(Set.of("platform:finance:settlement-ap:view")); + AuthenticatedUser apViewer = user(Set.of("platform:finance:settlement-ap:list")); var response = controller.getSettlement(apViewer, 88L); @@ -62,7 +62,7 @@ class FinanceSettlementBehaviorTest { assertThat(response.data()).isSameAs(payable); assertThat(response.requestId()).isNotBlank(); assertThatThrownBy(() -> controller.getSettlement( - user(Set.of("platform:finance:settlement-ar:view")), 88L)).isInstanceOf(AccessDeniedException.class); + user(Set.of("platform:finance:settlement-ar:list")), 88L)).isInstanceOf(AccessDeniedException.class); } @Test diff --git a/firstleg/src/main/java/com/baiyue/firstleg/controller/MerchantInboundBillingController.java b/firstleg/src/main/java/com/baiyue/firstleg/controller/MerchantInboundBillingController.java index 3e0176848..854814203 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/controller/MerchantInboundBillingController.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/controller/MerchantInboundBillingController.java @@ -29,7 +29,7 @@ public class MerchantInboundBillingController { @GetMapping("/{orderId}/inbound-billing") @PreAuthorize("hasAuthority('merchant:order:view')") @Operation(summary = "查询订单入库结果和最新费用", - description = "客户只读查看已达到 INBOUNDED + APPROVED + CONFIRMED 的二期费用,不需要确认费用。") + description = "客户只读查看已达到 WAIT_SPLIT + APPROVED + CONFIRMED 的二期费用,不需要确认费用。") public ApiResponse find( @AuthenticationPrincipal AuthenticatedUser user, @PathVariable long orderId) { return ApiResponse.success(service.find(FirstlegScopeGuard.merchant(user), orderId)); diff --git a/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformInboundMeasurementReviewController.java b/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformInboundMeasurementReviewController.java index 733ba36ee..8ddd133a3 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformInboundMeasurementReviewController.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformInboundMeasurementReviewController.java @@ -51,7 +51,7 @@ public class PlatformInboundMeasurementReviewController { @Valid @RequestBody MeasurementReviewRequest request) { validateScope(user, tenantId); service.approve(tenantId, user.userId(), user.username(), inboundId, idempotencyKey, request); - // 统一订单主线要求测量审核后直接进入 INBOUNDED,不把计费、库位或其他资料 + // 统一订单主线要求测量审核后直接进入 WAIT_SPLIT,不把计费、库位或其他资料 // 作为本版本的强制门禁。保留原 FeeSnapshotResponse 泛型以兼容既有客户端。 return ApiResponse.success(null); } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/domain/InboundListTab.java b/firstleg/src/main/java/com/baiyue/firstleg/domain/InboundListTab.java index 93336d26a..1a2fa6944 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/domain/InboundListTab.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/domain/InboundListTab.java @@ -10,8 +10,8 @@ public enum InboundListTab { WAIT_REVIEW(List.of("MEASURE_PENDING")), /** 待发送费用:等待计费、费用确认或正在完成入库。 */ WAIT_FEE_SEND(List.of("WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING")), - /** 已入库:费用已发送且入库完成。 */ - INBOUNDED(List.of("INBOUNDED")); + /** 待分柜:入库和费用发布完成,等待形成有效分柜方案。 */ + WAIT_SPLIT(List.of("WAIT_SPLIT")); private final List processStatuses; diff --git a/firstleg/src/main/java/com/baiyue/firstleg/domain/InboundProcessStatus.java b/firstleg/src/main/java/com/baiyue/firstleg/domain/InboundProcessStatus.java index 945bd89ff..c467933fe 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/domain/InboundProcessStatus.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/domain/InboundProcessStatus.java @@ -18,8 +18,6 @@ public final class InboundProcessStatus { public static final String FEE_CONFIRMING = "FEE_CONFIRMING"; /** 费用已确认,正在执行托盘、库位占用和入库收口。 */ public static final String INBOUNDING = "INBOUNDING"; - /** 历史已入库终态;存量数据仍允许进入分柜。 */ - public static final String INBOUNDED = "INBOUNDED"; /** 入库和费用发布完成,等待分柜。 */ public static final String WAIT_SPLIT = "WAIT_SPLIT"; /** 已进入装柜作业,PDA 可执行拣货。 */ @@ -33,7 +31,7 @@ public final class InboundProcessStatus { public static final Set VALUES = Set.of( WAIT_MEASURE, MEASURING, MEASURE_PENDING, MEASURE_REJECTED, - WAIT_PRICING, FEE_CONFIRMING, INBOUNDING, INBOUNDED, WAIT_SPLIT, + WAIT_PRICING, FEE_CONFIRMING, INBOUNDING, WAIT_SPLIT, LOADING, LOADED, DELIVERED, VOIDED ); diff --git a/firstleg/src/main/java/com/baiyue/firstleg/dto/UnifiedInboundQuery.java b/firstleg/src/main/java/com/baiyue/firstleg/dto/UnifiedInboundQuery.java index 003165aa6..f7d79abdb 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/dto/UnifiedInboundQuery.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/dto/UnifiedInboundQuery.java @@ -14,7 +14,7 @@ public record UnifiedInboundQuery( Long tenantId, @Size(max = 64) String keyword, @Size(max = 64) String orderNo, - @Schema(allowableValues = {"PENDING_REVIEW", "WAIT_MEASURE", "INBOUNDED", "WAIT_SPLIT", + @Schema(allowableValues = {"PENDING_REVIEW", "WAIT_MEASURE", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED"}) @Size(max = 32) String mainStatus, Long customerId, diff --git a/firstleg/src/main/java/com/baiyue/firstleg/loading/mapper/LoadingInboundMapper.java b/firstleg/src/main/java/com/baiyue/firstleg/loading/mapper/LoadingInboundMapper.java index d52773a8e..3359c09f3 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/loading/mapper/LoadingInboundMapper.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/loading/mapper/LoadingInboundMapper.java @@ -62,7 +62,7 @@ public interface LoadingInboundMapper { AND loaded.process_status<>'LOADED' AND loaded.deleted=0 SET root.process_status='LOADED', root.updated_by=#{operatorId}, root.version=root.version+1 WHERE root.tenant_id=#{tenantId} AND root.id=#{rootInboundId} AND root.deleted=0 - AND root.inbound_role='MASTER' AND root.process_status IN ('INBOUNDED','LOADING') + AND root.inbound_role='MASTER' AND root.process_status IN ('WAIT_SPLIT','LOADING') AND remainder.id IS NULL AND loaded.id IS NULL """) int aggregateRootLoaded(@Param("tenantId") long tenantId, diff --git a/firstleg/src/main/java/com/baiyue/firstleg/mapper/InboundSplitSettlementMapper.java b/firstleg/src/main/java/com/baiyue/firstleg/mapper/InboundSplitSettlementMapper.java index a252cd986..5917fe87d 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/mapper/InboundSplitSettlementMapper.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/mapper/InboundSplitSettlementMapper.java @@ -46,7 +46,7 @@ public interface InboundSplitSettlementMapper { express_tracking_no,public_remark,created_by,created_by_name,created_at,updated_by,updated_at,deleted,version, inbound_role,parent_inbound_id,root_inbound_id,split_sequence,split_status,split_purpose) SELECT #{childId},tenant_id,task_id,#{childOrderId},#{childOrderNo},#{childInboundCode}, - CASE WHEN #{splitPurpose}='LOADED' THEN 'LOADING' ELSE 'INBOUNDED' END,order_status_snapshot, + CASE WHEN #{splitPurpose}='LOADED' THEN 'LOADING' ELSE 'WAIT_SPLIT' END,order_status_snapshot, 'APPROVED',fee_status,fee_review_status,notification_status,expected_arrival_at,customer_id,customer_name, company_id,company_name,warehouse_id,warehouse_name,delivery_warehouse_id,delivery_warehouse_name,business_type, channel_code,channel_name,carrier_id,carrier_name,tax_mode,pricing_tax_mode,container_mode,customs_inspection, @@ -151,7 +151,7 @@ public interface InboundSplitSettlementMapper { SET package_count = GREATEST(COALESCE(package_count, total_package_count) - #{packageCount}, 0), split_status = 'PARTIALLY_SPLIT', updated_by = #{operatorId}, version = version + 1 WHERE id = #{parentInboundId} AND tenant_id = #{accountId} AND deleted = 0 - AND inbound_role = 'MASTER' AND process_status = 'INBOUNDED' AND version = #{expectedVersion} + AND inbound_role = 'MASTER' AND process_status = 'WAIT_SPLIT' AND version = #{expectedVersion} """) int markParentPartiallySplit(@Param("accountId") long accountId, @Param("parentInboundId") long parentInboundId, @Param("packageCount") int packageCount, @Param("operatorId") long operatorId, @@ -160,7 +160,7 @@ public interface InboundSplitSettlementMapper { @Update("UPDATE fl_inbound_order SET package_count=#{loadedPackageCount},process_status='LOADING'," + "split_status='FULLY_SPLIT',updated_by=#{operatorId},version=version+1 " + "WHERE id=#{sourceInboundId} AND tenant_id=#{accountId} AND deleted=0 " - + "AND process_status IN ('INBOUNDED','LOADING') AND split_status='NOT_SPLIT' " + + "AND process_status IN ('WAIT_SPLIT','LOADING') AND split_status='NOT_SPLIT' " + "AND COALESCE(package_count,total_package_count)>=#{loadedPackageCount} AND version=#{expectedVersion}") int markSourceLoaded(@Param("accountId") long accountId, @Param("sourceInboundId") long sourceInboundId, @Param("loadedPackageCount") int loadedPackageCount, diff --git a/firstleg/src/main/java/com/baiyue/firstleg/mapper/SmartLoadingCandidateMapper.java b/firstleg/src/main/java/com/baiyue/firstleg/mapper/SmartLoadingCandidateMapper.java index 7b8485b7e..a920cf373 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/mapper/SmartLoadingCandidateMapper.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/mapper/SmartLoadingCandidateMapper.java @@ -23,7 +23,7 @@ import com.baiyue.firstleg.dto.SmartLoadingDtos.Candidate; import org.apache.iba AND m.id=(SELECT MAX(m2.id) FROM fl_inbound_measurement m2 WHERE m2.tenant_id=o.tenant_id AND m2.inbound_id=o.id AND m2.status='APPROVED') JOIN fl_inbound_measurement_item i ON i.tenant_id=o.tenant_id AND i.measurement_id=m.id - WHERE o.warehouse_id=#{warehouseId} AND o.process_status IN ('WAIT_SPLIT','INBOUNDED') + WHERE o.warehouse_id=#{warehouseId} AND o.process_status='WAIT_SPLIT' AND (o.inbound_role='MASTER' OR (o.split_purpose='REMAINDER' AND o.split_status='NOT_SPLIT')) AND i.inbound_detail_id IS NOT NULL AND i.package_count>COALESCE((SELECT SUM(li.package_count) FROM fl_loading_plan_item li @@ -61,7 +61,7 @@ import com.baiyue.firstleg.dto.SmartLoadingDtos.Candidate; import org.apache.iba AND m.id=(SELECT MAX(m2.id) FROM fl_inbound_measurement m2 WHERE m2.tenant_id=o.tenant_id AND m2.inbound_id=o.id AND m2.status='APPROVED') JOIN fl_inbound_measurement_item i ON i.tenant_id=o.tenant_id AND i.measurement_id=m.id - WHERE o.warehouse_id=#{warehouseId} AND o.id=#{inboundId} AND o.process_status IN ('WAIT_SPLIT','INBOUNDED') + WHERE o.warehouse_id=#{warehouseId} AND o.id=#{inboundId} AND o.process_status='WAIT_SPLIT' AND (o.inbound_role='MASTER' OR (o.split_purpose='REMAINDER' AND o.split_status='NOT_SPLIT')) AND i.inbound_detail_id=#{detailId} ORDER BY m.id DESC LIMIT 1 FOR UPDATE diff --git a/firstleg/src/main/java/com/baiyue/firstleg/repository/FbaPricingRepository.java b/firstleg/src/main/java/com/baiyue/firstleg/repository/FbaPricingRepository.java index be2e42b92..e8b16f2d6 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/repository/FbaPricingRepository.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/repository/FbaPricingRepository.java @@ -209,7 +209,7 @@ public class FbaPricingRepository { .eq(InboundPricingEntity::getProcessStatus, processStatus) .in(InboundPricingEntity::getFeeStatus, List.of("UNPRICED", "CALCULATED")) .set(InboundPricingEntity::getFeeStatus, "CALCULATED"); - if (!List.of("WAIT_SPLIT", "INBOUNDED").contains(processStatus)) { + if (!"WAIT_SPLIT".equals(processStatus)) { update.set(InboundPricingEntity::getProcessStatus, "FEE_CONFIRMING"); } if ("KG".equals(chargeMode)) update.set(InboundPricingEntity::getChargeableWeightKg, chargeable); @@ -229,7 +229,7 @@ public class FbaPricingRepository { public int markInboundConfirmed(long tenantId, long inboundId) { return inbounds.update(null, new LambdaUpdateWrapper() .eq(InboundPricingEntity::getTenantId, tenantId).eq(InboundPricingEntity::getId, inboundId) - .in(InboundPricingEntity::getProcessStatus, List.of("FEE_CONFIRMING", "WAIT_SPLIT", "INBOUNDED")) + .in(InboundPricingEntity::getProcessStatus, List.of("FEE_CONFIRMING", "WAIT_SPLIT")) .eq(InboundPricingEntity::getFeeStatus, "CALCULATED") .set(InboundPricingEntity::getFeeStatus, "CONFIRMED")); } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/AutomaticInboundCompletionService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/AutomaticInboundCompletionService.java index 215980b1e..6db70d765 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/AutomaticInboundCompletionService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/AutomaticInboundCompletionService.java @@ -58,7 +58,7 @@ public class AutomaticInboundCompletionService { } private static boolean completed(InboundOrderEntity order) { - return ("WAIT_SPLIT".equals(order.getProcessStatus()) || "INBOUNDED".equals(order.getProcessStatus())) + return "WAIT_SPLIT".equals(order.getProcessStatus()) && "SENT".equals(order.getFeeStatus()); } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/FbaPricingService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/FbaPricingService.java index db192edbb..32474bab7 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/FbaPricingService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/FbaPricingService.java @@ -209,7 +209,7 @@ public class FbaPricingService { private InboundPricingEntity requireEligibleInbound(long tenantId, long inboundId) { InboundPricingEntity inbound = requireApprovedInbound(tenantId, inboundId); - if (!List.of("WAIT_PRICING", "FEE_CONFIRMING", "WAIT_SPLIT", "INBOUNDED").contains(inbound.getProcessStatus()) + if (!List.of("WAIT_PRICING", "FEE_CONFIRMING", "WAIT_SPLIT").contains(inbound.getProcessStatus()) || !List.of("UNPRICED", "CALCULATED").contains(inbound.getFeeStatus())) { throw conflict("当前入库单状态不允许费用试算"); } @@ -235,7 +235,7 @@ public class FbaPricingService { } private static boolean isCompletedInbound(String processStatus) { - return "WAIT_SPLIT".equals(processStatus) || "INBOUNDED".equals(processStatus); + return "WAIT_SPLIT".equals(processStatus); } private static PricingDestinationType destinationType(InboundPricingEntity inbound) { diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/MerchantInboundBillingService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/MerchantInboundBillingService.java index 41a9f399d..931065259 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/MerchantInboundBillingService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/MerchantInboundBillingService.java @@ -114,7 +114,7 @@ public class MerchantInboundBillingService { } private static boolean isPublished(InboundOrderEntity inbound) { - return (InboundProcessStatus.INBOUNDED.equals(inbound.getProcessStatus()) + return (InboundProcessStatus.WAIT_SPLIT.equals(inbound.getProcessStatus()) || "WAIT_SPLIT".equals(inbound.getProcessStatus())) && "APPROVED".equals(inbound.getMeasureReviewStatus()) && "CONFIRMED".equals(inbound.getFeeStatus()); diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/PdaPickSettlementService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/PdaPickSettlementService.java index c436cec15..93bfe6f8a 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/PdaPickSettlementService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/PdaPickSettlementService.java @@ -98,7 +98,7 @@ public class PdaPickSettlementService { ParentRow parent = rootInboundId == parentInboundId ? root : splits.lockParent(parentInboundId); if (parent == null || parent.tenantId() == null || parent.tenantId() <= 0 || !("MASTER".equals(parent.inboundRole()) || "CHILD".equals(parent.inboundRole())) - || !("INBOUNDED".equals(parent.processStatus()) || "LOADING".equals(parent.processStatus())) + || !("WAIT_SPLIT".equals(parent.processStatus()) || "LOADING".equals(parent.processStatus())) || ("MASTER".equals(parent.inboundRole()) && "FULLY_SPLIT".equals(parent.splitStatus()))) { throw conflict("待拆分主入库单不在可拆分状态"); } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/UnifiedInboundQueryService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/UnifiedInboundQueryService.java index b5f778838..d3a06e2ca 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/UnifiedInboundQueryService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/UnifiedInboundQueryService.java @@ -28,7 +28,7 @@ import java.util.stream.Collectors; @Service public class UnifiedInboundQueryService { private static final Set MAIN_STATUSES = Set.of( - "PENDING_REVIEW", "WAIT_MEASURE", "INBOUNDED", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED"); + "PENDING_REVIEW", "WAIT_MEASURE", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED"); private static final Set REVIEW_STATUSES = Set.of("UNRECORDED", "PENDING", "APPROVED", "REJECTED"); @Resource private InboundOrderRepository inbounds; @@ -140,7 +140,8 @@ public class UnifiedInboundQueryService { return switch (status) { case "WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED", "WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING" -> "WAIT_MEASURE"; - case "INBOUNDED", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED" -> status; + case "WAIT_SPLIT" -> "WAIT_SPLIT"; + case "LOADING", "LOADED", "DELIVERED" -> status; default -> null; }; } @@ -150,7 +151,7 @@ public class UnifiedInboundQueryService { return switch (mainStatus) { case "PENDING_REVIEW" -> "APPROVE_ORDER"; case "WAIT_MEASURE" -> "MEASURE_PENDING".equals(processStatus) ? "CONFIRM_MEASUREMENT" : null; - case "INBOUNDED", "WAIT_SPLIT" -> !("INBOUNDED".equals(processStatus) || "WAIT_SPLIT".equals(processStatus)) ? null + case "WAIT_SPLIT" -> !"WAIT_SPLIT".equals(processStatus) ? null : "CONFIRMED".equals(preloadStatus) ? null : preloadStatus == null ? "SMART_SPLIT" : "CONFIRM_PRELOAD"; case "LOADING" -> "COMPLETE_LOADING"; @@ -169,7 +170,6 @@ public class UnifiedInboundQueryService { return switch (mainStatus) { case "WAIT_MEASURE" -> List.of("WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED", "WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING"); - case "INBOUNDED" -> List.of("INBOUNDED"); case "WAIT_SPLIT" -> List.of("WAIT_SPLIT"); case "LOADING", "LOADED", "DELIVERED" -> List.of(mainStatus); default -> List.of("__NO_MATCH__"); diff --git a/firstleg/src/main/java/com/baiyue/firstleg/split/mapper/SplitInboundMapper.java b/firstleg/src/main/java/com/baiyue/firstleg/split/mapper/SplitInboundMapper.java index 93077291a..c608e8c06 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/split/mapper/SplitInboundMapper.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/split/mapper/SplitInboundMapper.java @@ -13,7 +13,7 @@ public interface SplitInboundMapper { @Update("UPDATE fl_inbound_order SET process_status='LOADING', updated_by=#{operatorId}, " + "version=version+1 WHERE tenant_id=#{tenantId} AND id=#{inboundId} AND deleted=0 " - + "AND process_status IN ('WAIT_SPLIT','INBOUNDED') AND version=#{expectedVersion}") + + "AND process_status='WAIT_SPLIT' AND version=#{expectedVersion}") int beginLoading(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId, @Param("operatorId") long operatorId, @Param("expectedVersion") int expectedVersion); diff --git a/firstleg/src/main/java/com/baiyue/firstleg/split/service/SplitPreloadService.java b/firstleg/src/main/java/com/baiyue/firstleg/split/service/SplitPreloadService.java index 4bfb7402a..62e12ce52 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/split/service/SplitPreloadService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/split/service/SplitPreloadService.java @@ -177,7 +177,7 @@ public class SplitPreloadService { } private static boolean isWaitingSplit(String status) { - return "WAIT_SPLIT".equals(status) || "INBOUNDED".equals(status); + return "WAIT_SPLIT".equals(status); } private void addHistory(long tenantId, long inboundId, String from, String to, String action, diff --git a/firstleg/src/main/resources/db/phase2-inbound-schema.sql b/firstleg/src/main/resources/db/phase2-inbound-schema.sql index 95b51d791..71c423dbe 100644 --- a/firstleg/src/main/resources/db/phase2-inbound-schema.sql +++ b/firstleg/src/main/resources/db/phase2-inbound-schema.sql @@ -11,7 +11,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_order ( order_id BIGINT UNSIGNED NOT NULL COMMENT '订单服务订单ID,不建立跨库外键', order_no VARCHAR(32) NOT NULL COMMENT '订单号快照', inbound_code VARCHAR(40) NOT NULL COMMENT '客户内唯一入仓码', - process_status VARCHAR(24) NOT NULL DEFAULT 'WAIT_MEASURE' COMMENT '入库流程状态:WAIT_MEASURE待测量、MEASURING测量中、MEASURE_PENDING待审核、MEASURE_REJECTED测量驳回、WAIT_PRICING待计费、FEE_CONFIRMING费用确认中、INBOUNDING入库收口中、INBOUNDED已入库、WAIT_SPLIT历史待分柜', + process_status VARCHAR(24) NOT NULL DEFAULT 'WAIT_MEASURE' COMMENT '入库流程状态:WAIT_MEASURE待测量、MEASURING测量中、MEASURE_PENDING待审核、MEASURE_REJECTED测量驳回、WAIT_PRICING待计费、FEE_CONFIRMING费用确认中、INBOUNDING入库收口中、WAIT_SPLIT待分柜', order_status_snapshot VARCHAR(24) NOT NULL COMMENT '订单状态快照', measure_review_status VARCHAR(24) NOT NULL DEFAULT 'NOT_SUBMITTED' COMMENT '测量审核状态:NOT_SUBMITTED未提交、PENDING待审核、APPROVED已通过、REJECTED已驳回', fee_status VARCHAR(24) NOT NULL DEFAULT 'UNPRICED' COMMENT '费用状态:UNPRICED未计费、CALCULATED已计算、CONFIRMED已确认、SENT已发送、VOIDED已作废', diff --git a/firstleg/src/main/resources/db/phase3-inbounded-status-unification.sql b/firstleg/src/main/resources/db/phase3-inbounded-status-unification.sql index 557d786eb..b4f273bd6 100644 --- a/firstleg/src/main/resources/db/phase3-inbounded-status-unification.sql +++ b/firstleg/src/main/resources/db/phase3-inbounded-status-unification.sql @@ -1,10 +1,9 @@ --- 新完成入库记录使用 WAIT_SPLIT,存量 INBOUNDED 继续作为可分柜兼容状态。 --- 本脚本不改写业务主表或历史状态,避免影响既有审计记录。 +-- 入库完成后的唯一状态为 WAIT_SPLIT。 USE baiyue_firstleg; -DROP PROCEDURE IF EXISTS assert_inbounded_compatibility; +DROP PROCEDURE IF EXISTS assert_wait_split_status; DELIMITER $$ -CREATE PROCEDURE assert_inbounded_compatibility() +CREATE PROCEDURE assert_wait_split_status() BEGIN IF (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_inbound_order' @@ -13,9 +12,9 @@ BEGIN WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_inbound_status_history' AND column_name IN ('from_status', 'to_status')) <> 2 THEN SIGNAL SQLSTATE '45000' - SET MESSAGE_TEXT = 'WAIT_SPLIT/INBOUNDED 兼容校验失败:入库状态字段不完整'; + SET MESSAGE_TEXT = 'WAIT_SPLIT 状态校验失败:入库状态字段不完整'; END IF; END$$ DELIMITER ; -CALL assert_inbounded_compatibility(); -DROP PROCEDURE assert_inbounded_compatibility; +CALL assert_wait_split_status(); +DROP PROCEDURE assert_wait_split_status; diff --git a/firstleg/src/main/resources/openapi/fba-pricing-v1.yaml b/firstleg/src/main/resources/openapi/fba-pricing-v1.yaml index 7a492901a..694f8417b 100644 --- a/firstleg/src/main/resources/openapi/fba-pricing-v1.yaml +++ b/firstleg/src/main/resources/openapi/fba-pricing-v1.yaml @@ -130,14 +130,14 @@ paths: /api/platform/v1/inbounds/{inboundId}/fees/calculate: post: summary: 按审核通过测量快照试算 FBA 费用 - description: 仅匹配当前唯一 ACTIVE 船期价格表中的 ACTIVE 价格行,按起运仓、渠道、目的仓、pricingTaxMode 和计费单位估价;不再以 carrierId 或客户计费规则作为前置。海派还需按FBA区域和重量阶梯匹配;测量审核后即使主流程已到 INBOUNDED,也可独立试算且不回退主流程状态;需要 firstleg:fba-pricing:calculate 权限。 + description: 仅匹配当前唯一 ACTIVE 船期价格表中的 ACTIVE 价格行,按起运仓、渠道、目的仓、pricingTaxMode 和计费单位估价;不再以 carrierId 或客户计费规则作为前置。海派还需按FBA区域和重量阶梯匹配;测量审核后即使主流程已到 WAIT_SPLIT,也可独立试算且不回退主流程状态;需要 firstleg:fba-pricing:calculate 权限。 parameters: [{$ref: '#/components/parameters/TenantId'}, {$ref: '#/components/parameters/InboundId'}, {$ref: '#/components/parameters/IdempotencyKey'}] requestBody: {content: {application/json: {schema: {$ref: '#/components/schemas/CalculateRequest'}}}} responses: {'200': {description: 成功或业务失败}, '403': {description: 无权限}} /api/platform/v1/inbounds/{inboundId}/fees/confirm: post: summary: 确认最新费用快照 - description: 确认费用只更新费用状态;主流程已到 INBOUNDED 时保持不变。 + description: 确认费用只更新费用状态;主流程已到 WAIT_SPLIT 时保持不变。 parameters: [{$ref: '#/components/parameters/TenantId'}, {$ref: '#/components/parameters/InboundId'}, {$ref: '#/components/parameters/IdempotencyKey'}] responses: {'200': {description: 成功或业务失败}, '403': {description: 无权限}} /api/platform/v1/inbounds/{inboundId}/fees/send: @@ -154,7 +154,7 @@ paths: /api/platform/v1/inbounds/{inboundId}/fees/published: put: summary: 调整已发布费用 - description: 需要 firstleg:fba-pricing:adjust 权限;生成新 SENT 版本并将旧版本标记 SUPERSEDED,不回退 INBOUNDED 状态。 + description: 需要 firstleg:fba-pricing:adjust 权限;生成新 SENT 版本并将旧版本标记 SUPERSEDED,不回退 WAIT_SPLIT 状态。 parameters: [{$ref: '#/components/parameters/TenantId'}, {$ref: '#/components/parameters/InboundId'}, {$ref: '#/components/parameters/IdempotencyKey'}] requestBody: required: true diff --git a/firstleg/src/main/resources/openapi/firstleg-v1.yaml b/firstleg/src/main/resources/openapi/firstleg-v1.yaml index 241498fae..093d4ba94 100644 --- a/firstleg/src/main/resources/openapi/firstleg-v1.yaml +++ b/firstleg/src/main/resources/openapi/firstleg-v1.yaml @@ -68,7 +68,7 @@ paths: post: summary: 按统一订单条件分页查询履约摘要(Order 内部) description: >- - mainStatus 接受七态产品状态;WAIT_SPLIT 为正式待分柜状态,INBOUNDED 仅兼容存量已入库记录。 + mainStatus 接受六态产品状态;WAIT_SPLIT 为入库完成后的唯一待分柜状态。 计费与入库处理中状态在完成前仍归并为 WAIT_MEASURE,不提前开放分柜。 security: - internalServiceToken: [] @@ -102,7 +102,7 @@ paths: /internal/firstleg/v1/inbounds/unified-orders/detail: post: summary: 按订单 ID 查询统一履约详情(Order 内部) - description: 时间线保留 Firstleg 内部 fromStatus/toStatus;summary.mainStatus 使用七态产品状态。 + description: 时间线保留 Firstleg 内部 fromStatus/toStatus;summary.mainStatus 使用六态产品状态。 security: - internalServiceToken: [] requestBody: @@ -390,7 +390,7 @@ paths: description: 需要 platform:firstleg:inbound:list 权限;默认查询全部企业,可选 tenantId 筛选;仅使用 V2 四个页签 parameters: - {name: tenantId, in: query, required: false, schema: {type: string, pattern: '^[1-9][0-9]*$'}} - - {name: tab, in: query, schema: {type: string, enum: [WAIT_MEASURE, WAIT_REVIEW, WAIT_FEE_SEND, INBOUNDED]}} + - {name: tab, in: query, schema: {type: string, enum: [WAIT_MEASURE, WAIT_REVIEW, WAIT_FEE_SEND, WAIT_SPLIT]}} responses: '200': description: 成功或业务失败,按响应体 code 判断;每条记录携带后续客户隔离操作所需的 tenantId,该字段当前表示客户 ID @@ -565,7 +565,7 @@ paths: /api/merchant/v1/first-leg/orders/{orderId}/inbound-billing: get: summary: 客户查询已完成入库结果和最新已发布费用 - description: 需要 merchant:order:view 权限;客户只读查看,不需要确认费用,仅 INBOUNDED + APPROVED + CONFIRMED 的二期终态可见。 + description: 需要 merchant:order:view 权限;客户只读查看,不需要确认费用,仅 WAIT_SPLIT + APPROVED + CONFIRMED 的二期终态可见。 parameters: - {name: orderId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}} responses: @@ -656,8 +656,8 @@ components: schemas: UnifiedMainStatus: type: string - enum: [PENDING_REVIEW, WAIT_MEASURE, INBOUNDED, WAIT_SPLIT, LOADING, LOADED, DELIVERED] - description: 七态产品主状态;WAIT_SPLIT 为正式待分柜状态,INBOUNDED 仅兼容存量已入库记录 + enum: [PENDING_REVIEW, WAIT_MEASURE, WAIT_SPLIT, LOADING, LOADED, DELIVERED] + description: 六态产品主状态;WAIT_SPLIT 为入库完成后的唯一待分柜状态 UnifiedInboundQuery: type: object properties: @@ -666,9 +666,9 @@ components: orderNo: {type: string, maxLength: 64, nullable: true} mainStatus: type: string - enum: [PENDING_REVIEW, WAIT_MEASURE, INBOUNDED, LOADING, LOADED, DELIVERED, WAIT_SPLIT] + enum: [PENDING_REVIEW, WAIT_MEASURE, WAIT_SPLIT, LOADING, LOADED, DELIVERED] nullable: true - description: WAIT_SPLIT 与 INBOUNDED 为独立筛选值 + description: WAIT_SPLIT 为唯一待分柜筛选值 customerId: {type: integer, format: int64, nullable: true} receivingWarehouseId: {type: integer, format: int64, nullable: true} destinationWarehouseId: {type: integer, format: int64, nullable: true} @@ -767,7 +767,7 @@ components: orderId: {type: integer, format: int64, nullable: true} orderNo: {type: string, nullable: true} inboundCode: {type: string, nullable: true} - processStatus: {type: string, nullable: true, description: Firstleg 内部状态;新入库完成记录进入 WAIT_SPLIT,历史记录可能为 INBOUNDED} + processStatus: {type: string, nullable: true, description: Firstleg 内部状态;入库完成记录进入 WAIT_SPLIT} orderStatusSnapshot: {type: string, nullable: true} measureReviewStatus: {type: string, nullable: true} feeStatus: {type: string, nullable: true} diff --git a/firstleg/src/test/java/com/baiyue/firstleg/controller/FbaPricingContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/controller/FbaPricingContractTest.java index 22af59af5..caa9d096f 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/controller/FbaPricingContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/controller/FbaPricingContractTest.java @@ -48,9 +48,9 @@ class FbaPricingContractTest { .contains("/api/platform/v1/inbounds/{inboundId}/fees/send:") .contains("/api/platform/v1/inbounds/{inboundId}/fees/published:") .contains("Idempotency-Key") - .contains("主流程已到 INBOUNDED,也可独立试算且不回退主流程状态") + .contains("主流程已到 WAIT_SPLIT,也可独立试算且不回退主流程状态") .contains("该动作涉及后续仓储流程,本轮二期验收不调用") - .contains("不回退 INBOUNDED 状态") + .contains("不回退 WAIT_SPLIT 状态") .doesNotContain("应收") .doesNotContain("应付") .doesNotContain("待客户确认"); diff --git a/firstleg/src/test/java/com/baiyue/firstleg/controller/InboundOrderControllerContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/controller/InboundOrderControllerContractTest.java index cabc48b84..9337857ce 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/controller/InboundOrderControllerContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/controller/InboundOrderControllerContractTest.java @@ -42,7 +42,7 @@ class InboundOrderControllerContractTest { openapi.indexOf("/api/platform/v1/first-leg/tasks:"), openapi.indexOf("/api/platform/v1/first-leg/tasks/{taskId}:")); assertThat(inboundList) - .doesNotContain("WAIT_SPLIT"); + .contains("WAIT_SPLIT"); assertThat(openapi) .contains("/api/platform/inbounds:") .contains("/api/platform/inbounds/{inboundId}:") @@ -53,7 +53,7 @@ class InboundOrderControllerContractTest { .contains("required: [id, tenantId]") .contains("pricingTaxMode: {type: string, enum: [TAX_INCLUDED, TAX_EXCLUDED]") .contains("tenantId: {type: string, pattern: '^[1-9][0-9]*$', description:") - .contains("[WAIT_MEASURE, WAIT_REVIEW, WAIT_FEE_SEND, INBOUNDED]") + .contains("[WAIT_MEASURE, WAIT_REVIEW, WAIT_FEE_SEND, WAIT_SPLIT]") .contains("platform:firstleg:inbound:list") .doesNotContain("WAIT_CUSTOMER_CONFIRM"); assertThat(inboundList).contains("#/components/schemas/InboundOrderPageEnvelope"); diff --git a/firstleg/src/test/java/com/baiyue/firstleg/controller/InternalInboundOrderReferenceControllerTest.java b/firstleg/src/test/java/com/baiyue/firstleg/controller/InternalInboundOrderReferenceControllerTest.java index 0931f2bad..2522afe1a 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/controller/InternalInboundOrderReferenceControllerTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/controller/InternalInboundOrderReferenceControllerTest.java @@ -54,7 +54,7 @@ class InternalInboundOrderReferenceControllerTest { @Test void unifiedQueryRequiresInternalTokenAndForwardsBatchRequest() { UnifiedInboundQuery request = new UnifiedInboundQuery( - 10001L, null, null, "INBOUNDED", null, null, null, + 10001L, null, null, "WAIT_SPLIT", null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, 1, 20); when(unifiedQueryService.page(request)).thenReturn(new PageResult<>(List.of(), 1, 20, 0, 0)); diff --git a/firstleg/src/test/java/com/baiyue/firstleg/controller/MerchantInboundBillingControllerContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/controller/MerchantInboundBillingControllerContractTest.java index 278bc4f99..76426d658 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/controller/MerchantInboundBillingControllerContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/controller/MerchantInboundBillingControllerContractTest.java @@ -38,7 +38,7 @@ class MerchantInboundBillingControllerContractTest { String openapi = new ClassPathResource("openapi/firstleg-v1.yaml") .getContentAsString(StandardCharsets.UTF_8); assertThat(openapi).contains("/api/merchant/v1/first-leg/orders/{orderId}/inbound-billing:") - .contains("INBOUNDED + APPROVED + CONFIRMED"); + .contains("WAIT_SPLIT + APPROVED + CONFIRMED"); } @Test diff --git a/firstleg/src/test/java/com/baiyue/firstleg/controller/UnifiedInboundOpenApiContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/controller/UnifiedInboundOpenApiContractTest.java index ad1021278..363795dcb 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/controller/UnifiedInboundOpenApiContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/controller/UnifiedInboundOpenApiContractTest.java @@ -12,7 +12,7 @@ import static org.assertj.core.api.Assertions.assertThat; class UnifiedInboundOpenApiContractTest { @Test - void documentsInternalUnifiedOrderEndpointsAndSevenStatuses() throws Exception { + void documentsInternalUnifiedOrderEndpointsAndSixStatuses() throws Exception { String yaml = new ClassPathResource("openapi/firstleg-v1.yaml") .getContentAsString(StandardCharsets.UTF_8); Map document = stringMap(new Yaml().load(yaml)); @@ -25,13 +25,13 @@ class UnifiedInboundOpenApiContractTest { Map schemas = stringMap(stringMap(document.get("components")).get("schemas")); Map responseStatus = stringMap(schemas.get("UnifiedMainStatus")); assertThat(stringList(responseStatus.get("enum"))).containsExactly( - "PENDING_REVIEW", "WAIT_MEASURE", "INBOUNDED", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED"); - assertThat(responseStatus.get("description").toString()).contains("WAIT_SPLIT", "INBOUNDED"); + "PENDING_REVIEW", "WAIT_MEASURE", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED"); + assertThat(responseStatus.get("description").toString()).contains("WAIT_SPLIT", "唯一待分柜状态"); Map query = stringMap(schemas.get("UnifiedInboundQuery")); Map queryStatus = stringMap(stringMap(query.get("properties")).get("mainStatus")); assertThat(stringList(queryStatus.get("enum"))).contains("WAIT_SPLIT"); - assertThat(queryStatus.get("description").toString()).contains("WAIT_SPLIT", "INBOUNDED", "独立"); + assertThat(queryStatus.get("description").toString()).contains("WAIT_SPLIT", "唯一待分柜筛选值"); Map inbound = stringMap(schemas.get("UnifiedInboundOrderSummary")); assertThat(stringMap(inbound.get("properties"))).containsKeys( diff --git a/firstleg/src/test/java/com/baiyue/firstleg/loading/service/LoadingDeliveryServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/loading/service/LoadingDeliveryServiceTest.java index 8667704e6..21f02c468 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/loading/service/LoadingDeliveryServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/loading/service/LoadingDeliveryServiceTest.java @@ -86,7 +86,7 @@ class LoadingDeliveryServiceTest { @Test void completesLoadedFulfillmentSliceBeforeAggregatingRoot() { when(repository.claimOperation(anyLong(), anyLong(), anyLong(), anyString(), anyString(), anyString())).thenReturn(true); - when(repository.findInbound(10, 20)).thenReturn(new Snapshot(20L, 10L, "INBOUNDED", 3)); + when(repository.findInbound(10, 20)).thenReturn(new Snapshot(20L, 10L, "WAIT_SPLIT", 3)); when(repository.completionTarget(10, 20)).thenReturn(new CompletionTarget( 20L, null, 20L, "LOADING", 3, "LOADING", 3)); LoadingPickItemMapper.InboundProgress progress = mock(LoadingPickItemMapper.InboundProgress.class); @@ -180,7 +180,7 @@ class LoadingDeliveryServiceTest { @Test void keepsHistoricalLoadedChildAsCompletionTarget() { CompletionTarget historical = new CompletionTarget( - 20L, 21L, 20L, "INBOUNDED", 3, "LOADING", 1); + 20L, 21L, 20L, "WAIT_SPLIT", 3, "LOADING", 1); assertThat(historical.completionInboundId()).isEqualTo(21L); } diff --git a/firstleg/src/test/java/com/baiyue/firstleg/repository/FbaPricingRepositoryRuleListTest.java b/firstleg/src/test/java/com/baiyue/firstleg/repository/FbaPricingRepositoryRuleListTest.java index be9fd8b35..9e48a0b7d 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/repository/FbaPricingRepositoryRuleListTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/repository/FbaPricingRepositoryRuleListTest.java @@ -156,7 +156,7 @@ class FbaPricingRepositoryRuleListTest { when(pricing.update(org.mockito.ArgumentMatchers.isNull(), org.mockito.ArgumentMatchers.any())) .thenReturn(1); - assertThat(repository.markInboundFeeSent(10001L, 88L, "INBOUNDED")).isEqualTo(1); + assertThat(repository.markInboundFeeSent(10001L, 88L, "WAIT_SPLIT")).isEqualTo(1); @SuppressWarnings("unchecked") ArgumentCaptor> wrapper = @@ -164,7 +164,7 @@ class FbaPricingRepositoryRuleListTest { verify(pricing).update(org.mockito.ArgumentMatchers.isNull(), wrapper.capture()); assertThat(wrapper.getValue().getSqlSet()).contains("feeStatus").doesNotContain("processStatus"); assertThat(wrapper.getValue().getCustomSqlSegment()).contains("tenantId", "id", "processStatus", "feeStatus"); - assertThat(params(wrapper.getValue()).values()).contains(10001L, 88L, "INBOUNDED", "CONFIRMED", "SENT"); + assertThat(params(wrapper.getValue()).values()).contains(10001L, 88L, "WAIT_SPLIT", "CONFIRMED", "SENT"); } @Test diff --git a/firstleg/src/test/java/com/baiyue/firstleg/repository/InboundStatusUnificationSqlContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/repository/InboundStatusUnificationSqlContractTest.java index 06f3c6305..eea8d70ea 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/repository/InboundStatusUnificationSqlContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/repository/InboundStatusUnificationSqlContractTest.java @@ -11,23 +11,23 @@ import static org.assertj.core.api.Assertions.assertThat; class InboundStatusUnificationSqlContractTest { @Test - void preservesHistoricalWaitSplitWithoutDataRewrite() throws Exception { + void enforcesCanonicalWaitSplitStatusWithoutCompatibilityRewrite() throws Exception { String module = normalize(new ClassPathResource("db/phase3-inbounded-status-unification.sql") .getContentAsString(StandardCharsets.UTF_8)).trim(); String unified = normalize(Files.readString(unifiedPath(), StandardCharsets.UTF_8)); assertThat(module) - .contains("CREATE PROCEDURE assert_inbounded_compatibility()") - .contains("新完成入库记录使用 WAIT_SPLIT") - .contains("存量 INBOUNDED") + .contains("CREATE PROCEDURE assert_wait_split_status()") + .contains("入库完成后的唯一状态为 WAIT_SPLIT") + .doesNotContain("兼容") .doesNotContain("UPDATE fl_inbound_order") .doesNotContain("UPDATE fl_inbound_status_history"); assertThat(unified) - .contains("新完成入库记录使用 WAIT_SPLIT") - .contains("存量 INBOUNDED") - .doesNotContain("SET process_status = 'INBOUNDED'") - .doesNotContain("SET from_status = 'INBOUNDED'") - .doesNotContain("SET to_status = 'INBOUNDED'"); + .contains("入库完成后的唯一状态为 WAIT_SPLIT") + .doesNotContain("历史 WAIT_SPLIT") + .doesNotContain("SET process_status = 'WAIT_SPLIT'") + .doesNotContain("SET from_status = 'WAIT_SPLIT'") + .doesNotContain("SET to_status = 'WAIT_SPLIT'"); } private static Path unifiedPath() { diff --git a/firstleg/src/test/java/com/baiyue/firstleg/repository/PdaSplitSettlementSqlContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/repository/PdaSplitSettlementSqlContractTest.java index 39bd6fc31..f3b541a5c 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/repository/PdaSplitSettlementSqlContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/repository/PdaSplitSettlementSqlContractTest.java @@ -76,7 +76,7 @@ class PdaSplitSettlementSqlContractTest { .getAnnotation(Insert.class).value()[0]; assertThat(clone) .contains("#{childOrderId},#{childOrderNo}") - .contains("CASE WHEN #{splitPurpose}='LOADED' THEN 'LOADING' ELSE 'INBOUNDED' END"); + .contains("CASE WHEN #{splitPurpose}='LOADED' THEN 'LOADING' ELSE 'WAIT_SPLIT' END"); } @Test @@ -89,7 +89,7 @@ class PdaSplitSettlementSqlContractTest { long.class, long.class, int.class, long.class, int.class) .getAnnotation(Update.class).value()[0]; assertThat(markSource) - .contains("process_status IN ('INBOUNDED','LOADING')") + .contains("process_status IN ('WAIT_SPLIT','LOADING')") .contains("package_count=#{loadedPackageCount}") .contains("split_status='FULLY_SPLIT'"); } diff --git a/firstleg/src/test/java/com/baiyue/firstleg/repository/Phase3FinalUnifiedSqlContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/repository/Phase3FinalUnifiedSqlContractTest.java index 31d436133..e159cacbe 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/repository/Phase3FinalUnifiedSqlContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/repository/Phase3FinalUnifiedSqlContractTest.java @@ -84,7 +84,7 @@ class Phase3FinalUnifiedSqlContractTest { "-- 来源:iam/src/main/resources/db/phase3-warehouse-roles.sql", "firstleg/src/main/resources/db/phase3-carrier-schedule-query-schema.sql"); assertBlock(unified, "-- 来源:iam/src/main/resources/db/phase3-warehouse-roles.sql", - "-- 新完成入库记录使用 WAIT_SPLIT;存量 INBOUNDED 继续作为可分柜兼容状态,统一迁移不改写主表或历史状态。", + "-- 入库完成后的唯一状态为 WAIT_SPLIT。", "iam/src/main/resources/db/phase3-warehouse-roles.sql"); } diff --git a/firstleg/src/test/java/com/baiyue/firstleg/repository/SmartLoadingCandidateMapperContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/repository/SmartLoadingCandidateMapperContractTest.java index 3772a35a7..69c8962dc 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/repository/SmartLoadingCandidateMapperContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/repository/SmartLoadingCandidateMapperContractTest.java @@ -21,7 +21,7 @@ class SmartLoadingCandidateMapperContractTest { .contains("m2.status='APPROVED'") .contains("i.inbound_detail_id IS NOT NULL") .contains("o.warehouse_id=#{warehouseId}") - .contains("o.process_status IN ('WAIT_SPLIT','INBOUNDED')") + .contains("o.process_status='WAIT_SPLIT'") .contains("SELECT SUM(li.package_count)") .contains("li.deleted=0") .contains("ORDER BY o.inbounded_at ASC, o.id ASC, i.id ASC"); @@ -38,7 +38,7 @@ class SmartLoadingCandidateMapperContractTest { .contains("m.status='APPROVED'") .contains("SELECT MAX(m2.id)") .contains("o.warehouse_id=#{warehouseId}") - .contains("o.process_status IN ('WAIT_SPLIT','INBOUNDED')") + .contains("o.process_status='WAIT_SPLIT'") .contains("SELECT SUM(li.package_count)") .contains("FOR UPDATE"); } diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/AutomaticInboundCompletionServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/AutomaticInboundCompletionServiceTest.java index bde1947ae..3820f378f 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/AutomaticInboundCompletionServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/AutomaticInboundCompletionServiceTest.java @@ -32,7 +32,7 @@ class AutomaticInboundCompletionServiceTest { order("FEE_CONFIRMING", "CALCULATED", 5), order("FEE_CONFIRMING", "CONFIRMED", 6), order("INBOUNDING", "CONFIRMED", 7), - order("INBOUNDED", "SENT", 8)); + order("WAIT_SPLIT", "SENT", 8)); FeeSnapshotResponse result = service.completeApproved(10001L, 88L, 9L, "客服", 30L, 4); @@ -48,7 +48,7 @@ class AutomaticInboundCompletionServiceTest { @Test void shouldHandleCase02() { FeeSnapshotResponse fee = fee(501L); - when(workflow.findOwned(10001L, 88L)).thenReturn(order("INBOUNDED", "SENT", 8)); + when(workflow.findOwned(10001L, 88L)).thenReturn(order("WAIT_SPLIT", "SENT", 8)); when(pricing.latest(10001L, 88L)).thenReturn(fee); assertThat(service.completeApproved(10001L, 88L, 9L, "客服", 30L, 4)).isSameAs(fee); @@ -65,7 +65,7 @@ class AutomaticInboundCompletionServiceTest { FeeSnapshotResponse fee = fee(501L); when(pricing.latest(10001L, 88L)).thenReturn(fee); when(workflow.findOwned(10001L, 88L)).thenReturn( - order("INBOUNDING", "CONFIRMED", 7), order("INBOUNDED", "SENT", 8)); + order("INBOUNDING", "CONFIRMED", 7), order("WAIT_SPLIT", "SENT", 8)); service.completeApproved(10001L, 88L, 9L, "客服", 30L, 4); diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/FbaPricingServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/FbaPricingServiceTest.java index 991093ca8..40699bec3 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/FbaPricingServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/FbaPricingServiceTest.java @@ -185,7 +185,7 @@ class FbaPricingServiceTest { void returnsOriginalSnapshotForRepeatedCalculation() { InboundPricingIdempotencyEntity completed = completed("FEE_CALCULATE", 501L); InboundPricingEntity inbound = inbound("10", "1"); - inbound.setProcessStatus("INBOUNDED"); + inbound.setProcessStatus("WAIT_SPLIT"); when(repository.findIdempotency(TENANT, OPERATOR, "FEE_CALCULATE", "same")).thenReturn(completed); when(repository.findFee(TENANT, 501L)).thenReturn(savedFee("CALCULATED")); when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound); @@ -195,7 +195,7 @@ class FbaPricingServiceTest { FeeSnapshotResponse response = service.calculate(TENANT, OPERATOR, INBOUND, NOW, "same"); assertThat(response.feeId()).isEqualTo(501L); - assertThat(response.nextProcessStatus()).isEqualTo("INBOUNDED"); + assertThat(response.nextProcessStatus()).isEqualTo("WAIT_SPLIT"); verify(repository, never()).matchRules(any(), any()); verify(repository, never()).insertFee(any()); } @@ -236,7 +236,7 @@ class FbaPricingServiceTest { @DisplayName("已入库费用发送只发布费用且不回退主流程状态") void sendsFeeAfterInboundWithoutRollingBackProcessStatus() { InboundPricingEntity inbound = inbound("10", "1"); - inbound.setProcessStatus("INBOUNDED"); + inbound.setProcessStatus("WAIT_SPLIT"); inbound.setFeeStatus("CONFIRMED"); InboundFeeEntity confirmed = savedFee("CONFIRMED"); when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound); @@ -245,14 +245,14 @@ class FbaPricingServiceTest { doAnswer(invocation -> { ((InboundPricingIdempotencyEntity) invocation.getArgument(0)).setId(900L); return null; }) .when(repository).insertIdempotency(any()); when(repository.sendFee(TENANT, 501L, OPERATOR, NOW)).thenReturn(1); - when(repository.markInboundFeeSent(TENANT, INBOUND, "INBOUNDED")).thenReturn(1); + when(repository.markInboundFeeSent(TENANT, INBOUND, "WAIT_SPLIT")).thenReturn(1); when(repository.completeIdempotency(any(), eq(501L), eq(NOW))).thenReturn(1); when(repository.findFee(TENANT, 501L)).thenReturn(confirmed); FeeSnapshotResponse response = service.send(TENANT, OPERATOR, INBOUND, "send-inbounded"); - assertThat(response.nextProcessStatus()).isEqualTo("INBOUNDED"); - verify(repository).markInboundFeeSent(TENANT, INBOUND, "INBOUNDED"); + assertThat(response.nextProcessStatus()).isEqualTo("WAIT_SPLIT"); + verify(repository).markInboundFeeSent(TENANT, INBOUND, "WAIT_SPLIT"); verify(financeOutbox).publishInbound(eq(FinanceFeeSourceOutboxService.PUBLISHED), eq(TENANT), eq(INBOUND), eq(501L), eq(1), eq(NOW)); } @@ -274,7 +274,7 @@ class FbaPricingServiceTest { @DisplayName("已发布费用可生成新版本且不回退已入库状态") void adjustsPublishedFeeWithoutRollingBackInbound() { InboundPricingEntity inbound = inbound("10", "1"); - inbound.setProcessStatus("INBOUNDED"); + inbound.setProcessStatus("WAIT_SPLIT"); inbound.setFeeStatus("SENT"); InboundFeeEntity current = savedFee("SENT"); AtomicReference inserted = new AtomicReference<>(); @@ -301,8 +301,8 @@ class FbaPricingServiceTest { assertThat(response.feeVersion()).isEqualTo(2); assertThat(response.feeStatus()).isEqualTo("SENT"); assertThat(response.totalAmount()).isEqualByComparingTo("120.00"); - assertThat(response.nextProcessStatus()).isEqualTo("INBOUNDED"); - assertThat(inbound.getProcessStatus()).isEqualTo("INBOUNDED"); + assertThat(response.nextProcessStatus()).isEqualTo("WAIT_SPLIT"); + assertThat(inbound.getProcessStatus()).isEqualTo("WAIT_SPLIT"); verify(repository).supersedeSent(TENANT, 501L); verify(financeOutbox).publishInbound(eq(FinanceFeeSourceOutboxService.ADJUSTED), eq(TENANT), eq(INBOUND), eq(502L), eq(2), eq(NOW)); diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/InboundMeasurementServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/InboundMeasurementServiceTest.java index 658cd50f0..dee4e165a 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/InboundMeasurementServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/InboundMeasurementServiceTest.java @@ -506,7 +506,7 @@ class InboundMeasurementServiceTest { @Test void editingAndResubmittingInboundMeasurementDoesNotRollBackMainStatus() { - InboundOrderEntity inbound = inbound("INBOUNDED"); + InboundOrderEntity inbound = inbound("WAIT_SPLIT"); inbound.setMeasureReviewStatus("APPROVED"); InboundMeasurementEntity measurement = new InboundMeasurementEntity(); measurement.setId(30L); @@ -524,7 +524,7 @@ class InboundMeasurementServiceTest { InboundMeasurementResponses.MeasurementSubmitted result = service.submit(1L, 10L, 9L, new InboundMeasurementRequests.SubmitRequest(30L, 2)); - assertThat(result.processStatus()).isEqualTo("INBOUNDED"); + assertThat(result.processStatus()).isEqualTo("WAIT_SPLIT"); assertThat(result.measureReviewStatus()).isEqualTo("APPROVED"); assertThat(measurement.getStatus()).isEqualTo("APPROVED"); verify(repository, never()).updateMeasurement(any()); diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/InboundOrderQueryServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/InboundOrderQueryServiceTest.java index 1681b5eb1..5fca37ab6 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/InboundOrderQueryServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/InboundOrderQueryServiceTest.java @@ -42,7 +42,7 @@ class InboundOrderQueryServiceTest { @Test void shouldAcceptInboundedAsThePhase2TerminalStatusFilter() { - InboundOrderQuery query = new InboundOrderQuery(null, null, "INBOUNDED", null, + InboundOrderQuery query = new InboundOrderQuery(null, null, "WAIT_SPLIT", null, "APPROVED", "CONFIRMED", null, null, null, null, null, null, null, null, null, null, null, 1, 200); PageResult expected = new PageResult<>(List.of(), 1, 200, 0, 0); @@ -59,7 +59,7 @@ class InboundOrderQueryServiceTest { assertThat(InboundListTab.WAIT_REVIEW.processStatuses()).containsExactly("MEASURE_PENDING"); assertThat(InboundListTab.WAIT_FEE_SEND.processStatuses()) .containsExactly("WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING"); - assertThat(InboundListTab.INBOUNDED.processStatuses()).containsExactly("INBOUNDED"); + assertThat(InboundListTab.WAIT_SPLIT.processStatuses()).containsExactly("WAIT_SPLIT"); assertThat(List.of(InboundListTab.values())).extracting(Enum::name) .doesNotContain("WAIT_CUSTOMER_CONFIRM"); } diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/MerchantInboundBillingServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/MerchantInboundBillingServiceTest.java index f624e6982..1e2e43395 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/MerchantInboundBillingServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/MerchantInboundBillingServiceTest.java @@ -36,13 +36,13 @@ class MerchantInboundBillingServiceTest { @Test void shouldHandleCase01() { - InboundOrderEntity inbound = inbound("INBOUNDED", "APPROVED", "CONFIRMED"); + InboundOrderEntity inbound = inbound("WAIT_SPLIT", "APPROVED", "CONFIRMED"); InboundOrderDetail detail = new InboundOrderDetail(mock(com.baiyue.firstleg.dto.InboundOrderSummary.class), "客户备注", List.of(), List.of( new InboundOrderDetail.FileSummary(1L, "RELATED", "public.pdf", "PUBLIC", 1, 1L, null), new InboundOrderDetail.FileSummary(2L, "INTERNAL", "internal.pdf", "INTERNAL", 1, 1L, null)), List.of(new InboundOrderDetail.InternalNoteSummary(1L, "内部备注", 1L, "客服", null)), - List.of(new InboundOrderDetail.StatusHistorySummary(null, "INBOUNDED", "COMPLETE", + List.of(new InboundOrderDetail.StatusHistorySummary(null, "WAIT_SPLIT", "COMPLETE", "内部原因", 1L, "客服", null))); InboundMeasurementResponses.Detail measurement = mock(InboundMeasurementResponses.Detail.class); FeeSnapshotResponse fee = mock(FeeSnapshotResponse.class); @@ -142,7 +142,7 @@ class MerchantInboundBillingServiceTest { @Test void shouldRejectHistoricalPublishedStatusCombination() { - InboundOrderEntity inbound = inbound("INBOUNDED", "APPROVED", "SENT"); + InboundOrderEntity inbound = inbound("WAIT_SPLIT", "APPROVED", "SENT"); when(orders.findByOrderId(10001L, 77L)).thenReturn(inbound); assertThatThrownBy(() -> service.find(10001L, 77L)) diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/PdaPickSettlementServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/PdaPickSettlementServiceTest.java index a53069524..f4fd32ed0 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/PdaPickSettlementServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/PdaPickSettlementServiceTest.java @@ -180,7 +180,7 @@ class PdaPickSettlementServiceTest { void usesTotalPackageCountWhenUnsplitMeasuredInboundHasZeroAvailableColumn() { LoadingPickItemEntity item = item(11L, 21L, 31L, 41L, 20, 20, "PICKED"); resolved(List.of(item)); - when(splits.lockParent(31L)).thenReturn(parent(20, 0, "INBOUNDED", "NOT_SPLIT")); + when(splits.lockParent(31L)).thenReturn(parent(20, 0, "WAIT_SPLIT", "NOT_SPLIT")); when(splits.lockLatestApprovedItemsForInbound(10001L, 31L)) .thenReturn(List.of(new MeasurementItemRow(51L, 61L, 41L, 20))); when(allocations.lockConfirmed(10001L, 31L)) @@ -336,7 +336,7 @@ class PdaPickSettlementServiceTest { "IN-001", "MASTER", 31L, "LOADING", 5, 5, "FULLY_SPLIT", 4)); when(splits.lockParent(inboundId)).thenReturn(new ParentRow(inboundId, 10001L, 902L, "ORDER-REMAINDER", "IN-001-" + (sourceVersion - 1), "CHILD", 31L, - "INBOUNDED", packageCount, packageCount, "NOT_SPLIT", sourceVersion)); + "WAIT_SPLIT", packageCount, packageCount, "NOT_SPLIT", sourceVersion)); when(splits.lockLatestApprovedItemsForInbound(10001L, inboundId)) .thenReturn(List.of(new MeasurementItemRow(measurementItemId, measurementId, detailId, packageCount))); when(allocations.lockConfirmed(10001L, inboundId)).thenAnswer(invocation -> @@ -408,7 +408,7 @@ class PdaPickSettlementServiceTest { } private static ParentRow parent(int packageCount) { - return parent(packageCount, packageCount, "INBOUNDED", "NOT_SPLIT"); + return parent(packageCount, packageCount, "WAIT_SPLIT", "NOT_SPLIT"); } private static ParentRow parent(int totalPackageCount, int packageCount, String processStatus, String splitStatus) { diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/UnifiedInboundQueryServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/UnifiedInboundQueryServiceTest.java index 51eb8293b..d0aee7573 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/UnifiedInboundQueryServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/UnifiedInboundQueryServiceTest.java @@ -32,7 +32,6 @@ class UnifiedInboundQueryServiceTest { assertThat(UnifiedInboundQueryService.mainStatus("FEE_CONFIRMING")).isEqualTo("WAIT_MEASURE"); assertThat(UnifiedInboundQueryService.mainStatus("INBOUNDING")).isEqualTo("WAIT_MEASURE"); assertThat(UnifiedInboundQueryService.mainStatus("WAIT_SPLIT")).isEqualTo("WAIT_SPLIT"); - assertThat(UnifiedInboundQueryService.mainStatus("INBOUNDED")).isEqualTo("INBOUNDED"); assertThat(UnifiedInboundQueryService.mainStatus("LOADING")).isEqualTo("LOADING"); assertThat(UnifiedInboundQueryService.mainStatus("LOADED")).isEqualTo("LOADED"); assertThat(UnifiedInboundQueryService.mainStatus("DELIVERED")).isEqualTo("DELIVERED"); @@ -44,25 +43,21 @@ class UnifiedInboundQueryServiceTest { assertThat(UnifiedInboundQueryService.nextAction("WAIT_MEASURE", "MEASURING", null)).isNull(); assertThat(UnifiedInboundQueryService.nextAction("WAIT_MEASURE", "MEASURE_PENDING", null)) .isEqualTo("CONFIRM_MEASUREMENT"); - assertThat(UnifiedInboundQueryService.nextAction("INBOUNDED", "INBOUNDED", null)) - .isEqualTo("SMART_SPLIT"); - assertThat(UnifiedInboundQueryService.nextAction("INBOUNDED", "INBOUNDED", "PENDING")) - .isEqualTo("CONFIRM_PRELOAD"); - assertThat(UnifiedInboundQueryService.nextAction("INBOUNDED", "INBOUNDING", null)).isNull(); - assertThat(UnifiedInboundQueryService.nextAction("INBOUNDED", "INBOUNDED", "CONFIRMED")).isNull(); assertThat(UnifiedInboundQueryService.nextAction("WAIT_SPLIT", "WAIT_SPLIT", null)) .isEqualTo("SMART_SPLIT"); + assertThat(UnifiedInboundQueryService.nextAction("WAIT_SPLIT", "WAIT_SPLIT", "PENDING")) + .isEqualTo("CONFIRM_PRELOAD"); + assertThat(UnifiedInboundQueryService.nextAction("WAIT_SPLIT", "INBOUNDING", null)).isNull(); + assertThat(UnifiedInboundQueryService.nextAction("WAIT_SPLIT", "WAIT_SPLIT", "CONFIRMED")).isNull(); assertThat(UnifiedInboundQueryService.nextAction("LOADED", "LOADED", null)).isNull(); assertThat(UnifiedInboundQueryService.nextAction("DELIVERED", "DELIVERED", null)).isNull(); } @Test - void inboundedAndWaitSplitFiltersRemainDistinct() throws Exception { + void waitSplitFilterUsesOnlyCanonicalStatus() throws Exception { var method = UnifiedInboundQueryService.class.getDeclaredMethod("internalStatuses", String.class); method.setAccessible(true); - assertThat(method.invoke(new UnifiedInboundQueryService(), "INBOUNDED")) - .isEqualTo(java.util.List.of("INBOUNDED")); assertThat(method.invoke(new UnifiedInboundQueryService(), "WAIT_SPLIT")) .isEqualTo(java.util.List.of("WAIT_SPLIT")); } @@ -70,14 +65,14 @@ class UnifiedInboundQueryServiceTest { @Test void phaseTwoMeasurementStatusesDoNotRequireUndeployedPhaseThreeTables() { assertThat(UnifiedInboundQueryService.requiresPhaseThreeSummary("WAIT_MEASURE")).isFalse(); - assertThat(UnifiedInboundQueryService.requiresPhaseThreeSummary("INBOUNDED")).isFalse(); + assertThat(UnifiedInboundQueryService.requiresPhaseThreeSummary("WAIT_SPLIT")).isFalse(); assertThat(UnifiedInboundQueryService.requiresPhaseThreeSummary("LOADING")).isTrue(); assertThat(UnifiedInboundQueryService.requiresPhaseThreeSummary("LOADED")).isTrue(); assertThat(UnifiedInboundQueryService.requiresPhaseThreeSummary("DELIVERED")).isTrue(); } @ParameterizedTest - @ValueSource(strings = {"WAIT_MEASURE", "INBOUNDED", "WAIT_SPLIT"}) + @ValueSource(strings = {"WAIT_MEASURE", "WAIT_SPLIT"}) void phaseTwoDetailDoesNotQueryPhaseThreeTables(String processStatus) { InboundOrderRepository inbounds = mock(InboundOrderRepository.class); UnifiedInboundReadRepository operations = mock(UnifiedInboundReadRepository.class); diff --git a/iam/src/main/resources/db/admin-prototype-menu-permissions.sql b/iam/src/main/resources/db/admin-prototype-menu-permissions.sql index ed408d7a8..d3b3c5038 100644 --- a/iam/src/main/resources/db/admin-prototype-menu-permissions.sql +++ b/iam/src/main/resources/db/admin-prototype-menu-permissions.sql @@ -31,7 +31,20 @@ ON DUPLICATE KEY UPDATE UPDATE iam_permission SET status='INACTIVE', deleted=1 WHERE tenant_id=0 AND app_code='PLATFORM' - AND permission_code IN ('platform:overseas-storage-fee:create','platform:overseas-storage-fee:delete'); + AND permission_code IN ( + 'platform:overseas-storage-fee:create','platform:overseas-storage-fee:delete', + 'platform:finance:bill:view','platform:finance:account:view', + 'platform:finance:fee-source:view','platform:finance:settlement-ar:view', + 'platform:finance:settlement-ap:view'); + +DELETE rp + FROM iam_role_permission rp + JOIN iam_permission p ON p.id=rp.permission_id AND p.tenant_id=rp.tenant_id + WHERE p.tenant_id=0 AND p.app_code='PLATFORM' + AND p.permission_code IN ( + 'platform:finance:bill:view','platform:finance:account:view', + 'platform:finance:fee-source:view','platform:finance:settlement-ar:view', + 'platform:finance:settlement-ap:view'); INSERT IGNORE INTO iam_role_permission (tenant_id, role_id, permission_id, created_at) SELECT 0, r.id, p.id, CURRENT_TIMESTAMP(3) @@ -60,9 +73,7 @@ UPDATE iam_permission p WHERE p.tenant_id=0 AND p.app_code='PLATFORM'; UPDATE iam_permission p - JOIN (SELECT 'platform:finance:account:view' code, 5105040 parent_id UNION ALL - SELECT 'platform:finance:fee-source:view', 5105041 UNION ALL - SELECT 'platform:finance:fee-source:review', 5105041 UNION ALL + JOIN (SELECT 'platform:finance:fee-source:review' code, 5105041 parent_id UNION ALL SELECT 'platform:finance:fee-source:settlement', 5105041 UNION ALL SELECT 'platform:finance:reconciliation:create', 5105042 UNION ALL SELECT 'platform:finance:reconciliation:review', 5105042 UNION ALL @@ -70,9 +81,7 @@ UPDATE iam_permission p SELECT 'platform:finance:reconciliation:adjust', 5105042 UNION ALL SELECT 'platform:finance:reconciliation:void', 5105042 UNION ALL SELECT 'platform:finance:settlement-ar:create', 5105043 UNION ALL - SELECT 'platform:finance:settlement-ar:view', 5105043 UNION ALL SELECT 'platform:finance:settlement-ap:create', 5105044 UNION ALL - SELECT 'platform:finance:settlement-ap:view', 5105044 UNION ALL SELECT 'platform:finance:payment:create', 5105044) mapping ON mapping.code=p.permission_code SET p.parent_id=mapping.parent_id diff --git a/iam/src/main/resources/db/finance-g1-g2-permissions.sql b/iam/src/main/resources/db/finance-g1-g2-permissions.sql index 1f2cc46be..2f9c91515 100644 --- a/iam/src/main/resources/db/finance-g1-g2-permissions.sql +++ b/iam/src/main/resources/db/finance-g1-g2-permissions.sql @@ -11,16 +11,12 @@ BEGIN SELECT 1 FROM iam_permission WHERE (id = 5105000 AND permission_code <> 'platform:finance') - OR (id = 5105022 AND permission_code <> 'platform:finance:account:view') - OR (id = 5105023 AND permission_code <> 'platform:finance:fee-source:view') OR (id = 5105024 AND permission_code <> 'platform:finance:fee-source:review') OR (id = 5105025 AND permission_code <> 'platform:finance:fee-source:settlement') OR (permission_code = 'platform:finance' AND id <> 5105000) - OR (permission_code = 'platform:finance:account:view' AND id <> 5105022) - OR (permission_code = 'platform:finance:fee-source:view' AND id <> 5105023) OR (permission_code = 'platform:finance:fee-source:review' AND id <> 5105024) OR (permission_code = 'platform:finance:fee-source:settlement' AND id <> 5105025) - OR (id IN (5105000, 5105022, 5105023, 5105024, 5105025) + OR (id IN (5105000, 5105024, 5105025) AND (tenant_id <> 0 OR app_code <> 'PLATFORM')) ) THEN SIGNAL SQLSTATE '45000' @@ -33,10 +29,6 @@ BEGIN VALUES (5105000, 0, 'PLATFORM', 'MENU', 0, 'platform:finance', '财务管理', '/finance', NULL, 60, 'ACTIVE', 1, 0), - (5105022, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:account:view', - '查看客户财务账户及流水', NULL, 'GET', 130, 'ACTIVE', 1, 0), - (5105023, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:fee-source:view', - '查看财务费用来源', NULL, 'GET', 140, 'ACTIVE', 1, 0), (5105024, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:fee-source:review', '审核财务费用来源', NULL, 'POST', 150, 'ACTIVE', 1, 0), (5105025, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:fee-source:settlement', @@ -47,6 +39,11 @@ BEGIN http_method = VALUES(http_method), sort_no = VALUES(sort_no), status = 'ACTIVE', built_in = 1, deleted = 0; + DELETE rp FROM iam_role_permission rp JOIN iam_permission p ON p.id=rp.permission_id AND p.tenant_id=rp.tenant_id + WHERE p.tenant_id=0 AND p.app_code='PLATFORM' AND p.permission_code IN ('platform:finance:account:view','platform:finance:fee-source:view'); + UPDATE iam_permission SET status='INACTIVE', deleted=1 + WHERE tenant_id=0 AND app_code='PLATFORM' AND permission_code IN ('platform:finance:account:view','platform:finance:fee-source:view'); + IF (SELECT COUNT(*) FROM iam_permission WHERE tenant_id = 0 @@ -57,10 +54,8 @@ BEGIN AND built_in = 1 AND deleted = 0 AND permission_code IN ( - 'platform:finance:account:view', - 'platform:finance:fee-source:view', 'platform:finance:fee-source:review', - 'platform:finance:fee-source:settlement')) <> 4 THEN + 'platform:finance:fee-source:settlement')) <> 2 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'finance G1/G2 permission catalog is incomplete'; END IF; diff --git a/iam/src/main/resources/db/finance-g3-g4-permissions.sql b/iam/src/main/resources/db/finance-g3-g4-permissions.sql index 92a43abd1..469905a3a 100644 --- a/iam/src/main/resources/db/finance-g3-g4-permissions.sql +++ b/iam/src/main/resources/db/finance-g3-g4-permissions.sql @@ -17,8 +17,6 @@ IF EXISTS ( OR (id = 5105030 AND permission_code <> 'platform:finance:reconciliation:void') OR (id = 5105031 AND permission_code <> 'platform:finance:settlement-ar:create') OR (id = 5105032 AND permission_code <> 'platform:finance:settlement-ap:create') - OR (id = 5105033 AND permission_code <> 'platform:finance:settlement-ar:view') - OR (id = 5105034 AND permission_code <> 'platform:finance:settlement-ap:view') OR (id = 5105035 AND permission_code <> 'platform:finance:payment:create') OR (permission_code = 'platform:finance:reconciliation:create' AND id <> 5105026) OR (permission_code = 'platform:finance:reconciliation:review' AND id <> 5105027) @@ -27,8 +25,6 @@ IF EXISTS ( OR (permission_code = 'platform:finance:reconciliation:void' AND id <> 5105030) OR (permission_code = 'platform:finance:settlement-ar:create' AND id <> 5105031) OR (permission_code = 'platform:finance:settlement-ap:create' AND id <> 5105032) - OR (permission_code = 'platform:finance:settlement-ar:view' AND id <> 5105033) - OR (permission_code = 'platform:finance:settlement-ap:view' AND id <> 5105034) OR (permission_code = 'platform:finance:payment:create' AND id <> 5105035) OR (id BETWEEN 5105026 AND 5105035 AND (tenant_id <> 0 OR app_code <> 'PLATFORM')) ) THEN @@ -46,8 +42,6 @@ VALUES (5105030, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:reconciliation:void', '作废或重开客户对账单', NULL, 'POST', 210, 'ACTIVE', 1, 0), (5105031, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:settlement-ar:create', '生成应收结算单', NULL, 'POST', 220, 'ACTIVE', 1, 0), (5105032, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:settlement-ap:create', '生成应付结算单', NULL, 'POST', 230, 'ACTIVE', 1, 0), - (5105033, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:settlement-ar:view', '查看应收结算单', NULL, 'GET', 240, 'ACTIVE', 1, 0), - (5105034, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:settlement-ap:view', '查看应付结算单', NULL, 'GET', 250, 'ACTIVE', 1, 0), (5105035, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:payment:create', '登记应付付款', NULL, 'POST', 260, 'ACTIVE', 1, 0) ON DUPLICATE KEY UPDATE permission_type = VALUES(permission_type), parent_id = VALUES(parent_id), @@ -55,13 +49,24 @@ ON DUPLICATE KEY UPDATE http_method = VALUES(http_method), sort_no = VALUES(sort_no), status = 'ACTIVE', built_in = 1, deleted = 0; +DELETE rp + FROM iam_role_permission rp + JOIN iam_permission p ON p.id=rp.permission_id AND p.tenant_id=rp.tenant_id + WHERE p.tenant_id=0 AND p.app_code='PLATFORM' + AND p.permission_code IN ('platform:finance:settlement-ar:view','platform:finance:settlement-ap:view'); + +UPDATE iam_permission + SET status='INACTIVE', deleted=1 + WHERE tenant_id=0 AND app_code='PLATFORM' + AND permission_code IN ('platform:finance:settlement-ar:view','platform:finance:settlement-ap:view'); + IF (SELECT COUNT(*) FROM iam_permission WHERE tenant_id=0 AND app_code='PLATFORM' AND parent_id=5105000 AND permission_code IN ('platform:finance:reconciliation:create','platform:finance:reconciliation:review', 'platform:finance:reconciliation:issue','platform:finance:reconciliation:adjust','platform:finance:reconciliation:void', 'platform:finance:settlement-ar:create','platform:finance:settlement-ap:create', - 'platform:finance:settlement-ar:view','platform:finance:settlement-ap:view','platform:finance:payment:create') - AND status='ACTIVE' AND deleted=0) <> 10 THEN + 'platform:finance:payment:create') + AND status='ACTIVE' AND deleted=0) <> 8 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='finance G3/G4 permission catalog is incomplete'; END IF; diff --git a/iam/src/test/java/com/baiyue/iam/repository/AdminPrototypeMenuPermissionSqlContractTest.java b/iam/src/test/java/com/baiyue/iam/repository/AdminPrototypeMenuPermissionSqlContractTest.java index 0ea05d28c..fab5961a5 100644 --- a/iam/src/test/java/com/baiyue/iam/repository/AdminPrototypeMenuPermissionSqlContractTest.java +++ b/iam/src/test/java/com/baiyue/iam/repository/AdminPrototypeMenuPermissionSqlContractTest.java @@ -31,6 +31,10 @@ class AdminPrototypeMenuPermissionSqlContractTest { .contains("'platform:finance:settlement-ar:create', 5105043") .contains("'platform:finance:settlement-ap:create', 5105044") .contains("'platform:finance:payment:create', 5105044)") + .contains("'platform:finance:bill:view','platform:finance:settlement-ar:view'") + .contains("'platform:finance:settlement-ap:view'") + .doesNotContain("SELECT 'platform:finance:settlement-ar:view', 5105043") + .doesNotContain("SELECT 'platform:finance:settlement-ap:view', 5105044") .contains("role_code='PLATFORM_ADMIN'"); } diff --git a/iam/src/test/java/com/baiyue/iam/repository/FinanceG1G2PermissionSqlContractTest.java b/iam/src/test/java/com/baiyue/iam/repository/FinanceG1G2PermissionSqlContractTest.java index dd7d603ec..7a1d6c5a9 100644 --- a/iam/src/test/java/com/baiyue/iam/repository/FinanceG1G2PermissionSqlContractTest.java +++ b/iam/src/test/java/com/baiyue/iam/repository/FinanceG1G2PermissionSqlContractTest.java @@ -62,14 +62,15 @@ class FinanceG1G2PermissionSqlContractTest { .contains("NULL, 'POST', 150, 'ACTIVE', 1, 0") .contains("NULL, 'PUT', 160, 'ACTIVE', 1, 0") .contains("permission_code IN (\n 'platform:finance',") - .contains("permission_code LIKE 'platform:finance:%' AND status='ACTIVE' AND deleted=0) <> 27") + .contains("permission_code LIKE 'platform:finance:%' AND status='ACTIVE' AND deleted=0) <> 24") .contains("平台管理员 Finance 授权不完整") .contains("Finance G1-G4"); assertThat(financeSection) .contains("SELECT 4000001, id, 0 FROM baiyue_iam.iam_permission") - .contains("platform:finance:account:view") - .contains("platform:finance:settlement-ar:view") - .contains("platform:finance:settlement-ap:view"); + .contains("platform:finance:account:view"); + assertThat(sql) + .contains("platform:finance:settlement-ar:list") + .contains("platform:finance:settlement-ap:list"); } private static Path resolveUnifiedSql() { diff --git a/iam/src/test/java/com/baiyue/iam/repository/FinanceG3G4PermissionSqlContractTest.java b/iam/src/test/java/com/baiyue/iam/repository/FinanceG3G4PermissionSqlContractTest.java index c917dce28..d13c2b7a9 100644 --- a/iam/src/test/java/com/baiyue/iam/repository/FinanceG3G4PermissionSqlContractTest.java +++ b/iam/src/test/java/com/baiyue/iam/repository/FinanceG3G4PermissionSqlContractTest.java @@ -14,8 +14,7 @@ class FinanceG3G4PermissionSqlContractTest { "platform:finance:reconciliation:create", "platform:finance:reconciliation:review", "platform:finance:reconciliation:issue", "platform:finance:reconciliation:adjust", "platform:finance:reconciliation:void", "platform:finance:settlement-ar:create", - "platform:finance:settlement-ap:create", "platform:finance:settlement-ar:view", - "platform:finance:settlement-ap:view", "platform:finance:payment:create" + "platform:finance:settlement-ap:create", "platform:finance:payment:create" }; @Test @@ -23,9 +22,7 @@ class FinanceG3G4PermissionSqlContractTest { String sql = new ClassPathResource("db/finance-g3-g4-permissions.sql") .getContentAsString(StandardCharsets.UTF_8); - for (int index = 0; index < CODES.length; index++) { - assertThat(sql).contains("(" + (5105026 + index) + ", 0, 'PLATFORM', 'BUTTON', 5105000, '" + CODES[index] + "'"); - } + for (String code : CODES) assertThat(sql).contains(code); assertThat(sql).contains("SIGNAL SQLSTATE '45000'") .contains("id = 5105026 AND permission_code <> 'platform:finance:reconciliation:create'") .contains("permission_code = 'platform:finance:payment:create' AND id <> 5105035") @@ -33,7 +30,10 @@ class FinanceG3G4PermissionSqlContractTest { .contains("finance G3/G4 permission id/code conflict") .contains("permission catalog is incomplete") .contains("finance permission parent is invalid") - .contains("CALL migrate_finance_g3_g4_permissions()"); + .contains("CALL migrate_finance_g3_g4_permissions()") + .contains("status='INACTIVE', deleted=1") + .doesNotContain("'platform:finance:settlement-ar:view', '查看应收结算单'") + .doesNotContain("'platform:finance:settlement-ap:view', '查看应付结算单'"); } @Test @@ -45,12 +45,13 @@ class FinanceG3G4PermissionSqlContractTest { assertThat(financeSection) .contains("migrate_finance_g1_g4_permissions") .contains("finance G1-G4 permission id/code conflict") - .contains("permission_code LIKE 'platform:finance:%' AND status='ACTIVE' AND deleted=0) <> 27"); + .contains("permission_code LIKE 'platform:finance:%' AND status='ACTIVE' AND deleted=0) <> 24"); assertThat(financeSection) .contains("SELECT 4000001, id, 0 FROM baiyue_iam.iam_permission") - .contains("platform:finance:reconciliation:create") - .contains("platform:finance:settlement-ar:view") - .contains("platform:finance:settlement-ap:view"); + .contains("platform:finance:reconciliation:create"); + assertThat(sql) + .contains("platform:finance:settlement-ar:list") + .contains("platform:finance:settlement-ap:list"); } private static Path resolveUnifiedSql() { diff --git a/scripts/README.md b/scripts/README.md index f6baba0f7..dc7710420 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -1,4 +1,29 @@ -# 后端全量回归 +# 全量 HTTP 验收脚本索引 + +全量验收只认测试环境 HTTP 脚本。它们通过 Gateway 调用已部署服务,按业务模块执行真实登录、权限、状态、幂等、跨服务和数据链路检查;脚本通过后,剩余工作是 Admin/Client 的真实接口调用与浏览器样式验收,以及 PDA 页面/设备验收。 + +| 验收目标 | 唯一入口 | 数据副作用 | 覆盖范围 | +|---|---|---|---| +| 平台后台全模块 HTTP 验收 | `scripts/admin-test-env-acceptance.mjs` | 会创建、更新或最终停用测试数据 | `--modules` 默认列表中的平台客户、仓储、订单、入库、分柜、装柜、船期、拖车、报关、财务、通知、统计等模块 | +| 客户工作台完整 HTTP 链路验收 | `scripts/client-test-env-acceptance.mjs` | 会创建测试企业、订单、入库和权限数据 | 注册/审核、客户隔离、订单、入库计费、文件、财务和客户侧权限链路 | +| Client 二期兼容 HTTP 验收 | `scripts/client-phase2-test-env-acceptance.mjs` | 会写入测试环境业务数据 | 二期客户入库、测量、费用和权限兼容场景;不是替代 Client 全量入口 | + +执行前必须确认目标环境、账号和数据清理/停用策略,并获得当前对话的测试环境验收授权。`admin-test-env-modules/*contract.test.mjs` 只验证模块脚本结构,不能代替 HTTP 验收。 + +推荐顺序:部署服务 → Admin 全模块 HTTP 验收 → Client 完整 HTTP 链路验收 → 前端真实页面调用和样式验收 → PDA 页面/设备验收。任何本地单测通过都不能替代 HTTP 全量验收。 + +## 本地质量检查(非全量验收) + +以下命令只用于代码质量和契约回归,不访问测试环境,不代表服务全量可用: + +| 检查 | 入口 | +|---|---| +| 后端 Maven 契约/单元测试 | `scripts/backend-full-regression.ps1` | +| Admin/Client 本地测试与类型检查 | `scripts/frontend-full-regression.ps1` | +| PDA 本地页面和状态测试 | `../apps/pda-uniapp/tests/*.test.mjs` | +| 本地数据库 SQL 静态/受控验收 | `scripts/local-db-full-acceptance.ps1` | + +## 后端全量回归 ## 入口 @@ -21,7 +46,7 @@ 脚本会检查 `pnpm` 和目标应用的 `package.json`;任一命令失败时立即透传退出码,不能同时跳过 Admin 和 Client。 -脚本固定运行: +后端脚本固定运行: ```text mvn -f <仓库根>/pom.xml -pl order,firstleg,iam,gateway,file,warehouse -am test @@ -65,8 +90,8 @@ Dry-run 只检查根 `pom.xml` 和关键回归测试文件并打印 Maven 命令 正式入口是 `local-db-full-acceptance.ps1`,只允许连接本机 `127.0.0.1` 或 `localhost`,默认端口 `3306`;测试库 `8.163.30.128` 会立即以退出码 2 拒绝。脚本固定校验统一 SQL SHA-256 `BE7C5C98712D5202F5D187EA9A37C64EAB9F6AA9AE772C89A51F186507B1AE53` 和 14706 行,并静态确认中文注释升级采用校验入队、每表一次 ALTER、结构签名复核、九库覆盖断言及唯一最终 `PASS`。 ```powershell -.\local-db-full-acceptance.ps1 -DryRun -.\local-db-full-acceptance.ps1 +.\scripts\local-db-full-acceptance.ps1 -DryRun +.\scripts\local-db-full-acceptance.ps1 ``` Dry-run 只校验 SQL 输入并打印空库、复跑、两类 picking 旧结构、WAIT_SPLIT/IAM、海外仓旧结构、中断恢复和最终结构的执行顺序,不启动 MySQL、不写库。真实运行每阶段写入 `.codex-tmp/p23-04-acceptance/acceptance-run-*.log`,不使用 `--force`,任一 SQL 错误立即停止并透传退出码,最后执行 `verify-final.sql`。脚本不保存或传入数据库密码,使用本机默认认证环境。 diff --git a/scripts/admin-test-env-acceptance.mjs b/scripts/admin-test-env-acceptance.mjs index 24410adf1..601c94a7f 100644 --- a/scripts/admin-test-env-acceptance.mjs +++ b/scripts/admin-test-env-acceptance.mjs @@ -510,7 +510,10 @@ export class AcceptanceRun { async preparePhase3ContainerFixture() { const originalMarker = this.marker - const fixtureMarker = `${originalMarker}-CAB` + // Keep the shared orchestration marker stable. Downstream loading, + // overview and finance modules select fixtures by context.marker; using a + // private -CAB suffix here made the producer invisible to those modules. + const fixtureMarker = originalMarker const createdKeys = ['orders', 'ocrInbound', 'testInbound', 'inboundMeasurement'] const contextKeys = ['inboundWeightOcr', 'ocrInbound', 'testInbound', 'createdInbound'] const createdSnapshot = new Map(createdKeys.map((key) => [key, { @@ -525,7 +528,10 @@ export class AcceptanceRun { await this.executeModule('orders-phase3-container-fixture', () => orders.run(this)) const measurement = await import(new URL('./admin-test-env-modules/inbound-measurement.mjs', import.meta.url)) await this.executeModule('inbound-measurement-phase3-container-fixture', () => measurement.run(this)) - const fixture = this.createdResources.inboundMeasurement || this.createdResources.orders + const fixture = this.createdResources.inboundMeasurement + if (!fixture?.inboundId || fixture.finalStatus !== 'APPROVED' || fixture.processStatus !== 'WAIT_SPLIT') { + throw new Error(`独立柜次测量 fixture 未进入 WAIT_SPLIT,禁止回退使用 WAIT_PRICING 订单: processStatus=${fixture?.processStatus || 'missing'}, measurementStatus=${fixture?.finalStatus || 'missing'}`) + } this.createdResources.orchestrationFixtures = { ...(this.createdResources.orchestrationFixtures || {}), phase3Container: fixture ? { diff --git a/scripts/admin-test-env-modules/finance-full-fixtures.mjs b/scripts/admin-test-env-modules/finance-full-fixtures.mjs index 8143a781a..39e5556ab 100644 --- a/scripts/admin-test-env-modules/finance-full-fixtures.mjs +++ b/scripts/admin-test-env-modules/finance-full-fixtures.mjs @@ -403,6 +403,14 @@ export async function run(context) { '使用订单域真实 waybillId、shipmentId 或 orderNo,不使用数据库主键冒充运单标识', OPTIONS) context.createdResources.financeFullFixtures = { marker: context.marker, customerId, + prerequisites: { + container: containerOwned && Boolean(containerFixture?.feeSourceId) && allocations.length > 0 + ? { ready: true, category: null } + : { ready: false, category: 'container-prerequisite', evidence: '缺少同 marker 已完成柜次、有效 containerBookingId、按柜费用来源或真实分摊' }, + financeSources: fixtures.length > 0 + ? { ready: true, category: null } + : { ready: false, category: 'finance-source-prerequisite', evidence: '本轮 marker 未产生可消费 Finance 费用来源' }, + }, orders: measured.map((item) => ({ orderId: id(item.orderId), orderNo: item.orderNo || null, shipmentId: item.shipmentId ? id(item.shipmentId) : null, waybillId: item.waybillId ? id(item.waybillId) : (item.shipmentId ? id(item.shipmentId) : null), inboundId: id(item.inboundId), diff --git a/scripts/admin-test-env-modules/finance-reconciliation-full.mjs b/scripts/admin-test-env-modules/finance-reconciliation-full.mjs index 2544c6b94..6e81b0659 100644 --- a/scripts/admin-test-env-modules/finance-reconciliation-full.mjs +++ b/scripts/admin-test-env-modules/finance-reconciliation-full.mjs @@ -323,20 +323,25 @@ export async function run(context) { finish(context, start) return } + fixture.root.reconciliationPrerequisite = null const permissionResponse = await context.request('GET', '/api/platform/v1/iam/authorization/permissions') const permissions = context.apiSuccess(MODULE, '查询当前账号 Finance 有效权限', permissionResponse, '', OPTIONS) const granted = new Set(list(permissions).map(String)) if (granted.has(LEGACY_STATEMENT_VIEW_PERMISSION)) { + fixture.root.reconciliationPrerequisite = { ready: false, category: 'permission-prerequisite', + required: [STATEMENT_LIST_PERMISSION], legacy: [LEGACY_STATEMENT_VIEW_PERMISSION] } blocker(context, '客户对账单旧 view 权限兼容阻断', - `检测到旧权限=${LEGACY_STATEMENT_VIEW_PERMISSION};列表和详情只能以正式权限=${STATEMENT_LIST_PERMISSION} 验收,旧 view 不得作为通过依据`, + `检测到旧权限=${LEGACY_STATEMENT_VIEW_PERMISSION};列表和详情只能以正式权限=${STATEMENT_LIST_PERMISSION} 验收,旧 view 不得作为通过依据;需由 IAM 角色初始化为验收账号授予 ${STATEMENT_LIST_PERMISSION} 并移除旧资源,Finance 脚本不兼容旧权限`, 'legacy-customer-statement-view-permission', permissionResponse) finish(context, start) return } const missingPermissions = REQUIRED_PERMISSIONS.filter((permission) => !granted.has(permission)) if (missingPermissions.length > 0) { + fixture.root.reconciliationPrerequisite = { ready: false, category: 'permission-prerequisite', + required: missingPermissions, legacy: [] } blocker(context, 'Finance 写权限门禁', - `缺少权限=${missingPermissions.join(',')}; 对账接口将返回 A0403,禁止继续写入`, + `缺少正式权限资源=${missingPermissions.join(',')}; 需由 IAM 角色初始化授予验收账号;对账接口将返回 A0403,禁止继续写入`, 'missing-finance-write-permissions', permissionResponse) finish(context, start) return @@ -353,6 +358,8 @@ export async function run(context) { const invalid = fixture.sources.filter((source) => !source.feeSourceId || !source.customerId || !source.businessDate || !source.waybillId || source.marker !== context.marker) if (invalid.length > 0) { + fixture.root.reconciliationPrerequisite = { ready: false, category: 'finance-source-prerequisite', + invalidSourceCount: invalid.length } blocker(context, 'Fixture 来源契约完整', `存在 ${invalid.length} 条来源缺少 feeSourceId/customerId/businessDate/运单标识/marker`, 'invalid-finance-fixture-contract') @@ -364,6 +371,8 @@ export async function run(context) { && source.direction === 'RECEIVABLE' && source.currency === 'CNY') const businessDates = new Set(receivables.map((source) => source.businessDate)) if (receivables.length < 4 || businessDates.size < 2) { + fixture.root.reconciliationPrerequisite = { ready: false, category: 'finance-source-prerequisite', + receivableCount: receivables.length, businessDateCount: businessDates.size } blocker(context, '三张 AR 与日期范围 Fixture 门禁', `至少需要同客户 4 条 CNY 应收来源且跨 2 个真实 businessDate;actual sources=${receivables.length}, dates=${businessDates.size}`, 'insufficient-receivable-date-fixture') diff --git a/scripts/admin-test-env-modules/finance-settlement-full-contract.test.mjs b/scripts/admin-test-env-modules/finance-settlement-full-contract.test.mjs index 09175f3fb..f96e5dcca 100644 --- a/scripts/admin-test-env-modules/finance-settlement-full-contract.test.mjs +++ b/scripts/admin-test-env-modules/finance-settlement-full-contract.test.mjs @@ -125,6 +125,9 @@ test('AR/AP 数组为空或不完整时原子阻断且不查询、不写入', as assert.equal(empty.gaps.length, 1) assert.equal(empty.steps[0].passed, false) assert.match(empty.steps[0].evidence, /actual AR=0, AP=0/) + assert.match(empty.steps[0].evidence, /上游 Finance fixture 前置条件未满足,非结算服务故障/) + assert.match(empty.gaps[0][4], /prerequisite/) + assert.equal(empty.calls.some((call) => call.method === 'POST'), false) const partial = fakeContext({ receivableSettlements: [ { settlementId: '101', statementId: '11', direction: 'RECEIVABLE', status: 'ISSUED' }, @@ -133,4 +136,6 @@ test('AR/AP 数组为空或不完整时原子阻断且不查询、不写入', as assert.equal(partial.calls.length, 0) assert.equal(partial.gaps.length, 1) assert.match(partial.steps[0].evidence, /actual AR=1, AP=1/) + assert.match(partial.steps[0].evidence, /receivable-settlement-prerequisite/) + assert.equal(partial.calls.some((call) => call.method === 'POST'), false) }) diff --git a/scripts/admin-test-env-modules/finance-settlement-full.mjs b/scripts/admin-test-env-modules/finance-settlement-full.mjs index c73ff788e..53fb548b9 100644 --- a/scripts/admin-test-env-modules/finance-settlement-full.mjs +++ b/scripts/admin-test-env-modules/finance-settlement-full.mjs @@ -128,9 +128,17 @@ export async function run(context) { const arSettlementIds = new Set(validReceivables.map(settlementId)) const arStatementIds = new Set(validReceivables.map(statementId)) if (validReceivables.length !== 3 || arSettlementIds.size !== 3 || arStatementIds.size !== 3 || validPayables.length !== 1) { + const upstream = fixtures.reconciliationPrerequisite?.ready === false + ? fixtures.reconciliationPrerequisite + : Object.values(fixtures.prerequisites || {}).find((item) => item?.ready === false) + const missing = [] + if (statements.filter((item) => item.status === 'ISSUED').length < 3) missing.push('reconciliation-issued-statement-prerequisite') + if (validReceivables.length !== 3) missing.push('receivable-settlement-prerequisite') + if (validPayables.length !== 1) missing.push('payable-settlement-prerequisite') + const category = upstream?.category || (missing.length === 1 ? missing[0] : 'upstream-finance-fixture-prerequisite') gap(context, '取得完整收付结算 Fixture', 'three AR and one AP settlements', - `需要 3 张关联本轮 ISSUED 对账单的独立 AR 和 1 张带 supplierId 的 AP;actual AR=${validReceivables.length}, AP=${validPayables.length}`, - 'incomplete-settlement-fixture') + `上游 Finance fixture 前置条件未满足,非结算服务故障;需要 3 张关联本轮 ISSUED 对账单的独立 AR 和 1 张带 supplierId 的 AP;actual AR=${validReceivables.length}, AP=${validPayables.length}; prerequisite=${category}; missing=${missing.join(',') || 'unknown'}`, + category) return } diff --git a/scripts/admin-test-env-modules/inbound-measurement.mjs b/scripts/admin-test-env-modules/inbound-measurement.mjs index ebcdaa981..df6dd3f50 100644 --- a/scripts/admin-test-env-modules/inbound-measurement.mjs +++ b/scripts/admin-test-env-modules/inbound-measurement.mjs @@ -24,6 +24,19 @@ function validPdf(response) { && response.bytes?.length > 4 && new TextDecoder().decode(response.bytes.slice(0, 4)) === '%PDF' } +async function waitForCompletedMeasurement(context, measurementPath, attempts = 30) { + let response = null + let measurement = null + for (let attempt = 1; attempt <= attempts; attempt += 1) { + response = await context.request('GET', measurementPath) + measurement = response?.http === 200 && String(response?.body?.code) === '200' + ? response.body.data : null + if (measurement?.processStatus === 'WAIT_SPLIT') return { response, measurement, attempts: attempt } + if (attempt < attempts) await new Promise((resolve) => setTimeout(resolve, 1_000)) + } + return { response, measurement, attempts } +} + async function provisionWarehouseResources(context, { inboundId, warehouseId, totalVolumeCbm }) { const palletCount = Math.ceil(Number(totalVolumeCbm) / DEFAULT_PALLET_CAPACITY_CBM) if (!warehouseId || !Number.isFinite(palletCount) || palletCount < 1) return false @@ -394,8 +407,13 @@ export async function run(context) { context.apiSuccess(MODULE, 'Admin 审核通过墨家测量', approvalResponse, `measurementId=${asId(draft.measurementId)}`, OPTIONS) - const finalResponse = await context.request('GET', measurementPath) - const finalMeasurement = context.apiSuccess(MODULE, '查询审核后的最终测量状态', finalResponse, '', OPTIONS) + // Measurement approval may enqueue pricing/inbound completion. Poll the + // public projection instead of treating the transient WAIT_PRICING state as + // a smart-loading candidate or manufacturing fixture state in the script. + const completed = await waitForCompletedMeasurement(context, measurementPath) + const finalResponse = completed.response + const finalMeasurement = context.apiSuccess(MODULE, '查询审核后的最终测量状态', finalResponse, + `pollAttempts=${completed.attempts}`, OPTIONS) context.assert(MODULE, '审核后入库与测量状态闭环且结果未变化', finalResponse, finalMeasurement?.processStatus === 'WAIT_SPLIT' && finalMeasurement?.measureReviewStatus === 'APPROVED' && finalMeasurement?.measurementStatus === 'APPROVED' @@ -407,6 +425,7 @@ export async function run(context) { context.createdResources.inboundMeasurement = { marker: context.marker, tenantId, inboundId, measurementId: asId(draft.measurementId), boxCode, finalStatus: finalMeasurement?.measurementStatus || null, + processStatus: finalMeasurement?.processStatus || null, } finishScenario(context, start) } diff --git a/scripts/admin-test-env-modules/phase3-container-loading.mjs b/scripts/admin-test-env-modules/phase3-container-loading.mjs index 0d9ec3474..01c6cb2e0 100644 --- a/scripts/admin-test-env-modules/phase3-container-loading.mjs +++ b/scripts/admin-test-env-modules/phase3-container-loading.mjs @@ -34,14 +34,20 @@ async function request(context, method, path, body, headers = {}) { } } -function fixture(context) { - return context.createdResources?.orchestrationFixtures?.phase3Container - || context.createdResources?.inboundMeasurement - || context.createdResources?.orders +export function selectContainerLoadingFixture(context) { + const candidates = [ + context.createdResources?.orchestrationFixtures?.phase3Container, + context.createdResources?.inboundMeasurement, + context.createdResources?.orders, + ].filter(Boolean) + // Finance and downstream modules temporarily switch context.marker for + // their own isolated runs. Never select the older CAB fixture merely because + // it is first in the object; ownership is the marker contract. + return candidates.find((candidate) => candidate.marker === context.marker) || candidates[0] } export async function run(context) { - const source = fixture(context) + const source = selectContainerLoadingFixture(context) const tenantId = id(source?.tenantId) const inboundId = id(source?.inboundId) if (!tenantId || !inboundId || source?.marker !== context.marker) { diff --git a/scripts/admin-test-env-modules/phase3-producer-chain-contract.test.mjs b/scripts/admin-test-env-modules/phase3-producer-chain-contract.test.mjs index 8d38c3ccc..191acb999 100644 --- a/scripts/admin-test-env-modules/phase3-producer-chain-contract.test.mjs +++ b/scripts/admin-test-env-modules/phase3-producer-chain-contract.test.mjs @@ -4,6 +4,7 @@ import test from 'node:test' import { selectCustomsFlow } from './phase3-customs.mjs' import { buildFinalizeResource } from './phase3-loading-finalize.mjs' +import { selectContainerLoadingFixture } from './phase3-container-loading.mjs' import { AcceptanceRun } from '../admin-test-env-acceptance.mjs' function source(name) { @@ -126,6 +127,7 @@ test('PDA inventory snapshot gates batch and records the only safe mutation gap' test('container loading creates and confirms booking before scoped candidates', () => { const value = source('./phase3-container-loading.mjs') + assert.match(value, /candidates\.find\(\(candidate\) => candidate\.marker === context\.marker\)/) assertOrder(value, [ "const bookingCreate = await request", "const submit = await request", @@ -147,6 +149,33 @@ test('container loading creates and confirms booking before scoped candidates', assert.doesNotMatch(value, /POST', CONTAINERS, containerPayload/) }) +test('container loading selects the current marker instead of a stale CAB fixture', () => { + const selected = selectContainerLoadingFixture({ + marker: 'ADMIN-RUN-AUTO1', + createdResources: { + orchestrationFixtures: { + phase3Container: { marker: 'ADMIN-RUN-CAB', inboundId: 'CAB-INBOUND' }, + }, + inboundMeasurement: { marker: 'ADMIN-RUN-AUTO1', inboundId: 'FINANCE-INBOUND' }, + orders: { marker: 'ADMIN-RUN-AUTO1', inboundId: 'FINANCE-ORDER-INBOUND' }, + }, + }) + assert.equal(selected.inboundId, 'FINANCE-INBOUND') + assert.equal(selected.marker, 'ADMIN-RUN-AUTO1') +}) + +test('container loading keeps an unmatched candidate so marker failure stays observable', () => { + const selected = selectContainerLoadingFixture({ + marker: 'ADMIN-RUN-AUTO1', + createdResources: { + orchestrationFixtures: { + phase3Container: { marker: 'ADMIN-RUN-CAB', inboundId: 'CAB-INBOUND' }, + }, + }, + }) + assert.equal(selected.marker, 'ADMIN-RUN-CAB') +}) + test('container loading enforces channel isolation and stable PDA identity across container number changes', () => { const value = source('./phase3-container-loading.mjs') assertOrder(value, [ @@ -269,6 +298,20 @@ test('phase3 loading provisions its own marker-isolated WAIT_SPLIT fixture', asy assert.equal(run.marker.includes('-LOAD'), false) }) +test('container fixture keeps the run marker and does not publish incomplete WAIT_PRICING resources', () => { + const entry = source('../admin-test-env-acceptance.mjs') + assert.match(entry, /const fixtureMarker = originalMarker/) + assert.match(entry, /fixture\.finalStatus !== 'APPROVED'/) + assert.match(entry, /禁止回退使用 WAIT_PRICING 订单/) +}) + +test('measurement fixture polls asynchronous completion before exporting loading identity', () => { + const measurement = source('./inbound-measurement.mjs') + assert.match(measurement, /waitForCompletedMeasurement/) + assert.match(measurement, /measurement\?\.processStatus === 'WAIT_SPLIT'/) + assert.match(measurement, /setTimeout\(resolve, 1_000\)/) +}) + test('customs and finalize consume the same loading inbound identity', () => { const loadingFixture = { marker: 'RUN-1', tenantId: 10001, orderId: 101, inboundId: 201 } const loading = {