Files
baiyue-firstleg/scripts/admin-test-env-modules/finance-full-fixtures-contract.test.mjs
T

368 lines
21 KiB
JavaScript

import test from 'node:test'
import assert from 'node:assert/strict'
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { buildContainerCostBody, buildSupplierBody, normalizeFinanceFixtureSources, run,
supplierCategoryForFeeItem, loadSupplier, ensureCustomerChargeRule } from './finance-full-fixtures.mjs'
for (const mode of ['single', 'default', 'missing', 'wrong mode', 'foreign customer', 'ambiguous', 'two defaults']) {
test(`fixed customer charge rules are read-only and fail closed: ${mode}`, async () => {
const good = { id: '701', customerId: '456', status: 'ACTIVE', businessType: 'FBA', taxMode: 'TAX_EXCLUDED', chargeMode: 'CBM', defaultRule: false }
let rules = [good]
if (mode === 'default') rules = [{ ...good, defaultRule: true }, { ...good, id: '702', chargeMode: 'KG' }]
if (mode === 'missing') rules = []
if (mode === 'wrong mode') rules = [{ ...good, chargeMode: 'KG' }]
if (mode === 'foreign customer') rules = [{ ...good, customerId: '999' }]
if (mode === 'ambiguous') rules = [good, { ...good, id: '702', chargeMode: 'KG' }]
if (mode === 'two defaults') rules = [{ ...good, defaultRule: true }, { ...good, id: '702', defaultRule: true }]
const calls = [], checks = [], before = structuredClone(rules)
const context = { options: { fixedCustomerId: '456' },
async request(method, path) { calls.push({ method, path }); assert.equal(method, 'GET');
return { body: { data: path.includes('/inbounds/') ? { summary: { businessType: 'FBA' } } : rules } } },
apiSuccess(_m, _n, response) { return response.body.data },
assert(_m, _n, _r, passed) { checks.push(passed) },
}
const invoke = () => ensureCustomerChargeRule(context, { tenantId: '456', inboundId: '100' }, { pricingTaxMode: 'TAX_EXCLUDED', chargeUnit: 'CBM' })
if (['single', 'default'].includes(mode)) assert.equal((await invoke()).id, '701')
else await assert.rejects(invoke(), /禁止自动创建或切换/)
assert.deepEqual(rules, before)
assert.equal(calls.length, 2)
assert.equal(checks.at(-1), ['single', 'default'].includes(mode))
})
}
test('fixed customer supplier selection paginates and never creates a fallback', async () => {
const calls = [], failures = []
const supplier = { id: '123', status: 'ACTIVE', category: 'TRUCKING', name: 'Existing' }
const context = {
options: { fixedCustomerId: '456' }, marker: 'REUSE',
async request(method, path) {
calls.push({ method, path })
const data = path.includes('/123?') ? supplier : path.includes('page=2')
? { records: [supplier], size: 20, total: 21 }
: { records: Array.from({ length: 20 }, () => ({ id: '99', status: 'ACTIVE', category: 'BOOKING' })), size: 20, total: 21 }
return { body: { data } }
},
apiSuccess(_module, _name, response) { return response.body.data },
addStep(...args) { failures.push(args) },
}
assert.equal((await loadSupplier(context, 'TRUCKING')).id, '123')
assert.equal(calls.length, 3)
assert.ok(calls.every(call => call.method === 'GET'))
context.request = async method => { assert.equal(method, 'GET'); return { body: { data: { records: [], total: 0 } } } }
assert.equal(await loadSupplier(context, 'TRUCKING'), null)
assert.equal(failures.length, 1)
})
const here = path.dirname(fileURLToPath(import.meta.url))
test('reused suppliers must match carrier and trucking scope in both list and detail', async () => {
for (const criteria of [{ carrierCode: 'MATSON' }, { truckingScope: 'OVERSEAS' }]) {
const good = { id: '123', category: 'BOOKING', status: 'ACTIVE', carrierCodes: ['MATSON'], truckingScope: 'OVERSEAS' }
let detail = good
const context = { options: { fixedCustomerId: '456' }, marker: 'REUSE',
async request(method, path) {
assert.equal(method, 'GET')
return { body: { data: path.includes('/123?') ? detail : { records: [
{ ...good, id: '999', carrierCodes: [], truckingScope: 'DOMESTIC' }, good], total: 2 } } }
}, apiSuccess(_module, _name, response) { return response.body.data }, addStep() {},
}
assert.equal((await loadSupplier(context, 'BOOKING', criteria)).id, '123')
detail = { ...good, carrierCodes: [], truckingScope: 'DOMESTIC' }
assert.equal(await loadSupplier(context, 'BOOKING', criteria), null)
}
})
const source = fs.readFileSync(path.join(here, 'finance-full-fixtures.mjs'), 'utf8')
test('finance fixture module is independently exported and marker isolated', () => {
assert.match(source, /export async function run\(context\)/)
assert.match(source, /context\.marker = `\$\{originalMarker\}-\$\{suffix\}`/)
assert.match(source, /createdResources\.financeFullFixtures/)
})
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/)
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/)
})
test('fee writes use verified Firstleg paths and do not fabricate historical IDs', () => {
assert.match(source, /listPath = `\$\{basePath\}\?accountId=/)
assert.match(source, /const basePath = `\/api\/platform\/v1\/loading-plans\//)
assert.match(source, /sourceType: detail\?\.sourceType \|\| 'MANUAL'/)
assert.match(source, /created\?\.marker === context\.marker/)
assert.match(source, /fee-details\?accountId=/)
assert.match(source, /chargeUnit === 'KG'/)
assert.match(source, /chargeUnit === 'CBM'/)
})
test('PAYABLE fee selection uses only the explicit nullable supplierCategory field', () => {
const selector = source.slice(source.indexOf('async function loadFeeItem'), source.indexOf('async function createManualFee'))
assert.match(selector, /item\.warehousePayable === true/)
assert.doesNotMatch(selector, /Boolean\(supplierCategoryForFeeItem/)
assert.equal(supplierCategoryForFeeItem({ supplierCategory: 'BOOKING', feeTypeName: '操作费' }), 'BOOKING')
assert.equal(supplierCategoryForFeeItem({ supplierCategory: null, feeTypeName: '拖车费' }), null)
assert.equal(supplierCategoryForFeeItem({ supplierCategory: 'INVALID' }), null)
assert.match(source, /payableSupplierCategory && source\.supplierCategory !== payableSupplierCategory/)
})
test('Finance PAYABLE fixture creates an isolated ACTIVE supplier through the formal API', () => {
const body = buildSupplierBody('ADMIN-API-20260815-FINANCE', 'BOOKING')
assert.equal(body.category, 'BOOKING')
assert.deepEqual(body.carrierCodes, ['MATSON'])
assert.equal(Object.hasOwn(body, 'scope'), false)
assert.equal(body.status, 'ACTIVE')
assert.match(body.name, /^财务验收供应商-/)
assert.match(source, /POST', '\/api\/platform\/v1\/suppliers\?tenantId=0'/)
assert.match(source, /detail\?\.status === 'ACTIVE' && \(!category \|\| detail\?\.category === category\)/)
})
test('Finance measurement can reuse real pallets from the selected pricing warehouse', () => {
assert.match(source, /context\.context\.allowAnyExistingPallet = true/)
})
test('fee detail writes preserve 19-digit fee item IDs as strings', () => {
assert.doesNotMatch(source, /feeItemId: Number\(/)
assert.match(source, /feeItemId: String\(feeItem\.id\)/)
assert.match(source, /feeItemId: String\(feeItems\.PAYABLE\.id\)/)
})
test('automatic fee fixture completes the reviewed pricing workflow without recalculating', () => {
assert.match(source, /latest\?\.feeId/)
assert.doesNotMatch(source, /latest\?\.id/)
assert.match(source, /`\$\{pricingBase\}\/latest\?\$\{tenantQuery\}`/)
assert.match(source, /request\(context, 'POST', `\$\{pricingBase\}\/confirm\?\$\{tenantQuery\}`\)/)
assert.match(source, /request\(context, 'POST', `\$\{pricingBase\}\/send\?\$\{tenantQuery\}`\)/)
assert.doesNotMatch(source, /request\(context, 'POST', `\$\{pricingBase\}\/calculate/)
})
test('shared fixture source names the finance full acceptance contract', () => {
for (const field of ['marker', 'customerId', 'orders', 'feeSources', 'container', 'allocations', 'supplier', 'voucherFileId', 'deploymentBlocker']) assert.match(source, new RegExp(`\\b${field}\\b`))
for (const field of ['feeSourceId', 'sourceType', 'direction', 'currency', 'amount', 'businessDate', 'billingBasis']) {
assert.match(source, new RegExp(field))
}
})
test('fixture source normalization uses only marker-owned orders and real allocation order links', () => {
const sources = [
{ feeSourceId: 'F-ORDER', orderId: 'O-1', businessDate: '2026-08-10' },
{ feeSourceId: 'F-CONTAINER', billingBasis: 'CONTAINER_SHARED', businessDate: '2026-08-11' },
]
const orders = [
{ orderId: 'O-1', orderNo: 'ORDER-1', waybillId: 'WB-1' },
{ orderId: 'O-2', orderNo: 'ORDER-2', shipmentId: 'SHIP-2' },
]
const normalized = normalizeFinanceFixtureSources(sources, orders, [{ orderId: 'O-2', allocatedAmount: 30 }],
'701', 'FIN-MARKER')
assert.deepEqual(normalized.map((source) => ({ feeSourceId: source.feeSourceId, customerId: source.customerId,
marker: source.marker, orderId: source.orderId, waybill: source.waybillId || source.shipmentId || source.orderNo })), [
{ feeSourceId: 'F-ORDER', customerId: '701', marker: 'FIN-MARKER', orderId: 'O-1', waybill: 'WB-1' },
{ feeSourceId: 'F-CONTAINER', customerId: '701', marker: 'FIN-MARKER', orderId: 'O-2', waybill: 'SHIP-2' },
])
const unbound = normalizeFinanceFixtureSources([sources[1]], orders, [], '701', 'FIN-MARKER')[0]
assert.equal(unbound.orderId, null)
assert.equal(unbound.waybillId || unbound.shipmentId || unbound.orderNo, null)
})
function fakeContext(overrides = {}) {
const steps = []
const context = {
marker: 'FIN-BEHAVIOR', token: 'test-token', options: { gateway: 'http://invalid.local' },
steps, scenarios: [], createdResources: {}, context: {},
addStep(module, name, response, passed, evidence, options = {}) {
steps.push({ module, name, response, passed, evidence, deploymentBlocker: Boolean(options.deploymentBlocker && !passed) })
},
assert(module, name, response, passed, evidence, options) { this.addStep(module, name, response, passed, evidence, options) },
addCoverageGap(module, name, coverage, evidence, code) { this.addStep(module, name, null, false, `${code}: ${evidence}`, { deploymentBlocker: true }) },
apiSuccess(module, name, response) { this.addStep(module, name, response, true, 'fake'); return response?.body?.data },
async request() { throw new Error('behavior test must use hooks') },
financeFixtureHooks: {}, ...overrides,
}
return context
}
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 }),
loadFeeItem: async (context, direction) => ({ id: direction === 'PAYABLE' ? '902' : '901', status: 'ACTIVE',
feeCode: direction === 'PAYABLE' ? 'FBA_HEAD_FREIGHT' : 'RECEIVABLE_SERVICE' }),
loadSupplier: async () => missing ? null : ({ id: 'SUPPLIER-1', name: '订舱供应商一', category: 'BOOKING', status: 'ACTIVE' }),
uploadVoucher: async () => missing ? null : 'VOUCHER-1',
verifyContainerCostSnapshotInput: async () => ({ valid: true, items: [{ accountId: '701', orderId: 'O-AUTO1', volumeCbm: 0.072 }] }),
loadPublishedAutomaticFee: async (context, resource) => {
automaticIndex += 1
const weight = automaticIndex % 2 === 1
return { feeSourceId: `F-${automaticIndex}`, orderId: resource.orderId, inboundId: resource.inboundId,
sourceType: 'FBA_PRICING', direction: 'RECEIVABLE', currency: 'CNY', amount: 100 + automaticIndex,
businessDate: weight ? '2026-08-10' : '2026-08-11', billingBasis: weight ? 'WEIGHT_KG' : 'VOLUME_CBM',
chargeUnit: weight ? 'KG' : 'CBM', chargeableQuantity: automaticIndex,
actualWeightKg: 54.4, actualVolumeCbm: 0.748 }
},
createManualFee: async (context, resource, item, direction, amount, basis, businessDate) => ({
feeSourceId: 'F-MANUAL', orderId: resource.orderId, inboundId: resource.inboundId, sourceType: 'MANUAL',
direction, currency: 'CNY', amount, businessDate, billingBasis: basis,
}),
}
}
test('run behavior emits consumer shape, both billing bases, distinct dates, supplier and voucher', async () => {
const context = fakeContext()
context.financeFixtureHooks = hookSet()
await run(context)
const fixture = context.createdResources.financeFullFixtures
assert.equal(fixture.customerId, '701')
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)
assert.equal(fixture.orders[0].totalVolumeCbm, 0.072)
assert.deepEqual(new Set(fixture.feeSources.map((item) => item.billingBasis).filter((value) => ['WEIGHT_KG', 'VOLUME_CBM'].includes(value))),
new Set(['WEIGHT_KG', 'VOLUME_CBM']))
assert.ok(new Set(fixture.feeSources.map((item) => item.businessDate)).size >= 2)
assert.equal(fixture.supplier.supplierId, 'SUPPLIER-1')
assert.equal(fixture.supplier.supplierName, '订舱供应商一')
assert.equal(fixture.supplier.category, 'BOOKING')
assert.equal(fixture.voucherFileId, 'VOUCHER-1')
assert.ok(Array.isArray(fixture.allocations))
assert.ok(fixture.feeSources.filter((item) => item.direction === 'RECEIVABLE').length >= 4)
assert.ok(new Set(fixture.feeSources.filter((item) => item.direction === 'RECEIVABLE').map((item) => item.businessDate)).size >= 2)
assert.equal(fixture.feeSources.every((item) => item.feeSourceId && item.customerId === fixture.customerId
&& item.businessDate && (item.waybillId || item.shipmentId || item.orderNo) && item.marker === context.marker), true)
assert.equal(fixture.feeSources.filter((item) => item.direction === 'PAYABLE').every((item) => item.supplierId
&& item.supplierName && item.supplierCategory === fixture.supplier.category), true)
})
test('run preserves real price-row profiles when creating automatic orders', async () => {
const context = fakeContext()
const profiles = [
{ versionId: 'PRICE-1', rateId: 'RATE-KG', chargeUnit: 'KG', originWarehouseId: '11', channelCode: 'SEA-KG', pricingTaxMode: 'BUY_DECLARATION', destinationCode: 'USLAX' },
{ versionId: 'PRICE-1', rateId: 'RATE-CBM', chargeUnit: 'CBM', originWarehouseId: '11', channelCode: 'SEA-CBM', pricingTaxMode: 'BUY_DECLARATION', destinationCode: 'USLAX' },
]
const seen = []
context.financeFixtureHooks = {
...hookSet(),
loadPricingEvidence: async () => ({ activeVersionIds: ['PRICE-1'], profiles }),
createOrder: async (ctx, suffix, profile) => {
seen.push(profile)
return { marker: `${ctx.marker}-${suffix}`, tenantId: '701', orderId: `O-${suffix}`, orderNo: `NO-${suffix}`, inboundId: `I-${suffix}` }
},
}
await run(context)
assert.equal(seen[0].chargeUnit, 'KG')
assert.equal(seen[1].chargeUnit, 'CBM')
assert.deepEqual(context.createdResources.financeFullFixtures.pricingEvidence.profiles, profiles)
})
test('run behavior preserves null IDs and deployment blockers when supplier, voucher and container are unavailable', async () => {
const context = fakeContext()
context.financeFixtureHooks = hookSet({ missing: true })
await run(context)
const fixture = context.createdResources.financeFullFixtures
assert.equal(fixture.supplier, null)
assert.equal(fixture.voucherFileId, null)
assert.deepEqual(fixture.container, { containerBookingId: null, containerNo: null })
assert.deepEqual(fixture.allocations, [])
assert.ok(fixture.deploymentBlocker.length >= 3)
assert.ok(context.steps.some((step) => step.deploymentBlocker && step.name.includes('供应商')))
assert.ok(context.steps.some((step) => step.deploymentBlocker && step.name.includes('voucherFileId')))
})
test('run reports missing measurement prerequisites without throwing or borrowing historical fixtures', async () => {
const context = fakeContext()
context.financeFixtureHooks = {
...hookSet(),
measureOrder: async () => null,
}
await run(context)
const fixture = context.createdResources.financeFullFixtures
assert.equal(fixture.orders.length, 0)
assert.deepEqual(fixture.container, { containerBookingId: null, containerNo: null })
assert.deepEqual(fixture.allocations, [])
assert.equal(fixture.prerequisites.container.category, 'container-prerequisite')
assert.ok(fixture.deploymentBlocker.some((item) => item.includes('缺少已完成柜次')))
assert.ok(context.steps.some((step) => step.deploymentBlocker && step.name.includes('真实订单/运单分摊')))
})
test('manual fee approval path keeps accountId as a query parameter', () => {
assert.match(source, /basePath = `\/api\/platform\/v1\/inbounds\/.*fee-details`/)
assert.match(source, /\$\{basePath\}\/\$\{feeId\}\/approve\?accountId=/)
assert.doesNotMatch(source, /fee-details\?accountId=\$\{encodeURIComponent\(resource\.tenantId\)\}`\)/)
})
test('automatic fixture reuses the published review-chain source without recalculating at WAIT_SPLIT', () => {
assert.match(source, /loadPublishedAutomaticFee/)
assert.match(source, /sourceType === 'FBA_PRICING'/)
assert.match(source, /reviewStatus === 'APPROVED'/)
assert.match(source, /publicationStatus === 'PUBLISHED'/)
assert.doesNotMatch(source, /settlementIncluded/)
assert.match(source, /const feeSourceId = id\(published\.sourceFeeId\)/)
assert.doesNotMatch(source, /\/fees\/calculate/)
assert.doesNotMatch(source, /\/fees\/confirm/)
assert.doesNotMatch(source, /\/fees\/send/)
})
test('missing published automatic source is a prerequisite blocker and never emits a fixture source', async () => {
const context = fakeContext()
let automaticReads = 0
context.financeFixtureHooks = {
...hookSet(),
loadPublishedAutomaticFee: async (ctx, resource) => {
automaticReads += 1
ctx.addStep('finance-full-fixtures', '审核链已生成可结算自动费用来源', null, false,
`inboundId=${resource.inboundId}; fixture 不重复试算/确认/发布`, { deploymentBlocker: true })
return null
},
}
await run(context)
assert.equal(automaticReads, 4)
assert.equal(context.createdResources.financeFullFixtures.feeSources
.filter((item) => item.sourceType === 'FBA_PRICING').length, 0)
assert.ok(context.steps.some((step) => step.deploymentBlocker
&& step.name.includes('可结算自动费用来源')))
})
test('fixture creates four real receivable manual sources before payable source', () => {
assert.match(source, /const receivableResources = measured\.slice\(0, 4\)/)
assert.match(source, /feeItems\.RECEIVABLE, 'RECEIVABLE'/)
assert.match(source, /'MANUAL_RECEIVABLE'/)
assert.match(source, /dateOffset\(-1 - \(index % 2\)\)/)
})
test('container cost uses loading plan identity and verifies snapshot fields before Finance', () => {
const body = buildContainerCostBody('RUN-1', '2088073097986560001',
{ loadingPlanId: '2088073275689709570' }, '2088073280000000001')
assert.equal(body.customerId, '2088073097986560001')
assert.equal(body.containerId, '2088073275689709570')
assert.equal(JSON.parse(JSON.stringify(body)).customerId, '2088073097986560001')
assert.match(source, /customerId: String\(customerId\)/)
assert.doesNotMatch(source, /customerId: Number\(customerId\)/)
assert.match(source, /containerId: String\(container\.loadingPlanId\)/)
assert.doesNotMatch(source, /containerId: Number\(container\.loadingPlanId\)/)
assert.match(source, /smart-loading\/plans\/\$\{encodeURIComponent\(container\.loadingPlanId\)\}/)
assert.match(source, /id\(item\?\.accountId\) === id\(customerId\)/)
assert.match(source, /Number\(item\?\.orderId\) > 0/)
assert.match(source, /Number\(item\?\.volumeCbm\) > 0/)
assert.match(source, /snapshotInput\.valid \? await createContainerCost\(context, customerId, container, voucherFileId, snapshotInput\)/)
assert.match(source, /publicSnapshotValid=\$\{snapshotInput\?\.valid === true\}/)
assert.match(source, /Finance -> Firstleg 内部快照服务部署\/契约/)
})