Merge remote-tracking branch 'origin/test' into dev
# Conflicts: # lastleg-service/src/main/java/com/baiyue/lastleg/service/LoadingCompletedConsumer.java # lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpWarehouseResourceGatewayTest.java # warehouse-overseas-service/src/main/java/com/baiyue/warehouse/service/WarehousePalletBatchService.java
This commit is contained in:
@@ -17,6 +17,10 @@ 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, /customer-rules\/versions\?tenantId=/)
|
||||
assert.match(source, /taxMode: 'TAX_EXCLUDED'/)
|
||||
assert.match(source, /inbound\?\.summary\?\.businessType \|\| profile\.businessType/)
|
||||
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'/)
|
||||
@@ -123,6 +127,7 @@ function hookSet({ missing = false } = {}) {
|
||||
let automaticIndex = 0
|
||||
return {
|
||||
loadPricingEvidence: async () => ({ activeVersionIds: ['PRICE-1'] }),
|
||||
ensureCustomerChargeRule: async () => ({ id: 'RULE-CBM', chargeMode: 'CBM' }),
|
||||
createOrder: async (context, suffix) => ({ marker: `${context.marker}-${suffix}`, tenantId: '701',
|
||||
orderId: `O-${suffix}`, orderNo: `NO-${suffix}`, waybillId: suffix === 'AUTO1' ? 'WB-1' : null, inboundId: `I-${suffix}` }),
|
||||
measureOrder: async (context, resource) => ({ ...resource, measurementId: `M-${resource.orderId}`, actualWeightKg: 12.5, totalVolumeCbm: 0.072 }),
|
||||
@@ -153,7 +158,7 @@ test('run behavior emits consumer shape, both billing bases, distinct dates, sup
|
||||
await run(context)
|
||||
const fixture = context.createdResources.financeFullFixtures
|
||||
assert.equal(fixture.customerId, '701')
|
||||
assert.equal(fixture.orders.length, 5)
|
||||
assert.equal(fixture.orders.length, 6)
|
||||
assert.equal(fixture.orders[0].orderNo, 'NO-AUTO1')
|
||||
assert.equal(fixture.orders[0].waybillId, 'WB-1')
|
||||
assert.equal(fixture.orders[0].actualWeightKg, 12.5)
|
||||
|
||||
@@ -55,8 +55,18 @@ async function createOrder(context, suffix, pricingProfile = null) {
|
||||
// The orders module discovers candidates independently. Reorder only those
|
||||
// real responses so its payload uses one exact active price-book row.
|
||||
globalThis.fetch = async (input, init = {}) => {
|
||||
const response = await originalFetch(input, init)
|
||||
const url = String(input)
|
||||
let effectiveInit = init
|
||||
if ((init.method || 'GET') === 'POST'
|
||||
&& url.includes('/api/merchant/v1/first-leg/pricing/estimate')
|
||||
&& typeof init.body === 'string') {
|
||||
const body = JSON.parse(init.body)
|
||||
effectiveInit = {
|
||||
...init,
|
||||
body: JSON.stringify({ ...body, chargeMode: pricingProfile.chargeUnit })
|
||||
}
|
||||
}
|
||||
const response = await originalFetch(input, effectiveInit)
|
||||
if ((init.method || 'GET') === 'GET' && (url.includes('/api/merchant/v1/warehouses?')
|
||||
|| url.includes('/api/merchant/v1/first-leg/pricing/channels?'))) {
|
||||
const body = await response.clone().json().catch(() => null)
|
||||
@@ -99,7 +109,7 @@ async function measureOrder(context, resource) {
|
||||
if (context.financeFixtureHooks?.measureOrder) return context.financeFixtureHooks.measureOrder(context, resource)
|
||||
const originalMarker = context.marker
|
||||
const createdKeys = ['orders', 'inboundMeasurement', 'ocrInbound', 'testInbound']
|
||||
const contextKeys = ['inboundWeightOcr', 'ocrInbound', 'testInbound', 'createdInbound', 'allowAnyExistingPallet']
|
||||
const contextKeys = ['inboundWeightOcr', 'ocrInbound', 'testInbound', 'createdInbound', 'allowAnyExistingPallet', 'waitSplitVariant']
|
||||
const createdSnapshot = new Map(createdKeys.map((key) => [key, { present: Object.hasOwn(context.createdResources, key), value: context.createdResources[key] }]))
|
||||
const contextSnapshot = new Map(contextKeys.map((key) => [key, { present: Object.hasOwn(context.context, key), value: context.context[key] }]))
|
||||
context.marker = resource.marker
|
||||
@@ -114,6 +124,13 @@ async function measureOrder(context, resource) {
|
||||
// Reuse that warehouse's real pallet master instead of requiring the bulk-data
|
||||
// generator's two fixed Yiwu pallet codes.
|
||||
context.context.allowAnyExistingPallet = true
|
||||
context.context.waitSplitVariant = {
|
||||
...(context.context.waitSplitVariant || {}),
|
||||
// The production pricing service chooses the dominant real billing basis.
|
||||
// Keep the CBM sample's measured gross weight below its measured volume
|
||||
// weight so the fixture proves the actual VOLUME_CBM path.
|
||||
grossWeightPerPalletKg: resource.pricingProfile?.chargeUnit === 'CBM' ? 20 : 100,
|
||||
}
|
||||
try {
|
||||
const measurement = await import(new URL('./inbound-measurement.mjs', import.meta.url))
|
||||
await measurement.run(context)
|
||||
@@ -299,7 +316,8 @@ async function loadPricingEvidence(context) {
|
||||
if (matches.length !== 1) continue
|
||||
profiles.push({ versionId: id(version.id), rateId: id(rate.id), chargeUnit: rate.chargeUnit,
|
||||
originWarehouseId: id(rate.originWarehouseId), channelCode: rate.channelCode,
|
||||
pricingTaxMode: rate.pricingTaxMode, destinationCode: rate.destinationCode })
|
||||
pricingTaxMode: rate.pricingTaxMode, destinationCode: rate.destinationCode,
|
||||
businessType: rate.businessType || 'FBA' })
|
||||
}
|
||||
}
|
||||
context.assert(MODULE, 'ACTIVE 价格簿包含 KG 与 CBM 可用真实组合', response,
|
||||
@@ -308,6 +326,57 @@ async function loadPricingEvidence(context) {
|
||||
return { activeVersionIds: active.map((item) => id(item.id)).filter(Boolean), profiles }
|
||||
}
|
||||
|
||||
async function ensureCustomerChargeRule(context, resource, profile) {
|
||||
if (context.financeFixtureHooks?.ensureCustomerChargeRule) {
|
||||
return context.financeFixtureHooks.ensureCustomerChargeRule(context, resource, profile)
|
||||
}
|
||||
if (!resource?.tenantId || profile?.pricingTaxMode !== 'TAX_EXCLUDED') return null
|
||||
const tenantId = id(resource.tenantId)
|
||||
const inboundResponse = await context.request('GET',
|
||||
`/api/platform/inbounds/${encodeURIComponent(resource.inboundId)}?tenantId=${encodeURIComponent(tenantId)}`)
|
||||
const inbound = context.apiSuccess(MODULE, '读取不包税入库计费业务类型', inboundResponse,
|
||||
`inboundId=${resource.inboundId}`, OPTIONS)
|
||||
const businessType = inbound?.summary?.businessType || profile.businessType
|
||||
if (!businessType) {
|
||||
blocker(context, '取得不包税入库计费业务类型',
|
||||
`inboundId=${resource.inboundId} 未返回 businessType,不能创建猜测的客户计费规则`)
|
||||
return null
|
||||
}
|
||||
const listPath = `/api/platform/v1/fba-pricing/customer-rules?tenantId=${encodeURIComponent(tenantId)}&customerId=${encodeURIComponent(tenantId)}`
|
||||
const listResponse = await context.request('GET', listPath)
|
||||
const listed = context.apiSuccess(MODULE, '查询不包税客户计费规则', listResponse,
|
||||
`customerId=${tenantId}, businessType=${businessType}`, OPTIONS)
|
||||
const applicable = rows(listed).filter((rule) => rule?.status === 'ACTIVE'
|
||||
&& rule?.businessType === businessType && rule?.taxMode === 'TAX_EXCLUDED')
|
||||
let desired = applicable.find((rule) => rule?.chargeMode === profile.chargeUnit)
|
||||
if (!desired) {
|
||||
const effectiveFrom = new Date(Date.now() - 86_400_000).toISOString().slice(0, 19)
|
||||
const body = {
|
||||
ruleCode: `FIN_P1_${profile.chargeUnit}_${tenantId}`.slice(0, 50), customerId: tenantId,
|
||||
customerName: String(inbound?.summary?.customerName || `财务P1验收客户-${tenantId}`).slice(0, 128), businessType,
|
||||
chargeMode: profile.chargeUnit, taxMode: 'TAX_EXCLUDED', conversionFactor: 1,
|
||||
roundingScale: 3, roundingMode: 'HALF_UP', currency: 'CNY', effectiveFrom,
|
||||
effectiveTo: null, remark: `${context.marker}-VOLUME_CBM`
|
||||
}
|
||||
const createResponse = await request(context, 'POST',
|
||||
`/api/platform/v1/fba-pricing/customer-rules/versions?tenantId=${encodeURIComponent(tenantId)}`, body)
|
||||
desired = context.apiSuccess(MODULE, '创建不包税客户计费规则', createResponse,
|
||||
`customerId=${tenantId}, chargeMode=${profile.chargeUnit}`, OPTIONS)
|
||||
}
|
||||
const needsDefault = applicable.length > 1
|
||||
|| (applicable.length === 1 && id(applicable[0]?.id) !== id(desired?.id))
|
||||
if (desired?.id && needsDefault && desired.defaultRule !== true) {
|
||||
const defaultResponse = await request(context, 'PUT',
|
||||
`/api/platform/v1/fba-pricing/customer-rules/${encodeURIComponent(desired.id)}/default?tenantId=${encodeURIComponent(tenantId)}`)
|
||||
context.apiSuccess(MODULE, '设置不包税客户唯一默认计费规则', defaultResponse,
|
||||
`ruleId=${desired.id}, chargeMode=${profile.chargeUnit}`, OPTIONS)
|
||||
}
|
||||
context.assert(MODULE, '不包税自动计费具备客户计费方式', listResponse,
|
||||
Boolean(desired?.id) && desired?.chargeMode === profile.chargeUnit,
|
||||
`customerId=${tenantId}, chargeMode=${desired?.chargeMode || 'missing'}, ruleId=${id(desired?.id) || 'missing'}`, OPTIONS)
|
||||
return desired || null
|
||||
}
|
||||
|
||||
async function createMarkerContainer(context, resource) {
|
||||
if (context.financeFixtureHooks?.createMarkerContainer) return context.financeFixtureHooks.createMarkerContainer(context, resource)
|
||||
if (!resource) {
|
||||
@@ -387,12 +456,15 @@ export async function run(context) {
|
||||
const pricingEvidence = await loadPricingEvidence(context)
|
||||
const resources = []
|
||||
const profiles = pricingEvidence.profiles || []
|
||||
for (const [index, suffix] of ['AUTO1', 'AUTO2', 'AUTO3', 'AUTO4', 'MANUAL'].entries()) {
|
||||
for (const [index, suffix] of ['AUTO1', 'AUTO2', 'AUTO3', 'AUTO4', 'MANUAL', 'CONTAINER'].entries()) {
|
||||
const resource = await createOrder(context, suffix, profiles[index % profiles.length] || null)
|
||||
context.assert(MODULE, `创建 ${suffix} marker 隔离订单与入库单`, null, Boolean(resource),
|
||||
resource ? `orderId=${resource.orderId}, inboundId=${resource.inboundId}` : `marker=${context.marker}-${suffix} 未返回独立入库单`, OPTIONS)
|
||||
if (resource) resources.push(resource)
|
||||
}
|
||||
for (const resource of resources.filter((item) => item.pricingProfile?.pricingTaxMode === 'TAX_EXCLUDED')) {
|
||||
await ensureCustomerChargeRule(context, resource, resource.pricingProfile)
|
||||
}
|
||||
const measured = []
|
||||
for (const resource of resources) {
|
||||
const result = await measureOrder(context, resource)
|
||||
@@ -430,7 +502,7 @@ export async function run(context) {
|
||||
if (fee) fixtures.push(fee)
|
||||
}
|
||||
}
|
||||
const manualResources = [measured.at(-1)].filter(Boolean)
|
||||
const manualResources = [measured[4]].filter(Boolean)
|
||||
if (feeItems.PAYABLE && supplier?.id) {
|
||||
for (const [index, manualResource] of manualResources.entries()) {
|
||||
const fee = await createManualFee(context, manualResource, feeItems.PAYABLE, 'PAYABLE', 80 + index * 10,
|
||||
@@ -440,7 +512,11 @@ export async function run(context) {
|
||||
}
|
||||
}
|
||||
|
||||
const container = (measured[0] ? await createMarkerContainer(context, measured[0]) : null)
|
||||
// A source whose fee has already been sent is intentionally frozen against
|
||||
// loading-plan membership changes. Use a dedicated measured order for the
|
||||
// container fixture so finance setup never conflicts with that invariant.
|
||||
const containerResource = measured[5]
|
||||
const container = (containerResource ? await createMarkerContainer(context, containerResource) : null)
|
||||
|| context.createdResources?.phase3ContainerLoading
|
||||
const containerOwned = (container?.marker === context.marker || String(container?.marker || '').startsWith(`${context.marker}-`))
|
||||
&& container?.loadingPlanId && container?.containerBookingId
|
||||
|
||||
Reference in New Issue
Block a user