Files
admin-web/src/modules/overseas-business/procurementFinanceForm.test.ts
T

120 lines
14 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { buildFinance, financeForm, feeRetryPermission, refundAvailable, targetPreview, replacementCandidates, replacementPreview } from './procurementFinanceForm'
import { readPendingProcurement } from './procurementPending'
import type { PlatformProcurement, ProcurementInternalFee, ProcurementFile } from './procurementTypes'
const base = { version: 7, idempotencyKey: 'finance-one', reason: '凭证核对完成' }
const proof: ProcurementFile = { id: '51', fileId: '501', category: 'VOUCHER', fileName: 'invoice.pdf', contentType: 'application/pdf', sizeBytes: 10, createdAt: '' }
function fee(id = '1', purpose = 'DUTY'): ProcurementInternalFee { return { id, feeNo: `PF-${id}`, purpose, confirmationOrigin: 'BUSINESS', feeItemCode: purpose, feeItemName: purpose, feeVersion: 1, active: true, determination: 'POSITIVE', amount: '70.00', currency: 'CNY', originalAmount: '10.00', originalCurrency: 'USD', exchangeRate: '7.000000', exchangeRateMonth: '2020-01', businessDate: '2020-01-02', parentFeeId: null, sourceFeeId: null, voucherFileId: '501', voucherFileName: 'invoice.pdf', walletStatus: 'SETTLED', walletLedgerId: `900${id}`, walletErrorCode: null, walletErrorMessage: null, remark: null } }
function detail(fees: ProcurementInternalFee[] = []): PlatformProcurement {
return { customerView: { order: { id: '1', orderNo: 'OP-1', customerId: '9', customerName: '客户', purchaseCategory: '样品', productName: '外套', productSpecification: null, productUrl: null, purchaseQuantity: 10, budgetUnitPrice: '1.00', currency: 'CNY', receivingAddress: '深圳', contactName: '收件人', contactPhone: '13800000000', estimatedArrivalAt: null, plannedWarehouseName: null, actualWarehouseName: null, actualWarehouseCode: null, status: 'WAIT_CHARGES', version: 7, createdAt: '', updatedAt: '' }, budget: { totalAmount: '10.00', currency: 'CNY', authorizedAt: null }, purchased: true, purchaseConfirmed: true, purchaseNeedsAttention: false, progress: [], boxes: [], shipments: [], fees: fees.map(row => ({ feeNo: row.feeNo, purpose: row.purpose, name: row.feeItemName, version: row.feeVersion, active: row.active, determination: row.determination, amount: row.amount, currency: row.currency, originalAmount: row.originalAmount, originalCurrency: row.originalCurrency, exchangeRate: row.exchangeRate, exchangeRateMonth: row.exchangeRateMonth, businessDate: row.businessDate, parentFeeNo: null, walletStatus: row.walletStatus, sourceLinked: ['SETTLED', 'REFUNDED', 'ZERO', 'NOT_APPLICABLE'].includes(row.walletStatus) })), exceptionSummaries: [] }, purchaseFacts: { domestic_received_at: '2020-01-04T10:00:00' }, physical: { receipts: [], dispositions: [], packingVersion: 1, packingStatus: 'CONFIRMED', cartons: [] }, transport: { overseasCargo: [], domesticCargo: [], overseasLabels: null, domesticLabels: null, shipments: [] }, files: [proof], fees, feeCorrections: [], controls: { notes: [], remaining: [], closures: [], closureItems: [] }, reviewHistory: [], operations: [], actions: [], internalRemark: null }
}
function chain() { const root = fee(); const extra = { ...fee('2', 'ADJUSTMENT'), amount: '20.00', originalAmount: '20.00', originalCurrency: 'CNY', exchangeRate: '1', parentFeeId: '1' }; const refund = { ...fee('3', 'REFUND'), amount: '10.00', originalAmount: '10.00', originalCurrency: 'CNY', exchangeRate: '1', parentFeeId: '1', walletStatus: 'REFUNDED' }; return detail([root, extra, refund]) }
describe('procurement fee determination and correction rules', () => {
it('builds independent additional fees with a scoped voucher and recoverable payload', () => {
const value = detail(); value.purchaseFacts.purchased_at = '2020-01-02T10:00:00'
const form = financeForm(value)
Object.assign(form.additional, { name: '保管费', amount: '3.25', currency: 'USD', businessDate: '2020-01-03', voucherFileId: '501' })
const payload = buildFinance('fees/additional', form, value, [proof], base, '')
expect(payload).toEqual({ ...base, name: '保管费', amount: '3.25', currency: 'USD', businessDate: '2020-01-03', voucherFileId: '501' })
expect(readPendingProcurement(JSON.stringify({ command: 'fees/additional', payload }))).toEqual({ command: 'fees/additional', payload })
expect(() => buildFinance('fees/additional', form, value, [], base, '')).toThrow('凭证')
form.additional.businessDate = '2999-01-01'
expect(() => buildFinance('fees/additional', form, value, [proof], base, '')).toThrow('真实业务日期')
delete value.purchaseFacts.purchased_at
expect(() => buildFinance('fees/additional', form, value, [proof], base, '')).toThrow('已发生购买')
})
it('prevents ordinary creation after initial confirmation', () => {
const value = detail(); value.operations.push({ action: 'CONFIRM_CHARGES' } as PlatformProcurement['operations'][number])
expect(() => buildFinance('charges/confirm', financeForm(value), value, [proof], base, '')).toThrow('首次核费后')
expect(feeRetryPermission({ ...fee(), confirmationOrigin: 'FINANCIAL' })).toContain('finance-adjust')
})
it('requires every fixed determination and does not treat missing values as zero', () => {
const value = detail(); const form = financeForm(value)
expect(() => buildFinance('charges/confirm', form, value, [proof], base, '')).toThrow('未知不能视为零元')
for (const row of form.fixed) Object.assign(row, { determination: 'ZERO', amount: '0.00', businessDate: '2020-01-04', reason: '明确免收' })
const payload = buildFinance('charges/confirm', form, value, [proof], base, '')
expect(payload.internationalFreight?.value?.determination).toBe('ZERO'); expect(payload.otherDetermination).toBe('NOT_APPLICABLE'); expect(payload.otherFees).toEqual([])
expect(readPendingProcurement(JSON.stringify({ command: 'charges/confirm', payload }))).toEqual({ command: 'charges/confirm', payload })
})
it('requires evidence for a positive amount and validates zero and not-applicable amounts', () => {
const value = detail(); const form = financeForm(value)
for (const row of form.fixed) Object.assign(row, { determination: 'NOT_APPLICABLE', amount: '0.00', businessDate: '2020-01-04', reason: '不适用' })
Object.assign(form.fixed[0], { determination: 'POSITIVE', amount: '10.01' })
expect(() => buildFinance('charges/confirm', form, value, [], base, '')).toThrow('费用凭证')
form.fixed[0].voucherFileId = '501'; const payload = buildFinance('charges/confirm', form, value, [proof], base, '')
expect(payload.internationalFreight?.value).toMatchObject({ amount: '10.01', voucherFileId: '501' })
form.fixed[1].amount = '2.00'; expect(() => buildFinance('charges/confirm', form, value, [proof], base, '')).toThrow('0.00')
})
it('reuses immutable existing fee versions without sending new charge values', () => {
const value = detail([fee('1', 'INTERNATIONAL_FREIGHT'), fee('2', 'DUTY'), fee('3', 'DOMESTIC_FREIGHT'), fee('4', 'OTHER')]); const form = financeForm(value)
const payload = buildFinance('charges/confirm', form, value, [proof], base, '')
expect(payload.internationalFreight).toEqual({ feeId: '1' }); expect(payload.duty).toEqual({ feeId: '2' }); expect(payload.domesticFreight).toEqual({ feeId: '3' }); expect(payload.otherFees?.[0].selection).toEqual({ feeId: '4' })
form.others = []; form.otherDetermination = 'NOT_APPLICABLE'; expect(() => buildFinance('charges/confirm', form, value, [proof], base, '')).toThrow('省略有效费用')
})
it('calculates a target using the original frozen rate and net successful chain only', () => {
const value = chain(); expect(targetPreview(value, value.fees[0], '12.00')).toEqual({ target: '84.00', net: '80.00', difference: '4.00' })
const form = financeForm(value); form.feeId = '1'; form.targetOriginalAmount = '12.00'
expect(buildFinance('fees/correct-target', form, value, [proof], base, '501')).toEqual({ ...base, feeId: '1', targetOriginalAmount: '12.00', voucherFileId: '501' })
expect(() => targetPreview(value, value.fees[1], '12.00')).toThrow('原实际费用')
})
it('treats actual international freight as the correction root even when it references the original estimate', () => {
const actual = { ...fee('2', 'INTERNATIONAL_FREIGHT'), parentFeeId: '1' }
const estimate = { ...fee('1', 'INTERNATIONAL_ESTIMATE'), amount: '100.00', walletStatus: 'SETTLED' }
const value = detail([estimate, actual]); const form = financeForm(value); form.feeId = '2'; form.targetOriginalAmount = '12.00'
expect(targetPreview(value, actual, '12.00')).toEqual({ target: '84.00', net: '70.00', difference: '14.00' })
expect(buildFinance('fees/correct-target', form, value, [proof], base, '501').feeId).toBe('2')
})
it('replaces only the latest terminated unpaid version using its frozen rate and preserves original amounts', () => {
const original = { ...fee('1', 'INTERNATIONAL_FREIGHT'), active: false, walletStatus: 'TERMINATED', walletLedgerId: null }
const value = detail([original]); const form = financeForm(value); form.feeId = '1'; form.targetOriginalAmount = '12.00'
expect(replacementPreview(value, original, '12.00')).toEqual({ target: '84.00', net: '0.00', difference: '84.00' })
const payload = buildFinance('fees/replace-failed', form, value, [proof], base, '501')
expect(payload).toEqual({ ...base, feeId: '1', targetOriginalAmount: '12.00', voucherFileId: '501' }); expect(readPendingProcurement(JSON.stringify({ command: 'fees/replace-failed', payload }))).not.toBeNull()
value.fees.push({ ...fee('2', 'INTERNATIONAL_FREIGHT'), feeVersion: 2, walletStatus: 'FREEZE_PENDING' })
expect(replacementCandidates(value)).toEqual([]); expect(original.amount).toBe('70.00')
})
it('keeps sample replacement within customer authorization and makes financial replacements retry under finance permission', () => {
const original = { ...fee('1', 'SAMPLE_PAYMENT'), active: false, walletStatus: 'RELEASED' }; const value = detail([original])
expect(() => replacementPreview(value, original, '12.00')).toThrow('客户授权预算')
value.customerView.budget.totalAmount = '100.00'; expect(replacementPreview(value, original, '12.00').target).toBe('84.00')
const replacement = { ...fee('2', 'SAMPLE_PAYMENT'), confirmationOrigin: 'FINANCIAL' as const, walletStatus: 'FREEZE_FAILED', feeVersion: 2 }; value.fees.push(replacement)
value.feeCorrections = [{ correctionNo: 'PC-1', rootFeeNo: 'PF-1', targetOriginalAmount: '12.00', originalCurrency: 'USD', exchangeRate: '7', exchangeRateMonth: '2020-01', targetAmount: '84.00', previousNetAmount: '0.00', differenceAmount: '84.00', feeNos: ['PF-2'] }]
expect(feeRetryPermission(replacement)).toBe('platform:overseas-business:purchase:finance-adjust')
})
it('preserves cent accuracy beyond JavaScript safe integers and rounds half up', () => {
const value = detail([fee()]); value.fees[0].exchangeRate = '1.000000'
expect(targetPreview(value, value.fees[0], '90071992547409.93')).toEqual({ target: '90071992547409.93', net: '70.00', difference: '90071992547339.93' })
value.fees[0].exchangeRate = '7.000000'; expect(() => targetPreview(value, value.fees[0], '90071992547409.93')).toThrow('超出支持范围')
value.fees[0].exchangeRate = '0.500000'; expect(targetPreview(value, value.fees[0], '0.01').target).toBe('0.01')
})
it('blocks a new target while any related adjustment remains unsettled or unlinked', () => {
const value = chain(); value.customerView.fees[1].sourceLinked = false
expect(() => targetPreview(value, value.fees[0], '12.00')).toThrow('未完成的调整')
})
it('caps refunds against the selected original consumption without borrowing another charge balance', () => {
const value = chain(); expect(refundAvailable(value, value.fees[0])).toBe(6000n); expect(refundAvailable(value, value.fees[1])).toBe(2000n)
const form = financeForm(value); form.feeId = '1'; form.refundAmount = '60.00'
const payload = buildFinance('fees/refund', form, value, [proof], base, '501'); expect(payload.amount).toBe('60.00')
expect(readPendingProcurement(JSON.stringify({ command: 'fees/refund', payload }))).not.toBeNull()
form.refundAmount = '60.01'; expect(() => buildFinance('fees/refund', form, value, [proof], base, '501')).toThrow('剩余可退')
form.refundAmount = '0.00'; expect(() => buildFinance('fees/refund', form, value, [proof], base, '501')).toThrow('正额')
})
it('selects retry permission by fee purpose and rejects unknown outcomes', () => {
const value = detail([{ ...fee('1', 'SAMPLE_PAYMENT'), walletStatus: 'FREEZE_FAILED' }]); const form = financeForm(value); form.feeId = '1'
expect(feeRetryPermission(value.fees[0])).toBe('platform:overseas-business:purchase:buy')
expect(buildFinance('fees/retry', form, value, [], base, '')).toEqual({ ...base, feeId: '1' })
value.fees[0].walletStatus = 'FREEZE_PENDING'; expect(() => buildFinance('fees/retry', form, value, [], base, '')).toThrow('未知结果')
expect(feeRetryPermission(fee('2', 'INTERNATIONAL_ESTIMATE'))).toContain(':estimate'); expect(feeRetryPermission(fee('3', 'REFUND'))).toContain(':finance-adjust')
})
it('only releases an effective estimate and does not construct a refund request', () => {
const value = detail([{ ...fee('1', 'INTERNATIONAL_ESTIMATE'), walletStatus: 'FROZEN' }]); const form = financeForm(value); form.feeId = '1'
expect(buildFinance('fees/release-estimate', form, value, [], base, '')).toEqual({ ...base, feeId: '1' })
value.fees[0].walletStatus = 'SETTLED'; expect(() => buildFinance('fees/release-estimate', form, value, [], base, '')).toThrow('有效预估')
})
it('rejects a persisted selection that mixes an existing reference and a new amount', () => {
const selection = { feeId: '1', value: { determination: 'ZERO', amount: '0.00', currency: 'CNY', businessDate: '2020-01-04', reason: '免费' } }
expect(() => readPendingProcurement(JSON.stringify({ command: 'charges/confirm', payload: { ...base, internationalFreight: selection, duty: selection, domesticFreight: selection, otherDetermination: 'NOT_APPLICABLE', otherFees: [] } }))).toThrow('ambiguous')
})
})