Merge remote-tracking branch 'origin/test' into dev
This commit is contained in:
+6
-1
@@ -107,7 +107,12 @@ class ContainerWorkspacePermissionSqlContractTest {
|
|||||||
assertThat(start).isGreaterThanOrEqualTo(0);
|
assertThat(start).isGreaterThanOrEqualTo(0);
|
||||||
assertThat(bodyStart).isGreaterThan(start);
|
assertThat(bodyStart).isGreaterThan(start);
|
||||||
assertThat(end).isGreaterThan(bodyStart);
|
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 {
|
private static String sql() throws Exception {
|
||||||
|
|||||||
@@ -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', () => {
|
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))
|
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, /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, /customer-rules\/versions\?tenantId=/)
|
||||||
assert.match(source, /taxMode: 'TAX_EXCLUDED'/)
|
assert.match(source, /taxMode: 'TAX_EXCLUDED'/)
|
||||||
assert.match(source, /inbound\?\.summary\?\.businessType \|\| profile\.businessType/)
|
assert.match(source, /inbound\?\.summary\?\.businessType \|\| profile\.businessType/)
|
||||||
|
|||||||
@@ -298,6 +298,23 @@ async function uploadVoucher(context, customerId) {
|
|||||||
|
|
||||||
async function loadPricingEvidence(context) {
|
async function loadPricingEvidence(context) {
|
||||||
if (context.financeFixtureHooks?.loadPricingEvidence) return context.financeFixtureHooks.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 response = await context.request('GET', '/api/platform/v1/fba-pricing/carrier-rates/price-books?status=ACTIVE')
|
||||||
const versions = context.apiSuccess(MODULE, '读取真实生效价格配置', response, '', OPTIONS)
|
const versions = context.apiSuccess(MODULE, '读取真实生效价格配置', response, '', OPTIONS)
|
||||||
const active = rows(versions).filter((item) => item?.status === 'ACTIVE')
|
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 : []
|
const rates = Array.isArray(detail?.rates) ? detail.rates : []
|
||||||
for (const rate of rates) {
|
for (const rate of rates) {
|
||||||
if (rate?.destinationType !== 'FBA' || !rate?.originWarehouseId || !rate?.channelCode || !rate?.pricingTaxMode || !rate?.destinationCode) continue
|
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
|
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)
|
const matches = rates.filter((item) => id(item.originWarehouseId) === id(rate.originWarehouseId)
|
||||||
&& item.channelCode === rate.channelCode && item.destinationType === rate.destinationType
|
&& item.channelCode === rate.channelCode && item.destinationType === rate.destinationType
|
||||||
|
|||||||
@@ -38,4 +38,16 @@ test('initial fee preparation is reusable without confirming or publishing the f
|
|||||||
test('automatic fee adjustment confirms the ten percent tolerance boundary', async () => {
|
test('automatic fee adjustment confirms the ten percent tolerance boundary', async () => {
|
||||||
const source = await readFile(moduleFile, 'utf8')
|
const source = await readFile(moduleFile, 'utf8')
|
||||||
assert.match(source, /reason: `\$\{context\.marker\}-自动费用版本调整`, toleranceConfirmed: true/)
|
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/)
|
||||||
|
})
|
||||||
|
|
||||||
|
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, /客户确认后平台不得再次修改计费量或对客单价/)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export async function run(context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const adjustedQuantity = Math.max(0.0001, money(initial.chargeableQuantity) + 0.01)
|
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 adjustedAdditional = money(initial.additionalAmount) + 0.01
|
||||||
const adjustResponse = await context.request('PUT',
|
const adjustResponse = await context.request('PUT',
|
||||||
`/api/platform/v1/inbounds/${encodeURIComponent(inboundId)}/fees/published?tenantId=${encodeURIComponent(tenantId)}`,
|
`/api/platform/v1/inbounds/${encodeURIComponent(inboundId)}/fees/published?tenantId=${encodeURIComponent(tenantId)}`,
|
||||||
@@ -93,6 +93,9 @@ export async function run(context) {
|
|||||||
Number(adjusted?.feeVersion) > Number(initial.feeVersion) && adjusted?.feeStatus === 'CALCULATED'
|
Number(adjusted?.feeVersion) > Number(initial.feeVersion) && adjusted?.feeStatus === 'CALCULATED'
|
||||||
&& adjusted?.nextProcessStatus === 'WAIT_SPLIT' && money(adjusted?.totalAmount) >= adjustedAdditional,
|
&& adjusted?.nextProcessStatus === 'WAIT_SPLIT' && money(adjusted?.totalAmount) >= adjustedAdditional,
|
||||||
`oldVersion=${initial.feeVersion}, newVersion=${adjusted?.feeVersion ?? 'missing'}, status=${adjusted?.feeStatus || 'missing'}, process=${adjusted?.nextProcessStatus || 'missing'}`, OPTIONS)
|
`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 }
|
if (!adjusted?.feeId) { finish(context, start); return }
|
||||||
|
|
||||||
const adjustedDetails = await listDetails(context, tenantId, inboundId, '回查调整后的派生详情版本')
|
const adjustedDetails = await listDetails(context, tenantId, inboundId, '回查调整后的派生详情版本')
|
||||||
@@ -122,9 +125,70 @@ export async function run(context) {
|
|||||||
&& id(row.sourceFeeId) === id(sent?.feeId))
|
&& id(row.sourceFeeId) === id(sent?.feeId))
|
||||||
context.assert(MODULE, '仅人工发布后费用为 SENT 且派生明细为 PUBLISHED', sendResponse,
|
context.assert(MODULE, '仅人工发布后费用为 SENT 且派生明细为 PUBLISHED', sendResponse,
|
||||||
sent?.feeStatus === 'SENT' && sent?.nextProcessStatus === 'WAIT_SPLIT'
|
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)
|
`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 feeItemsResponse = await context.request('GET', '/api/platform/v1/fee-items?page=1&size=200&status=ACTIVE')
|
||||||
const feeItemsPage = context.apiSuccess(MODULE, '查询可用于其他应收的费用项目', feeItemsResponse, '', OPTIONS)
|
const feeItemsPage = context.apiSuccess(MODULE, '查询可用于其他应收的费用项目', feeItemsResponse, '', OPTIONS)
|
||||||
const feeItems = Array.isArray(feeItemsPage?.records) ? feeItemsPage.records
|
const feeItems = Array.isArray(feeItemsPage?.records) ? feeItemsPage.records
|
||||||
@@ -198,6 +262,6 @@ export async function run(context) {
|
|||||||
|
|
||||||
context.createdResources.inboundFees = { marker: context.marker, tenantId, inboundId,
|
context.createdResources.inboundFees = { marker: context.marker, tenantId, inboundId,
|
||||||
freightFeeId: id(sent.feeId), freightVersion: sent.feeVersion, manualDetailId: id(manualV2.id),
|
freightFeeId: id(sent.feeId), freightVersion: sent.feeVersion, manualDetailId: id(manualV2.id),
|
||||||
manualVersion: manualV2.sourceFeeVersion }
|
manualVersion: manualV2.sourceFeeVersion, customerConfirmationStatus: customerConfirmed.customerConfirmationStatus }
|
||||||
finish(context, start)
|
finish(context, start)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user