贴标申请,仓储转运

This commit is contained in:
2026-09-24 09:01:14 +08:00
parent 10a2ab3e42
commit 186b6be7d8
27 changed files with 1203 additions and 118 deletions
+2
View File
@@ -2,6 +2,8 @@
`db/local-return-relabel-multiple-applications.sql`:本地 `baiyue_lastleg.ll_relabel_application` 单次结构修复,将尾程订单关联的唯一索引替换为普通索引,允许同一退货单剩余货物分批换标;不修改业务行,单号与幂等键唯一索引保留。凭据及目标连接由操作者显式提供,只在已授权本地库执行;DDL失败停止,已执行后不要重复运行,回查SHOW INDEX。新库结构同步在Lastleg基线和海外区域SQL中。
`db/recent-schema-incremental-20260923.sql`:汇总 2026-09-21 至 2026-09-22 的近期结构变更,依次显式指向 `baiyue_iam`、`baiyue_firstleg`、`baiyue_lastleg`、`baiyue_lastlag_warehouse`;补齐国内 PDA 作业仓字段及索引、人工 FBA 报价可空字段、墨家 SKU 测量字段、换标申请普通索引和 A0 共享溢出位字段。所有字段和索引变更均经 `information_schema` 判断,可重复执行;不创建数据库、不含凭据、权限种子或业务状态修正,末尾输出逐库 `READY/CHECK_REQUIRED`。由操作者选择并确认目标连接后执行,任一 DDL 报错立即停止。
`local-return-relabel-acceptance.mjs` 的 `LOCAL_RETURN_VERIFY_REMAINING=true` 专项新建3件退货,先换1件并客户确认,再对同一退货单剩余2件中的1件新建第二申请,验证同键重放、第二轮完工后剩余1件;不执行出库,报告 scope 为 `REMAINING_RETURN_RELABEL`,不代表出库验收通过。仍保留单据、使用正式API和合成照片,默认单票模式不变。
`local-return-relabel-outbound.mjs`:仅本机网关,读取显式 `LOCAL_RETURN_SOURCE` 单票报告,为其中同客户、同仓、已完成换标的唯一一件可用库存执行平台创建出库、原键重放、PDA拣货及完成。沿用平台账号环境参数,报告默认 `reports/local-return-relabel-outbound-<marker>.json` 或 `--output`;创建临时PDA并finally停用,不改客户权限。平台库存核验不替代源报告的客户读权限失败。会推进库存与出库状态,保留单据及报告;失败后按已保存单号核查,不自动清理,不可对已出库库存盲目重跑。
@@ -47,7 +47,7 @@ test('平台管理员由外部bcrypt引导,强制首登改密且不创建客
})
test('必要系统种子完整保留,但不写入示例资源与业务事实', () => {
for (const [name, count] of Object.entries({ iam_role: 8, iam_permission: 367, iam_role_permission: 543,
for (const [name, count] of Object.entries({ iam_role: 8, iam_permission: 370, iam_role_permission: 552,
iam_data_scope: 7, wh_warehouse: 318, wh_warehouse_address: 318, wh_service_capability: 4,
fl_fee_item: 112, fl_price_channel_catalog: 15, fl_price_origin_alias: 7, fl_price_warehouse_mapping: 4,
file_export_task_guard: 1, fin_subject: 1 })) assert.equal(seedRows(cn, name).length, count, name)
@@ -73,7 +73,29 @@ test('国内IAM权限与角色授权没有丢失或悬挂,客户模板仍完
const admin = roleAuthorities(cn, 'PLATFORM_ADMIN')
for (const code of ['platform:order:declaration:edit', 'platform:firstleg:inbound:hold',
'platform:overseas-warehouse:last-mile-order:list', 'platform:overseas-business:purchase:list',
'platform:overseas-master-data:quote:list', 'platform:finance:fee-source:review']) assert.ok(admin.has(code), code)
'platform:overseas-master-data:quote:list', 'platform:finance:fee-source:review',
'platform:mojia-sku-sync:list', 'platform:mojia-sku-sync:edit',
'platform:mojia-sku-sync:retry']) assert.ok(admin.has(code), code)
const operator = roleAuthorities(cn, 'ORDER_OPERATOR')
for (const code of ['platform:mojia-sku-sync:list', 'platform:mojia-sku-sync:edit',
'platform:mojia-sku-sync:retry']) assert.ok(operator.has(code), code)
})
test('近期头程与海外结构完整进入各自统一初始化入口', () => {
const membership = table(cn, 'baiyue_iam', 'iam_membership')
assert.match(membership, /domestic_warehouse_id bigint DEFAULT NULL/i)
assert.match(membership, /idx_membership_domestic_warehouse \(tenant_id,domestic_warehouse_id,status\)/i)
const receipt = table(cn, 'baiyue_firstleg', 'fl_mojia_measurement_receipt')
for (const field of ['measurement_kind', 'weight_kg', 'sync_status', 'sync_failure_code',
'sync_failure_reason', 'lingxing_sku_id', 'revision']) assert.match(receipt, new RegExp(`\\b${field}\\b`, 'i'))
assert.match(receipt, /weight_kg decimal\(18,3\) NOT NULL/i)
const relabel = table(overseas, 'baiyue_lastleg', 'll_relabel_application')
assert.match(relabel, /KEY idx_ll_relabel_application_order \(lastleg_order_id\)/i)
assert.doesNotMatch(relabel, /UNIQUE KEY uk_ll_relabel_application_order/i)
assert.match(table(overseas, 'baiyue_lastlag_warehouse', 'wh_location'),
/shared_overflow tinyint\(1\) NOT NULL DEFAULT '?0'?/i)
})
test('海外固定身份投影完全匹配国内管理员的当前海外权限', () => {
+261
View File
@@ -0,0 +1,261 @@
-- 国内头程 PDA 操作员初始化(MySQL 8.0+,可重复执行)
--
-- 初始化内容:
-- 1. 平台角色 CN_FIRSTLEG_PDA_OPERATOR(国内头程 PDA 作业员)
-- 2. 张三 / pda_zhangsan,绑定 CN-YIWU-01(义乌仓)
-- 3. 李四 / pda_lisi,绑定 CN-YIWU-01(义乌仓)
--
-- 目标库:同一 MySQL 实例中的 baiyue_auth、baiyue_iam、baiyue_warehouse。
-- 前置条件:IAM 已包含 iam_membership.domestic_warehouse_id,且义乌仓主数据已初始化。
-- 密码列只写 BCrypt 哈希;首次插入时要求首次登录修改密码。重复执行不会覆盖用户后来修改的密码。
SET NAMES utf8mb4;
USE baiyue_iam;
DROP PROCEDURE IF EXISTS assert_domestic_pda_operator_seed_ready;
DELIMITER $$
CREATE PROCEDURE assert_domestic_pda_operator_seed_ready()
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_schema = 'baiyue_auth' AND table_name = 'auth_account'
) OR NOT EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_schema = 'baiyue_iam' AND table_name = 'iam_membership'
) OR NOT EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_schema = 'baiyue_warehouse' AND table_name = 'wh_warehouse'
) THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '国内 PDA 初始化失败:baiyue_auth、baiyue_iam 或 baiyue_warehouse 基础表未部署';
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'baiyue_iam' AND table_name = 'iam_membership'
AND column_name = 'domestic_warehouse_id'
) THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '国内 PDA 初始化失败:请先部署 iam_membership.domestic_warehouse_id';
END IF;
IF (SELECT COUNT(*)
FROM baiyue_warehouse.wh_warehouse
WHERE tenant_id = 0 AND warehouse_code = 'CN-YIWU-01'
AND warehouse_type = 'DOMESTIC' AND status = 'ACTIVE' AND deleted = 0) <> 1 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '国内 PDA 初始化失败:未找到唯一启用的国内仓 CN-YIWU-01';
END IF;
IF EXISTS (
SELECT 1 FROM iam_role
WHERE (id = 4000007 AND (tenant_id <> 0 OR app_code <> 'PLATFORM' OR role_code <> 'CN_FIRSTLEG_PDA_OPERATOR'))
OR (tenant_id = 0 AND app_code = 'PLATFORM' AND role_code = 'CN_FIRSTLEG_PDA_OPERATOR' AND id <> 4000007)
) THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '国内 PDA 初始化失败:角色ID 4000007 或角色编码已被其他数据占用';
END IF;
IF EXISTS (
SELECT 1 FROM iam_data_scope
WHERE (id = 6000007 AND role_id <> 4000007)
OR (role_id = 4000007 AND id <> 6000007)
) THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '国内 PDA 初始化失败:数据范围ID 6000007 已被占用';
END IF;
IF (SELECT COUNT(*) FROM iam_permission
WHERE tenant_id = 0 AND app_code = 'PLATFORM' AND status = 'ACTIVE' AND deleted = 0
AND permission_code IN ('platform:firstleg:pda:view', 'platform:firstleg:pda:work')) <> 2 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '国内 PDA 初始化失败:PDA view/work 权限尚未完整部署';
END IF;
END$$
DELIMITER ;
CALL assert_domestic_pda_operator_seed_ready();
DROP PROCEDURE assert_domestic_pda_operator_seed_ready;
SET @domestic_warehouse_id = (
SELECT id FROM baiyue_warehouse.wh_warehouse
WHERE tenant_id = 0 AND warehouse_code = 'CN-YIWU-01'
AND warehouse_type = 'DOMESTIC' AND status = 'ACTIVE' AND deleted = 0
LIMIT 1
);
-- 优先复用同名账号已有ID;首次执行使用下面的稳定ID。
SET @zhangsan_auth_id = COALESCE(
(SELECT id FROM baiyue_auth.auth_account WHERE username = 'pda_zhangsan' AND deleted = 0 LIMIT 1),
194000000000000101
);
SET @lisi_auth_id = COALESCE(
(SELECT id FROM baiyue_auth.auth_account WHERE username = 'pda_lisi' AND deleted = 0 LIMIT 1),
194000000000000102
);
SET @zhangsan_user_id = COALESCE(
(SELECT id FROM iam_user WHERE auth_user_id = @zhangsan_auth_id AND deleted = 0 LIMIT 1),
194000000000000201
);
SET @lisi_user_id = COALESCE(
(SELECT id FROM iam_user WHERE auth_user_id = @lisi_auth_id AND deleted = 0 LIMIT 1),
194000000000000202
);
SET @zhangsan_membership_id = COALESCE(
(SELECT id FROM iam_membership WHERE tenant_id = 0 AND user_id = @zhangsan_user_id AND deleted = 0 LIMIT 1),
194000000000000301
);
SET @lisi_membership_id = COALESCE(
(SELECT id FROM iam_membership WHERE tenant_id = 0 AND user_id = @lisi_user_id AND deleted = 0 LIMIT 1),
194000000000000302
);
DROP PROCEDURE IF EXISTS assert_domestic_pda_operator_ids_available;
DELIMITER $$
CREATE PROCEDURE assert_domestic_pda_operator_ids_available()
BEGIN
IF EXISTS (
SELECT 1 FROM baiyue_auth.auth_account
WHERE (id = @zhangsan_auth_id AND username <> 'pda_zhangsan')
OR (id = @lisi_auth_id AND username <> 'pda_lisi')
OR (username = 'pda_zhangsan' AND id <> @zhangsan_auth_id)
OR (username = 'pda_lisi' AND id <> @lisi_auth_id)
) THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '国内 PDA 初始化失败:张三或李四的认证账号ID/用户名冲突';
END IF;
IF EXISTS (
SELECT 1 FROM iam_user
WHERE (id = @zhangsan_user_id AND auth_user_id <> @zhangsan_auth_id)
OR (id = @lisi_user_id AND auth_user_id <> @lisi_auth_id)
OR (auth_user_id = @zhangsan_auth_id AND id <> @zhangsan_user_id)
OR (auth_user_id = @lisi_auth_id AND id <> @lisi_user_id)
) THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '国内 PDA 初始化失败:张三或李四的 IAM 用户ID冲突';
END IF;
IF EXISTS (
SELECT 1 FROM iam_membership
WHERE (id = @zhangsan_membership_id AND (tenant_id <> 0 OR user_id <> @zhangsan_user_id))
OR (id = @lisi_membership_id AND (tenant_id <> 0 OR user_id <> @lisi_user_id))
OR (tenant_id = 0 AND member_no = 'CN-PDA-YIWU-ZS' AND user_id <> @zhangsan_user_id)
OR (tenant_id = 0 AND member_no = 'CN-PDA-YIWU-LS' AND user_id <> @lisi_user_id)
) THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '国内 PDA 初始化失败:张三或李四的成员ID/工号冲突';
END IF;
END$$
DELIMITER ;
CALL assert_domestic_pda_operator_ids_available();
DROP PROCEDURE assert_domestic_pda_operator_ids_available;
START TRANSACTION;
INSERT INTO iam_role (
id, tenant_id, app_code, role_code, role_name, role_type, status,
description, deleted, version
) VALUES (
4000007, 0, 'PLATFORM', 'CN_FIRSTLEG_PDA_OPERATOR', '国内头程 PDA 作业员',
'SYSTEM', 'ACTIVE', '负责使用国内 PDA 执行柜次拣货、现场取证和装柜完成', 0, 0
)
ON DUPLICATE KEY UPDATE
role_name = VALUES(role_name), role_type = 'SYSTEM', status = 'ACTIVE',
description = VALUES(description), deleted = 0;
-- 该角色只保留国内 PDA 查看、作业两项最小权限。
DELETE role_permission
FROM iam_role_permission role_permission
JOIN iam_permission permission ON permission.id = role_permission.permission_id
WHERE role_permission.role_id = 4000007
AND permission.permission_code NOT IN ('platform:firstleg:pda:view', 'platform:firstleg:pda:work');
INSERT IGNORE INTO iam_role_permission (role_id, permission_id, tenant_id)
SELECT 4000007, id, 0
FROM iam_permission
WHERE tenant_id = 0 AND app_code = 'PLATFORM' AND status = 'ACTIVE' AND deleted = 0
AND permission_code IN ('platform:firstleg:pda:view', 'platform:firstleg:pda:work');
INSERT INTO iam_data_scope (id, role_id, tenant_id, scope_type, scope_expression)
VALUES (6000007, 4000007, 0, 'TENANT', NULL)
ON DUPLICATE KEY UPDATE tenant_id = 0, scope_type = 'TENANT', scope_expression = NULL;
-- 首次插入使用既定临时密码的 BCrypt(12) 哈希;重复执行保留账号当前密码。
INSERT INTO baiyue_auth.auth_account (
id, username, phone, password_hash, display_name, account_status,
failed_login_count, locked_until, token_version, password_change_required,
default_tenant_id, operator_tenant_id, default_membership_id, default_app_code, deleted
) VALUES
(@zhangsan_auth_id, 'pda_zhangsan', NULL,
'$2a$12$/BdqRpdgVPAu6aoizfuTJ.UhtgpZYEAkwdvworS5GVirUBSwCcuKG',
'张三', 'ACTIVE', 0, NULL, 1, 1, 0, 0, @zhangsan_membership_id, 'PLATFORM', 0),
(@lisi_auth_id, 'pda_lisi', NULL,
'$2a$12$Uv.KUNtVSAwT1R0Sk3Gb6uaytWitd7Hrda5xrZl0zKxLY1/SCUyXe',
'李四', 'ACTIVE', 0, NULL, 1, 1, 0, 0, @lisi_membership_id, 'PLATFORM', 0)
ON DUPLICATE KEY UPDATE
display_name = VALUES(display_name), account_status = 'ACTIVE',
failed_login_count = 0, locked_until = NULL,
default_tenant_id = 0, operator_tenant_id = 0,
default_membership_id = VALUES(default_membership_id), default_app_code = 'PLATFORM', deleted = 0;
INSERT INTO iam_user (
id, auth_user_id, user_type, display_name, phone, email, status, deleted, version
) VALUES
(@zhangsan_user_id, @zhangsan_auth_id, 'INTERNAL', '张三', NULL, NULL, 'ACTIVE', 0, 0),
(@lisi_user_id, @lisi_auth_id, 'INTERNAL', '李四', NULL, NULL, 'ACTIVE', 0, 0)
ON DUPLICATE KEY UPDATE
user_type = 'INTERNAL', display_name = VALUES(display_name), status = 'ACTIVE', deleted = 0;
INSERT INTO iam_membership (
id, tenant_id, user_id, member_type, member_no,
overseas_warehouse_id, domestic_warehouse_id,
status, joined_at, left_at, deleted, version
) VALUES
(@zhangsan_membership_id, 0, @zhangsan_user_id, 'EMPLOYEE', 'CN-PDA-YIWU-ZS',
NULL, @domestic_warehouse_id, 'ACTIVE', CURRENT_TIMESTAMP(3), NULL, 0, 0),
(@lisi_membership_id, 0, @lisi_user_id, 'EMPLOYEE', 'CN-PDA-YIWU-LS',
NULL, @domestic_warehouse_id, 'ACTIVE', CURRENT_TIMESTAMP(3), NULL, 0, 0)
ON DUPLICATE KEY UPDATE
member_type = 'EMPLOYEE', member_no = VALUES(member_no),
overseas_warehouse_id = NULL, domestic_warehouse_id = VALUES(domestic_warehouse_id),
status = 'ACTIVE', joined_at = COALESCE(joined_at, CURRENT_TIMESTAMP(3)),
left_at = NULL, deleted = 0;
-- 国内、海外 PDA 角色互斥;保留人员可能已有的其他非 PDA 角色。
DELETE membership_role
FROM iam_membership_role membership_role
JOIN iam_role role ON role.id = membership_role.role_id
WHERE membership_role.membership_id IN (@zhangsan_membership_id, @lisi_membership_id)
AND role.role_code = 'OVERSEAS_WAREHOUSE_PDA_OPERATOR';
INSERT INTO iam_membership_role (membership_id, role_id, tenant_id) VALUES
(@zhangsan_membership_id, 4000007, 0),
(@lisi_membership_id, 4000007, 0)
ON DUPLICATE KEY UPDATE tenant_id = 0;
INSERT IGNORE INTO iam_security_subject_version (auth_user_id, permission_version)
VALUES (@zhangsan_auth_id, 1), (@lisi_auth_id, 1);
COMMIT;
-- 验证结果:应返回两行,且 role_code、warehouse_code、account_status、member_status 均为有效值。
SELECT account.username,
user_profile.display_name,
role.role_code,
warehouse.warehouse_code,
warehouse.warehouse_name,
account.account_status,
membership.status AS member_status
FROM baiyue_auth.auth_account account
JOIN iam_user user_profile ON user_profile.auth_user_id = account.id AND user_profile.deleted = 0
JOIN iam_membership membership ON membership.user_id = user_profile.id
AND membership.tenant_id = 0 AND membership.deleted = 0
JOIN iam_membership_role membership_role ON membership_role.membership_id = membership.id
JOIN iam_role role ON role.id = membership_role.role_id
AND role.role_code = 'CN_FIRSTLEG_PDA_OPERATOR'
JOIN baiyue_warehouse.wh_warehouse warehouse ON warehouse.id = membership.domestic_warehouse_id
WHERE account.username IN ('pda_zhangsan', 'pda_lisi')
ORDER BY account.username;
@@ -0,0 +1,385 @@
-- 2026-09-23 近期字段与表结构统一增量(MySQL 8.0+)
--
-- 范围:2026-09-21 至 2026-09-22 合入的结构变化。
-- 特性:显式切换到所属 schema;字段和索引均通过 information_schema 判断;可重复执行。
-- 边界:不创建数据库、不删除业务字段/表、不包含权限种子或业务数据修正。
-- 注意:DDL 在 MySQL 中会隐式提交;任一步失败后应停止执行并核对目标实例。
SET NAMES utf8mb4;
-- ============================================================================
-- IAM:国内头程 PDA 操作员绑定作业仓
-- ============================================================================
USE baiyue_iam;
SET @ddl = IF(
EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_schema = DATABASE() AND table_name = 'iam_membership'
)
AND NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'iam_membership'
AND column_name = 'domestic_warehouse_id'
),
'ALTER TABLE iam_membership ADD COLUMN domestic_warehouse_id BIGINT NULL COMMENT ''国内头程PDA操作员绑定的唯一国内作业仓ID'' AFTER overseas_warehouse_id',
'SELECT 1'
);
PREPARE recent_schema_stmt FROM @ddl;
EXECUTE recent_schema_stmt;
DEALLOCATE PREPARE recent_schema_stmt;
SET @domestic_warehouse_index_columns = (
SELECT GROUP_CONCAT(column_name ORDER BY seq_in_index SEPARATOR ',')
FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND table_name = 'iam_membership'
AND index_name = 'idx_membership_domestic_warehouse'
);
SET @domestic_warehouse_index_non_unique = (
SELECT MIN(non_unique)
FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND table_name = 'iam_membership'
AND index_name = 'idx_membership_domestic_warehouse'
);
SET @ddl = CASE
WHEN NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'iam_membership'
AND column_name = 'domestic_warehouse_id'
) THEN 'SELECT 1'
WHEN @domestic_warehouse_index_columns IS NULL THEN
'ALTER TABLE iam_membership ADD KEY idx_membership_domestic_warehouse (tenant_id, domestic_warehouse_id, status)'
WHEN @domestic_warehouse_index_columns <> 'tenant_id,domestic_warehouse_id,status'
OR @domestic_warehouse_index_non_unique <> 1 THEN
'ALTER TABLE iam_membership DROP INDEX idx_membership_domestic_warehouse, ADD KEY idx_membership_domestic_warehouse (tenant_id, domestic_warehouse_id, status)'
ELSE 'SELECT 1'
END;
PREPARE recent_schema_stmt FROM @ddl;
EXECUTE recent_schema_stmt;
DEALLOCATE PREPARE recent_schema_stmt;
-- ============================================================================
-- Firstleg:人工 FBA 报价允许缺少船司价格快照
-- ============================================================================
USE baiyue_firstleg;
SET @ddl = IF(
EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'fl_inbound_fee'
AND column_name = 'carrier_rate_id' AND is_nullable = 'NO'
),
'ALTER TABLE fl_inbound_fee MODIFY COLUMN carrier_rate_id BIGINT UNSIGNED NULL COMMENT ''船司FBA价格ID;人工报价为空''',
'SELECT 1'
);
PREPARE recent_schema_stmt FROM @ddl;
EXECUTE recent_schema_stmt;
DEALLOCATE PREPARE recent_schema_stmt;
SET @ddl = IF(
EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'fl_inbound_fee'
AND column_name = 'carrier_rate_version' AND is_nullable = 'NO'
),
'ALTER TABLE fl_inbound_fee MODIFY COLUMN carrier_rate_version INT UNSIGNED NULL COMMENT ''船司价格版本快照;人工报价为空''',
'SELECT 1'
);
PREPARE recent_schema_stmt FROM @ddl;
EXECUTE recent_schema_stmt;
DEALLOCATE PREPARE recent_schema_stmt;
SET @ddl = IF(
EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'fl_inbound_fee'
AND column_name = 'rate_effective_from' AND is_nullable = 'NO'
),
'ALTER TABLE fl_inbound_fee MODIFY COLUMN rate_effective_from DATETIME(3) NULL COMMENT ''价格生效开始快照;人工报价为空''',
'SELECT 1'
);
PREPARE recent_schema_stmt FROM @ddl;
EXECUTE recent_schema_stmt;
DEALLOCATE PREPARE recent_schema_stmt;
-- ============================================================================
-- Firstleg:墨家测量收件箱扩展为箱唛/SKU 双分流
-- ============================================================================
SET @ddl = IF(
NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'fl_mojia_measurement_receipt'
AND column_name = 'measurement_kind'
),
'ALTER TABLE fl_mojia_measurement_receipt ADD COLUMN measurement_kind VARCHAR(16) NOT NULL DEFAULT ''BOX_LABEL'' COMMENT ''测量分流:BOX_LABEL箱唛;SKU一件代发SKU'' AFTER box_code',
'SELECT 1'
);
PREPARE recent_schema_stmt FROM @ddl;
EXECUTE recent_schema_stmt;
DEALLOCATE PREPARE recent_schema_stmt;
SET @ddl = IF(
NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'fl_mojia_measurement_receipt'
AND column_name = 'weight_kg'
),
'ALTER TABLE fl_mojia_measurement_receipt ADD COLUMN weight_kg DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT ''设备实测重量kg'' AFTER height_cm',
'SELECT 1'
);
PREPARE recent_schema_stmt FROM @ddl;
EXECUTE recent_schema_stmt;
DEALLOCATE PREPARE recent_schema_stmt;
SET @ddl = IF(
EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'fl_mojia_measurement_receipt'
AND column_name = 'weight_kg'
AND (column_type <> 'decimal(18,3)' OR is_nullable <> 'NO' OR column_default IS NOT NULL
OR column_comment <> '设备实测重量kg')
),
'ALTER TABLE fl_mojia_measurement_receipt MODIFY COLUMN weight_kg DECIMAL(18,3) NOT NULL COMMENT ''设备实测重量kg''',
'SELECT 1'
);
PREPARE recent_schema_stmt FROM @ddl;
EXECUTE recent_schema_stmt;
DEALLOCATE PREPARE recent_schema_stmt;
SET @ddl = IF(
NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'fl_mojia_measurement_receipt'
AND column_name = 'sync_status'
),
'ALTER TABLE fl_mojia_measurement_receipt ADD COLUMN sync_status VARCHAR(24) NULL COMMENT ''SKU同步状态:PENDING/SYNCING/SUCCESS/FAILED'' AFTER status',
'SELECT 1'
);
PREPARE recent_schema_stmt FROM @ddl;
EXECUTE recent_schema_stmt;
DEALLOCATE PREPARE recent_schema_stmt;
SET @ddl = IF(
NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'fl_mojia_measurement_receipt'
AND column_name = 'sync_failure_code'
),
'ALTER TABLE fl_mojia_measurement_receipt ADD COLUMN sync_failure_code VARCHAR(64) NULL COMMENT ''SKU同步失败业务码'' AFTER sync_status',
'SELECT 1'
);
PREPARE recent_schema_stmt FROM @ddl;
EXECUTE recent_schema_stmt;
DEALLOCATE PREPARE recent_schema_stmt;
SET @ddl = IF(
NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'fl_mojia_measurement_receipt'
AND column_name = 'sync_failure_reason'
),
'ALTER TABLE fl_mojia_measurement_receipt ADD COLUMN sync_failure_reason VARCHAR(500) NULL COMMENT ''脱敏后的SKU同步失败原因'' AFTER sync_failure_code',
'SELECT 1'
);
PREPARE recent_schema_stmt FROM @ddl;
EXECUTE recent_schema_stmt;
DEALLOCATE PREPARE recent_schema_stmt;
SET @ddl = IF(
NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'fl_mojia_measurement_receipt'
AND column_name = 'lingxing_sku_id'
),
'ALTER TABLE fl_mojia_measurement_receipt ADD COLUMN lingxing_sku_id VARCHAR(128) NULL COMMENT ''领星SKU内部标识'' AFTER sync_failure_reason',
'SELECT 1'
);
PREPARE recent_schema_stmt FROM @ddl;
EXECUTE recent_schema_stmt;
DEALLOCATE PREPARE recent_schema_stmt;
SET @ddl = IF(
NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'fl_mojia_measurement_receipt'
AND column_name = 'revision'
),
'ALTER TABLE fl_mojia_measurement_receipt ADD COLUMN revision INT NOT NULL DEFAULT 1 COMMENT ''当前测量版本号'' AFTER lingxing_sku_id',
'SELECT 1'
);
PREPARE recent_schema_stmt FROM @ddl;
EXECUTE recent_schema_stmt;
DEALLOCATE PREPARE recent_schema_stmt;
SET @ddl = IF(
EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'fl_mojia_measurement_receipt'
AND column_name = 'match_failure_code' AND is_nullable = 'NO'
),
'ALTER TABLE fl_mojia_measurement_receipt MODIFY COLUMN match_failure_code VARCHAR(16) NULL COMMENT ''正式箱唛业务匹配失败码''',
'SELECT 1'
);
PREPARE recent_schema_stmt FROM @ddl;
EXECUTE recent_schema_stmt;
DEALLOCATE PREPARE recent_schema_stmt;
SET @ddl = IF(
EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'fl_mojia_measurement_receipt'
AND column_name = 'match_failure_reason' AND is_nullable = 'NO'
),
'ALTER TABLE fl_mojia_measurement_receipt MODIFY COLUMN match_failure_reason VARCHAR(500) NULL COMMENT ''脱敏后的箱唛匹配失败原因''',
'SELECT 1'
);
PREPARE recent_schema_stmt FROM @ddl;
EXECUTE recent_schema_stmt;
DEALLOCATE PREPARE recent_schema_stmt;
SET @ddl = IF(
EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'fl_mojia_measurement_receipt'
AND column_name = 'box_code'
AND column_comment <> '设备扫描的原始箱码或不带#的SKU'
),
'ALTER TABLE fl_mojia_measurement_receipt MODIFY COLUMN box_code VARCHAR(128) NOT NULL COMMENT ''设备扫描的原始箱码或不带#的SKU''',
'SELECT 1'
);
PREPARE recent_schema_stmt FROM @ddl;
EXECUTE recent_schema_stmt;
DEALLOCATE PREPARE recent_schema_stmt;
SET @ddl = IF(
EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'fl_mojia_measurement_receipt'
AND column_name = 'status'
AND column_comment <> '原始收件状态;SKU记录使用SKU_PENDING_SYNC'
),
'ALTER TABLE fl_mojia_measurement_receipt MODIFY COLUMN status VARCHAR(24) NOT NULL DEFAULT ''UNMATCHED'' COMMENT ''原始收件状态;SKU记录使用SKU_PENDING_SYNC''',
'SELECT 1'
);
PREPARE recent_schema_stmt FROM @ddl;
EXECUTE recent_schema_stmt;
DEALLOCATE PREPARE recent_schema_stmt;
-- ============================================================================
-- Lastleg:同一尾程单允许多次换标申请,订单关联由唯一索引改为普通索引
-- ============================================================================
USE baiyue_lastleg;
SET @ddl = IF(
EXISTS (
SELECT 1 FROM information_schema.statistics
WHERE table_schema = DATABASE() AND table_name = 'll_relabel_application'
AND index_name = 'uk_ll_relabel_application_order'
),
'ALTER TABLE ll_relabel_application DROP INDEX uk_ll_relabel_application_order',
'SELECT 1'
);
PREPARE recent_schema_stmt FROM @ddl;
EXECUTE recent_schema_stmt;
DEALLOCATE PREPARE recent_schema_stmt;
SET @relabel_order_index_columns = (
SELECT GROUP_CONCAT(column_name ORDER BY seq_in_index SEPARATOR ',')
FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND table_name = 'll_relabel_application'
AND index_name = 'idx_ll_relabel_application_order'
);
SET @relabel_order_index_non_unique = (
SELECT MIN(non_unique)
FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND table_name = 'll_relabel_application'
AND index_name = 'idx_ll_relabel_application_order'
);
SET @ddl = CASE
WHEN @relabel_order_index_columns IS NULL THEN
'ALTER TABLE ll_relabel_application ADD KEY idx_ll_relabel_application_order (lastleg_order_id)'
WHEN @relabel_order_index_columns <> 'lastleg_order_id'
OR @relabel_order_index_non_unique <> 1 THEN
'ALTER TABLE ll_relabel_application DROP INDEX idx_ll_relabel_application_order, ADD KEY idx_ll_relabel_application_order (lastleg_order_id)'
ELSE 'SELECT 1'
END;
PREPARE recent_schema_stmt FROM @ddl;
EXECUTE recent_schema_stmt;
DEALLOCATE PREPARE recent_schema_stmt;
-- ============================================================================
-- Overseas Warehouse:A0 系统共享溢出库位标记
-- 注意:正式 schema 名沿用项目现有拼写 baiyue_lastlag_warehouse。
-- ============================================================================
USE baiyue_lastlag_warehouse;
SET @ddl = IF(
NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'wh_location'
AND column_name = 'shared_overflow'
),
'ALTER TABLE wh_location ADD COLUMN shared_overflow TINYINT(1) NOT NULL DEFAULT 0 COMMENT ''是否系统共享溢出库位(A0)'' AFTER location_type',
'SELECT 1'
);
PREPARE recent_schema_stmt FROM @ddl;
EXECUTE recent_schema_stmt;
DEALLOCATE PREPARE recent_schema_stmt;
SET @ddl = IF(
EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'wh_location'
AND column_name = 'shared_overflow'
AND (column_type <> 'tinyint(1)' OR is_nullable <> 'NO'
OR COALESCE(column_default, '') <> '0'
OR column_comment <> '是否系统共享溢出库位(A0)')
),
'ALTER TABLE wh_location MODIFY COLUMN shared_overflow TINYINT(1) NOT NULL DEFAULT 0 COMMENT ''是否系统共享溢出库位(A0)''',
'SELECT 1'
);
PREPARE recent_schema_stmt FROM @ddl;
EXECUTE recent_schema_stmt;
DEALLOCATE PREPARE recent_schema_stmt;
-- 最终只读核对;四行均应返回 READY。
SELECT 'baiyue_iam' AS target_schema,
IF(
EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'baiyue_iam' AND table_name = 'iam_membership' AND column_name = 'domestic_warehouse_id')
AND (SELECT GROUP_CONCAT(column_name ORDER BY seq_in_index SEPARATOR ',') FROM information_schema.statistics WHERE table_schema = 'baiyue_iam' AND table_name = 'iam_membership' AND index_name = 'idx_membership_domestic_warehouse') = 'tenant_id,domestic_warehouse_id,status',
'READY', 'CHECK_REQUIRED'
) AS schema_status
UNION ALL
SELECT 'baiyue_firstleg',
IF(
(SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_mojia_measurement_receipt' AND column_name IN ('measurement_kind','weight_kg','sync_status','sync_failure_code','sync_failure_reason','lingxing_sku_id','revision')) = 7
AND (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_inbound_fee' AND column_name IN ('carrier_rate_id','carrier_rate_version','rate_effective_from') AND is_nullable = 'YES') = 3,
'READY', 'CHECK_REQUIRED'
)
UNION ALL
SELECT 'baiyue_lastleg',
IF(
NOT EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema = 'baiyue_lastleg' AND table_name = 'll_relabel_application' AND index_name = 'uk_ll_relabel_application_order')
AND (SELECT GROUP_CONCAT(column_name ORDER BY seq_in_index SEPARATOR ',') FROM information_schema.statistics WHERE table_schema = 'baiyue_lastleg' AND table_name = 'll_relabel_application' AND index_name = 'idx_ll_relabel_application_order') = 'lastleg_order_id'
AND (SELECT MIN(non_unique) FROM information_schema.statistics WHERE table_schema = 'baiyue_lastleg' AND table_name = 'll_relabel_application' AND index_name = 'idx_ll_relabel_application_order') = 1,
'READY', 'CHECK_REQUIRED'
)
UNION ALL
SELECT 'baiyue_lastlag_warehouse',
IF(
EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'baiyue_lastlag_warehouse'
AND table_name = 'wh_location'
AND column_name = 'shared_overflow'
AND column_type = 'tinyint(1)'
AND is_nullable = 'NO'
AND COALESCE(column_default, '') = '0'
),
'READY', 'CHECK_REQUIRED'
);
@@ -89,7 +89,9 @@ assert(transferDeliveries.length === 1,
`Expected one destination delivery, got ${transferDeliveries.length}`)
const deliveryNo = transferDeliveries[0].deliveryNo
assert(Array.isArray(task.items) && task.items.length === 2, 'Expected two PDA pallet items')
const expectedPdaItems = scenario.partialRelabel ? 1 : 2
assert(Array.isArray(task.items) && task.items.length === expectedPdaItems,
`Expected ${expectedPdaItems} PDA pallet items`)
if (!alreadyDeparted) {
if (task.items.every(item => Number(item.pickedPackages) === 0)) {
await api(overseas, platformToken, 'PUT', `/api/platform/v1/last-leg/dispatches/${dispatchNo}`, {
@@ -9,6 +9,7 @@ const overseas = process.env.ADMIN_OVERSEAS_GATEWAY || 'http://127.0.0.1:9300'
let customerId = process.env.OVERSEAS_TEST_CUSTOMER_ID || ''
let warehouseId = process.env.OVERSEAS_TEST_WAREHOUSE_ID || ''
const destinationCode = process.env.OVERSEAS_TEST_DESTINATION_CODE || 'ONT8'
const partialRelabel = process.env.WAREHOUSE_TRANSFER_PARTIAL_RELABEL === 'true'
const output = resolve(process.env.WAREHOUSE_TRANSFER_REMAINING_OUTPUT
|| 'reports/warehouse-transfer-remaining-acceptance.json')
const marker = `WT-REMAIN-${new Date().toISOString().replace(/\D/g, '').slice(0, 14)}-${process.pid}`
@@ -189,13 +190,14 @@ try {
}
const cartonMarks = [`CM-${marker}-1`, `CM-${marker}-2`]
const cartonRelabelBindings = partialRelabel ? cartonMarks.slice(0, 1) : cartonMarks
const transfer = await api(overseas, merchant.overseasAccessToken, 'POST',
'/api/merchant/v1/last-leg/warehouse-transfers', {
operatingWarehouseId: warehouseId,
fbaWarehouseCode: destinationCode,
destinationType: 'FBA',
destinationWarehouseCode: destinationCode,
destinationWarehouseName: destinationCode,
fbaWarehouseCode: partialRelabel ? null : destinationCode,
destinationType: partialRelabel ? null : 'FBA',
destinationWarehouseCode: partialRelabel ? null : destinationCode,
destinationWarehouseName: partialRelabel ? null : destinationCode,
deliveryAddress: `Acceptance inbound address ${marker}`,
packageCount: 2,
volumeCbm: 0.12,
@@ -214,7 +216,7 @@ try {
idempotencyKey: key('CREATE'),
items: [{ skuCode: `SKU-${marker}`.slice(0, 128), productNameZh: 'Acceptance cargo',
packageCount: 2, shippingMark: cartonMarks.join(',') }],
relabelItems: cartonMarks.map((cartonMark, index) => ({
relabelItems: cartonRelabelBindings.map((cartonMark, index) => ({
operationType: 'CARTON_AND_SKU', customerCartonMarks: [cartonMark],
originalSku: `SKU-${marker}`.slice(0, 128),
newSku: `NEW-${marker}-${index + 1}`.slice(0, 128), quantity: 1,
@@ -278,9 +280,18 @@ try {
status: transferDetail.status, relabelNo,
})
const reviewedRelabel = await api(overseas, admin.overseasAccessToken, 'PUT',
`/api/platform/v1/last-leg/relabel-applications/${relabelNo}/status`, {
status: 'PENDING_PROCESSING', remark: marker,
})
assert(reviewedRelabel.application.status === 'PENDING_PROCESSING',
`Linked relabel review did not approve execution: ${reviewedRelabel.application.status}`)
step('Platform approved linked relabel for PDA execution', { status: reviewedRelabel.application.status })
let relabel = await api(overseas, pda.token, 'POST',
`/api/pda/v1/last-leg/relabel-tasks/${relabelNo}/claim`, { idempotencyKey: key('RELABEL-CLAIM') })
assert(relabel.items.length === 2, `Expected two linked relabel items, got ${relabel.items.length}`)
assert(relabel.items.length === (partialRelabel ? 1 : 2),
`Expected ${partialRelabel ? 1 : 2} linked relabel items, got ${relabel.items.length}`)
for (const [index, item] of relabel.items.entries()) {
relabel = await api(overseas, pda.token, 'POST',
`/api/pda/v1/last-leg/relabel-tasks/${relabelNo}/cartons/scan`, {
@@ -330,7 +341,8 @@ try {
transferDetail = await api(overseas, merchant.overseasAccessToken, 'GET',
`/api/merchant/v1/last-leg/warehouse-transfers/${transferNo}`)
assert(confirmedRelabel.application.status === 'READY_FOR_OUTBOUND', 'Linked relabel did not reach outbound-ready')
assert(transferDetail.status === 'READY_FOR_DISPATCH', 'Warehouse transfer did not return to dispatch-ready')
assert(transferDetail.status === (partialRelabel ? 'WAIT_FBA_DESTINATION' : 'READY_FOR_DISPATCH'),
`Warehouse transfer reached unexpected status ${transferDetail.status}`)
step('Completed linked relabel and released transfer for dispatch', {
relabelStatus: confirmedRelabel.application.status, transferStatus: transferDetail.status,
})
@@ -339,17 +351,30 @@ try {
`/api/platform/v1/last-leg/dispatches/candidates?orderNo=${encodeURIComponent(relabelNo)}&sourceStage=RECEIVED`)
const targetIds = new Set(targetPallets.map((value) => id(value.palletId)))
const rows = candidates.filter((value) => value.sourceStage === 'RELABEL' && targetIds.has(id(value.warehousePalletId)))
assert(rows.length === 2, `Expected two smart-dispatch pallets, got ${rows.length}`)
assert(rows.length === (partialRelabel ? 1 : 2),
`Expected ${partialRelabel ? 1 : 2} smart-dispatch pallets, got ${rows.length}`)
const dispatch = await api(overseas, admin.overseasAccessToken, 'POST',
'/api/platform/v1/last-leg/dispatches/pre-arrange', {
planPalletIds: rows.map((value) => id(value.warehousePalletId)), route: marker, relabelNo,
})
step('Created smart pre-dispatch', { dispatchNo: dispatch.dispatchNo, status: dispatch.status })
if (partialRelabel) {
const outboundCandidates = await api(overseas, admin.overseasAccessToken, 'GET',
`/api/platform/v1/last-leg/outbound-orders/warehouse-transfer/candidates?customerId=${encodeURIComponent(customerId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
const remaining = outboundCandidates.filter((value) => value.transferNo === transferNo)
assert(remaining.length === 1 && Number(remaining[0].packageCount) === 1,
`Expected one unlabelled carton in warehouse-transfer outbound candidates, got ${JSON.stringify(remaining)}`)
step('Verified split outbound routing', {
relabelDispatchPallets: rows.length, remainingOutboundCandidates: remaining.length,
remainingPackageCount: remaining[0].packageCount,
})
}
const sourceReport = resolve('reports', `${marker}-relabel-source.json`)
const fulfillmentReport = resolve('reports', `${marker}-fulfillment.json`)
await writeFile(sourceReport, JSON.stringify({ marker, passed: true, scenarios: [{
name: 'SPLIT-TWO-TARGETS', relabelNo, dispatchNo: dispatch.dispatchNo,
name: 'SPLIT-TWO-TARGETS', relabelNo, dispatchNo: dispatch.dispatchNo, partialRelabel,
}] }, null, 2), 'utf8')
const child = spawnSync(process.execPath, [resolve('scripts', 'overseas-relabel-fulfillment-acceptance.mjs')], {
cwd: resolve('.'), stdio: 'inherit', env: {
@@ -366,11 +391,70 @@ try {
})
if (child.error) throw child.error
if (child.status !== 0) throw new Error(`Fulfillment acceptance failed with exit code ${child.status}`)
const finalTransfer = await api(overseas, merchant.overseasAccessToken, 'GET',
let finalTransfer = await api(overseas, merchant.overseasAccessToken, 'GET',
`/api/merchant/v1/last-leg/warehouse-transfers/${transferNo}`)
let remainingDispatchNo = null
let remainingFulfillmentReport = null
if (partialRelabel) {
assert(finalTransfer.status === 'WAIT_FBA_DESTINATION',
`Partial relabel dispatch incorrectly closed the transfer: ${finalTransfer.status}`)
const beforeBind = await api(overseas, admin.overseasAccessToken, 'GET',
`/api/platform/v1/last-leg/outbound-orders/warehouse-transfer/candidates?customerId=${encodeURIComponent(customerId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
const remainingBeforeBind = beforeBind.filter((value) => value.transferNo === transferNo)
assert(remainingBeforeBind.length === 1 && Number(remainingBeforeBind[0].packageCount) === 1
&& remainingBeforeBind[0].palletId && remainingBeforeBind[0].warehousePalletId,
`Expected one selectable remaining pallet before destination binding, got ${JSON.stringify(remainingBeforeBind)}`)
await api(overseas, admin.overseasAccessToken, 'POST',
'/api/platform/v1/last-leg/warehouse-transfers/bind-destination', {
transfers: [{ customerId, transferNo }], destinationType: 'FBA',
destinationWarehouseCode: destinationCode, destinationWarehouseName: destinationCode,
idempotencyKey: key('BIND-REMAINING-DESTINATION'),
})
const afterBind = await api(overseas, admin.overseasAccessToken, 'GET',
`/api/platform/v1/last-leg/outbound-orders/warehouse-transfer/candidates?customerId=${encodeURIComponent(customerId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
assert(!afterBind.some((value) => value.transferNo === transferNo),
'Destination-bound transfer remained in the destination-completion outbound list')
const remainingCandidates = await api(overseas, admin.overseasAccessToken, 'GET',
`/api/platform/v1/last-leg/dispatches/candidates?orderNo=${encodeURIComponent(transferNo)}&sourceStage=RECEIVED`)
const remainingRows = remainingCandidates.filter((value) => value.sourceStage === 'RECEIVED'
&& Number(value.packageCount) === 1 && value.destinationWarehouseCode === destinationCode)
assert(remainingRows.length === 1,
`Expected one destination-bound smart-dispatch pallet, got ${JSON.stringify(remainingRows)}`)
const remainingDispatch = await api(overseas, admin.overseasAccessToken, 'POST',
'/api/platform/v1/last-leg/dispatches/pre-arrange', {
planPalletIds: remainingRows.map((value) => id(value.planPalletId)), route: `${marker}-REMAINING`,
})
remainingDispatchNo = remainingDispatch.dispatchNo
const remainingSourceReport = resolve('reports', `${marker}-remaining-source.json`)
remainingFulfillmentReport = resolve('reports', `${marker}-remaining-fulfillment.json`)
await writeFile(remainingSourceReport, JSON.stringify({ marker, passed: true, scenarios: [{
name: 'SPLIT-TWO-TARGETS', relabelNo: '', dispatchNo: remainingDispatchNo, partialRelabel: true,
}] }, null, 2), 'utf8')
const remainingChild = spawnSync(process.execPath,
[resolve('scripts', 'overseas-relabel-fulfillment-acceptance.mjs')], {
cwd: resolve('.'), stdio: 'inherit', env: {
...process.env, ADMIN_TEST_GATEWAY: domestic, ADMIN_OVERSEAS_GATEWAY: overseas,
ADMIN_OVERSEAS_ACCESS_TOKEN: admin.overseasAccessToken,
MERCHANT_OVERSEAS_ACCESS_TOKEN: merchant.overseasAccessToken,
PDA_OVERSEAS_ACCESS_TOKEN: pda.token, OVERSEAS_TEST_CUSTOMER_ID: customerId,
OVERSEAS_ACCEPTANCE_SOURCE: remainingSourceReport,
OVERSEAS_FULFILLMENT_OUTPUT: remainingFulfillmentReport,
},
})
if (remainingChild.error) throw remainingChild.error
if (remainingChild.status !== 0) throw new Error(`Remaining fulfillment failed with exit code ${remainingChild.status}`)
finalTransfer = await api(overseas, merchant.overseasAccessToken, 'GET',
`/api/merchant/v1/last-leg/warehouse-transfers/${transferNo}`)
assert(finalTransfer.status === 'OUTBOUNDED',
`Remaining transfer did not complete outbound: ${finalTransfer.status}`)
step('Bound remaining carton destination and completed its smart dispatch', {
remainingDispatchNo, remainingPackageCount: 1, transferStatus: finalTransfer.status,
})
}
Object.assign(report, {
passed: true, transferNo, relabelNo, dispatchNo: dispatch.dispatchNo,
transferStatus: finalTransfer.status, fulfillmentReport, finishedAt: new Date().toISOString(),
remainingDispatchNo, transferStatus: finalTransfer.status, fulfillmentReport,
remainingFulfillmentReport, finishedAt: new Date().toISOString(),
})
} catch (error) {
report.error = error instanceof Error ? error.message : String(error)