From 257c019b3669aa24438d65885dbdb12351266dfb Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Mon, 24 Aug 2026 11:48:16 +0800 Subject: [PATCH] fix phase3 acceptance roots and payable validation --- deploy/sql/头程数据库统一增量-MySQL8.sql | 27 +++++++++++++++++ .../service/FeePaymentSnapshotValidator.java | 2 +- .../db/phase3-split-preload-schema.sql | 10 +++++++ .../PdaSplitSettlementSqlContractTest.java | 23 ++++++++++++++ .../FeePaymentSnapshotValidatorTest.java | 8 +++++ scripts/admin-test-env-acceptance.mjs | 4 +++ .../finance-downloads.mjs | 2 +- .../finance-full-fixtures-contract.test.mjs | 2 ++ .../finance-full-fixtures.mjs | 4 ++- .../admin-test-env-modules/inbound-fees.mjs | 2 +- .../phase3-container-loading.mjs | 2 +- .../admin-test-env-modules/phase3-customs.mjs | 30 +++++++++++-------- ...e3-loading-dynamic-final-contract.test.mjs | 3 +- .../admin-test-env-modules/phase3-loading.mjs | 19 ++++++------ .../phase3-producer-chain-contract.test.mjs | 7 +++-- 15 files changed, 115 insertions(+), 30 deletions(-) diff --git a/deploy/sql/头程数据库统一增量-MySQL8.sql b/deploy/sql/头程数据库统一增量-MySQL8.sql index a29e85c56..51b63c660 100644 --- a/deploy/sql/头程数据库统一增量-MySQL8.sql +++ b/deploy/sql/头程数据库统一增量-MySQL8.sql @@ -7492,6 +7492,9 @@ CREATE TABLE IF NOT EXISTS fl_preload_plan ( version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号,每次更新递增', items_json JSON NOT NULL COMMENT '可编辑分柜明细JSON快照;人工确认后作为预装依据', remark VARCHAR(1000) NULL COMMENT '预装备注', + confirmed_by BIGINT UNSIGNED NULL COMMENT '确认预装的操作人ID', + confirmed_by_name VARCHAR(64) NULL COMMENT '确认预装的操作人姓名快照', + confirmed_at DATETIME(3) NULL COMMENT '预装确认时间', created_by BIGINT UNSIGNED NOT NULL COMMENT '创建预装清单的操作人ID', created_by_name VARCHAR(64) NULL COMMENT '创建操作人姓名快照', updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改操作人ID', @@ -7504,6 +7507,30 @@ CREATE TABLE IF NOT EXISTS fl_preload_plan ( CONSTRAINT fk_preload_inbound FOREIGN KEY (inbound_id) REFERENCES fl_inbound_order (id) ) ENGINE=InnoDB COMMENT='订单分柜预装清单'; +DROP PROCEDURE IF EXISTS migrate_preload_confirmation_columns; +DELIMITER $$ +CREATE PROCEDURE migrate_preload_confirmation_columns() +BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.columns + WHERE table_schema=DATABASE() AND table_name='fl_preload_plan' AND column_name='confirmed_by') THEN + ALTER TABLE fl_preload_plan + ADD COLUMN confirmed_by BIGINT UNSIGNED NULL COMMENT '确认预装的操作人ID' AFTER remark; + END IF; + IF NOT EXISTS (SELECT 1 FROM information_schema.columns + WHERE table_schema=DATABASE() AND table_name='fl_preload_plan' AND column_name='confirmed_by_name') THEN + ALTER TABLE fl_preload_plan + ADD COLUMN confirmed_by_name VARCHAR(64) NULL COMMENT '确认预装的操作人姓名快照' AFTER confirmed_by; + END IF; + IF NOT EXISTS (SELECT 1 FROM information_schema.columns + WHERE table_schema=DATABASE() AND table_name='fl_preload_plan' AND column_name='confirmed_at') THEN + ALTER TABLE fl_preload_plan + ADD COLUMN confirmed_at DATETIME(3) NULL COMMENT '预装确认时间' AFTER confirmed_by_name; + END IF; +END$$ +DELIMITER ; +CALL migrate_preload_confirmation_columns(); +DROP PROCEDURE IF EXISTS migrate_preload_confirmation_columns; + CREATE TABLE IF NOT EXISTS fl_split_operation ( id BIGINT UNSIGNED NOT NULL COMMENT '幂等记录ID', tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/FeePaymentSnapshotValidator.java b/firstleg/src/main/java/com/baiyue/firstleg/service/FeePaymentSnapshotValidator.java index 64a88398a..679368549 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/FeePaymentSnapshotValidator.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/FeePaymentSnapshotValidator.java @@ -41,7 +41,7 @@ public class FeePaymentSnapshotValidator { } return Snapshot.empty(); } - if (!"PAYABLE".equals(direction) || !MODES.contains(mode)) { + if (!"PAYABLE".equals(direction) || mode == null || !MODES.contains(mode)) { throw bad("应付费用必须选择ON_ACCOUNT或DIRECT_PAID"); } if ("ON_ACCOUNT".equals(mode)) { diff --git a/firstleg/src/main/resources/db/phase3-split-preload-schema.sql b/firstleg/src/main/resources/db/phase3-split-preload-schema.sql index 8004ba31b..2740d4d5c 100644 --- a/firstleg/src/main/resources/db/phase3-split-preload-schema.sql +++ b/firstleg/src/main/resources/db/phase3-split-preload-schema.sql @@ -10,6 +10,9 @@ CREATE TABLE IF NOT EXISTS fl_preload_plan ( version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号,每次更新递增', items_json JSON NOT NULL COMMENT '可编辑分柜明细JSON快照;人工确认后作为预装依据', remark VARCHAR(1000) NULL COMMENT '预装备注', + confirmed_by BIGINT UNSIGNED NULL COMMENT '确认预装的操作人ID', + confirmed_by_name VARCHAR(64) NULL COMMENT '确认预装的操作人姓名快照', + confirmed_at DATETIME(3) NULL COMMENT '预装确认时间', created_by BIGINT UNSIGNED NOT NULL COMMENT '创建预装清单的操作人ID', created_by_name VARCHAR(64) NULL COMMENT '创建操作人姓名快照', updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改操作人ID', @@ -62,6 +65,13 @@ BEGIN END$$ DELIMITER ; +CALL phase3_split_add_column('fl_preload_plan', 'confirmed_by', + 'BIGINT UNSIGNED NULL COMMENT ''确认预装的操作人ID'' AFTER remark'); +CALL phase3_split_add_column('fl_preload_plan', 'confirmed_by_name', + 'VARCHAR(64) NULL COMMENT ''确认预装的操作人姓名快照'' AFTER confirmed_by'); +CALL phase3_split_add_column('fl_preload_plan', 'confirmed_at', + 'DATETIME(3) NULL COMMENT ''预装确认时间'' AFTER confirmed_by_name'); + CALL phase3_split_add_column('fl_inbound_order', 'parent_inbound_id', 'BIGINT UNSIGNED NULL COMMENT ''生成本单的主入库单ID;仅二级子单有值'' AFTER id'); CALL phase3_split_add_column('fl_inbound_order', 'split_level', diff --git a/firstleg/src/test/java/com/baiyue/firstleg/repository/PdaSplitSettlementSqlContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/repository/PdaSplitSettlementSqlContractTest.java index d8204c704..fde74c866 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/repository/PdaSplitSettlementSqlContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/repository/PdaSplitSettlementSqlContractTest.java @@ -10,10 +10,33 @@ import org.junit.jupiter.api.Test; import org.springframework.core.io.ClassPathResource; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import static org.assertj.core.api.Assertions.assertThat; class PdaSplitSettlementSqlContractTest { + @Test + void addsPreloadConfirmationAuditColumnsForIncrementalPdaAllocation() throws Exception { + String moduleSql = new ClassPathResource("db/phase3-split-preload-schema.sql") + .getContentAsString(StandardCharsets.UTF_8); + String unifiedSql = Files.readString(Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql"), + StandardCharsets.UTF_8); + + assertThat(moduleSql) + .contains("confirmed_by BIGINT UNSIGNED NULL") + .contains("confirmed_by_name VARCHAR(64) NULL") + .contains("confirmed_at DATETIME(3) NULL") + .contains("phase3_split_add_column('fl_preload_plan', 'confirmed_by'") + .contains("phase3_split_add_column('fl_preload_plan', 'confirmed_by_name'") + .contains("phase3_split_add_column('fl_preload_plan', 'confirmed_at'"); + assertThat(unifiedSql) + .contains("CREATE PROCEDURE migrate_preload_confirmation_columns()") + .contains("ADD COLUMN confirmed_by BIGINT UNSIGNED NULL") + .contains("ADD COLUMN confirmed_by_name VARCHAR(64) NULL") + .contains("ADD COLUMN confirmed_at DATETIME(3) NULL"); + } + @Test void protectsLoadedAndRemainderSettlement() throws Exception { String sql = new ClassPathResource("db/phase3-inbound-parent-child-schema.sql") diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/FeePaymentSnapshotValidatorTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/FeePaymentSnapshotValidatorTest.java index 98ccc8e15..51664c9e3 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/FeePaymentSnapshotValidatorTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/FeePaymentSnapshotValidatorTest.java @@ -37,6 +37,14 @@ class FeePaymentSnapshotValidatorTest { .isInstanceOf(BizException.class).hasMessageContaining("账期应付不得"); } + @Test + void payableWithoutSettlementModeReturnsBusinessValidationInsteadOfNullPointer() { + assertThatThrownBy(() -> validator.validate(9L, "PAYABLE", null, + null, null, null, null)) + .isInstanceOf(BizException.class) + .hasMessageContaining("应付费用必须选择ON_ACCOUNT或DIRECT_PAID"); + } + @Test void directPaidRequiresTimeMethodAndReferenceOrVoucher() { LocalDateTime paidAt = LocalDateTime.of(2026, 8, 20, 10, 0); diff --git a/scripts/admin-test-env-acceptance.mjs b/scripts/admin-test-env-acceptance.mjs index 16421131d..06eae4b46 100644 --- a/scripts/admin-test-env-acceptance.mjs +++ b/scripts/admin-test-env-acceptance.mjs @@ -740,12 +740,16 @@ export class AcceptanceRun { await this.restoreAvailableLocationsAfterA0(warehouseId, disabledLocations) } const fixture = this.createdResources.inboundMeasurement || this.createdResources.orders + const fixtureOrder = this.createdResources.orders || {} const loadingFixture = fixture ? { marker: originalMarker, sourceMarker: fixtureMarker, tenantId: fixture.tenantId, orderId: fixture.orderId || this.createdResources.orders?.orderId, inboundId: fixture.inboundId, + customerRemark: fixtureOrder.customerRemark, + internalRemark: fixtureOrder.internalRemark, + cargoRemark: fixtureOrder.cargoRemark, } : { marker: originalMarker, sourceMarker: fixtureMarker, failed: true } const incrementalMarker = `${originalMarker}-LOAD-INC` this.marker = incrementalMarker diff --git a/scripts/admin-test-env-modules/finance-downloads.mjs b/scripts/admin-test-env-modules/finance-downloads.mjs index ce39d69f1..9a1bc590c 100644 --- a/scripts/admin-test-env-modules/finance-downloads.mjs +++ b/scripts/admin-test-env-modules/finance-downloads.mjs @@ -13,7 +13,7 @@ export const REQUIRED_PERMISSIONS = Object.freeze([ ]) export const EXPORT_SPECS = Object.freeze({ - FINANCE_CUSTOMER_ACCOUNT_LIST: ['客户编码', '客户', '联系人', '联系电话', '待收(CNY)', '待开账(CNY)', '本期已收(CNY)', '待付(CNY)', '客户状态', '最近变动'], + FINANCE_CUSTOMER_ACCOUNT_LIST: ['客户编码', '客户', '联系人', '联系电话', '待收(CNY)', '待开账(CNY)', '本期已收(CNY)', '总成本(CNY)', '已付成本(CNY)', '待付(CNY)', '客户状态', '最近变动'], FINANCE_CUSTOMER_ACCOUNT_LEDGER: ['流水号', '发生时间', '业务类型', '关联单据', '应收变动(CNY)', '应付变动(CNY)', '变动后未收(CNY)', '操作人', '备注'], FINANCE_FEE_SOURCE_LIST: ['客户', '收付方向', '来源类型', '来源单据ID', '来源版本', '入仓号', '柜次号', '业务发生日', '费用项目', '计费量', '单价(CNY)', '金额(CNY)', '审核状态', '来源状态', '已生成对账单'], FINANCE_RECONCILIATION_STATEMENT_LIST: ['对账单号', '客户', '结算起始日', '结算结束日', '账期(天)', '货币', '总应收金额(CNY)', '已收金额(CNY)', '未收金额(CNY)', '是否结清', '审核状态', '备注', '创建人', '创建时间', '更新人', '更新时间'], diff --git a/scripts/admin-test-env-modules/finance-full-fixtures-contract.test.mjs b/scripts/admin-test-env-modules/finance-full-fixtures-contract.test.mjs index 2d9e6fe13..6c5932849 100644 --- a/scripts/admin-test-env-modules/finance-full-fixtures-contract.test.mjs +++ b/scripts/admin-test-env-modules/finance-full-fixtures-contract.test.mjs @@ -17,6 +17,8 @@ test('finance fixture module is independently exported and marker isolated', () test('fixture covers weight, volume, manual order, and container allocation contracts', () => { for (const basis of ['WEIGHT_KG', 'VOLUME_CBM', 'MANUAL_ORDER_FEE', 'CONTAINER_SHARED']) assert.match(source, new RegExp(basis)) + assert.match(source, /direction === 'PAYABLE' \? \{ settlementMode: 'ON_ACCOUNT' \}/) + assert.match(source, /supplierId: id\(supplier\.id\), settlementMode: 'ON_ACCOUNT'/) assert.match(source, /request\(context, 'POST', '\/api\/platform\/finance\/v1\/cost-entries'/) assert.match(source, /no-marker-owned-container-snapshot/) }) diff --git a/scripts/admin-test-env-modules/finance-full-fixtures.mjs b/scripts/admin-test-env-modules/finance-full-fixtures.mjs index f68388559..1416a309c 100644 --- a/scripts/admin-test-env-modules/finance-full-fixtures.mjs +++ b/scripts/admin-test-env-modules/finance-full-fixtures.mjs @@ -150,6 +150,7 @@ async function createManualFee(context, resource, feeItem, direction, amount, ba const listPath = `${basePath}?accountId=${encodeURIComponent(resource.tenantId)}` const body = { feeItemId: String(feeItem.id), direction, businessDate, unitPrice: amount, currency: 'CNY', amount, + ...(direction === 'PAYABLE' ? { settlementMode: 'ON_ACCOUNT' } : {}), supplierId: supplier?.id ? String(supplier.id) : null, remark: `${context.marker}-${basis}` } const response = await request(context, 'POST', listPath, body) @@ -443,7 +444,8 @@ export async function run(context) { const basePath = `/api/platform/v1/loading-plans/${encodeURIComponent(container.loadingPlanId)}/fee-details` const path = `${basePath}?accountId=${encodeURIComponent(container.tenantId)}` const body = { feeItemId: String(feeItems.PAYABLE.id), direction: 'PAYABLE', businessDate: dateOffset(-2), - amount: 300, currency: 'CNY', supplierId: id(supplier.id), remark: `${context.marker}-CONTAINER_SHARED` } + amount: 300, currency: 'CNY', supplierId: id(supplier.id), settlementMode: 'ON_ACCOUNT', + remark: `${context.marker}-CONTAINER_SHARED` } const response = await request(context, 'POST', path, body) if (response?.http === 404) { blocker(context, '按柜费用来源接口已部署并可路由', diff --git a/scripts/admin-test-env-modules/inbound-fees.mjs b/scripts/admin-test-env-modules/inbound-fees.mjs index eb8073179..a27ad73ee 100644 --- a/scripts/admin-test-env-modules/inbound-fees.mjs +++ b/scripts/admin-test-env-modules/inbound-fees.mjs @@ -111,7 +111,7 @@ export async function run(context) { && published?.publicationStatus === 'PUBLISHED' && published?.reviewStatus === 'APPROVED', `status=${sent?.feeStatus || 'missing'}, process=${sent?.nextProcessStatus || 'missing'}, publication=${published?.publicationStatus || 'missing'}`, OPTIONS) - const feeItemsResponse = await context.request('GET', '/api/platform/v1/fee-items?page=1&size=500&status=ACTIVE') + const feeItemsResponse = await context.request('GET', '/api/platform/v1/fee-items?page=1&size=200&status=ACTIVE') const feeItemsPage = context.apiSuccess(MODULE, '查询可用于其他应收的费用项目', feeItemsResponse, '', OPTIONS) const feeItems = Array.isArray(feeItemsPage?.records) ? feeItemsPage.records : Array.isArray(feeItemsPage) ? feeItemsPage : [] diff --git a/scripts/admin-test-env-modules/phase3-container-loading.mjs b/scripts/admin-test-env-modules/phase3-container-loading.mjs index bd83bd062..5a09b0a54 100644 --- a/scripts/admin-test-env-modules/phase3-container-loading.mjs +++ b/scripts/admin-test-env-modules/phase3-container-loading.mjs @@ -812,7 +812,7 @@ export async function run(context) { context.createdResources.phase3ContainerLoading = { marker: context.marker, tenantId, inboundId, warehouseId, containerBookingId: containerId, - loadingPlanId: planId, pdaTaskId: taskId, cabinetNo: stableIdentity.cabinetNo, + loadingPlanId: planId, oceanBookingId: bookingId, pdaTaskId: taskId, cabinetNo: stableIdentity.cabinetNo, containerNo: replacementContainerNo, pdaIdentityStable: true, finalStatus: 'COMPLETED', } diff --git a/scripts/admin-test-env-modules/phase3-customs.mjs b/scripts/admin-test-env-modules/phase3-customs.mjs index c051792ea..de3230435 100644 --- a/scripts/admin-test-env-modules/phase3-customs.mjs +++ b/scripts/admin-test-env-modules/phase3-customs.mjs @@ -90,22 +90,26 @@ export function selectCustomsFlow(createdResources) { async function ensureReleasedOceanBooking(context, container) { const containerBookingId = id(container?.containerBookingId) const loadingPlanId = id(container?.loadingPlanId) - let ocean = context.createdResources?.oceanBooking - if (ocean?.finalStatus !== 'RELEASED' - || id(ocean?.containerBookingId) !== containerBookingId - || id(ocean?.loadingPlanId) !== loadingPlanId) { - const oceanBookings = await import(new URL('./ocean-bookings.mjs', import.meta.url)) - await oceanBookings.run(context) - ocean = context.createdResources?.oceanBooking - } - if (ocean?.finalStatus !== 'RELEASED' - || id(ocean?.containerBookingId) !== containerBookingId - || id(ocean?.loadingPlanId) !== loadingPlanId) { + const oceanBookingId = id(container?.oceanBookingId) + if (!oceanBookingId) { context.addStep(MODULE, '报关前订舱已放舱并绑定实际装柜柜次', null, false, - `oceanStatus=${ocean?.finalStatus || 'missing'}, oceanContainer=${id(ocean?.containerBookingId) || 'missing'}, containerBookingId=${containerBookingId || 'missing'}`, OPTIONS) + `oceanBookingId=missing, containerBookingId=${containerBookingId || 'missing'}`, OPTIONS) return false } - + const response = await context.request('GET', `/api/platform/v1/ocean-bookings/${encodeURIComponent(oceanBookingId)}`) + const detail = context.apiSuccess(MODULE, '报关前回读装柜链正式订舱', response, '', OPTIONS) + const allocated = detail?.containers?.some?.((item) => id(item?.containerBookingId) === containerBookingId) + if (detail?.oceanBooking?.status !== 'RELEASED' || !allocated) { + context.addStep(MODULE, '报关前订舱已放舱并绑定实际装柜柜次', response, false, + `oceanStatus=${detail?.oceanBooking?.status || 'missing'}, oceanBookingId=${oceanBookingId}, containerBookingId=${containerBookingId || 'missing'}, allocated=${allocated === true}`, OPTIONS) + return false + } + context.createdResources.oceanBooking = { + id: oceanBookingId, marker: context.marker, finalStatus: 'RELEASED', + containerBookingId, loadingPlanId, + } + context.addStep(MODULE, '报关前订舱已放舱并绑定实际装柜柜次', response, true, + `oceanBookingId=${oceanBookingId}, containerBookingId=${containerBookingId}`, OPTIONS) return true } diff --git a/scripts/admin-test-env-modules/phase3-loading-dynamic-final-contract.test.mjs b/scripts/admin-test-env-modules/phase3-loading-dynamic-final-contract.test.mjs index d7d54bf16..e3d6a3c4f 100644 --- a/scripts/admin-test-env-modules/phase3-loading-dynamic-final-contract.test.mjs +++ b/scripts/admin-test-env-modules/phase3-loading-dynamic-final-contract.test.mjs @@ -67,11 +67,12 @@ test('HTTP chain refreshes after stale inventory and keeps one stable PDA identi '已拣项减量低于 minPackageCount 返回 A0409', '已拣项删除返回 A0409', '已拣项删除和减量失败后 plan/task/items 零写入', - '订舱拖车报关未齐备时 LOADED 硬门禁拒绝', + '订舱或五类证据等保留门禁未齐备时 LOADED 拒绝', ]) assert.match(loading, /String\(staleIncrementBatch\.body\?\.message \|\| ''\)\.includes\('库存已变化,请刷新后重试'\)/) assert.match(loading, /id\(incrementedTask\?\.loadingPlanId\) === loadingPlanId/) assert.match(loading, /id\(incrementedTask\?\.containerBookingId\) === containerBookingId/) + assert.match(loading, /const taskDetailPath = `\$\{pdaBase\}\/tasks\/\$\{canonicalTaskId\}\?warehouseId=/) assert.match(loading, /loadingPlanStatus: 'LOADING', finalStatus: 'AWAITING_REVIEW'/) assert.doesNotMatch(loading, /\/loading\/complete\?tenantId=/) }) diff --git a/scripts/admin-test-env-modules/phase3-loading.mjs b/scripts/admin-test-env-modules/phase3-loading.mjs index 9f8be083d..2da661cf5 100644 --- a/scripts/admin-test-env-modules/phase3-loading.mjs +++ b/scripts/admin-test-env-modules/phase3-loading.mjs @@ -187,7 +187,7 @@ export async function run(context) { `loadingPlanId=${loadingPlanId}, version=${assignedLoadingPlan?.version ?? 'missing'}`, OPTIONS) const assignedItem = assignedLoadingPlan?.items?.find?.((item) => id(item.inboundId) === inboundId) - const orderRemarks = context.createdResources?.orders || {} + const orderRemarks = source || {} context.assert(MODULE, '装柜明细客户备注与最新内部备注来源精确且不冒用货物备注', assignmentResponse, assignedItem?.customerRemark === orderRemarks.customerRemark && assignedItem?.internalRemark === orderRemarks.internalRemark @@ -462,6 +462,7 @@ export async function run(context) { `${pdaBase}/tasks?loadingPlanId=${encodeURIComponent(loadingPlanId)}&warehouseId=${encodeURIComponent(warehouseId)}`) const canonicalTask = context.apiSuccess(MODULE, '读取柜次级 PDA 唯一事实任务', canonicalTaskResponse, '', OPTIONS) const canonicalTaskId = id(canonicalTask?.id) + const taskDetailPath = `${pdaBase}/tasks/${canonicalTaskId}?warehouseId=${encodeURIComponent(warehouseId)}` const canonicalItem = canonicalTask?.items?.find?.((item) => id(item.inboundId) === inboundId) context.assert(MODULE, '柜次 PDA 任务与桥接入库明细一致', canonicalTaskResponse, Boolean(canonicalTaskId) && canonicalTaskId === confirmedTaskId && id(canonicalTask?.loadingPlanId) === loadingPlanId @@ -985,7 +986,7 @@ export async function run(context) { if (dynamicCandidate?.inboundDetailId) { const planBeforeIncrementResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`) const planBeforeIncrement = context.apiSuccess(MODULE, '读取 LOADING 动态增量前计划', planBeforeIncrementResponse, '', OPTIONS) - const taskBeforeIncrementResponse = await context.request('GET', `${pdaBase}/tasks/${canonicalTaskId}`) + const taskBeforeIncrementResponse = await context.request('GET', taskDetailPath) const taskBeforeIncrement = context.apiSuccess(MODULE, '读取 LOADING 动态增量前任务', taskBeforeIncrementResponse, '', OPTIONS) const evidenceBeforeIncrement = JSON.stringify(evidenceListData?.evidences || []) const incrementResponse = await request(context, 'POST', @@ -997,7 +998,7 @@ export async function run(context) { const incrementedPlan = context.apiSuccess(MODULE, 'LOADING 阶段向同一 PDA task 增加入库单', incrementResponse, '', OPTIONS) const incrementalPlanItem = incrementedPlan?.items?.find?.((item) => id(item?.inboundId) === incrementalInboundId) const preservedRemarkItem = incrementedPlan?.items?.find?.((item) => id(item?.id) === id(assignedItem.id)) - const incrementedTaskResponse = await context.request('GET', `${pdaBase}/tasks/${canonicalTaskId}`) + const incrementedTaskResponse = await context.request('GET', taskDetailPath) const incrementedTask = context.apiSuccess(MODULE, '增量后回查稳定 PDA 四元身份', incrementedTaskResponse, '', OPTIONS) const incrementalTaskItem = incrementedTask?.items?.find?.((item) => id(item?.loadingPlanItemId) === id(incrementalPlanItem?.id)) @@ -1032,7 +1033,7 @@ export async function run(context) { staleIncrementBatch.http === 200 && codeOf(staleIncrementBatch) === 'A0409' && String(staleIncrementBatch.body?.message || '').includes('库存已变化,请刷新后重试'), responseEvidence(staleIncrementBatch), OPTIONS) - const taskAfterStaleResponse = await context.request('GET', `${pdaBase}/tasks/${canonicalTaskId}`) + const taskAfterStaleResponse = await context.request('GET', taskDetailPath) const taskAfterStale = context.apiSuccess(MODULE, '旧 token 冲突后回查增量任务', taskAfterStaleResponse, '', OPTIONS) const planAfterStaleResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`) const planAfterStale = context.apiSuccess(MODULE, '旧 token 冲突后回查增量计划', planAfterStaleResponse, '', OPTIONS) @@ -1085,7 +1086,7 @@ export async function run(context) { effectiveDynamicPlan = adjustedPlan reassignedPlanItem = adjustedItem } - const refreshedDynamicTaskResponse = await context.request('GET', `${pdaBase}/tasks/${canonicalTaskId}`) + const refreshedDynamicTaskResponse = await context.request('GET', taskDetailPath) const refreshedDynamicTask = context.apiSuccess(MODULE, '重新增量后刷新 PDA task detail', refreshedDynamicTaskResponse, '', OPTIONS) const refreshedDynamicTaskItem = refreshedDynamicTask?.items?.find?.((item) => id(item?.loadingPlanItemId) === id(reassignedPlanItem?.id)) @@ -1108,7 +1109,7 @@ export async function run(context) { const pickedDynamicPlanResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`) const pickedDynamicPlan = context.apiSuccess(MODULE, '动态新增项拣完后回查 minPackageCount', pickedDynamicPlanResponse, '', OPTIONS) const pickedDynamicPlanItem = pickedDynamicPlan?.items?.find?.((item) => id(item?.id) === id(reassignedPlanItem?.id)) - const beforePickedMutationTaskResponse = await context.request('GET', `${pdaBase}/tasks/${canonicalTaskId}`) + const beforePickedMutationTaskResponse = await context.request('GET', taskDetailPath) const beforePickedMutationTask = context.apiSuccess(MODULE, '已拣项负向前读取任务快照', beforePickedMutationTaskResponse, '', OPTIONS) const reducePickedResponse = await request(context, 'PATCH', `${smartLoadingBase}/plans/${loadingPlanId}/items/${encodeURIComponent(id(pickedDynamicPlanItem?.id))}`, { @@ -1127,7 +1128,7 @@ export async function run(context) { deletePickedResponse.http === 200 && codeOf(deletePickedResponse) === 'A0409', responseEvidence(deletePickedResponse), OPTIONS) const afterPickedMutationPlanResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`) const afterPickedMutationPlan = context.apiSuccess(MODULE, '已拣项负向后回查计划', afterPickedMutationPlanResponse, '', OPTIONS) - const afterPickedMutationTaskResponse = await context.request('GET', `${pdaBase}/tasks/${canonicalTaskId}`) + const afterPickedMutationTaskResponse = await context.request('GET', taskDetailPath) const afterPickedMutationTask = context.apiSuccess(MODULE, '已拣项负向后回查任务', afterPickedMutationTaskResponse, '', OPTIONS) context.assert(MODULE, '已拣项删除和减量失败后 plan/task/items 零写入', afterPickedMutationPlanResponse, Number(afterPickedMutationPlan?.version) === Number(pickedDynamicPlan?.version) @@ -1148,7 +1149,7 @@ export async function run(context) { beforeHardGate?.summary?.processStatus === 'LOADING' && Number.isInteger(beforeHardGateVersion), `status=${beforeHardGate?.summary?.processStatus || 'missing'}, version=${beforeHardGate?.summary?.version ?? 'missing'}`, OPTIONS) if (!Number.isInteger(beforeHardGateVersion)) return - const beforeBlockedTaskResponse = await context.request('GET', `${pdaBase}/tasks/${canonicalTaskId}`) + const beforeBlockedTaskResponse = await context.request('GET', taskDetailPath) const beforeBlockedTask = context.apiSuccess(MODULE, '读取下游门禁拒绝前 PDA 任务快照', beforeBlockedTaskResponse, '', OPTIONS) const beforeBlockedPlanResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`) const beforeBlockedPlan = context.apiSuccess(MODULE, '读取下游门禁拒绝前计划快照', beforeBlockedPlanResponse, '', OPTIONS) @@ -1161,7 +1162,7 @@ export async function run(context) { responseEvidence(blockedBeforeDownstream), OPTIONS) const blockedDetailResponse = await context.request('GET', detailPath) const blockedDetail = context.apiSuccess(MODULE, '回查 LOADED 硬门禁拒绝后的入库状态', blockedDetailResponse, '', OPTIONS) - const blockedTaskResponse = await context.request('GET', `${pdaBase}/tasks/${canonicalTaskId}`) + const blockedTaskResponse = await context.request('GET', taskDetailPath) const blockedTask = context.apiSuccess(MODULE, '回查 LOADED 硬门禁拒绝后的 PDA 任务', blockedTaskResponse, '', OPTIONS) const blockedPlanResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`) const blockedPlan = context.apiSuccess(MODULE, '回查 LOADED 硬门禁拒绝后的计划', blockedPlanResponse, '', OPTIONS) diff --git a/scripts/admin-test-env-modules/phase3-producer-chain-contract.test.mjs b/scripts/admin-test-env-modules/phase3-producer-chain-contract.test.mjs index fdfe9583a..15aa9de48 100644 --- a/scripts/admin-test-env-modules/phase3-producer-chain-contract.test.mjs +++ b/scripts/admin-test-env-modules/phase3-producer-chain-contract.test.mjs @@ -467,14 +467,17 @@ test('phase3 loading fixture forces A0 through reversible public location status assert.match(orders, /receivingWarehouseId: id\(master\.receiving\.id\)/) }) -test('customs provisions released booking without pre-completing truck', () => { +test('customs reuses the loading chain released booking without pre-completing truck', () => { const value = source('./phase3-customs.mjs') assertOrder(value, [ 'selectCustomsFlow(context.createdResources)', 'if (!await ensureReleasedOceanBooking(context, container)) return', 'const slicesPath =', ]) - assert.match(value, /oceanBookings\.run\(context\)/) + assert.match(value, /const oceanBookingId = id\(container\?\.oceanBookingId\)/) + assert.match(value, /ocean-bookings\/\$\{encodeURIComponent\(oceanBookingId\)\}/) + assert.match(value, /detail\?\.containers\?\.some/) + assert.doesNotMatch(value, /oceanBookings\.run\(context\)/) assert.doesNotMatch(value, /serviceCenter\.run\(context\)/) assert.doesNotMatch(value, /truck\?\.finalStatus !== 'COMPLETED'/) assert.match(value, /request\('POST', '\/api\/platform\/v1\/suppliers\?tenantId=0'/)