From 0d016bc5fcd6b7ee09f1969335949c9d7c636714 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 9 Sep 2026 18:33:39 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=E9=80=82=E9=85=8D=E8=B4=A2=E5=8A=A1?= =?UTF-8?q?=E9=AA=8C=E6=94=B6FBA=E5=86=85=E9=83=A8=E7=BC=96=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../finance-full-fixtures-contract.test.mjs | 3 +++ .../finance-full-fixtures.mjs | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+) 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 ab28ff460..26230cb77 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 @@ -18,6 +18,9 @@ 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, /chargeMode: pricingProfile\.chargeUnit/) + assert.match(source, /warehouse-admin\?tenantId=0&page=/) + assert.match(source, /item\?\.internalCode && item\?\.warehouseCode/) + assert.match(source, /fbaInternalCodes\.has\(String\(rate\.destinationCode\)/) assert.match(source, /customer-rules\/versions\?tenantId=/) assert.match(source, /taxMode: 'TAX_EXCLUDED'/) assert.match(source, /inbound\?\.summary\?\.businessType \|\| profile\.businessType/) diff --git a/scripts/admin-test-env-modules/finance-full-fixtures.mjs b/scripts/admin-test-env-modules/finance-full-fixtures.mjs index cea821aa6..da7911eeb 100644 --- a/scripts/admin-test-env-modules/finance-full-fixtures.mjs +++ b/scripts/admin-test-env-modules/finance-full-fixtures.mjs @@ -298,6 +298,23 @@ async function uploadVoucher(context, customerId) { async function loadPricingEvidence(context) { if (context.financeFixtureHooks?.loadPricingEvidence) return context.financeFixtureHooks.loadPricingEvidence(context) + const fbaInternalCodes = new Set() + let warehousePage = 1 + let warehouseTotal = 0 + do { + const warehouseResponse = await context.request('GET', + `/api/platform/v1/warehouse-admin?tenantId=0&page=${warehousePage}&size=200&warehouseType=FBA&status=ACTIVE`) + const warehouseResult = context.apiSuccess(MODULE, `读取具备内部编号的 FBA 仓第 ${warehousePage} 页`, + warehouseResponse, '', OPTIONS) + const warehouses = rows(warehouseResult) + warehouseTotal = Number(warehouseResult?.total || warehouses.length) + warehouses.filter((item) => item?.internalCode && item?.warehouseCode) + .forEach((item) => fbaInternalCodes.add(String(item.warehouseCode).trim().toUpperCase())) + if (warehouses.length === 0) break + warehousePage += 1 + } while ((warehousePage - 1) * 200 < warehouseTotal) + context.assert(MODULE, '自动计费候选使用已配置内部编号的 FBA 仓', null, + fbaInternalCodes.size > 0, `warehouses=${fbaInternalCodes.size}`, OPTIONS) const response = await context.request('GET', '/api/platform/v1/fba-pricing/carrier-rates/price-books?status=ACTIVE') const versions = context.apiSuccess(MODULE, '读取真实生效价格配置', response, '', OPTIONS) const active = rows(versions).filter((item) => item?.status === 'ACTIVE') @@ -308,6 +325,7 @@ async function loadPricingEvidence(context) { const rates = Array.isArray(detail?.rates) ? detail.rates : [] for (const rate of rates) { if (rate?.destinationType !== 'FBA' || !rate?.originWarehouseId || !rate?.channelCode || !rate?.pricingTaxMode || !rate?.destinationCode) continue + if (!fbaInternalCodes.has(String(rate.destinationCode).trim().toUpperCase())) continue if (!['KG', 'CBM'].includes(rate.chargeUnit) || profiles.some((item) => item.chargeUnit === rate.chargeUnit)) continue const matches = rates.filter((item) => id(item.originWarehouseId) === id(rate.originWarehouseId) && item.channelCode === rate.channelCode && item.destinationType === rate.destinationType From 1c4f35f61c20a2082ae6c277accaf5f844531ef7 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 9 Sep 2026 18:34:07 +0800 Subject: [PATCH 2/4] test: verify manual inbound unit price flow --- .../inbound-fees-contract.test.mjs | 3 +++ scripts/admin-test-env-modules/inbound-fees.mjs | 10 ++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/scripts/admin-test-env-modules/inbound-fees-contract.test.mjs b/scripts/admin-test-env-modules/inbound-fees-contract.test.mjs index 0aca65035..e20b644b0 100644 --- a/scripts/admin-test-env-modules/inbound-fees-contract.test.mjs +++ b/scripts/admin-test-env-modules/inbound-fees-contract.test.mjs @@ -27,4 +27,7 @@ test('inbound fee lifecycle covers frozen source, version, publication and WAIT_ test('automatic fee adjustment confirms the ten percent tolerance boundary', async () => { const source = await readFile(moduleFile, 'utf8') assert.match(source, /reason: `\$\{context\.marker\}-自动费用版本调整`, toleranceConfirmed: true/) + assert.match(source, /adjustedUnitPrice = Number\(\(Math\.max\(0, money\(initial\.unitPrice\)\) \+ 0\.37\)\.toFixed\(4\)\)/) + assert.match(source, /人工对客单价写入最新费用版本/) + assert.match(source, /money\(published\?\.unitPrice\) === adjustedUnitPrice/) }) diff --git a/scripts/admin-test-env-modules/inbound-fees.mjs b/scripts/admin-test-env-modules/inbound-fees.mjs index 2d35c0b4c..433f192ff 100644 --- a/scripts/admin-test-env-modules/inbound-fees.mjs +++ b/scripts/admin-test-env-modules/inbound-fees.mjs @@ -66,7 +66,7 @@ export async function run(context) { } const adjustedQuantity = Math.max(0.0001, money(initial.chargeableQuantity) + 0.01) - const adjustedUnitPrice = Math.max(0, money(initial.unitPrice)) + const adjustedUnitPrice = Number((Math.max(0, money(initial.unitPrice)) + 0.37).toFixed(4)) const adjustedAdditional = money(initial.additionalAmount) + 0.01 const adjustResponse = await context.request('PUT', `/api/platform/v1/inbounds/${encodeURIComponent(inboundId)}/fees/published?tenantId=${encodeURIComponent(tenantId)}`, @@ -79,6 +79,9 @@ export async function run(context) { Number(adjusted?.feeVersion) > Number(initial.feeVersion) && adjusted?.feeStatus === 'CALCULATED' && adjusted?.nextProcessStatus === 'WAIT_SPLIT' && money(adjusted?.totalAmount) >= adjustedAdditional, `oldVersion=${initial.feeVersion}, newVersion=${adjusted?.feeVersion ?? 'missing'}, status=${adjusted?.feeStatus || 'missing'}, process=${adjusted?.nextProcessStatus || 'missing'}`, OPTIONS) + context.assert(MODULE, '人工对客单价写入最新费用版本', adjustResponse, + money(adjusted?.unitPrice) === adjustedUnitPrice, + `referenceUnitPrice=${initial.unitPrice ?? 'missing'}, selectedUnitPrice=${adjustedUnitPrice}, persistedUnitPrice=${adjusted?.unitPrice ?? 'missing'}`, OPTIONS) if (!adjusted?.feeId) { finish(context, start); return } const adjustedDetails = await listDetails(context, tenantId, inboundId, '回查调整后的派生详情版本') @@ -108,7 +111,10 @@ export async function run(context) { && id(row.sourceFeeId) === id(sent?.feeId)) context.assert(MODULE, '仅人工发布后费用为 SENT 且派生明细为 PUBLISHED', sendResponse, sent?.feeStatus === 'SENT' && sent?.nextProcessStatus === 'WAIT_SPLIT' - && published?.publicationStatus === 'PUBLISHED' && published?.reviewStatus === 'APPROVED', + && money(sent?.unitPrice) === adjustedUnitPrice + && published?.publicationStatus === 'PUBLISHED' && published?.reviewStatus === 'APPROVED' + && money(published?.unitPrice) === adjustedUnitPrice + && money(published?.amount) === money(sent?.totalAmount), `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=200&status=ACTIVE') From d9603aa7dad089ef04788bc669e2d5b12c19328b Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 9 Sep 2026 18:38:22 +0800 Subject: [PATCH 3/4] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E5=A2=9E=E9=87=8F=E4=B8=AD=E7=9A=84IAM=E6=9D=83=E9=99=90?= =?UTF-8?q?=E5=8C=BA=E5=9D=97=E8=BE=B9=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/sql/头程数据库统一增量-MySQL8.sql | 5 +++-- .../ContainerWorkspacePermissionSqlContractTest.java | 7 ++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/deploy/sql/头程数据库统一增量-MySQL8.sql b/deploy/sql/头程数据库统一增量-MySQL8.sql index 48e19d649..beb436e62 100644 --- a/deploy/sql/头程数据库统一增量-MySQL8.sql +++ b/deploy/sql/头程数据库统一增量-MySQL8.sql @@ -18493,6 +18493,9 @@ DELIMITER ; CALL migrate_container_workspace_permissions(); DROP PROCEDURE IF EXISTS migrate_container_workspace_permissions; +-- 货柜双工作台权限结束,恢复头程数据库上下文 +USE baiyue_firstleg; + -- 财务 P0:海外费用原币、计费依据与钱包结算关联 DROP PROCEDURE IF EXISTS add_finance_p0_column; DELIMITER $$ @@ -18524,8 +18527,6 @@ DROP PROCEDURE IF EXISTS add_finance_p0_column; SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema='baiyue_finance' AND table_name='fin_fee_source' AND index_name='idx_fin_fee_source_wallet_hold'), 'SELECT 1', 'ALTER TABLE baiyue_finance.fin_fee_source ADD KEY idx_fin_fee_source_wallet_hold(wallet_hold_id)')); PREPARE statement FROM @ddl; EXECUTE statement; DEALLOCATE PREPARE statement; --- 货柜双工作台权限结束,恢复头程数据库上下文 -USE baiyue_firstleg; CALL baiyue_firstleg.assert_unified_final_schema(); DROP PROCEDURE IF EXISTS baiyue_firstleg.assert_unified_final_schema; diff --git a/iam/src/test/java/com/baiyue/iam/repository/ContainerWorkspacePermissionSqlContractTest.java b/iam/src/test/java/com/baiyue/iam/repository/ContainerWorkspacePermissionSqlContractTest.java index 40f5c5878..64b215294 100644 --- a/iam/src/test/java/com/baiyue/iam/repository/ContainerWorkspacePermissionSqlContractTest.java +++ b/iam/src/test/java/com/baiyue/iam/repository/ContainerWorkspacePermissionSqlContractTest.java @@ -107,7 +107,12 @@ class ContainerWorkspacePermissionSqlContractTest { assertThat(start).isGreaterThanOrEqualTo(0); assertThat(bodyStart).isGreaterThan(start); assertThat(end).isGreaterThan(bodyStart); - assertThat(unified.substring(bodyStart, end).trim()).isEqualTo(sql().trim()); + assertThat(normalizeLineEndings(unified.substring(bodyStart, end)).trim()) + .isEqualTo(normalizeLineEndings(sql()).trim()); + } + + private static String normalizeLineEndings(String value) { + return value.replace("\r\n", "\n"); } private static String sql() throws Exception { From 7678fd85ff73aa43a0144a16b0b0ddd22a3e907d Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 9 Sep 2026 18:43:08 +0800 Subject: [PATCH 4/4] test: cover customer inbound fee confirmation --- .../inbound-fees-contract.test.mjs | 9 +++ .../admin-test-env-modules/inbound-fees.mjs | 60 ++++++++++++++++++- 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/scripts/admin-test-env-modules/inbound-fees-contract.test.mjs b/scripts/admin-test-env-modules/inbound-fees-contract.test.mjs index e20b644b0..d0ac0094c 100644 --- a/scripts/admin-test-env-modules/inbound-fees-contract.test.mjs +++ b/scripts/admin-test-env-modules/inbound-fees-contract.test.mjs @@ -31,3 +31,12 @@ test('automatic fee adjustment confirms the ten percent tolerance boundary', asy assert.match(source, /人工对客单价写入最新费用版本/) assert.match(source, /money\(published\?\.unitPrice\) === adjustedUnitPrice/) }) + +test('customer confirms chargeable quantity and manual unit price in one locked version', async () => { + const source = await readFile(moduleFile, 'utf8') + assert.match(source, /Client 读取已推送计费量、对客单价和费用合计/) + assert.match(source, /Client 一次确认计费量、对客单价和费用/) + assert.match(source, /customerConfirmationStatus === 'CONFIRMED'/) + assert.match(source, /客户费用确认重放返回同一锁定版本/) + assert.match(source, /客户确认后平台不得再次修改计费量或对客单价/) +}) diff --git a/scripts/admin-test-env-modules/inbound-fees.mjs b/scripts/admin-test-env-modules/inbound-fees.mjs index 433f192ff..0db92fe68 100644 --- a/scripts/admin-test-env-modules/inbound-fees.mjs +++ b/scripts/admin-test-env-modules/inbound-fees.mjs @@ -117,6 +117,64 @@ export async function run(context) { && money(published?.amount) === money(sent?.totalAmount), `status=${sent?.feeStatus || 'missing'}, process=${sent?.nextProcessStatus || 'missing'}, publication=${published?.publicationStatus || 'missing'}`, OPTIONS) + const orderId = id(context.createdResources.orders?.orderId) + const merchantLoginResponse = await context.request('POST', '/api/auth/login', { + loginId: context.options.merchantUsername, password: context.options.merchantPassword, + }, '') + const merchantLogin = context.apiSuccess(MODULE, 'Client 登录并准备确认计费量和费用', merchantLoginResponse, + `orderId=${orderId || 'missing'}`, OPTIONS) + const merchantToken = merchantLogin?.accessToken + context.assert(MODULE, '本轮客户订单与登录令牌可用于费用确认', merchantLoginResponse, + Boolean(orderId && merchantToken), `orderId=${orderId || 'missing'}, token=${merchantToken ? 'present' : 'missing'}`, OPTIONS) + if (!orderId || !merchantToken) { finish(context, start); return } + + const merchantBillingPath = `/api/merchant/v1/first-leg/orders/${encodeURIComponent(orderId)}/inbound-billing` + const merchantBillingResponse = await context.request('GET', merchantBillingPath, undefined, merchantToken) + const merchantBilling = context.apiSuccess(MODULE, 'Client 读取已推送计费量、对客单价和费用合计', + merchantBillingResponse, '', OPTIONS) + context.assert(MODULE, '客户看到的平台推送版本、对客单价和合计完全一致', merchantBillingResponse, + Number(merchantBilling?.latestFee?.feeVersion) === Number(sent.feeVersion) + && merchantBilling?.latestFee?.feeStatus === 'SENT' + && money(merchantBilling?.latestFee?.unitPrice) === adjustedUnitPrice + && money(merchantBilling?.latestFee?.chargeableQuantity) === money(sent.chargeableQuantity) + && money(merchantBilling?.latestFee?.totalAmount) === money(sent.totalAmount), + `version=${merchantBilling?.latestFee?.feeVersion ?? 'missing'}, unitPrice=${merchantBilling?.latestFee?.unitPrice ?? 'missing'}, total=${merchantBilling?.latestFee?.totalAmount ?? 'missing'}`, OPTIONS) + + const customerConfirmKey = `${context.marker}-customer-fee-confirm`.slice(0, 128) + const customerConfirmResponse = await context.request('POST', `${merchantBillingPath}/confirm`, + { expectedFeeVersion: Number(sent.feeVersion) }, merchantToken, requestHeaders(customerConfirmKey)) + const customerConfirmed = context.apiSuccess(MODULE, 'Client 一次确认计费量、对客单价和费用', + customerConfirmResponse, '', OPTIONS) + context.assert(MODULE, '客户确认锁定原推送版本及其单价和合计', customerConfirmResponse, + customerConfirmed?.customerConfirmationStatus === 'CONFIRMED' + && Number(customerConfirmed?.feeVersion) === Number(sent.feeVersion) + && money(customerConfirmed?.unitPrice) === adjustedUnitPrice + && money(customerConfirmed?.chargeableQuantity) === money(sent.chargeableQuantity) + && money(customerConfirmed?.totalAmount) === money(sent.totalAmount) + && Boolean(customerConfirmed?.customerConfirmedAt), + `confirmation=${customerConfirmed?.customerConfirmationStatus || 'missing'}, version=${customerConfirmed?.feeVersion ?? 'missing'}, unitPrice=${customerConfirmed?.unitPrice ?? 'missing'}`, OPTIONS) + + const customerConfirmReplayResponse = await context.request('POST', `${merchantBillingPath}/confirm`, + { expectedFeeVersion: Number(sent.feeVersion) }, merchantToken, requestHeaders(customerConfirmKey)) + const customerConfirmReplay = context.apiSuccess(MODULE, 'Client 费用确认幂等重放', + customerConfirmReplayResponse, '', OPTIONS) + context.assert(MODULE, '客户费用确认重放返回同一锁定版本', customerConfirmReplayResponse, + customerConfirmReplay?.customerConfirmationStatus === 'CONFIRMED' + && id(customerConfirmReplay?.feeId) === id(customerConfirmed?.feeId) + && Number(customerConfirmReplay?.feeVersion) === Number(customerConfirmed?.feeVersion), + `feeId=${id(customerConfirmReplay?.feeId) || 'missing'}, version=${customerConfirmReplay?.feeVersion ?? 'missing'}`, OPTIONS) + + const lockedAdjustResponse = await context.request('PUT', + `/api/platform/v1/inbounds/${encodeURIComponent(inboundId)}/fees/published?tenantId=${encodeURIComponent(tenantId)}`, + { expectedFeeVersion: Number(sent.feeVersion), chargeableQuantity: money(sent.chargeableQuantity), + unitPrice: Number((adjustedUnitPrice + 0.01).toFixed(4)), additionalAmount: money(sent.additionalAmount), + reason: `${context.marker}-客户确认后禁止改单价`, toleranceConfirmed: true }, undefined, + requestHeaders(`${context.marker}-locked-fee-adjust`)) + context.assert(MODULE, '客户确认后平台不得再次修改计费量或对客单价', lockedAdjustResponse, + lockedAdjustResponse.http === 200 && !success(lockedAdjustResponse) + && String(lockedAdjustResponse.body?.code) === 'A0409', + `code=${lockedAdjustResponse.body?.code ?? 'missing'}, message=${lockedAdjustResponse.body?.message || 'missing'}`, OPTIONS) + 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 @@ -190,6 +248,6 @@ export async function run(context) { context.createdResources.inboundFees = { marker: context.marker, tenantId, inboundId, freightFeeId: id(sent.feeId), freightVersion: sent.feeVersion, manualDetailId: id(manualV2.id), - manualVersion: manualV2.sourceFeeVersion } + manualVersion: manualV2.sourceFeeVersion, customerConfirmationStatus: customerConfirmed.customerConfirmationStatus } finish(context, start) }