优化
This commit is contained in:
@@ -49,13 +49,13 @@
|
|||||||
- 通用配置在 `config/nacos/{local,cn}/`;服务日志、上传文件、`target/`、`reports/` 不属于源码。
|
- 通用配置在 `config/nacos/{local,cn}/`;服务日志、上传文件、`target/`、`reports/` 不属于源码。
|
||||||
- 全量验证:`mvn -DskipTests validate`、`mvn test`;单服务优先 `mvn -pl <module> -am test`。不要在本地启动 Java/Nacos/数据库,除非用户明确要求。
|
- 全量验证:`mvn -DskipTests validate`、`mvn test`;单服务优先 `mvn -pl <module> -am test`。不要在本地启动 Java/Nacos/数据库,除非用户明确要求。
|
||||||
|
|
||||||
## 全量测试脚本路由
|
## 全量 HTTP 验收脚本路由
|
||||||
|
|
||||||
- 本地后端静态/单元回归:在 `baiyue-firstleg/` 执行 `pwsh scripts/backend-full-regression.ps1 -DryRun` 先检查覆盖文件,再按需去掉 `-DryRun` 执行 `order + firstleg` Maven 测试;脚本不访问数据库、Gateway 或测试环境。
|
- 全量验收唯一指向测试环境 HTTP:平台端执行 `scripts/admin-test-env-acceptance.mjs`,客户端执行 `scripts/client-test-env-acceptance.mjs`;二期兼容场景才单独执行 `scripts/client-phase2-test-env-acceptance.mjs`。
|
||||||
- 本地前端回归:在 `baiyue-firstleg/` 执行 `pwsh scripts/frontend-full-regression.ps1 -DryRun` 先检查入口,再按需执行 Admin/Client 的 `pnpm test` 和 `pnpm typecheck`;脚本不启动开发服务器、不访问 Gateway。
|
- Admin 总入口默认按模块列表覆盖平台全链路;需要定位单个业务时使用 `--modules <name>`,模块名称和静态合同先查 `scripts/admin-test-env-modules/README.md`。
|
||||||
- 测试环境 API 全量验收:`scripts/admin-test-env-acceptance.mjs`、`scripts/client-test-env-acceptance.mjs`、`scripts/phase2-test-env-acceptance.ps1` 及 `scripts/admin-test-env-modules/` 属于有数据写入的场景脚本,会创建测试客户、订单或业务记录;只有用户在当前对话明确授权测试环境验收时才执行,并使用报告目录留存结果。
|
- 这些脚本会创建、更新或最终停用测试数据,必须在当前对话明确授权测试环境验收后执行,并保存 JSON 报告;未授权不得请求 Gateway。
|
||||||
- `scripts/local-db-full-acceptance.ps1` 仅允许 `127.0.0.1`/`localhost` 数据库,先用 `-DryRun` 校验统一 SQL 快照和注释门禁;不得改参数连接远程或测试数据库。
|
- 后端 Maven、前端 `pnpm test/typecheck`、PDA `node --test` 和数据库 Dry-run 都是本地质量检查,不属于全量 HTTP 验收。
|
||||||
- 验收脚本按“先本地静态回归,再测试环境 API 验收,最后人工浏览器样式验收”顺序使用;脚本通过不等于页面视觉、权限和真实数据链路全部通过。
|
- HTTP 全量通过后,才进入 Admin/Client 真实页面调用、浏览器样式和 PDA 页面/设备验收;自动化 HTTP 通过不代表视觉验收完成。
|
||||||
|
|
||||||
## 阶段文档入口
|
## 阶段文档入口
|
||||||
|
|
||||||
|
|||||||
@@ -2401,7 +2401,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_order (
|
|||||||
order_id BIGINT UNSIGNED NOT NULL COMMENT '订单服务订单ID,不建立跨库外键',
|
order_id BIGINT UNSIGNED NOT NULL COMMENT '订单服务订单ID,不建立跨库外键',
|
||||||
order_no VARCHAR(32) NOT NULL COMMENT '订单号快照',
|
order_no VARCHAR(32) NOT NULL COMMENT '订单号快照',
|
||||||
inbound_code VARCHAR(40) 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 '订单状态快照',
|
order_status_snapshot VARCHAR(24) NOT NULL COMMENT '订单状态快照',
|
||||||
measure_review_status VARCHAR(24) NOT NULL DEFAULT 'NOT_SUBMITTED' COMMENT '测量审核状态:NOT_SUBMITTED未提交、PENDING待审核、APPROVED已通过、REJECTED已驳回',
|
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已作废',
|
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', 'pallet_count', '已分配总托盘数快照');
|
||||||
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'parent_inbound_id', '直属主入仓单ID;主单为空');
|
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', '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', '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', 'root_inbound_id', '根主入仓单ID;存量主单可为空,按自身ID解释');
|
||||||
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'split_purpose', '履约分片用途:MASTER、LOADED、REMAINDER');
|
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)
|
(id, tenant_id, app_code, permission_type, parent_id, permission_code, permission_name, route_path, http_method, sort_no, status, deleted)
|
||||||
VALUES
|
VALUES
|
||||||
(5105000, 0, 'PLATFORM', 'MENU', 0, 'platform:finance', '财务管理', '/finance', NULL, 60, 'ACTIVE', 0),
|
(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),
|
(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),
|
(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),
|
(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 = 5105030 AND permission_code <> 'platform:finance:reconciliation:void')
|
||||||
OR (id = 5105031 AND permission_code <> 'platform:finance:settlement-ar:create')
|
OR (id = 5105031 AND permission_code <> 'platform:finance:settlement-ar:create')
|
||||||
OR (id = 5105032 AND permission_code <> 'platform:finance:settlement-ap: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 = 5105035 AND permission_code <> 'platform:finance:payment:create')
|
||||||
OR (id = 5105036 AND permission_code <> 'platform:finance:settlement:void')
|
OR (id = 5105036 AND permission_code <> 'platform:finance:settlement:void')
|
||||||
OR (permission_code = 'platform:finance:account:view' AND id <> 5105022)
|
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:reconciliation:void' AND id <> 5105030)
|
||||||
OR (permission_code = 'platform:finance:settlement-ar:create' AND id <> 5105031)
|
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-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:payment:create' AND id <> 5105035)
|
||||||
OR (permission_code = 'platform:finance:settlement:void' AND id <> 5105036)
|
OR (permission_code = 'platform:finance:settlement:void' AND id <> 5105036)
|
||||||
OR (id BETWEEN 5105022 AND 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),
|
(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),
|
(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),
|
(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),
|
(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)
|
(5105036, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:settlement:void', '作废或重开结算单', NULL, 'POST', 270, 'ACTIVE', 1, 0)
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
@@ -14001,10 +13994,8 @@ BEGIN
|
|||||||
'platform:finance:reconciliation:void',
|
'platform:finance:reconciliation:void',
|
||||||
'platform:finance:settlement-ar:create',
|
'platform:finance:settlement-ar:create',
|
||||||
'platform:finance:settlement-ap:create',
|
'platform:finance:settlement-ap:create',
|
||||||
'platform:finance:settlement-ar:view',
|
|
||||||
'platform:finance:settlement-ap:view',
|
|
||||||
'platform:finance:payment:create',
|
'platform:finance:payment:create',
|
||||||
'platform:finance:settlement:void')) <> 15 THEN
|
'platform:finance:settlement:void')) <> 13 THEN
|
||||||
SIGNAL SQLSTATE '45000'
|
SIGNAL SQLSTATE '45000'
|
||||||
SET MESSAGE_TEXT = 'finance G1-G4 permission catalog is incomplete';
|
SET MESSAGE_TEXT = 'finance G1-G4 permission catalog is incomplete';
|
||||||
END IF;
|
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
|
SELECT 4000001, id, 0 FROM baiyue_iam.iam_permission
|
||||||
WHERE tenant_id=0 AND app_code='PLATFORM' AND permission_code IN (
|
WHERE tenant_id=0 AND app_code='PLATFORM' AND permission_code IN (
|
||||||
'platform:finance',
|
'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:update', 'platform:finance:bill:issue',
|
||||||
'platform:finance:bill:adjust', 'platform:finance:bill:void',
|
'platform:finance:bill:adjust', 'platform:finance:bill:void',
|
||||||
'platform:finance:receipt:view', 'platform:finance:receipt:create',
|
'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:create', 'platform:finance:reconciliation:review',
|
||||||
'platform:finance:reconciliation:issue', 'platform:finance:reconciliation:adjust',
|
'platform:finance:reconciliation:issue', 'platform:finance:reconciliation:adjust',
|
||||||
'platform:finance:reconciliation:void', 'platform:finance:settlement-ar:create',
|
'platform:finance:reconciliation:void', 'platform:finance:settlement-ar:create',
|
||||||
'platform:finance:settlement-ap:create', 'platform:finance:settlement-ar:view',
|
'platform:finance:settlement-ap:create', 'platform:finance:payment:create',
|
||||||
'platform:finance:settlement-ap:view', 'platform:finance:payment:create',
|
|
||||||
'platform:finance:settlement:void')
|
'platform:finance:settlement:void')
|
||||||
ON DUPLICATE KEY UPDATE tenant_id=VALUES(tenant_id);
|
ON DUPLICATE KEY UPDATE tenant_id=VALUES(tenant_id);
|
||||||
INSERT INTO baiyue_iam.iam_role_permission (role_id, permission_id, 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 $$
|
DELIMITER $$
|
||||||
CREATE PROCEDURE assert_finance_permissions()
|
CREATE PROCEDURE assert_finance_permissions()
|
||||||
BEGIN
|
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 权限不完整';
|
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='统一迁移失败:平台 Finance 权限不完整';
|
||||||
END IF;
|
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
|
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 只读权限不完整';
|
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='统一迁移失败:客户 Finance 只读权限不完整';
|
||||||
END IF;
|
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 授权不完整';
|
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='统一迁移失败:平台管理员 Finance 授权不完整';
|
||||||
END IF;
|
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
|
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$$
|
END$$
|
||||||
DELIMITER ;
|
DELIMITER ;
|
||||||
|
|
||||||
-- 二、三期对外统一使用 INBOUNDED;历史 WAIT_SPLIT 仅由查询适配为 INBOUNDED,统一迁移不改写主表或历史状态。
|
-- 二、三期入库完成后的唯一状态为 WAIT_SPLIT。
|
||||||
USE baiyue_firstleg;
|
USE baiyue_firstleg;
|
||||||
CREATE TABLE IF NOT EXISTS fl_container_exception_history (
|
CREATE TABLE IF NOT EXISTS fl_container_exception_history (
|
||||||
id BIGINT UNSIGNED NOT NULL COMMENT '异常历史ID',
|
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)
|
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'
|
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');
|
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
|
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)
|
(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
|
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');
|
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,
|
SELECT 'PASS' AS migration_status,
|
||||||
'统一增量全部执行完成;父子订单、履约分片、装柜门禁、订舱拖车、报关船期、船司查询、仓库角色、费用、Finance人工财务、公共资源及可靠审计结构校验通过' AS message;
|
'统一增量全部执行完成;父子订单、履约分片、装柜门禁、订舱拖车、报关船期、船司查询、仓库角色、费用、Finance人工财务、公共资源及可靠审计结构校验通过' AS message;
|
||||||
|
|||||||
+3
-3
@@ -54,7 +54,7 @@
|
|||||||
| `PROCESSING` | 履约处理中。 |
|
| `PROCESSING` | 履约处理中。 |
|
||||||
| `COMPLETED` | 订单完成。 |
|
| `COMPLETED` | 订单完成。 |
|
||||||
|
|
||||||
统一订单列表的主状态是展示分组,不等于订单表的内部状态:`PENDING_REVIEW`、`WAIT_MEASURE`、`INBOUNDED`、`LOADING`、`LOADED`、`DELIVERED`。
|
统一订单列表的主状态是展示分组,不等于订单表的内部状态:`PENDING_REVIEW`、`WAIT_MEASURE`、`WAIT_SPLIT`、`LOADING`、`LOADED`、`DELIVERED`。
|
||||||
|
|
||||||
## 仓库与库位
|
## 仓库与库位
|
||||||
|
|
||||||
@@ -89,13 +89,13 @@
|
|||||||
| `WAIT_PRICING` | 测量通过或异常放行,等待计费。 |
|
| `WAIT_PRICING` | 测量通过或异常放行,等待计费。 |
|
||||||
| `FEE_CONFIRMING` | 已生成费用,等待费用确认和发送。 |
|
| `FEE_CONFIRMING` | 已生成费用,等待费用确认和发送。 |
|
||||||
| `INBOUNDING` | 费用已确认,正在占用托盘、库位并完成入库。 |
|
| `INBOUNDING` | 费用已确认,正在占用托盘、库位并完成入库。 |
|
||||||
| `INBOUNDED` | 入库完成,可进入智能装柜或预装确认;费用状态独立维护。 |
|
| `WAIT_SPLIT` | 入库完成,可进入智能装柜或预装确认;费用状态独立维护。 |
|
||||||
| `LOADING` | 已进入装柜作业,PDA 可执行拣货。 |
|
| `LOADING` | 已进入装柜作业,PDA 可执行拣货。 |
|
||||||
| `LOADED` | 装柜完成,等待派送完成。 |
|
| `LOADED` | 装柜完成,等待派送完成。 |
|
||||||
| `DELIVERED` | 已完成派送。 |
|
| `DELIVERED` | 已完成派送。 |
|
||||||
| `VOIDED` | 已作废,不允许继续处理。 |
|
| `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`。
|
||||||
|
|
||||||
### 测量、重量和费用状态
|
### 测量、重量和费用状态
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -27,7 +27,7 @@ public class FinancePhase1OperationController {
|
|||||||
public FinancePhase1OperationController(FinancePhase1OperationService service) { this.service = service; }
|
public FinancePhase1OperationController(FinancePhase1OperationService service) { this.service = service; }
|
||||||
|
|
||||||
@GetMapping("/accounts")
|
@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,
|
public Object listDerivedCustomerAccounts(@RequestParam(defaultValue = "1") int page,
|
||||||
@RequestParam(defaultValue = "20") int pageSize,
|
@RequestParam(defaultValue = "20") int pageSize,
|
||||||
@RequestParam(required = false) String keyword,
|
@RequestParam(required = false) String keyword,
|
||||||
@@ -36,7 +36,7 @@ public class FinancePhase1OperationController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/accounts/{customerId}/ledger")
|
@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,
|
public Object listDerivedCustomerAccountLedger(@PathVariable long customerId,
|
||||||
@RequestParam(defaultValue = "1") int page,
|
@RequestParam(defaultValue = "1") int page,
|
||||||
@RequestParam(defaultValue = "20") int pageSize,
|
@RequestParam(defaultValue = "20") int pageSize,
|
||||||
@@ -47,7 +47,7 @@ public class FinancePhase1OperationController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/fee-sources/{feeSourceId}")
|
@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) {
|
public ApiResponse<?> getFinanceFeeSource(@PathVariable long feeSourceId) {
|
||||||
return FinanceResponses.success(service.getFinanceFeeSource(feeSourceId));
|
return FinanceResponses.success(service.getFinanceFeeSource(feeSourceId));
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-7
@@ -41,7 +41,7 @@ public class FinanceSettlementController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/receivable-settlements")
|
@GetMapping("/receivable-settlements")
|
||||||
@PreAuthorize("hasAnyAuthority('platform:finance:settlement-ar:list','platform:finance:settlement-ar:view')")
|
@PreAuthorize("hasAuthority('platform:finance:settlement-ar:list')")
|
||||||
@Operation(summary = "查询正式应收结算单")
|
@Operation(summary = "查询正式应收结算单")
|
||||||
public SettlementListEnvelope receivableSettlements(
|
public SettlementListEnvelope receivableSettlements(
|
||||||
@Parameter(description = "可选客户筛选;省略时查询全部客户")
|
@Parameter(description = "可选客户筛选;省略时查询全部客户")
|
||||||
@@ -50,7 +50,7 @@ public class FinanceSettlementController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/payable-settlements")
|
@GetMapping("/payable-settlements")
|
||||||
@PreAuthorize("hasAnyAuthority('platform:finance:settlement-ap:list','platform:finance:settlement-ap:view')")
|
@PreAuthorize("hasAuthority('platform:finance:settlement-ap:list')")
|
||||||
@Operation(summary = "查询正式应付结算单")
|
@Operation(summary = "查询正式应付结算单")
|
||||||
public SettlementListEnvelope payableSettlements(
|
public SettlementListEnvelope payableSettlements(
|
||||||
@Parameter(description = "可选客户筛选;省略时查询全部客户")
|
@Parameter(description = "可选客户筛选;省略时查询全部客户")
|
||||||
@@ -77,17 +77,14 @@ public class FinanceSettlementController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/settlements/{settlementId}")
|
@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,
|
public SettlementDetailEnvelope getSettlement(@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
@PathVariable long settlementId) {
|
@PathVariable long settlementId) {
|
||||||
SettlementDetail detail = service.getSettlement(settlementId);
|
SettlementDetail detail = service.getSettlement(settlementId);
|
||||||
String direction = detail.summary().direction();
|
String direction = detail.summary().direction();
|
||||||
String listPermission = "RECEIVABLE".equals(direction)
|
String listPermission = "RECEIVABLE".equals(direction)
|
||||||
? "platform:finance:settlement-ar:list" : "platform:finance:settlement-ap:list";
|
? "platform:finance:settlement-ar:list" : "platform:finance:settlement-ap:list";
|
||||||
String legacyViewPermission = "RECEIVABLE".equals(direction)
|
if (user == null || !user.permissions().contains(listPermission)) {
|
||||||
? "platform:finance:settlement-ar:view" : "platform:finance:settlement-ap:view";
|
|
||||||
if (user == null || (!user.permissions().contains(listPermission)
|
|
||||||
&& !user.permissions().contains(legacyViewPermission))) {
|
|
||||||
throw new AccessDeniedException("无权查看该方向结算单");
|
throw new AccessDeniedException("无权查看该方向结算单");
|
||||||
}
|
}
|
||||||
return success(detail);
|
return success(detail);
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ public class PlatformFinanceController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/customer-accounts/{customerId}")
|
@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 = "客户人工应收账户及流水")
|
@Operation(summary = "客户人工应收账户及流水")
|
||||||
public ApiResponse<?> customerAccount(@PathVariable long customerId) {
|
public ApiResponse<?> customerAccount(@PathVariable long customerId) {
|
||||||
return FinanceResponses.success(queryService.customerAccount(customerId));
|
return FinanceResponses.success(queryService.customerAccount(customerId));
|
||||||
@@ -59,7 +59,7 @@ public class PlatformFinanceController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/fee-pool")
|
@GetMapping("/fee-pool")
|
||||||
@PreAuthorize("hasAnyAuthority('platform:finance:customer-arap:list','platform:finance:bill:view')")
|
@PreAuthorize("hasAuthority('platform:finance:customer-arap:list')")
|
||||||
@Operation(summary = "客户应收应付费用池")
|
@Operation(summary = "客户应收应付费用池")
|
||||||
public ApiResponse<?> feePool(
|
public ApiResponse<?> feePool(
|
||||||
@Parameter(description = "可选客户筛选;省略时查询全部客户")
|
@Parameter(description = "可选客户筛选;省略时查询全部客户")
|
||||||
@@ -69,7 +69,7 @@ public class PlatformFinanceController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/statements")
|
@GetMapping("/statements")
|
||||||
@PreAuthorize("hasAnyAuthority('platform:finance:customer-statement:list','platform:finance:bill:view')")
|
@PreAuthorize("hasAuthority('platform:finance:customer-statement:list')")
|
||||||
@Operation(summary = "客户对账单")
|
@Operation(summary = "客户对账单")
|
||||||
public ApiResponse<?> statements(
|
public ApiResponse<?> statements(
|
||||||
@Parameter(description = "可选客户筛选;省略时查询全部客户")
|
@Parameter(description = "可选客户筛选;省略时查询全部客户")
|
||||||
@@ -94,7 +94,7 @@ public class PlatformFinanceController {
|
|||||||
public ApiResponse<?> terms(@PathVariable long tenantId) { return FinanceResponses.success(termsView(tenantId, service.customerTerms(tenantId))); }
|
public ApiResponse<?> terms(@PathVariable long tenantId) { return FinanceResponses.success(termsView(tenantId, service.customerTerms(tenantId))); }
|
||||||
|
|
||||||
@GetMapping("/bills")
|
@GetMapping("/bills")
|
||||||
@PreAuthorize("hasAnyAuthority('platform:finance:customer-statement:list','platform:finance:bill:view')")
|
@PreAuthorize("hasAuthority('platform:finance:customer-statement:list')")
|
||||||
@Operation(summary = "查询平台账单")
|
@Operation(summary = "查询平台账单")
|
||||||
public ApiResponse<?> bills(@RequestParam long customerId) { return FinanceResponses.success(queryService.statements(customerId, false)); }
|
public ApiResponse<?> bills(@RequestParam long customerId) { return FinanceResponses.success(queryService.statements(customerId, false)); }
|
||||||
|
|
||||||
@@ -122,11 +122,11 @@ public class PlatformFinanceController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/bills/{billId}")
|
@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)); }
|
public ApiResponse<?> bill(@PathVariable long billId) { return FinanceResponses.success(queryService.platformStatement(billId)); }
|
||||||
|
|
||||||
@GetMapping("/bills/{billId}/revisions")
|
@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) {
|
public ApiResponse<?> revisions(@PathVariable long billId, @RequestParam long customerId) {
|
||||||
return FinanceResponses.success(queryService.billRevisions(customerId, billId));
|
return FinanceResponses.success(queryService.billRevisions(customerId, billId));
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -36,7 +36,7 @@ public class ReconciliationStatementController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/reconciliation-statements/{statementId}")
|
@GetMapping("/reconciliation-statements/{statementId}")
|
||||||
@PreAuthorize("hasAnyAuthority('platform:finance:customer-statement:list','platform:finance:bill:view')")
|
@PreAuthorize("hasAuthority('platform:finance:customer-statement:list')")
|
||||||
public ApiResponse<ReconciliationDetail> getReconciliationStatement(@PathVariable long statementId) {
|
public ApiResponse<ReconciliationDetail> getReconciliationStatement(@PathVariable long statementId) {
|
||||||
return FinanceResponses.success(service.detailForPlatform(statementId));
|
return FinanceResponses.success(service.detailForPlatform(statementId));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,20 +15,20 @@ class FinancePhase1PermissionContractTest {
|
|||||||
@Test
|
@Test
|
||||||
void freezesG1AndG2ControllerPermissionsAndHttpActions() throws Exception {
|
void freezesG1AndG2ControllerPermissionsAndHttpActions() throws Exception {
|
||||||
assertEndpoint("listDerivedCustomerAccounts", GetMapping.class,
|
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);
|
int.class, int.class, String.class, String.class);
|
||||||
assertEndpoint("listDerivedCustomerAccountLedger", GetMapping.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,
|
long.class, int.class, int.class,
|
||||||
String.class, java.time.LocalDate.class, java.time.LocalDate.class);
|
String.class, java.time.LocalDate.class, java.time.LocalDate.class);
|
||||||
assertEndpoint("getFinanceFeeSource", GetMapping.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,
|
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.common.security.AuthenticatedUser.class, long.class, String.class,
|
||||||
com.baiyue.finance.dto.FinancePhase1Dtos.ReviewRequest.class);
|
com.baiyue.finance.dto.FinancePhase1Dtos.ReviewRequest.class);
|
||||||
assertEndpoint("updateFeeSourceSettlementInclusion", PutMapping.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.common.security.AuthenticatedUser.class, long.class, String.class,
|
||||||
com.baiyue.finance.dto.FinancePhase1Dtos.SettlementInclusionRequest.class);
|
com.baiyue.finance.dto.FinancePhase1Dtos.SettlementInclusionRequest.class);
|
||||||
}
|
}
|
||||||
@@ -36,13 +36,10 @@ class FinancePhase1PermissionContractTest {
|
|||||||
private static void assertEndpoint(String methodName,
|
private static void assertEndpoint(String methodName,
|
||||||
Class<? extends java.lang.annotation.Annotation> mapping,
|
Class<? extends java.lang.annotation.Annotation> mapping,
|
||||||
String authority,
|
String authority,
|
||||||
String compatibleAuthority,
|
|
||||||
Class<?>... parameterTypes) throws Exception {
|
Class<?>... parameterTypes) throws Exception {
|
||||||
Method method = FinancePhase1OperationController.class.getMethod(methodName, parameterTypes);
|
Method method = FinancePhase1OperationController.class.getMethod(methodName, parameterTypes);
|
||||||
assertThat(method.isAnnotationPresent(mapping)).isTrue();
|
assertThat(method.isAnnotationPresent(mapping)).isTrue();
|
||||||
assertThat(method.getAnnotation(PreAuthorize.class).value()).contains(authority);
|
assertThat(method.getAnnotation(PreAuthorize.class).value()).contains(authority);
|
||||||
if (compatibleAuthority != null) {
|
assertThat(method.getAnnotation(PreAuthorize.class).value()).doesNotContain(":view");
|
||||||
assertThat(method.getAnnotation(PreAuthorize.class).value()).contains(compatibleAuthority);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package com.baiyue.finance;
|
|||||||
|
|
||||||
import com.baiyue.finance.controller.PlatformFinanceController;
|
import com.baiyue.finance.controller.PlatformFinanceController;
|
||||||
import com.baiyue.finance.controller.FinanceSettlementController;
|
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 io.swagger.v3.oas.annotations.Parameter;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
@@ -30,10 +32,35 @@ class FinancePlatformListContractTest {
|
|||||||
Map<String, String> expected = Map.of(
|
Map<String, String> expected = Map.of(
|
||||||
"feePool", "platform:finance:customer-arap:list",
|
"feePool", "platform:finance:customer-arap:list",
|
||||||
"statements", "platform:finance:customer-statement:list",
|
"statements", "platform:finance:customer-statement:list",
|
||||||
"receivableSettlements", "platform:finance:settlement-ar:view",
|
"receivableSettlements", "platform:finance:settlement-ar:list",
|
||||||
"payableSettlements", "platform:finance:settlement-ap:view");
|
"payableSettlements", "platform:finance:settlement-ap:list");
|
||||||
for (var entry : expected.entrySet()) {
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ class FinanceSettlementBehaviorTest {
|
|||||||
FinanceSettlementController controller = new FinanceSettlementController(controllerService);
|
FinanceSettlementController controller = new FinanceSettlementController(controllerService);
|
||||||
SettlementDetail payable = detail("PAYABLE", "ISSUED", BigDecimal.ZERO, new BigDecimal("100.00"), 0);
|
SettlementDetail payable = detail("PAYABLE", "ISSUED", BigDecimal.ZERO, new BigDecimal("100.00"), 0);
|
||||||
when(controllerService.getSettlement(88L)).thenReturn(payable);
|
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);
|
var response = controller.getSettlement(apViewer, 88L);
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ class FinanceSettlementBehaviorTest {
|
|||||||
assertThat(response.data()).isSameAs(payable);
|
assertThat(response.data()).isSameAs(payable);
|
||||||
assertThat(response.requestId()).isNotBlank();
|
assertThat(response.requestId()).isNotBlank();
|
||||||
assertThatThrownBy(() -> controller.getSettlement(
|
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
|
@Test
|
||||||
|
|||||||
+1
-1
@@ -29,7 +29,7 @@ public class MerchantInboundBillingController {
|
|||||||
@GetMapping("/{orderId}/inbound-billing")
|
@GetMapping("/{orderId}/inbound-billing")
|
||||||
@PreAuthorize("hasAuthority('merchant:order:view')")
|
@PreAuthorize("hasAuthority('merchant:order:view')")
|
||||||
@Operation(summary = "查询订单入库结果和最新费用",
|
@Operation(summary = "查询订单入库结果和最新费用",
|
||||||
description = "客户只读查看已达到 INBOUNDED + APPROVED + CONFIRMED 的二期费用,不需要确认费用。")
|
description = "客户只读查看已达到 WAIT_SPLIT + APPROVED + CONFIRMED 的二期费用,不需要确认费用。")
|
||||||
public ApiResponse<MerchantInboundBillingResponse> find(
|
public ApiResponse<MerchantInboundBillingResponse> find(
|
||||||
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable long orderId) {
|
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable long orderId) {
|
||||||
return ApiResponse.success(service.find(FirstlegScopeGuard.merchant(user), orderId));
|
return ApiResponse.success(service.find(FirstlegScopeGuard.merchant(user), orderId));
|
||||||
|
|||||||
+1
-1
@@ -51,7 +51,7 @@ public class PlatformInboundMeasurementReviewController {
|
|||||||
@Valid @RequestBody MeasurementReviewRequest request) {
|
@Valid @RequestBody MeasurementReviewRequest request) {
|
||||||
validateScope(user, tenantId);
|
validateScope(user, tenantId);
|
||||||
service.approve(tenantId, user.userId(), user.username(), inboundId, idempotencyKey, request);
|
service.approve(tenantId, user.userId(), user.username(), inboundId, idempotencyKey, request);
|
||||||
// 统一订单主线要求测量审核后直接进入 INBOUNDED,不把计费、库位或其他资料
|
// 统一订单主线要求测量审核后直接进入 WAIT_SPLIT,不把计费、库位或其他资料
|
||||||
// 作为本版本的强制门禁。保留原 FeeSnapshotResponse 泛型以兼容既有客户端。
|
// 作为本版本的强制门禁。保留原 FeeSnapshotResponse 泛型以兼容既有客户端。
|
||||||
return ApiResponse.success(null);
|
return ApiResponse.success(null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ public enum InboundListTab {
|
|||||||
WAIT_REVIEW(List.of("MEASURE_PENDING")),
|
WAIT_REVIEW(List.of("MEASURE_PENDING")),
|
||||||
/** 待发送费用:等待计费、费用确认或正在完成入库。 */
|
/** 待发送费用:等待计费、费用确认或正在完成入库。 */
|
||||||
WAIT_FEE_SEND(List.of("WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING")),
|
WAIT_FEE_SEND(List.of("WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING")),
|
||||||
/** 已入库:费用已发送且入库完成。 */
|
/** 待分柜:入库和费用发布完成,等待形成有效分柜方案。 */
|
||||||
INBOUNDED(List.of("INBOUNDED"));
|
WAIT_SPLIT(List.of("WAIT_SPLIT"));
|
||||||
|
|
||||||
private final List<String> processStatuses;
|
private final List<String> processStatuses;
|
||||||
|
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ public final class InboundProcessStatus {
|
|||||||
public static final String FEE_CONFIRMING = "FEE_CONFIRMING";
|
public static final String FEE_CONFIRMING = "FEE_CONFIRMING";
|
||||||
/** 费用已确认,正在执行托盘、库位占用和入库收口。 */
|
/** 费用已确认,正在执行托盘、库位占用和入库收口。 */
|
||||||
public static final String INBOUNDING = "INBOUNDING";
|
public static final String INBOUNDING = "INBOUNDING";
|
||||||
/** 历史已入库终态;存量数据仍允许进入分柜。 */
|
|
||||||
public static final String INBOUNDED = "INBOUNDED";
|
|
||||||
/** 入库和费用发布完成,等待分柜。 */
|
/** 入库和费用发布完成,等待分柜。 */
|
||||||
public static final String WAIT_SPLIT = "WAIT_SPLIT";
|
public static final String WAIT_SPLIT = "WAIT_SPLIT";
|
||||||
/** 已进入装柜作业,PDA 可执行拣货。 */
|
/** 已进入装柜作业,PDA 可执行拣货。 */
|
||||||
@@ -33,7 +31,7 @@ public final class InboundProcessStatus {
|
|||||||
|
|
||||||
public static final Set<String> VALUES = Set.of(
|
public static final Set<String> VALUES = Set.of(
|
||||||
WAIT_MEASURE, MEASURING, MEASURE_PENDING, MEASURE_REJECTED,
|
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
|
LOADING, LOADED, DELIVERED, VOIDED
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ public record UnifiedInboundQuery(
|
|||||||
Long tenantId,
|
Long tenantId,
|
||||||
@Size(max = 64) String keyword,
|
@Size(max = 64) String keyword,
|
||||||
@Size(max = 64) String orderNo,
|
@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"})
|
"LOADING", "LOADED", "DELIVERED"})
|
||||||
@Size(max = 32) String mainStatus,
|
@Size(max = 32) String mainStatus,
|
||||||
Long customerId,
|
Long customerId,
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ public interface LoadingInboundMapper {
|
|||||||
AND loaded.process_status<>'LOADED' AND loaded.deleted=0
|
AND loaded.process_status<>'LOADED' AND loaded.deleted=0
|
||||||
SET root.process_status='LOADED', root.updated_by=#{operatorId}, root.version=root.version+1
|
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
|
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
|
AND remainder.id IS NULL AND loaded.id IS NULL
|
||||||
""")
|
""")
|
||||||
int aggregateRootLoaded(@Param("tenantId") long tenantId,
|
int aggregateRootLoaded(@Param("tenantId") long tenantId,
|
||||||
|
|||||||
@@ -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,
|
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)
|
inbound_role,parent_inbound_id,root_inbound_id,split_sequence,split_status,split_purpose)
|
||||||
SELECT #{childId},tenant_id,task_id,#{childOrderId},#{childOrderNo},#{childInboundCode},
|
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,
|
'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,
|
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,
|
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),
|
SET package_count = GREATEST(COALESCE(package_count, total_package_count) - #{packageCount}, 0),
|
||||||
split_status = 'PARTIALLY_SPLIT', updated_by = #{operatorId}, version = version + 1
|
split_status = 'PARTIALLY_SPLIT', updated_by = #{operatorId}, version = version + 1
|
||||||
WHERE id = #{parentInboundId} AND tenant_id = #{accountId} AND deleted = 0
|
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,
|
int markParentPartiallySplit(@Param("accountId") long accountId, @Param("parentInboundId") long parentInboundId,
|
||||||
@Param("packageCount") int packageCount, @Param("operatorId") long operatorId,
|
@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',"
|
@Update("UPDATE fl_inbound_order SET package_count=#{loadedPackageCount},process_status='LOADING',"
|
||||||
+ "split_status='FULLY_SPLIT',updated_by=#{operatorId},version=version+1 "
|
+ "split_status='FULLY_SPLIT',updated_by=#{operatorId},version=version+1 "
|
||||||
+ "WHERE id=#{sourceInboundId} AND tenant_id=#{accountId} AND deleted=0 "
|
+ "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}")
|
+ "AND COALESCE(package_count,total_package_count)>=#{loadedPackageCount} AND version=#{expectedVersion}")
|
||||||
int markSourceLoaded(@Param("accountId") long accountId, @Param("sourceInboundId") long sourceInboundId,
|
int markSourceLoaded(@Param("accountId") long accountId, @Param("sourceInboundId") long sourceInboundId,
|
||||||
@Param("loadedPackageCount") int loadedPackageCount,
|
@Param("loadedPackageCount") int loadedPackageCount,
|
||||||
|
|||||||
@@ -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
|
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')
|
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
|
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 (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.inbound_detail_id IS NOT NULL
|
||||||
AND i.package_count>COALESCE((SELECT SUM(li.package_count) FROM fl_loading_plan_item li
|
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
|
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')
|
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
|
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 (o.inbound_role='MASTER' OR (o.split_purpose='REMAINDER' AND o.split_status='NOT_SPLIT'))
|
||||||
AND i.inbound_detail_id=#{detailId}
|
AND i.inbound_detail_id=#{detailId}
|
||||||
ORDER BY m.id DESC LIMIT 1 FOR UPDATE
|
ORDER BY m.id DESC LIMIT 1 FOR UPDATE
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ public class FbaPricingRepository {
|
|||||||
.eq(InboundPricingEntity::getProcessStatus, processStatus)
|
.eq(InboundPricingEntity::getProcessStatus, processStatus)
|
||||||
.in(InboundPricingEntity::getFeeStatus, List.of("UNPRICED", "CALCULATED"))
|
.in(InboundPricingEntity::getFeeStatus, List.of("UNPRICED", "CALCULATED"))
|
||||||
.set(InboundPricingEntity::getFeeStatus, "CALCULATED");
|
.set(InboundPricingEntity::getFeeStatus, "CALCULATED");
|
||||||
if (!List.of("WAIT_SPLIT", "INBOUNDED").contains(processStatus)) {
|
if (!"WAIT_SPLIT".equals(processStatus)) {
|
||||||
update.set(InboundPricingEntity::getProcessStatus, "FEE_CONFIRMING");
|
update.set(InboundPricingEntity::getProcessStatus, "FEE_CONFIRMING");
|
||||||
}
|
}
|
||||||
if ("KG".equals(chargeMode)) update.set(InboundPricingEntity::getChargeableWeightKg, chargeable);
|
if ("KG".equals(chargeMode)) update.set(InboundPricingEntity::getChargeableWeightKg, chargeable);
|
||||||
@@ -229,7 +229,7 @@ public class FbaPricingRepository {
|
|||||||
public int markInboundConfirmed(long tenantId, long inboundId) {
|
public int markInboundConfirmed(long tenantId, long inboundId) {
|
||||||
return inbounds.update(null, new LambdaUpdateWrapper<InboundPricingEntity>()
|
return inbounds.update(null, new LambdaUpdateWrapper<InboundPricingEntity>()
|
||||||
.eq(InboundPricingEntity::getTenantId, tenantId).eq(InboundPricingEntity::getId, inboundId)
|
.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")
|
.eq(InboundPricingEntity::getFeeStatus, "CALCULATED")
|
||||||
.set(InboundPricingEntity::getFeeStatus, "CONFIRMED"));
|
.set(InboundPricingEntity::getFeeStatus, "CONFIRMED"));
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -58,7 +58,7 @@ public class AutomaticInboundCompletionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static boolean completed(InboundOrderEntity order) {
|
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());
|
&& "SENT".equals(order.getFeeStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ public class FbaPricingService {
|
|||||||
|
|
||||||
private InboundPricingEntity requireEligibleInbound(long tenantId, long inboundId) {
|
private InboundPricingEntity requireEligibleInbound(long tenantId, long inboundId) {
|
||||||
InboundPricingEntity inbound = requireApprovedInbound(tenantId, 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())) {
|
|| !List.of("UNPRICED", "CALCULATED").contains(inbound.getFeeStatus())) {
|
||||||
throw conflict("当前入库单状态不允许费用试算");
|
throw conflict("当前入库单状态不允许费用试算");
|
||||||
}
|
}
|
||||||
@@ -235,7 +235,7 @@ public class FbaPricingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isCompletedInbound(String processStatus) {
|
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) {
|
private static PricingDestinationType destinationType(InboundPricingEntity inbound) {
|
||||||
|
|||||||
+1
-1
@@ -114,7 +114,7 @@ public class MerchantInboundBillingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isPublished(InboundOrderEntity inbound) {
|
private static boolean isPublished(InboundOrderEntity inbound) {
|
||||||
return (InboundProcessStatus.INBOUNDED.equals(inbound.getProcessStatus())
|
return (InboundProcessStatus.WAIT_SPLIT.equals(inbound.getProcessStatus())
|
||||||
|| "WAIT_SPLIT".equals(inbound.getProcessStatus()))
|
|| "WAIT_SPLIT".equals(inbound.getProcessStatus()))
|
||||||
&& "APPROVED".equals(inbound.getMeasureReviewStatus())
|
&& "APPROVED".equals(inbound.getMeasureReviewStatus())
|
||||||
&& "CONFIRMED".equals(inbound.getFeeStatus());
|
&& "CONFIRMED".equals(inbound.getFeeStatus());
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ public class PdaPickSettlementService {
|
|||||||
ParentRow parent = rootInboundId == parentInboundId ? root : splits.lockParent(parentInboundId);
|
ParentRow parent = rootInboundId == parentInboundId ? root : splits.lockParent(parentInboundId);
|
||||||
if (parent == null || parent.tenantId() == null || parent.tenantId() <= 0
|
if (parent == null || parent.tenantId() == null || parent.tenantId() <= 0
|
||||||
|| !("MASTER".equals(parent.inboundRole()) || "CHILD".equals(parent.inboundRole()))
|
|| !("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()))) {
|
|| ("MASTER".equals(parent.inboundRole()) && "FULLY_SPLIT".equals(parent.splitStatus()))) {
|
||||||
throw conflict("待拆分主入库单不在可拆分状态");
|
throw conflict("待拆分主入库单不在可拆分状态");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import java.util.stream.Collectors;
|
|||||||
@Service
|
@Service
|
||||||
public class UnifiedInboundQueryService {
|
public class UnifiedInboundQueryService {
|
||||||
private static final Set<String> MAIN_STATUSES = Set.of(
|
private static final Set<String> 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<String> REVIEW_STATUSES = Set.of("UNRECORDED", "PENDING", "APPROVED", "REJECTED");
|
private static final Set<String> REVIEW_STATUSES = Set.of("UNRECORDED", "PENDING", "APPROVED", "REJECTED");
|
||||||
|
|
||||||
@Resource private InboundOrderRepository inbounds;
|
@Resource private InboundOrderRepository inbounds;
|
||||||
@@ -140,7 +140,8 @@ public class UnifiedInboundQueryService {
|
|||||||
return switch (status) {
|
return switch (status) {
|
||||||
case "WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED",
|
case "WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED",
|
||||||
"WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING" -> "WAIT_MEASURE";
|
"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;
|
default -> null;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -150,7 +151,7 @@ public class UnifiedInboundQueryService {
|
|||||||
return switch (mainStatus) {
|
return switch (mainStatus) {
|
||||||
case "PENDING_REVIEW" -> "APPROVE_ORDER";
|
case "PENDING_REVIEW" -> "APPROVE_ORDER";
|
||||||
case "WAIT_MEASURE" -> "MEASURE_PENDING".equals(processStatus) ? "CONFIRM_MEASUREMENT" : null;
|
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
|
: "CONFIRMED".equals(preloadStatus) ? null
|
||||||
: preloadStatus == null ? "SMART_SPLIT" : "CONFIRM_PRELOAD";
|
: preloadStatus == null ? "SMART_SPLIT" : "CONFIRM_PRELOAD";
|
||||||
case "LOADING" -> "COMPLETE_LOADING";
|
case "LOADING" -> "COMPLETE_LOADING";
|
||||||
@@ -169,7 +170,6 @@ public class UnifiedInboundQueryService {
|
|||||||
return switch (mainStatus) {
|
return switch (mainStatus) {
|
||||||
case "WAIT_MEASURE" -> List.of("WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED",
|
case "WAIT_MEASURE" -> List.of("WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED",
|
||||||
"WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING");
|
"WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING");
|
||||||
case "INBOUNDED" -> List.of("INBOUNDED");
|
|
||||||
case "WAIT_SPLIT" -> List.of("WAIT_SPLIT");
|
case "WAIT_SPLIT" -> List.of("WAIT_SPLIT");
|
||||||
case "LOADING", "LOADED", "DELIVERED" -> List.of(mainStatus);
|
case "LOADING", "LOADED", "DELIVERED" -> List.of(mainStatus);
|
||||||
default -> List.of("__NO_MATCH__");
|
default -> List.of("__NO_MATCH__");
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ public interface SplitInboundMapper {
|
|||||||
|
|
||||||
@Update("UPDATE fl_inbound_order SET process_status='LOADING', updated_by=#{operatorId}, "
|
@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 "
|
+ "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,
|
int beginLoading(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId,
|
||||||
@Param("operatorId") long operatorId, @Param("expectedVersion") int expectedVersion);
|
@Param("operatorId") long operatorId, @Param("expectedVersion") int expectedVersion);
|
||||||
|
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ public class SplitPreloadService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isWaitingSplit(String status) {
|
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,
|
private void addHistory(long tenantId, long inboundId, String from, String to, String action,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_order (
|
|||||||
order_id BIGINT UNSIGNED NOT NULL COMMENT '订单服务订单ID,不建立跨库外键',
|
order_id BIGINT UNSIGNED NOT NULL COMMENT '订单服务订单ID,不建立跨库外键',
|
||||||
order_no VARCHAR(32) NOT NULL COMMENT '订单号快照',
|
order_no VARCHAR(32) NOT NULL COMMENT '订单号快照',
|
||||||
inbound_code VARCHAR(40) 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 '订单状态快照',
|
order_status_snapshot VARCHAR(24) NOT NULL COMMENT '订单状态快照',
|
||||||
measure_review_status VARCHAR(24) NOT NULL DEFAULT 'NOT_SUBMITTED' COMMENT '测量审核状态:NOT_SUBMITTED未提交、PENDING待审核、APPROVED已通过、REJECTED已驳回',
|
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已作废',
|
fee_status VARCHAR(24) NOT NULL DEFAULT 'UNPRICED' COMMENT '费用状态:UNPRICED未计费、CALCULATED已计算、CONFIRMED已确认、SENT已发送、VOIDED已作废',
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
-- 新完成入库记录使用 WAIT_SPLIT,存量 INBOUNDED 继续作为可分柜兼容状态。
|
-- 入库完成后的唯一状态为 WAIT_SPLIT。
|
||||||
-- 本脚本不改写业务主表或历史状态,避免影响既有审计记录。
|
|
||||||
USE baiyue_firstleg;
|
USE baiyue_firstleg;
|
||||||
|
|
||||||
DROP PROCEDURE IF EXISTS assert_inbounded_compatibility;
|
DROP PROCEDURE IF EXISTS assert_wait_split_status;
|
||||||
DELIMITER $$
|
DELIMITER $$
|
||||||
CREATE PROCEDURE assert_inbounded_compatibility()
|
CREATE PROCEDURE assert_wait_split_status()
|
||||||
BEGIN
|
BEGIN
|
||||||
IF (SELECT COUNT(*) FROM information_schema.columns
|
IF (SELECT COUNT(*) FROM information_schema.columns
|
||||||
WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_inbound_order'
|
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'
|
WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_inbound_status_history'
|
||||||
AND column_name IN ('from_status', 'to_status')) <> 2 THEN
|
AND column_name IN ('from_status', 'to_status')) <> 2 THEN
|
||||||
SIGNAL SQLSTATE '45000'
|
SIGNAL SQLSTATE '45000'
|
||||||
SET MESSAGE_TEXT = 'WAIT_SPLIT/INBOUNDED 兼容校验失败:入库状态字段不完整';
|
SET MESSAGE_TEXT = 'WAIT_SPLIT 状态校验失败:入库状态字段不完整';
|
||||||
END IF;
|
END IF;
|
||||||
END$$
|
END$$
|
||||||
DELIMITER ;
|
DELIMITER ;
|
||||||
CALL assert_inbounded_compatibility();
|
CALL assert_wait_split_status();
|
||||||
DROP PROCEDURE assert_inbounded_compatibility;
|
DROP PROCEDURE assert_wait_split_status;
|
||||||
|
|||||||
@@ -130,14 +130,14 @@ paths:
|
|||||||
/api/platform/v1/inbounds/{inboundId}/fees/calculate:
|
/api/platform/v1/inbounds/{inboundId}/fees/calculate:
|
||||||
post:
|
post:
|
||||||
summary: 按审核通过测量快照试算 FBA 费用
|
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'}]
|
parameters: [{$ref: '#/components/parameters/TenantId'}, {$ref: '#/components/parameters/InboundId'}, {$ref: '#/components/parameters/IdempotencyKey'}]
|
||||||
requestBody: {content: {application/json: {schema: {$ref: '#/components/schemas/CalculateRequest'}}}}
|
requestBody: {content: {application/json: {schema: {$ref: '#/components/schemas/CalculateRequest'}}}}
|
||||||
responses: {'200': {description: 成功或业务失败}, '403': {description: 无权限}}
|
responses: {'200': {description: 成功或业务失败}, '403': {description: 无权限}}
|
||||||
/api/platform/v1/inbounds/{inboundId}/fees/confirm:
|
/api/platform/v1/inbounds/{inboundId}/fees/confirm:
|
||||||
post:
|
post:
|
||||||
summary: 确认最新费用快照
|
summary: 确认最新费用快照
|
||||||
description: 确认费用只更新费用状态;主流程已到 INBOUNDED 时保持不变。
|
description: 确认费用只更新费用状态;主流程已到 WAIT_SPLIT 时保持不变。
|
||||||
parameters: [{$ref: '#/components/parameters/TenantId'}, {$ref: '#/components/parameters/InboundId'}, {$ref: '#/components/parameters/IdempotencyKey'}]
|
parameters: [{$ref: '#/components/parameters/TenantId'}, {$ref: '#/components/parameters/InboundId'}, {$ref: '#/components/parameters/IdempotencyKey'}]
|
||||||
responses: {'200': {description: 成功或业务失败}, '403': {description: 无权限}}
|
responses: {'200': {description: 成功或业务失败}, '403': {description: 无权限}}
|
||||||
/api/platform/v1/inbounds/{inboundId}/fees/send:
|
/api/platform/v1/inbounds/{inboundId}/fees/send:
|
||||||
@@ -154,7 +154,7 @@ paths:
|
|||||||
/api/platform/v1/inbounds/{inboundId}/fees/published:
|
/api/platform/v1/inbounds/{inboundId}/fees/published:
|
||||||
put:
|
put:
|
||||||
summary: 调整已发布费用
|
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'}]
|
parameters: [{$ref: '#/components/parameters/TenantId'}, {$ref: '#/components/parameters/InboundId'}, {$ref: '#/components/parameters/IdempotencyKey'}]
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ paths:
|
|||||||
post:
|
post:
|
||||||
summary: 按统一订单条件分页查询履约摘要(Order 内部)
|
summary: 按统一订单条件分页查询履约摘要(Order 内部)
|
||||||
description: >-
|
description: >-
|
||||||
mainStatus 接受七态产品状态;WAIT_SPLIT 为正式待分柜状态,INBOUNDED 仅兼容存量已入库记录。
|
mainStatus 接受六态产品状态;WAIT_SPLIT 为入库完成后的唯一待分柜状态。
|
||||||
计费与入库处理中状态在完成前仍归并为 WAIT_MEASURE,不提前开放分柜。
|
计费与入库处理中状态在完成前仍归并为 WAIT_MEASURE,不提前开放分柜。
|
||||||
security:
|
security:
|
||||||
- internalServiceToken: []
|
- internalServiceToken: []
|
||||||
@@ -102,7 +102,7 @@ paths:
|
|||||||
/internal/firstleg/v1/inbounds/unified-orders/detail:
|
/internal/firstleg/v1/inbounds/unified-orders/detail:
|
||||||
post:
|
post:
|
||||||
summary: 按订单 ID 查询统一履约详情(Order 内部)
|
summary: 按订单 ID 查询统一履约详情(Order 内部)
|
||||||
description: 时间线保留 Firstleg 内部 fromStatus/toStatus;summary.mainStatus 使用七态产品状态。
|
description: 时间线保留 Firstleg 内部 fromStatus/toStatus;summary.mainStatus 使用六态产品状态。
|
||||||
security:
|
security:
|
||||||
- internalServiceToken: []
|
- internalServiceToken: []
|
||||||
requestBody:
|
requestBody:
|
||||||
@@ -390,7 +390,7 @@ paths:
|
|||||||
description: 需要 platform:firstleg:inbound:list 权限;默认查询全部企业,可选 tenantId 筛选;仅使用 V2 四个页签
|
description: 需要 platform:firstleg:inbound:list 权限;默认查询全部企业,可选 tenantId 筛选;仅使用 V2 四个页签
|
||||||
parameters:
|
parameters:
|
||||||
- {name: tenantId, in: query, required: false, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
- {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:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: 成功或业务失败,按响应体 code 判断;每条记录携带后续客户隔离操作所需的 tenantId,该字段当前表示客户 ID
|
description: 成功或业务失败,按响应体 code 判断;每条记录携带后续客户隔离操作所需的 tenantId,该字段当前表示客户 ID
|
||||||
@@ -565,7 +565,7 @@ paths:
|
|||||||
/api/merchant/v1/first-leg/orders/{orderId}/inbound-billing:
|
/api/merchant/v1/first-leg/orders/{orderId}/inbound-billing:
|
||||||
get:
|
get:
|
||||||
summary: 客户查询已完成入库结果和最新已发布费用
|
summary: 客户查询已完成入库结果和最新已发布费用
|
||||||
description: 需要 merchant:order:view 权限;客户只读查看,不需要确认费用,仅 INBOUNDED + APPROVED + CONFIRMED 的二期终态可见。
|
description: 需要 merchant:order:view 权限;客户只读查看,不需要确认费用,仅 WAIT_SPLIT + APPROVED + CONFIRMED 的二期终态可见。
|
||||||
parameters:
|
parameters:
|
||||||
- {name: orderId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
- {name: orderId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||||
responses:
|
responses:
|
||||||
@@ -656,8 +656,8 @@ components:
|
|||||||
schemas:
|
schemas:
|
||||||
UnifiedMainStatus:
|
UnifiedMainStatus:
|
||||||
type: string
|
type: string
|
||||||
enum: [PENDING_REVIEW, WAIT_MEASURE, INBOUNDED, WAIT_SPLIT, LOADING, LOADED, DELIVERED]
|
enum: [PENDING_REVIEW, WAIT_MEASURE, WAIT_SPLIT, LOADING, LOADED, DELIVERED]
|
||||||
description: 七态产品主状态;WAIT_SPLIT 为正式待分柜状态,INBOUNDED 仅兼容存量已入库记录
|
description: 六态产品主状态;WAIT_SPLIT 为入库完成后的唯一待分柜状态
|
||||||
UnifiedInboundQuery:
|
UnifiedInboundQuery:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -666,9 +666,9 @@ components:
|
|||||||
orderNo: {type: string, maxLength: 64, nullable: true}
|
orderNo: {type: string, maxLength: 64, nullable: true}
|
||||||
mainStatus:
|
mainStatus:
|
||||||
type: string
|
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
|
nullable: true
|
||||||
description: WAIT_SPLIT 与 INBOUNDED 为独立筛选值
|
description: WAIT_SPLIT 为唯一待分柜筛选值
|
||||||
customerId: {type: integer, format: int64, nullable: true}
|
customerId: {type: integer, format: int64, nullable: true}
|
||||||
receivingWarehouseId: {type: integer, format: int64, nullable: true}
|
receivingWarehouseId: {type: integer, format: int64, nullable: true}
|
||||||
destinationWarehouseId: {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}
|
orderId: {type: integer, format: int64, nullable: true}
|
||||||
orderNo: {type: string, nullable: true}
|
orderNo: {type: string, nullable: true}
|
||||||
inboundCode: {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}
|
orderStatusSnapshot: {type: string, nullable: true}
|
||||||
measureReviewStatus: {type: string, nullable: true}
|
measureReviewStatus: {type: string, nullable: true}
|
||||||
feeStatus: {type: string, nullable: true}
|
feeStatus: {type: string, nullable: true}
|
||||||
|
|||||||
@@ -48,9 +48,9 @@ class FbaPricingContractTest {
|
|||||||
.contains("/api/platform/v1/inbounds/{inboundId}/fees/send:")
|
.contains("/api/platform/v1/inbounds/{inboundId}/fees/send:")
|
||||||
.contains("/api/platform/v1/inbounds/{inboundId}/fees/published:")
|
.contains("/api/platform/v1/inbounds/{inboundId}/fees/published:")
|
||||||
.contains("Idempotency-Key")
|
.contains("Idempotency-Key")
|
||||||
.contains("主流程已到 INBOUNDED,也可独立试算且不回退主流程状态")
|
.contains("主流程已到 WAIT_SPLIT,也可独立试算且不回退主流程状态")
|
||||||
.contains("该动作涉及后续仓储流程,本轮二期验收不调用")
|
.contains("该动作涉及后续仓储流程,本轮二期验收不调用")
|
||||||
.contains("不回退 INBOUNDED 状态")
|
.contains("不回退 WAIT_SPLIT 状态")
|
||||||
.doesNotContain("应收")
|
.doesNotContain("应收")
|
||||||
.doesNotContain("应付")
|
.doesNotContain("应付")
|
||||||
.doesNotContain("待客户确认");
|
.doesNotContain("待客户确认");
|
||||||
|
|||||||
+2
-2
@@ -42,7 +42,7 @@ class InboundOrderControllerContractTest {
|
|||||||
openapi.indexOf("/api/platform/v1/first-leg/tasks:"),
|
openapi.indexOf("/api/platform/v1/first-leg/tasks:"),
|
||||||
openapi.indexOf("/api/platform/v1/first-leg/tasks/{taskId}:"));
|
openapi.indexOf("/api/platform/v1/first-leg/tasks/{taskId}:"));
|
||||||
assertThat(inboundList)
|
assertThat(inboundList)
|
||||||
.doesNotContain("WAIT_SPLIT");
|
.contains("WAIT_SPLIT");
|
||||||
assertThat(openapi)
|
assertThat(openapi)
|
||||||
.contains("/api/platform/inbounds:")
|
.contains("/api/platform/inbounds:")
|
||||||
.contains("/api/platform/inbounds/{inboundId}:")
|
.contains("/api/platform/inbounds/{inboundId}:")
|
||||||
@@ -53,7 +53,7 @@ class InboundOrderControllerContractTest {
|
|||||||
.contains("required: [id, tenantId]")
|
.contains("required: [id, tenantId]")
|
||||||
.contains("pricingTaxMode: {type: string, enum: [TAX_INCLUDED, TAX_EXCLUDED]")
|
.contains("pricingTaxMode: {type: string, enum: [TAX_INCLUDED, TAX_EXCLUDED]")
|
||||||
.contains("tenantId: {type: string, pattern: '^[1-9][0-9]*$', description:")
|
.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")
|
.contains("platform:firstleg:inbound:list")
|
||||||
.doesNotContain("WAIT_CUSTOMER_CONFIRM");
|
.doesNotContain("WAIT_CUSTOMER_CONFIRM");
|
||||||
assertThat(inboundList).contains("#/components/schemas/InboundOrderPageEnvelope");
|
assertThat(inboundList).contains("#/components/schemas/InboundOrderPageEnvelope");
|
||||||
|
|||||||
+1
-1
@@ -54,7 +54,7 @@ class InternalInboundOrderReferenceControllerTest {
|
|||||||
@Test
|
@Test
|
||||||
void unifiedQueryRequiresInternalTokenAndForwardsBatchRequest() {
|
void unifiedQueryRequiresInternalTokenAndForwardsBatchRequest() {
|
||||||
UnifiedInboundQuery request = new UnifiedInboundQuery(
|
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, null, null, null, null, null, null, null, null, null, null,
|
||||||
null, null, 1, 20);
|
null, null, 1, 20);
|
||||||
when(unifiedQueryService.page(request)).thenReturn(new PageResult<>(List.of(), 1, 20, 0, 0));
|
when(unifiedQueryService.page(request)).thenReturn(new PageResult<>(List.of(), 1, 20, 0, 0));
|
||||||
|
|||||||
+1
-1
@@ -38,7 +38,7 @@ class MerchantInboundBillingControllerContractTest {
|
|||||||
String openapi = new ClassPathResource("openapi/firstleg-v1.yaml")
|
String openapi = new ClassPathResource("openapi/firstleg-v1.yaml")
|
||||||
.getContentAsString(StandardCharsets.UTF_8);
|
.getContentAsString(StandardCharsets.UTF_8);
|
||||||
assertThat(openapi).contains("/api/merchant/v1/first-leg/orders/{orderId}/inbound-billing:")
|
assertThat(openapi).contains("/api/merchant/v1/first-leg/orders/{orderId}/inbound-billing:")
|
||||||
.contains("INBOUNDED + APPROVED + CONFIRMED");
|
.contains("WAIT_SPLIT + APPROVED + CONFIRMED");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
+4
-4
@@ -12,7 +12,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||||||
|
|
||||||
class UnifiedInboundOpenApiContractTest {
|
class UnifiedInboundOpenApiContractTest {
|
||||||
@Test
|
@Test
|
||||||
void documentsInternalUnifiedOrderEndpointsAndSevenStatuses() throws Exception {
|
void documentsInternalUnifiedOrderEndpointsAndSixStatuses() throws Exception {
|
||||||
String yaml = new ClassPathResource("openapi/firstleg-v1.yaml")
|
String yaml = new ClassPathResource("openapi/firstleg-v1.yaml")
|
||||||
.getContentAsString(StandardCharsets.UTF_8);
|
.getContentAsString(StandardCharsets.UTF_8);
|
||||||
Map<String, Object> document = stringMap(new Yaml().load(yaml));
|
Map<String, Object> document = stringMap(new Yaml().load(yaml));
|
||||||
@@ -25,13 +25,13 @@ class UnifiedInboundOpenApiContractTest {
|
|||||||
Map<String, Object> schemas = stringMap(stringMap(document.get("components")).get("schemas"));
|
Map<String, Object> schemas = stringMap(stringMap(document.get("components")).get("schemas"));
|
||||||
Map<String, Object> responseStatus = stringMap(schemas.get("UnifiedMainStatus"));
|
Map<String, Object> responseStatus = stringMap(schemas.get("UnifiedMainStatus"));
|
||||||
assertThat(stringList(responseStatus.get("enum"))).containsExactly(
|
assertThat(stringList(responseStatus.get("enum"))).containsExactly(
|
||||||
"PENDING_REVIEW", "WAIT_MEASURE", "INBOUNDED", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED");
|
"PENDING_REVIEW", "WAIT_MEASURE", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED");
|
||||||
assertThat(responseStatus.get("description").toString()).contains("WAIT_SPLIT", "INBOUNDED");
|
assertThat(responseStatus.get("description").toString()).contains("WAIT_SPLIT", "唯一待分柜状态");
|
||||||
|
|
||||||
Map<String, Object> query = stringMap(schemas.get("UnifiedInboundQuery"));
|
Map<String, Object> query = stringMap(schemas.get("UnifiedInboundQuery"));
|
||||||
Map<String, Object> queryStatus = stringMap(stringMap(query.get("properties")).get("mainStatus"));
|
Map<String, Object> queryStatus = stringMap(stringMap(query.get("properties")).get("mainStatus"));
|
||||||
assertThat(stringList(queryStatus.get("enum"))).contains("WAIT_SPLIT");
|
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<String, Object> inbound = stringMap(schemas.get("UnifiedInboundOrderSummary"));
|
Map<String, Object> inbound = stringMap(schemas.get("UnifiedInboundOrderSummary"));
|
||||||
assertThat(stringMap(inbound.get("properties"))).containsKeys(
|
assertThat(stringMap(inbound.get("properties"))).containsKeys(
|
||||||
|
|||||||
+2
-2
@@ -86,7 +86,7 @@ class LoadingDeliveryServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
void completesLoadedFulfillmentSliceBeforeAggregatingRoot() {
|
void completesLoadedFulfillmentSliceBeforeAggregatingRoot() {
|
||||||
when(repository.claimOperation(anyLong(), anyLong(), anyLong(), anyString(), anyString(), anyString())).thenReturn(true);
|
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(
|
when(repository.completionTarget(10, 20)).thenReturn(new CompletionTarget(
|
||||||
20L, null, 20L, "LOADING", 3, "LOADING", 3));
|
20L, null, 20L, "LOADING", 3, "LOADING", 3));
|
||||||
LoadingPickItemMapper.InboundProgress progress = mock(LoadingPickItemMapper.InboundProgress.class);
|
LoadingPickItemMapper.InboundProgress progress = mock(LoadingPickItemMapper.InboundProgress.class);
|
||||||
@@ -180,7 +180,7 @@ class LoadingDeliveryServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
void keepsHistoricalLoadedChildAsCompletionTarget() {
|
void keepsHistoricalLoadedChildAsCompletionTarget() {
|
||||||
CompletionTarget historical = new CompletionTarget(
|
CompletionTarget historical = new CompletionTarget(
|
||||||
20L, 21L, 20L, "INBOUNDED", 3, "LOADING", 1);
|
20L, 21L, 20L, "WAIT_SPLIT", 3, "LOADING", 1);
|
||||||
|
|
||||||
assertThat(historical.completionInboundId()).isEqualTo(21L);
|
assertThat(historical.completionInboundId()).isEqualTo(21L);
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -156,7 +156,7 @@ class FbaPricingRepositoryRuleListTest {
|
|||||||
when(pricing.update(org.mockito.ArgumentMatchers.isNull(), org.mockito.ArgumentMatchers.any()))
|
when(pricing.update(org.mockito.ArgumentMatchers.isNull(), org.mockito.ArgumentMatchers.any()))
|
||||||
.thenReturn(1);
|
.thenReturn(1);
|
||||||
|
|
||||||
assertThat(repository.markInboundFeeSent(10001L, 88L, "INBOUNDED")).isEqualTo(1);
|
assertThat(repository.markInboundFeeSent(10001L, 88L, "WAIT_SPLIT")).isEqualTo(1);
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
ArgumentCaptor<LambdaUpdateWrapper<InboundPricingEntity>> wrapper =
|
ArgumentCaptor<LambdaUpdateWrapper<InboundPricingEntity>> wrapper =
|
||||||
@@ -164,7 +164,7 @@ class FbaPricingRepositoryRuleListTest {
|
|||||||
verify(pricing).update(org.mockito.ArgumentMatchers.isNull(), wrapper.capture());
|
verify(pricing).update(org.mockito.ArgumentMatchers.isNull(), wrapper.capture());
|
||||||
assertThat(wrapper.getValue().getSqlSet()).contains("feeStatus").doesNotContain("processStatus");
|
assertThat(wrapper.getValue().getSqlSet()).contains("feeStatus").doesNotContain("processStatus");
|
||||||
assertThat(wrapper.getValue().getCustomSqlSegment()).contains("tenantId", "id", "processStatus", "feeStatus");
|
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
|
@Test
|
||||||
|
|||||||
+9
-9
@@ -11,23 +11,23 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||||||
|
|
||||||
class InboundStatusUnificationSqlContractTest {
|
class InboundStatusUnificationSqlContractTest {
|
||||||
@Test
|
@Test
|
||||||
void preservesHistoricalWaitSplitWithoutDataRewrite() throws Exception {
|
void enforcesCanonicalWaitSplitStatusWithoutCompatibilityRewrite() throws Exception {
|
||||||
String module = normalize(new ClassPathResource("db/phase3-inbounded-status-unification.sql")
|
String module = normalize(new ClassPathResource("db/phase3-inbounded-status-unification.sql")
|
||||||
.getContentAsString(StandardCharsets.UTF_8)).trim();
|
.getContentAsString(StandardCharsets.UTF_8)).trim();
|
||||||
String unified = normalize(Files.readString(unifiedPath(), StandardCharsets.UTF_8));
|
String unified = normalize(Files.readString(unifiedPath(), StandardCharsets.UTF_8));
|
||||||
|
|
||||||
assertThat(module)
|
assertThat(module)
|
||||||
.contains("CREATE PROCEDURE assert_inbounded_compatibility()")
|
.contains("CREATE PROCEDURE assert_wait_split_status()")
|
||||||
.contains("新完成入库记录使用 WAIT_SPLIT")
|
.contains("入库完成后的唯一状态为 WAIT_SPLIT")
|
||||||
.contains("存量 INBOUNDED")
|
.doesNotContain("兼容")
|
||||||
.doesNotContain("UPDATE fl_inbound_order")
|
.doesNotContain("UPDATE fl_inbound_order")
|
||||||
.doesNotContain("UPDATE fl_inbound_status_history");
|
.doesNotContain("UPDATE fl_inbound_status_history");
|
||||||
assertThat(unified)
|
assertThat(unified)
|
||||||
.contains("新完成入库记录使用 WAIT_SPLIT")
|
.contains("入库完成后的唯一状态为 WAIT_SPLIT")
|
||||||
.contains("存量 INBOUNDED")
|
.doesNotContain("历史 WAIT_SPLIT")
|
||||||
.doesNotContain("SET process_status = 'INBOUNDED'")
|
.doesNotContain("SET process_status = 'WAIT_SPLIT'")
|
||||||
.doesNotContain("SET from_status = 'INBOUNDED'")
|
.doesNotContain("SET from_status = 'WAIT_SPLIT'")
|
||||||
.doesNotContain("SET to_status = 'INBOUNDED'");
|
.doesNotContain("SET to_status = 'WAIT_SPLIT'");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Path unifiedPath() {
|
private static Path unifiedPath() {
|
||||||
|
|||||||
+2
-2
@@ -76,7 +76,7 @@ class PdaSplitSettlementSqlContractTest {
|
|||||||
.getAnnotation(Insert.class).value()[0];
|
.getAnnotation(Insert.class).value()[0];
|
||||||
assertThat(clone)
|
assertThat(clone)
|
||||||
.contains("#{childOrderId},#{childOrderNo}")
|
.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
|
@Test
|
||||||
@@ -89,7 +89,7 @@ class PdaSplitSettlementSqlContractTest {
|
|||||||
long.class, long.class, int.class, long.class, int.class)
|
long.class, long.class, int.class, long.class, int.class)
|
||||||
.getAnnotation(Update.class).value()[0];
|
.getAnnotation(Update.class).value()[0];
|
||||||
assertThat(markSource)
|
assertThat(markSource)
|
||||||
.contains("process_status IN ('INBOUNDED','LOADING')")
|
.contains("process_status IN ('WAIT_SPLIT','LOADING')")
|
||||||
.contains("package_count=#{loadedPackageCount}")
|
.contains("package_count=#{loadedPackageCount}")
|
||||||
.contains("split_status='FULLY_SPLIT'");
|
.contains("split_status='FULLY_SPLIT'");
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -84,7 +84,7 @@ class Phase3FinalUnifiedSqlContractTest {
|
|||||||
"-- 来源:iam/src/main/resources/db/phase3-warehouse-roles.sql",
|
"-- 来源:iam/src/main/resources/db/phase3-warehouse-roles.sql",
|
||||||
"firstleg/src/main/resources/db/phase3-carrier-schedule-query-schema.sql");
|
"firstleg/src/main/resources/db/phase3-carrier-schedule-query-schema.sql");
|
||||||
assertBlock(unified, "-- 来源:iam/src/main/resources/db/phase3-warehouse-roles.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");
|
"iam/src/main/resources/db/phase3-warehouse-roles.sql");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -21,7 +21,7 @@ class SmartLoadingCandidateMapperContractTest {
|
|||||||
.contains("m2.status='APPROVED'")
|
.contains("m2.status='APPROVED'")
|
||||||
.contains("i.inbound_detail_id IS NOT NULL")
|
.contains("i.inbound_detail_id IS NOT NULL")
|
||||||
.contains("o.warehouse_id=#{warehouseId}")
|
.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("SELECT SUM(li.package_count)")
|
||||||
.contains("li.deleted=0")
|
.contains("li.deleted=0")
|
||||||
.contains("ORDER BY o.inbounded_at ASC, o.id ASC, i.id ASC");
|
.contains("ORDER BY o.inbounded_at ASC, o.id ASC, i.id ASC");
|
||||||
@@ -38,7 +38,7 @@ class SmartLoadingCandidateMapperContractTest {
|
|||||||
.contains("m.status='APPROVED'")
|
.contains("m.status='APPROVED'")
|
||||||
.contains("SELECT MAX(m2.id)")
|
.contains("SELECT MAX(m2.id)")
|
||||||
.contains("o.warehouse_id=#{warehouseId}")
|
.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("SELECT SUM(li.package_count)")
|
||||||
.contains("FOR UPDATE");
|
.contains("FOR UPDATE");
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -32,7 +32,7 @@ class AutomaticInboundCompletionServiceTest {
|
|||||||
order("FEE_CONFIRMING", "CALCULATED", 5),
|
order("FEE_CONFIRMING", "CALCULATED", 5),
|
||||||
order("FEE_CONFIRMING", "CONFIRMED", 6),
|
order("FEE_CONFIRMING", "CONFIRMED", 6),
|
||||||
order("INBOUNDING", "CONFIRMED", 7),
|
order("INBOUNDING", "CONFIRMED", 7),
|
||||||
order("INBOUNDED", "SENT", 8));
|
order("WAIT_SPLIT", "SENT", 8));
|
||||||
|
|
||||||
FeeSnapshotResponse result = service.completeApproved(10001L, 88L, 9L, "客服", 30L, 4);
|
FeeSnapshotResponse result = service.completeApproved(10001L, 88L, 9L, "客服", 30L, 4);
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ class AutomaticInboundCompletionServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
void shouldHandleCase02() {
|
void shouldHandleCase02() {
|
||||||
FeeSnapshotResponse fee = fee(501L);
|
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);
|
when(pricing.latest(10001L, 88L)).thenReturn(fee);
|
||||||
|
|
||||||
assertThat(service.completeApproved(10001L, 88L, 9L, "客服", 30L, 4)).isSameAs(fee);
|
assertThat(service.completeApproved(10001L, 88L, 9L, "客服", 30L, 4)).isSameAs(fee);
|
||||||
@@ -65,7 +65,7 @@ class AutomaticInboundCompletionServiceTest {
|
|||||||
FeeSnapshotResponse fee = fee(501L);
|
FeeSnapshotResponse fee = fee(501L);
|
||||||
when(pricing.latest(10001L, 88L)).thenReturn(fee);
|
when(pricing.latest(10001L, 88L)).thenReturn(fee);
|
||||||
when(workflow.findOwned(10001L, 88L)).thenReturn(
|
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);
|
service.completeApproved(10001L, 88L, 9L, "客服", 30L, 4);
|
||||||
|
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ class FbaPricingServiceTest {
|
|||||||
void returnsOriginalSnapshotForRepeatedCalculation() {
|
void returnsOriginalSnapshotForRepeatedCalculation() {
|
||||||
InboundPricingIdempotencyEntity completed = completed("FEE_CALCULATE", 501L);
|
InboundPricingIdempotencyEntity completed = completed("FEE_CALCULATE", 501L);
|
||||||
InboundPricingEntity inbound = inbound("10", "1");
|
InboundPricingEntity inbound = inbound("10", "1");
|
||||||
inbound.setProcessStatus("INBOUNDED");
|
inbound.setProcessStatus("WAIT_SPLIT");
|
||||||
when(repository.findIdempotency(TENANT, OPERATOR, "FEE_CALCULATE", "same")).thenReturn(completed);
|
when(repository.findIdempotency(TENANT, OPERATOR, "FEE_CALCULATE", "same")).thenReturn(completed);
|
||||||
when(repository.findFee(TENANT, 501L)).thenReturn(savedFee("CALCULATED"));
|
when(repository.findFee(TENANT, 501L)).thenReturn(savedFee("CALCULATED"));
|
||||||
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
|
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
|
||||||
@@ -195,7 +195,7 @@ class FbaPricingServiceTest {
|
|||||||
FeeSnapshotResponse response = service.calculate(TENANT, OPERATOR, INBOUND, NOW, "same");
|
FeeSnapshotResponse response = service.calculate(TENANT, OPERATOR, INBOUND, NOW, "same");
|
||||||
|
|
||||||
assertThat(response.feeId()).isEqualTo(501L);
|
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()).matchRules(any(), any());
|
||||||
verify(repository, never()).insertFee(any());
|
verify(repository, never()).insertFee(any());
|
||||||
}
|
}
|
||||||
@@ -236,7 +236,7 @@ class FbaPricingServiceTest {
|
|||||||
@DisplayName("已入库费用发送只发布费用且不回退主流程状态")
|
@DisplayName("已入库费用发送只发布费用且不回退主流程状态")
|
||||||
void sendsFeeAfterInboundWithoutRollingBackProcessStatus() {
|
void sendsFeeAfterInboundWithoutRollingBackProcessStatus() {
|
||||||
InboundPricingEntity inbound = inbound("10", "1");
|
InboundPricingEntity inbound = inbound("10", "1");
|
||||||
inbound.setProcessStatus("INBOUNDED");
|
inbound.setProcessStatus("WAIT_SPLIT");
|
||||||
inbound.setFeeStatus("CONFIRMED");
|
inbound.setFeeStatus("CONFIRMED");
|
||||||
InboundFeeEntity confirmed = savedFee("CONFIRMED");
|
InboundFeeEntity confirmed = savedFee("CONFIRMED");
|
||||||
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
|
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
|
||||||
@@ -245,14 +245,14 @@ class FbaPricingServiceTest {
|
|||||||
doAnswer(invocation -> { ((InboundPricingIdempotencyEntity) invocation.getArgument(0)).setId(900L); return null; })
|
doAnswer(invocation -> { ((InboundPricingIdempotencyEntity) invocation.getArgument(0)).setId(900L); return null; })
|
||||||
.when(repository).insertIdempotency(any());
|
.when(repository).insertIdempotency(any());
|
||||||
when(repository.sendFee(TENANT, 501L, OPERATOR, NOW)).thenReturn(1);
|
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.completeIdempotency(any(), eq(501L), eq(NOW))).thenReturn(1);
|
||||||
when(repository.findFee(TENANT, 501L)).thenReturn(confirmed);
|
when(repository.findFee(TENANT, 501L)).thenReturn(confirmed);
|
||||||
|
|
||||||
FeeSnapshotResponse response = service.send(TENANT, OPERATOR, INBOUND, "send-inbounded");
|
FeeSnapshotResponse response = service.send(TENANT, OPERATOR, INBOUND, "send-inbounded");
|
||||||
|
|
||||||
assertThat(response.nextProcessStatus()).isEqualTo("INBOUNDED");
|
assertThat(response.nextProcessStatus()).isEqualTo("WAIT_SPLIT");
|
||||||
verify(repository).markInboundFeeSent(TENANT, INBOUND, "INBOUNDED");
|
verify(repository).markInboundFeeSent(TENANT, INBOUND, "WAIT_SPLIT");
|
||||||
verify(financeOutbox).publishInbound(eq(FinanceFeeSourceOutboxService.PUBLISHED),
|
verify(financeOutbox).publishInbound(eq(FinanceFeeSourceOutboxService.PUBLISHED),
|
||||||
eq(TENANT), eq(INBOUND), eq(501L), eq(1), eq(NOW));
|
eq(TENANT), eq(INBOUND), eq(501L), eq(1), eq(NOW));
|
||||||
}
|
}
|
||||||
@@ -274,7 +274,7 @@ class FbaPricingServiceTest {
|
|||||||
@DisplayName("已发布费用可生成新版本且不回退已入库状态")
|
@DisplayName("已发布费用可生成新版本且不回退已入库状态")
|
||||||
void adjustsPublishedFeeWithoutRollingBackInbound() {
|
void adjustsPublishedFeeWithoutRollingBackInbound() {
|
||||||
InboundPricingEntity inbound = inbound("10", "1");
|
InboundPricingEntity inbound = inbound("10", "1");
|
||||||
inbound.setProcessStatus("INBOUNDED");
|
inbound.setProcessStatus("WAIT_SPLIT");
|
||||||
inbound.setFeeStatus("SENT");
|
inbound.setFeeStatus("SENT");
|
||||||
InboundFeeEntity current = savedFee("SENT");
|
InboundFeeEntity current = savedFee("SENT");
|
||||||
AtomicReference<InboundFeeEntity> inserted = new AtomicReference<>();
|
AtomicReference<InboundFeeEntity> inserted = new AtomicReference<>();
|
||||||
@@ -301,8 +301,8 @@ class FbaPricingServiceTest {
|
|||||||
assertThat(response.feeVersion()).isEqualTo(2);
|
assertThat(response.feeVersion()).isEqualTo(2);
|
||||||
assertThat(response.feeStatus()).isEqualTo("SENT");
|
assertThat(response.feeStatus()).isEqualTo("SENT");
|
||||||
assertThat(response.totalAmount()).isEqualByComparingTo("120.00");
|
assertThat(response.totalAmount()).isEqualByComparingTo("120.00");
|
||||||
assertThat(response.nextProcessStatus()).isEqualTo("INBOUNDED");
|
assertThat(response.nextProcessStatus()).isEqualTo("WAIT_SPLIT");
|
||||||
assertThat(inbound.getProcessStatus()).isEqualTo("INBOUNDED");
|
assertThat(inbound.getProcessStatus()).isEqualTo("WAIT_SPLIT");
|
||||||
verify(repository).supersedeSent(TENANT, 501L);
|
verify(repository).supersedeSent(TENANT, 501L);
|
||||||
verify(financeOutbox).publishInbound(eq(FinanceFeeSourceOutboxService.ADJUSTED),
|
verify(financeOutbox).publishInbound(eq(FinanceFeeSourceOutboxService.ADJUSTED),
|
||||||
eq(TENANT), eq(INBOUND), eq(502L), eq(2), eq(NOW));
|
eq(TENANT), eq(INBOUND), eq(502L), eq(2), eq(NOW));
|
||||||
|
|||||||
+2
-2
@@ -506,7 +506,7 @@ class InboundMeasurementServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void editingAndResubmittingInboundMeasurementDoesNotRollBackMainStatus() {
|
void editingAndResubmittingInboundMeasurementDoesNotRollBackMainStatus() {
|
||||||
InboundOrderEntity inbound = inbound("INBOUNDED");
|
InboundOrderEntity inbound = inbound("WAIT_SPLIT");
|
||||||
inbound.setMeasureReviewStatus("APPROVED");
|
inbound.setMeasureReviewStatus("APPROVED");
|
||||||
InboundMeasurementEntity measurement = new InboundMeasurementEntity();
|
InboundMeasurementEntity measurement = new InboundMeasurementEntity();
|
||||||
measurement.setId(30L);
|
measurement.setId(30L);
|
||||||
@@ -524,7 +524,7 @@ class InboundMeasurementServiceTest {
|
|||||||
InboundMeasurementResponses.MeasurementSubmitted result = service.submit(1L, 10L, 9L,
|
InboundMeasurementResponses.MeasurementSubmitted result = service.submit(1L, 10L, 9L,
|
||||||
new InboundMeasurementRequests.SubmitRequest(30L, 2));
|
new InboundMeasurementRequests.SubmitRequest(30L, 2));
|
||||||
|
|
||||||
assertThat(result.processStatus()).isEqualTo("INBOUNDED");
|
assertThat(result.processStatus()).isEqualTo("WAIT_SPLIT");
|
||||||
assertThat(result.measureReviewStatus()).isEqualTo("APPROVED");
|
assertThat(result.measureReviewStatus()).isEqualTo("APPROVED");
|
||||||
assertThat(measurement.getStatus()).isEqualTo("APPROVED");
|
assertThat(measurement.getStatus()).isEqualTo("APPROVED");
|
||||||
verify(repository, never()).updateMeasurement(any());
|
verify(repository, never()).updateMeasurement(any());
|
||||||
|
|||||||
+2
-2
@@ -42,7 +42,7 @@ class InboundOrderQueryServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldAcceptInboundedAsThePhase2TerminalStatusFilter() {
|
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,
|
"APPROVED", "CONFIRMED", null, null, null, null, null, null, null,
|
||||||
null, null, null, null, 1, 200);
|
null, null, null, null, 1, 200);
|
||||||
PageResult<InboundOrderSummary> expected = new PageResult<>(List.of(), 1, 200, 0, 0);
|
PageResult<InboundOrderSummary> 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_REVIEW.processStatuses()).containsExactly("MEASURE_PENDING");
|
||||||
assertThat(InboundListTab.WAIT_FEE_SEND.processStatuses())
|
assertThat(InboundListTab.WAIT_FEE_SEND.processStatuses())
|
||||||
.containsExactly("WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING");
|
.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)
|
assertThat(List.of(InboundListTab.values())).extracting(Enum::name)
|
||||||
.doesNotContain("WAIT_CUSTOMER_CONFIRM");
|
.doesNotContain("WAIT_CUSTOMER_CONFIRM");
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -36,13 +36,13 @@ class MerchantInboundBillingServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldHandleCase01() {
|
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),
|
InboundOrderDetail detail = new InboundOrderDetail(mock(com.baiyue.firstleg.dto.InboundOrderSummary.class),
|
||||||
"客户备注", List.of(), List.of(
|
"客户备注", List.of(), List.of(
|
||||||
new InboundOrderDetail.FileSummary(1L, "RELATED", "public.pdf", "PUBLIC", 1, 1L, null),
|
new InboundOrderDetail.FileSummary(1L, "RELATED", "public.pdf", "PUBLIC", 1, 1L, null),
|
||||||
new InboundOrderDetail.FileSummary(2L, "INTERNAL", "internal.pdf", "INTERNAL", 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.InternalNoteSummary(1L, "内部备注", 1L, "客服", null)),
|
||||||
List.of(new InboundOrderDetail.StatusHistorySummary(null, "INBOUNDED", "COMPLETE",
|
List.of(new InboundOrderDetail.StatusHistorySummary(null, "WAIT_SPLIT", "COMPLETE",
|
||||||
"内部原因", 1L, "客服", null)));
|
"内部原因", 1L, "客服", null)));
|
||||||
InboundMeasurementResponses.Detail measurement = mock(InboundMeasurementResponses.Detail.class);
|
InboundMeasurementResponses.Detail measurement = mock(InboundMeasurementResponses.Detail.class);
|
||||||
FeeSnapshotResponse fee = mock(FeeSnapshotResponse.class);
|
FeeSnapshotResponse fee = mock(FeeSnapshotResponse.class);
|
||||||
@@ -142,7 +142,7 @@ class MerchantInboundBillingServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldRejectHistoricalPublishedStatusCombination() {
|
void shouldRejectHistoricalPublishedStatusCombination() {
|
||||||
InboundOrderEntity inbound = inbound("INBOUNDED", "APPROVED", "SENT");
|
InboundOrderEntity inbound = inbound("WAIT_SPLIT", "APPROVED", "SENT");
|
||||||
when(orders.findByOrderId(10001L, 77L)).thenReturn(inbound);
|
when(orders.findByOrderId(10001L, 77L)).thenReturn(inbound);
|
||||||
|
|
||||||
assertThatThrownBy(() -> service.find(10001L, 77L))
|
assertThatThrownBy(() -> service.find(10001L, 77L))
|
||||||
|
|||||||
+3
-3
@@ -180,7 +180,7 @@ class PdaPickSettlementServiceTest {
|
|||||||
void usesTotalPackageCountWhenUnsplitMeasuredInboundHasZeroAvailableColumn() {
|
void usesTotalPackageCountWhenUnsplitMeasuredInboundHasZeroAvailableColumn() {
|
||||||
LoadingPickItemEntity item = item(11L, 21L, 31L, 41L, 20, 20, "PICKED");
|
LoadingPickItemEntity item = item(11L, 21L, 31L, 41L, 20, 20, "PICKED");
|
||||||
resolved(List.of(item));
|
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))
|
when(splits.lockLatestApprovedItemsForInbound(10001L, 31L))
|
||||||
.thenReturn(List.of(new MeasurementItemRow(51L, 61L, 41L, 20)));
|
.thenReturn(List.of(new MeasurementItemRow(51L, 61L, 41L, 20)));
|
||||||
when(allocations.lockConfirmed(10001L, 31L))
|
when(allocations.lockConfirmed(10001L, 31L))
|
||||||
@@ -336,7 +336,7 @@ class PdaPickSettlementServiceTest {
|
|||||||
"IN-001", "MASTER", 31L, "LOADING", 5, 5, "FULLY_SPLIT", 4));
|
"IN-001", "MASTER", 31L, "LOADING", 5, 5, "FULLY_SPLIT", 4));
|
||||||
when(splits.lockParent(inboundId)).thenReturn(new ParentRow(inboundId, 10001L, 902L,
|
when(splits.lockParent(inboundId)).thenReturn(new ParentRow(inboundId, 10001L, 902L,
|
||||||
"ORDER-REMAINDER", "IN-001-" + (sourceVersion - 1), "CHILD", 31L,
|
"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))
|
when(splits.lockLatestApprovedItemsForInbound(10001L, inboundId))
|
||||||
.thenReturn(List.of(new MeasurementItemRow(measurementItemId, measurementId, detailId, packageCount)));
|
.thenReturn(List.of(new MeasurementItemRow(measurementItemId, measurementId, detailId, packageCount)));
|
||||||
when(allocations.lockConfirmed(10001L, inboundId)).thenAnswer(invocation ->
|
when(allocations.lockConfirmed(10001L, inboundId)).thenAnswer(invocation ->
|
||||||
@@ -408,7 +408,7 @@ class PdaPickSettlementServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static ParentRow parent(int packageCount) {
|
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) {
|
private static ParentRow parent(int totalPackageCount, int packageCount, String processStatus, String splitStatus) {
|
||||||
|
|||||||
+7
-12
@@ -32,7 +32,6 @@ class UnifiedInboundQueryServiceTest {
|
|||||||
assertThat(UnifiedInboundQueryService.mainStatus("FEE_CONFIRMING")).isEqualTo("WAIT_MEASURE");
|
assertThat(UnifiedInboundQueryService.mainStatus("FEE_CONFIRMING")).isEqualTo("WAIT_MEASURE");
|
||||||
assertThat(UnifiedInboundQueryService.mainStatus("INBOUNDING")).isEqualTo("WAIT_MEASURE");
|
assertThat(UnifiedInboundQueryService.mainStatus("INBOUNDING")).isEqualTo("WAIT_MEASURE");
|
||||||
assertThat(UnifiedInboundQueryService.mainStatus("WAIT_SPLIT")).isEqualTo("WAIT_SPLIT");
|
assertThat(UnifiedInboundQueryService.mainStatus("WAIT_SPLIT")).isEqualTo("WAIT_SPLIT");
|
||||||
assertThat(UnifiedInboundQueryService.mainStatus("INBOUNDED")).isEqualTo("INBOUNDED");
|
|
||||||
assertThat(UnifiedInboundQueryService.mainStatus("LOADING")).isEqualTo("LOADING");
|
assertThat(UnifiedInboundQueryService.mainStatus("LOADING")).isEqualTo("LOADING");
|
||||||
assertThat(UnifiedInboundQueryService.mainStatus("LOADED")).isEqualTo("LOADED");
|
assertThat(UnifiedInboundQueryService.mainStatus("LOADED")).isEqualTo("LOADED");
|
||||||
assertThat(UnifiedInboundQueryService.mainStatus("DELIVERED")).isEqualTo("DELIVERED");
|
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", "MEASURING", null)).isNull();
|
||||||
assertThat(UnifiedInboundQueryService.nextAction("WAIT_MEASURE", "MEASURE_PENDING", null))
|
assertThat(UnifiedInboundQueryService.nextAction("WAIT_MEASURE", "MEASURE_PENDING", null))
|
||||||
.isEqualTo("CONFIRM_MEASUREMENT");
|
.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))
|
assertThat(UnifiedInboundQueryService.nextAction("WAIT_SPLIT", "WAIT_SPLIT", null))
|
||||||
.isEqualTo("SMART_SPLIT");
|
.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("LOADED", "LOADED", null)).isNull();
|
||||||
assertThat(UnifiedInboundQueryService.nextAction("DELIVERED", "DELIVERED", null)).isNull();
|
assertThat(UnifiedInboundQueryService.nextAction("DELIVERED", "DELIVERED", null)).isNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void inboundedAndWaitSplitFiltersRemainDistinct() throws Exception {
|
void waitSplitFilterUsesOnlyCanonicalStatus() throws Exception {
|
||||||
var method = UnifiedInboundQueryService.class.getDeclaredMethod("internalStatuses", String.class);
|
var method = UnifiedInboundQueryService.class.getDeclaredMethod("internalStatuses", String.class);
|
||||||
method.setAccessible(true);
|
method.setAccessible(true);
|
||||||
|
|
||||||
assertThat(method.invoke(new UnifiedInboundQueryService(), "INBOUNDED"))
|
|
||||||
.isEqualTo(java.util.List.of("INBOUNDED"));
|
|
||||||
assertThat(method.invoke(new UnifiedInboundQueryService(), "WAIT_SPLIT"))
|
assertThat(method.invoke(new UnifiedInboundQueryService(), "WAIT_SPLIT"))
|
||||||
.isEqualTo(java.util.List.of("WAIT_SPLIT"));
|
.isEqualTo(java.util.List.of("WAIT_SPLIT"));
|
||||||
}
|
}
|
||||||
@@ -70,14 +65,14 @@ class UnifiedInboundQueryServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
void phaseTwoMeasurementStatusesDoNotRequireUndeployedPhaseThreeTables() {
|
void phaseTwoMeasurementStatusesDoNotRequireUndeployedPhaseThreeTables() {
|
||||||
assertThat(UnifiedInboundQueryService.requiresPhaseThreeSummary("WAIT_MEASURE")).isFalse();
|
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("LOADING")).isTrue();
|
||||||
assertThat(UnifiedInboundQueryService.requiresPhaseThreeSummary("LOADED")).isTrue();
|
assertThat(UnifiedInboundQueryService.requiresPhaseThreeSummary("LOADED")).isTrue();
|
||||||
assertThat(UnifiedInboundQueryService.requiresPhaseThreeSummary("DELIVERED")).isTrue();
|
assertThat(UnifiedInboundQueryService.requiresPhaseThreeSummary("DELIVERED")).isTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@ValueSource(strings = {"WAIT_MEASURE", "INBOUNDED", "WAIT_SPLIT"})
|
@ValueSource(strings = {"WAIT_MEASURE", "WAIT_SPLIT"})
|
||||||
void phaseTwoDetailDoesNotQueryPhaseThreeTables(String processStatus) {
|
void phaseTwoDetailDoesNotQueryPhaseThreeTables(String processStatus) {
|
||||||
InboundOrderRepository inbounds = mock(InboundOrderRepository.class);
|
InboundOrderRepository inbounds = mock(InboundOrderRepository.class);
|
||||||
UnifiedInboundReadRepository operations = mock(UnifiedInboundReadRepository.class);
|
UnifiedInboundReadRepository operations = mock(UnifiedInboundReadRepository.class);
|
||||||
|
|||||||
@@ -31,7 +31,20 @@ ON DUPLICATE KEY UPDATE
|
|||||||
UPDATE iam_permission
|
UPDATE iam_permission
|
||||||
SET status='INACTIVE', deleted=1
|
SET status='INACTIVE', deleted=1
|
||||||
WHERE tenant_id=0 AND app_code='PLATFORM'
|
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)
|
INSERT IGNORE INTO iam_role_permission (tenant_id, role_id, permission_id, created_at)
|
||||||
SELECT 0, r.id, p.id, CURRENT_TIMESTAMP(3)
|
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';
|
WHERE p.tenant_id=0 AND p.app_code='PLATFORM';
|
||||||
|
|
||||||
UPDATE iam_permission p
|
UPDATE iam_permission p
|
||||||
JOIN (SELECT 'platform:finance:account:view' code, 5105040 parent_id UNION ALL
|
JOIN (SELECT 'platform:finance:fee-source:review' code, 5105041 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:fee-source:settlement', 5105041 UNION ALL
|
||||||
SELECT 'platform:finance:reconciliation:create', 5105042 UNION ALL
|
SELECT 'platform:finance:reconciliation:create', 5105042 UNION ALL
|
||||||
SELECT 'platform:finance:reconciliation:review', 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:adjust', 5105042 UNION ALL
|
||||||
SELECT 'platform:finance:reconciliation:void', 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: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:create', 5105044 UNION ALL
|
||||||
SELECT 'platform:finance:settlement-ap:view', 5105044 UNION ALL
|
|
||||||
SELECT 'platform:finance:payment:create', 5105044) mapping
|
SELECT 'platform:finance:payment:create', 5105044) mapping
|
||||||
ON mapping.code=p.permission_code
|
ON mapping.code=p.permission_code
|
||||||
SET p.parent_id=mapping.parent_id
|
SET p.parent_id=mapping.parent_id
|
||||||
|
|||||||
@@ -11,16 +11,12 @@ BEGIN
|
|||||||
SELECT 1
|
SELECT 1
|
||||||
FROM iam_permission
|
FROM iam_permission
|
||||||
WHERE (id = 5105000 AND permission_code <> 'platform:finance')
|
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 = 5105024 AND permission_code <> 'platform:finance:fee-source:review')
|
||||||
OR (id = 5105025 AND permission_code <> 'platform:finance:fee-source:settlement')
|
OR (id = 5105025 AND permission_code <> 'platform:finance:fee-source:settlement')
|
||||||
OR (permission_code = 'platform:finance' AND id <> 5105000)
|
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:review' AND id <> 5105024)
|
||||||
OR (permission_code = 'platform:finance:fee-source:settlement' AND id <> 5105025)
|
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'))
|
AND (tenant_id <> 0 OR app_code <> 'PLATFORM'))
|
||||||
) THEN
|
) THEN
|
||||||
SIGNAL SQLSTATE '45000'
|
SIGNAL SQLSTATE '45000'
|
||||||
@@ -33,10 +29,6 @@ BEGIN
|
|||||||
VALUES
|
VALUES
|
||||||
(5105000, 0, 'PLATFORM', 'MENU', 0, 'platform:finance',
|
(5105000, 0, 'PLATFORM', 'MENU', 0, 'platform:finance',
|
||||||
'财务管理', '/finance', NULL, 60, 'ACTIVE', 1, 0),
|
'财务管理', '/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',
|
(5105024, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:fee-source:review',
|
||||||
'审核财务费用来源', NULL, 'POST', 150, 'ACTIVE', 1, 0),
|
'审核财务费用来源', NULL, 'POST', 150, 'ACTIVE', 1, 0),
|
||||||
(5105025, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:fee-source:settlement',
|
(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),
|
http_method = VALUES(http_method), sort_no = VALUES(sort_no),
|
||||||
status = 'ACTIVE', built_in = 1, deleted = 0;
|
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(*)
|
IF (SELECT COUNT(*)
|
||||||
FROM iam_permission
|
FROM iam_permission
|
||||||
WHERE tenant_id = 0
|
WHERE tenant_id = 0
|
||||||
@@ -57,10 +54,8 @@ BEGIN
|
|||||||
AND built_in = 1
|
AND built_in = 1
|
||||||
AND deleted = 0
|
AND deleted = 0
|
||||||
AND permission_code IN (
|
AND permission_code IN (
|
||||||
'platform:finance:account:view',
|
|
||||||
'platform:finance:fee-source:view',
|
|
||||||
'platform:finance:fee-source:review',
|
'platform:finance:fee-source:review',
|
||||||
'platform:finance:fee-source:settlement')) <> 4 THEN
|
'platform:finance:fee-source:settlement')) <> 2 THEN
|
||||||
SIGNAL SQLSTATE '45000'
|
SIGNAL SQLSTATE '45000'
|
||||||
SET MESSAGE_TEXT = 'finance G1/G2 permission catalog is incomplete';
|
SET MESSAGE_TEXT = 'finance G1/G2 permission catalog is incomplete';
|
||||||
END IF;
|
END IF;
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ IF EXISTS (
|
|||||||
OR (id = 5105030 AND permission_code <> 'platform:finance:reconciliation:void')
|
OR (id = 5105030 AND permission_code <> 'platform:finance:reconciliation:void')
|
||||||
OR (id = 5105031 AND permission_code <> 'platform:finance:settlement-ar:create')
|
OR (id = 5105031 AND permission_code <> 'platform:finance:settlement-ar:create')
|
||||||
OR (id = 5105032 AND permission_code <> 'platform:finance:settlement-ap: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 = 5105035 AND permission_code <> 'platform:finance:payment:create')
|
||||||
OR (permission_code = 'platform:finance:reconciliation:create' AND id <> 5105026)
|
OR (permission_code = 'platform:finance:reconciliation:create' AND id <> 5105026)
|
||||||
OR (permission_code = 'platform:finance:reconciliation:review' AND id <> 5105027)
|
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:reconciliation:void' AND id <> 5105030)
|
||||||
OR (permission_code = 'platform:finance:settlement-ar:create' AND id <> 5105031)
|
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-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:payment:create' AND id <> 5105035)
|
||||||
OR (id BETWEEN 5105026 AND 5105035 AND (tenant_id <> 0 OR app_code <> 'PLATFORM'))
|
OR (id BETWEEN 5105026 AND 5105035 AND (tenant_id <> 0 OR app_code <> 'PLATFORM'))
|
||||||
) THEN
|
) THEN
|
||||||
@@ -46,8 +42,6 @@ VALUES
|
|||||||
(5105030, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:reconciliation:void', '作废或重开客户对账单', NULL, 'POST', 210, 'ACTIVE', 1, 0),
|
(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),
|
(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),
|
(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)
|
(5105035, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:payment:create', '登记应付付款', NULL, 'POST', 260, 'ACTIVE', 1, 0)
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
permission_type = VALUES(permission_type), parent_id = VALUES(parent_id),
|
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),
|
http_method = VALUES(http_method), sort_no = VALUES(sort_no),
|
||||||
status = 'ACTIVE', built_in = 1, deleted = 0;
|
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
|
IF (SELECT COUNT(*) FROM iam_permission
|
||||||
WHERE tenant_id=0 AND app_code='PLATFORM' AND parent_id=5105000
|
WHERE tenant_id=0 AND app_code='PLATFORM' AND parent_id=5105000
|
||||||
AND permission_code IN ('platform:finance:reconciliation:create','platform:finance:reconciliation:review',
|
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:reconciliation:issue','platform:finance:reconciliation:adjust','platform:finance:reconciliation:void',
|
||||||
'platform:finance:settlement-ar:create','platform:finance:settlement-ap:create',
|
'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:payment:create')
|
||||||
AND status='ACTIVE' AND deleted=0) <> 10 THEN
|
AND status='ACTIVE' AND deleted=0) <> 8 THEN
|
||||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='finance G3/G4 permission catalog is incomplete';
|
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='finance G3/G4 permission catalog is incomplete';
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
|
|||||||
+4
@@ -31,6 +31,10 @@ class AdminPrototypeMenuPermissionSqlContractTest {
|
|||||||
.contains("'platform:finance:settlement-ar:create', 5105043")
|
.contains("'platform:finance:settlement-ar:create', 5105043")
|
||||||
.contains("'platform:finance:settlement-ap:create', 5105044")
|
.contains("'platform:finance:settlement-ap:create', 5105044")
|
||||||
.contains("'platform:finance:payment: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'");
|
.contains("role_code='PLATFORM_ADMIN'");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+5
-4
@@ -62,14 +62,15 @@ class FinanceG1G2PermissionSqlContractTest {
|
|||||||
.contains("NULL, 'POST', 150, 'ACTIVE', 1, 0")
|
.contains("NULL, 'POST', 150, 'ACTIVE', 1, 0")
|
||||||
.contains("NULL, 'PUT', 160, 'ACTIVE', 1, 0")
|
.contains("NULL, 'PUT', 160, 'ACTIVE', 1, 0")
|
||||||
.contains("permission_code IN (\n 'platform:finance',")
|
.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 授权不完整")
|
||||||
.contains("Finance G1-G4");
|
.contains("Finance G1-G4");
|
||||||
assertThat(financeSection)
|
assertThat(financeSection)
|
||||||
.contains("SELECT 4000001, id, 0 FROM baiyue_iam.iam_permission")
|
.contains("SELECT 4000001, id, 0 FROM baiyue_iam.iam_permission")
|
||||||
.contains("platform:finance:account:view")
|
.contains("platform:finance:account:view");
|
||||||
.contains("platform:finance:settlement-ar:view")
|
assertThat(sql)
|
||||||
.contains("platform:finance:settlement-ap:view");
|
.contains("platform:finance:settlement-ar:list")
|
||||||
|
.contains("platform:finance:settlement-ap:list");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Path resolveUnifiedSql() {
|
private static Path resolveUnifiedSql() {
|
||||||
|
|||||||
+11
-10
@@ -14,8 +14,7 @@ class FinanceG3G4PermissionSqlContractTest {
|
|||||||
"platform:finance:reconciliation:create", "platform:finance:reconciliation:review",
|
"platform:finance:reconciliation:create", "platform:finance:reconciliation:review",
|
||||||
"platform:finance:reconciliation:issue", "platform:finance:reconciliation:adjust",
|
"platform:finance:reconciliation:issue", "platform:finance:reconciliation:adjust",
|
||||||
"platform:finance:reconciliation:void", "platform:finance:settlement-ar:create",
|
"platform:finance:reconciliation:void", "platform:finance:settlement-ar:create",
|
||||||
"platform:finance:settlement-ap:create", "platform:finance:settlement-ar:view",
|
"platform:finance:settlement-ap:create", "platform:finance:payment:create"
|
||||||
"platform:finance:settlement-ap:view", "platform:finance:payment:create"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -23,9 +22,7 @@ class FinanceG3G4PermissionSqlContractTest {
|
|||||||
String sql = new ClassPathResource("db/finance-g3-g4-permissions.sql")
|
String sql = new ClassPathResource("db/finance-g3-g4-permissions.sql")
|
||||||
.getContentAsString(StandardCharsets.UTF_8);
|
.getContentAsString(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
for (int index = 0; index < CODES.length; index++) {
|
for (String code : CODES) assertThat(sql).contains(code);
|
||||||
assertThat(sql).contains("(" + (5105026 + index) + ", 0, 'PLATFORM', 'BUTTON', 5105000, '" + CODES[index] + "'");
|
|
||||||
}
|
|
||||||
assertThat(sql).contains("SIGNAL SQLSTATE '45000'")
|
assertThat(sql).contains("SIGNAL SQLSTATE '45000'")
|
||||||
.contains("id = 5105026 AND permission_code <> 'platform:finance:reconciliation:create'")
|
.contains("id = 5105026 AND permission_code <> 'platform:finance:reconciliation:create'")
|
||||||
.contains("permission_code = 'platform:finance:payment:create' AND id <> 5105035")
|
.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("finance G3/G4 permission id/code conflict")
|
||||||
.contains("permission catalog is incomplete")
|
.contains("permission catalog is incomplete")
|
||||||
.contains("finance permission parent is invalid")
|
.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
|
@Test
|
||||||
@@ -45,12 +45,13 @@ class FinanceG3G4PermissionSqlContractTest {
|
|||||||
assertThat(financeSection)
|
assertThat(financeSection)
|
||||||
.contains("migrate_finance_g1_g4_permissions")
|
.contains("migrate_finance_g1_g4_permissions")
|
||||||
.contains("finance G1-G4 permission id/code conflict")
|
.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)
|
assertThat(financeSection)
|
||||||
.contains("SELECT 4000001, id, 0 FROM baiyue_iam.iam_permission")
|
.contains("SELECT 4000001, id, 0 FROM baiyue_iam.iam_permission")
|
||||||
.contains("platform:finance:reconciliation:create")
|
.contains("platform:finance:reconciliation:create");
|
||||||
.contains("platform:finance:settlement-ar:view")
|
assertThat(sql)
|
||||||
.contains("platform:finance:settlement-ap:view");
|
.contains("platform:finance:settlement-ar:list")
|
||||||
|
.contains("platform:finance:settlement-ap:list");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Path resolveUnifiedSql() {
|
private static Path resolveUnifiedSql() {
|
||||||
|
|||||||
+29
-4
@@ -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。
|
脚本会检查 `pnpm` 和目标应用的 `package.json`;任一命令失败时立即透传退出码,不能同时跳过 Admin 和 Client。
|
||||||
|
|
||||||
脚本固定运行:
|
后端脚本固定运行:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
mvn -f <仓库根>/pom.xml -pl order,firstleg,iam,gateway,file,warehouse -am test
|
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`。
|
正式入口是 `local-db-full-acceptance.ps1`,只允许连接本机 `127.0.0.1` 或 `localhost`,默认端口 `3306`;测试库 `8.163.30.128` 会立即以退出码 2 拒绝。脚本固定校验统一 SQL SHA-256 `BE7C5C98712D5202F5D187EA9A37C64EAB9F6AA9AE772C89A51F186507B1AE53` 和 14706 行,并静态确认中文注释升级采用校验入队、每表一次 ALTER、结构签名复核、九库覆盖断言及唯一最终 `PASS`。
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
.\local-db-full-acceptance.ps1 -DryRun
|
.\scripts\local-db-full-acceptance.ps1 -DryRun
|
||||||
.\local-db-full-acceptance.ps1
|
.\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`。脚本不保存或传入数据库密码,使用本机默认认证环境。
|
Dry-run 只校验 SQL 输入并打印空库、复跑、两类 picking 旧结构、WAIT_SPLIT/IAM、海外仓旧结构、中断恢复和最终结构的执行顺序,不启动 MySQL、不写库。真实运行每阶段写入 `.codex-tmp/p23-04-acceptance/acceptance-run-*.log`,不使用 `--force`,任一 SQL 错误立即停止并透传退出码,最后执行 `verify-final.sql`。脚本不保存或传入数据库密码,使用本机默认认证环境。
|
||||||
|
|||||||
@@ -510,7 +510,10 @@ export class AcceptanceRun {
|
|||||||
|
|
||||||
async preparePhase3ContainerFixture() {
|
async preparePhase3ContainerFixture() {
|
||||||
const originalMarker = this.marker
|
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 createdKeys = ['orders', 'ocrInbound', 'testInbound', 'inboundMeasurement']
|
||||||
const contextKeys = ['inboundWeightOcr', 'ocrInbound', 'testInbound', 'createdInbound']
|
const contextKeys = ['inboundWeightOcr', 'ocrInbound', 'testInbound', 'createdInbound']
|
||||||
const createdSnapshot = new Map(createdKeys.map((key) => [key, {
|
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))
|
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))
|
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))
|
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 = {
|
||||||
...(this.createdResources.orchestrationFixtures || {}),
|
...(this.createdResources.orchestrationFixtures || {}),
|
||||||
phase3Container: fixture ? {
|
phase3Container: fixture ? {
|
||||||
|
|||||||
@@ -403,6 +403,14 @@ export async function run(context) {
|
|||||||
'使用订单域真实 waybillId、shipmentId 或 orderNo,不使用数据库主键冒充运单标识', OPTIONS)
|
'使用订单域真实 waybillId、shipmentId 或 orderNo,不使用数据库主键冒充运单标识', OPTIONS)
|
||||||
context.createdResources.financeFullFixtures = {
|
context.createdResources.financeFullFixtures = {
|
||||||
marker: context.marker, customerId,
|
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,
|
orders: measured.map((item) => ({ orderId: id(item.orderId), orderNo: item.orderNo || null,
|
||||||
shipmentId: item.shipmentId ? id(item.shipmentId) : null,
|
shipmentId: item.shipmentId ? id(item.shipmentId) : null,
|
||||||
waybillId: item.waybillId ? id(item.waybillId) : (item.shipmentId ? id(item.shipmentId) : null), inboundId: id(item.inboundId),
|
waybillId: item.waybillId ? id(item.waybillId) : (item.shipmentId ? id(item.shipmentId) : null), inboundId: id(item.inboundId),
|
||||||
|
|||||||
@@ -323,20 +323,25 @@ export async function run(context) {
|
|||||||
finish(context, start)
|
finish(context, start)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
fixture.root.reconciliationPrerequisite = null
|
||||||
const permissionResponse = await context.request('GET', '/api/platform/v1/iam/authorization/permissions')
|
const permissionResponse = await context.request('GET', '/api/platform/v1/iam/authorization/permissions')
|
||||||
const permissions = context.apiSuccess(MODULE, '查询当前账号 Finance 有效权限', permissionResponse, '', OPTIONS)
|
const permissions = context.apiSuccess(MODULE, '查询当前账号 Finance 有效权限', permissionResponse, '', OPTIONS)
|
||||||
const granted = new Set(list(permissions).map(String))
|
const granted = new Set(list(permissions).map(String))
|
||||||
if (granted.has(LEGACY_STATEMENT_VIEW_PERMISSION)) {
|
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 权限兼容阻断',
|
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)
|
'legacy-customer-statement-view-permission', permissionResponse)
|
||||||
finish(context, start)
|
finish(context, start)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const missingPermissions = REQUIRED_PERMISSIONS.filter((permission) => !granted.has(permission))
|
const missingPermissions = REQUIRED_PERMISSIONS.filter((permission) => !granted.has(permission))
|
||||||
if (missingPermissions.length > 0) {
|
if (missingPermissions.length > 0) {
|
||||||
|
fixture.root.reconciliationPrerequisite = { ready: false, category: 'permission-prerequisite',
|
||||||
|
required: missingPermissions, legacy: [] }
|
||||||
blocker(context, 'Finance 写权限门禁',
|
blocker(context, 'Finance 写权限门禁',
|
||||||
`缺少权限=${missingPermissions.join(',')}; 对账接口将返回 A0403,禁止继续写入`,
|
`缺少正式权限资源=${missingPermissions.join(',')}; 需由 IAM 角色初始化授予验收账号;对账接口将返回 A0403,禁止继续写入`,
|
||||||
'missing-finance-write-permissions', permissionResponse)
|
'missing-finance-write-permissions', permissionResponse)
|
||||||
finish(context, start)
|
finish(context, start)
|
||||||
return
|
return
|
||||||
@@ -353,6 +358,8 @@ export async function run(context) {
|
|||||||
const invalid = fixture.sources.filter((source) => !source.feeSourceId || !source.customerId
|
const invalid = fixture.sources.filter((source) => !source.feeSourceId || !source.customerId
|
||||||
|| !source.businessDate || !source.waybillId || source.marker !== context.marker)
|
|| !source.businessDate || !source.waybillId || source.marker !== context.marker)
|
||||||
if (invalid.length > 0) {
|
if (invalid.length > 0) {
|
||||||
|
fixture.root.reconciliationPrerequisite = { ready: false, category: 'finance-source-prerequisite',
|
||||||
|
invalidSourceCount: invalid.length }
|
||||||
blocker(context, 'Fixture 来源契约完整',
|
blocker(context, 'Fixture 来源契约完整',
|
||||||
`存在 ${invalid.length} 条来源缺少 feeSourceId/customerId/businessDate/运单标识/marker`,
|
`存在 ${invalid.length} 条来源缺少 feeSourceId/customerId/businessDate/运单标识/marker`,
|
||||||
'invalid-finance-fixture-contract')
|
'invalid-finance-fixture-contract')
|
||||||
@@ -364,6 +371,8 @@ export async function run(context) {
|
|||||||
&& source.direction === 'RECEIVABLE' && source.currency === 'CNY')
|
&& source.direction === 'RECEIVABLE' && source.currency === 'CNY')
|
||||||
const businessDates = new Set(receivables.map((source) => source.businessDate))
|
const businessDates = new Set(receivables.map((source) => source.businessDate))
|
||||||
if (receivables.length < 4 || businessDates.size < 2) {
|
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 门禁',
|
blocker(context, '三张 AR 与日期范围 Fixture 门禁',
|
||||||
`至少需要同客户 4 条 CNY 应收来源且跨 2 个真实 businessDate;actual sources=${receivables.length}, dates=${businessDates.size}`,
|
`至少需要同客户 4 条 CNY 应收来源且跨 2 个真实 businessDate;actual sources=${receivables.length}, dates=${businessDates.size}`,
|
||||||
'insufficient-receivable-date-fixture')
|
'insufficient-receivable-date-fixture')
|
||||||
|
|||||||
@@ -125,6 +125,9 @@ test('AR/AP 数组为空或不完整时原子阻断且不查询、不写入', as
|
|||||||
assert.equal(empty.gaps.length, 1)
|
assert.equal(empty.gaps.length, 1)
|
||||||
assert.equal(empty.steps[0].passed, false)
|
assert.equal(empty.steps[0].passed, false)
|
||||||
assert.match(empty.steps[0].evidence, /actual AR=0, AP=0/)
|
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: [
|
const partial = fakeContext({ receivableSettlements: [
|
||||||
{ settlementId: '101', statementId: '11', direction: 'RECEIVABLE', status: 'ISSUED' },
|
{ 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.calls.length, 0)
|
||||||
assert.equal(partial.gaps.length, 1)
|
assert.equal(partial.gaps.length, 1)
|
||||||
assert.match(partial.steps[0].evidence, /actual AR=1, AP=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)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -128,9 +128,17 @@ export async function run(context) {
|
|||||||
const arSettlementIds = new Set(validReceivables.map(settlementId))
|
const arSettlementIds = new Set(validReceivables.map(settlementId))
|
||||||
const arStatementIds = new Set(validReceivables.map(statementId))
|
const arStatementIds = new Set(validReceivables.map(statementId))
|
||||||
if (validReceivables.length !== 3 || arSettlementIds.size !== 3 || arStatementIds.size !== 3 || validPayables.length !== 1) {
|
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',
|
gap(context, '取得完整收付结算 Fixture', 'three AR and one AP settlements',
|
||||||
`需要 3 张关联本轮 ISSUED 对账单的独立 AR 和 1 张带 supplierId 的 AP;actual AR=${validReceivables.length}, AP=${validPayables.length}`,
|
`上游 Finance fixture 前置条件未满足,非结算服务故障;需要 3 张关联本轮 ISSUED 对账单的独立 AR 和 1 张带 supplierId 的 AP;actual AR=${validReceivables.length}, AP=${validPayables.length}; prerequisite=${category}; missing=${missing.join(',') || 'unknown'}`,
|
||||||
'incomplete-settlement-fixture')
|
category)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,19 @@ function validPdf(response) {
|
|||||||
&& response.bytes?.length > 4 && new TextDecoder().decode(response.bytes.slice(0, 4)) === '%PDF'
|
&& 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 }) {
|
async function provisionWarehouseResources(context, { inboundId, warehouseId, totalVolumeCbm }) {
|
||||||
const palletCount = Math.ceil(Number(totalVolumeCbm) / DEFAULT_PALLET_CAPACITY_CBM)
|
const palletCount = Math.ceil(Number(totalVolumeCbm) / DEFAULT_PALLET_CAPACITY_CBM)
|
||||||
if (!warehouseId || !Number.isFinite(palletCount) || palletCount < 1) return false
|
if (!warehouseId || !Number.isFinite(palletCount) || palletCount < 1) return false
|
||||||
@@ -394,8 +407,13 @@ export async function run(context) {
|
|||||||
context.apiSuccess(MODULE, 'Admin 审核通过墨家测量', approvalResponse,
|
context.apiSuccess(MODULE, 'Admin 审核通过墨家测量', approvalResponse,
|
||||||
`measurementId=${asId(draft.measurementId)}`, OPTIONS)
|
`measurementId=${asId(draft.measurementId)}`, OPTIONS)
|
||||||
|
|
||||||
const finalResponse = await context.request('GET', measurementPath)
|
// Measurement approval may enqueue pricing/inbound completion. Poll the
|
||||||
const finalMeasurement = context.apiSuccess(MODULE, '查询审核后的最终测量状态', finalResponse, '', OPTIONS)
|
// 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,
|
context.assert(MODULE, '审核后入库与测量状态闭环且结果未变化', finalResponse,
|
||||||
finalMeasurement?.processStatus === 'WAIT_SPLIT' && finalMeasurement?.measureReviewStatus === 'APPROVED'
|
finalMeasurement?.processStatus === 'WAIT_SPLIT' && finalMeasurement?.measureReviewStatus === 'APPROVED'
|
||||||
&& finalMeasurement?.measurementStatus === 'APPROVED'
|
&& finalMeasurement?.measurementStatus === 'APPROVED'
|
||||||
@@ -407,6 +425,7 @@ export async function run(context) {
|
|||||||
context.createdResources.inboundMeasurement = {
|
context.createdResources.inboundMeasurement = {
|
||||||
marker: context.marker, tenantId, inboundId, measurementId: asId(draft.measurementId),
|
marker: context.marker, tenantId, inboundId, measurementId: asId(draft.measurementId),
|
||||||
boxCode, finalStatus: finalMeasurement?.measurementStatus || null,
|
boxCode, finalStatus: finalMeasurement?.measurementStatus || null,
|
||||||
|
processStatus: finalMeasurement?.processStatus || null,
|
||||||
}
|
}
|
||||||
finishScenario(context, start)
|
finishScenario(context, start)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,14 +34,20 @@ async function request(context, method, path, body, headers = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function fixture(context) {
|
export function selectContainerLoadingFixture(context) {
|
||||||
return context.createdResources?.orchestrationFixtures?.phase3Container
|
const candidates = [
|
||||||
|| context.createdResources?.inboundMeasurement
|
context.createdResources?.orchestrationFixtures?.phase3Container,
|
||||||
|| context.createdResources?.orders
|
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) {
|
export async function run(context) {
|
||||||
const source = fixture(context)
|
const source = selectContainerLoadingFixture(context)
|
||||||
const tenantId = id(source?.tenantId)
|
const tenantId = id(source?.tenantId)
|
||||||
const inboundId = id(source?.inboundId)
|
const inboundId = id(source?.inboundId)
|
||||||
if (!tenantId || !inboundId || source?.marker !== context.marker) {
|
if (!tenantId || !inboundId || source?.marker !== context.marker) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import test from 'node:test'
|
|||||||
|
|
||||||
import { selectCustomsFlow } from './phase3-customs.mjs'
|
import { selectCustomsFlow } from './phase3-customs.mjs'
|
||||||
import { buildFinalizeResource } from './phase3-loading-finalize.mjs'
|
import { buildFinalizeResource } from './phase3-loading-finalize.mjs'
|
||||||
|
import { selectContainerLoadingFixture } from './phase3-container-loading.mjs'
|
||||||
import { AcceptanceRun } from '../admin-test-env-acceptance.mjs'
|
import { AcceptanceRun } from '../admin-test-env-acceptance.mjs'
|
||||||
|
|
||||||
function source(name) {
|
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', () => {
|
test('container loading creates and confirms booking before scoped candidates', () => {
|
||||||
const value = source('./phase3-container-loading.mjs')
|
const value = source('./phase3-container-loading.mjs')
|
||||||
|
assert.match(value, /candidates\.find\(\(candidate\) => candidate\.marker === context\.marker\)/)
|
||||||
assertOrder(value, [
|
assertOrder(value, [
|
||||||
"const bookingCreate = await request",
|
"const bookingCreate = await request",
|
||||||
"const submit = 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/)
|
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', () => {
|
test('container loading enforces channel isolation and stable PDA identity across container number changes', () => {
|
||||||
const value = source('./phase3-container-loading.mjs')
|
const value = source('./phase3-container-loading.mjs')
|
||||||
assertOrder(value, [
|
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)
|
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', () => {
|
test('customs and finalize consume the same loading inbound identity', () => {
|
||||||
const loadingFixture = { marker: 'RUN-1', tenantId: 10001, orderId: 101, inboundId: 201 }
|
const loadingFixture = { marker: 'RUN-1', tenantId: 10001, orderId: 101, inboundId: 201 }
|
||||||
const loading = {
|
const loading = {
|
||||||
|
|||||||
Reference in New Issue
Block a user