This commit is contained in:
2026-08-19 09:14:35 +08:00
parent c188482c24
commit 80cfca5056
36 changed files with 854 additions and 1044 deletions
+27 -20
View File
@@ -1,12 +1,25 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { http } from '@/shared/api/http'
import { actionReconciliationStatement, adjustFinanceBill, createFinanceCost, createFinanceReceipt, createFinanceSettlementPayment, createPayableSettlement, createReceivableSettlement, createReconciliationStatement, getFinanceBill, getFinanceCustomerAccount, getFinanceCustomerAccountSummary, getFinanceCustomerTerms, getFinanceFeeSource, getFinanceSettlement, getReconciliationStatement, issueFinanceBill, listFinanceAccountLedger, listFinanceAccounts, listFinanceBillRevisions, listFinanceFeePool, listFinanceFeeSources, listFinancePayableSettlements, listFinanceReceivableSettlements, listFinanceStatements, reviewFinanceFeeSource, updateFinanceBill, updateFinanceCustomerTerms, updateFinanceFeeSourceSettlementInclusion, uploadFinanceVoucher, voidFinanceBill } from './finance'
import { actionReconciliationStatement, adjustFinanceBill, createFinanceCost, createFinanceReceipt, createFinanceSettlementPayment, createPayableSettlement, createReceivableSettlement, createReconciliationStatement, editFinanceFeeSource, getFinanceBill, getFinanceCustomerAccount, getFinanceCustomerAccountSummary, getFinanceCustomerTerms, getFinanceFeeSource, getFinanceSettlement, getReceivableStatistics, getReconciliationStatement, listFinanceAccountLedger, listFinanceAccounts, listFinanceBillRevisions, listFinanceFeePool, listFinanceFeeSources, listFinancePayableSettlements, listFinanceReceivableSettlements, listFinanceStatements, reviewFinanceFeeSource, updateFinanceBill, updateFinanceCustomerTerms, uploadFinanceVoucher, voidFinanceBill } from './finance'
describe('finance api', () => {
beforeEach(() => {
vi.restoreAllMocks()
})
it('queries the dedicated monthly CNY receivable statistics read model', async () => {
const get = vi.spyOn(http, 'get').mockResolvedValue({ code: '200', data: {
statisticsMonth: '2026-07', currency: 'CNY', totalReceivable: '300.000000', totalReceived: '100.000000',
totalOutstanding: '200.000000', totalCustomers: 1, page: 1, pageSize: 20,
records: [{ customerId: '9007199254740993', customerName: '客户A', currency: 'CNY', totalReceivable: '300.000000', totalReceived: '100.000000', totalOutstanding: '200.000000' }],
} })
const result = await getReceivableStatistics({ month: '2026-07', customerId: '9007199254740993', page: 1, pageSize: 20 })
expect(get).toHaveBeenCalledWith('/platform/finance/v1/receivable-statistics', { params: { month: '2026-07', customerId: '9007199254740993', page: 1, pageSize: 20 } })
expect(result).toMatchObject({ currency: 'CNY', totalReceivable: '300.000000', totalReceived: '100.000000', totalOutstanding: '200.000000', records: [{ customerId: '9007199254740993', customerName: '客户A' }] })
})
it('uses only the formal settlement customer summary and keeps money as decimal strings', async () => {
const get = vi.spyOn(http, 'get').mockResolvedValue({ code: '200', data: { customerId: '9007199254740993', currency: 'CNY', totalReceivable: '123456.780000', receivedAmount: '100000.000000', outstandingReceivable: '23456.780000', pendingBilling: '0.000000', outstandingPayable: '0.000000', asOf: '2026-08-13T04:00:00Z', lastChangedAt: null } })
@@ -18,13 +31,13 @@ describe('finance api', () => {
it('uses the customer statement read-model path', async () => {
const get = vi.spyOn(http, 'get').mockResolvedValue({
code: '200', message: 'ok', data: [{ statementId: 1, customerId: 10001, statementNo: 'B-1', customerName: '客户A', periodStart: '2026-08-01', periodEnd: '2026-08-31', creditDays: 30, lineCount: 3, sourceCount: 2, sourceAmount: 120, adjustmentAmount: -20, totalReceivable: 100, receivedAmount: 0, outstandingAmount: 100, settled: false, submittedAt: '2026-08-02T09:00:00', reviewedAt: '2026-08-03T09:00:00', issuedAt: null, status: 'DRAFT', version: 2, createdBy: 7, createdByName: '创建人A', updatedBy: 8, updatedByName: '更新人B' }],
code: '200', message: 'ok', data: [{ statementId: 1, customerId: 10001, statementNo: 'B-1', customerName: '客户A', periodStart: '2026-08-01', periodEnd: '2026-08-31', creditDays: 30, lineCount: 3, sourceCount: 2, sourceAmount: 120, adjustmentAmount: -20, totalReceivable: 100, receivedAmount: 0, outstandingAmount: 100, settled: false, submittedAt: '2026-08-02T09:00:00', reviewedAt: '2026-08-03T09:00:00', issuedAt: null, status: 'DRAFT', version: 2, customerConfirmationStatus: 'NOT_REQUIRED', createdBy: 7, createdByName: '创建人A', updatedBy: 8, updatedByName: '更新人B' }],
})
const result = await listFinanceStatements('10001')
expect(get).toHaveBeenCalledWith('/platform/finance/v1/statements', { params: { customerId: '10001' } })
expect(result[0]).toMatchObject({ id: '1', customerId: '10001', billNo: 'B-1', periodStart: '2026-08-01', periodEnd: '2026-08-31', lineCount: 3, sourceCount: 2, sourceAmount: 120, adjustmentAmount: -20, currency: 'CNY', status: 'DRAFT', version: 2, createdBy: '7', createdByName: '创建人A', updatedBy: '8', updatedByName: '更新人B' })
expect(result[0]).toMatchObject({ id: '1', customerId: '10001', billNo: 'B-1', periodStart: '2026-08-01', periodEnd: '2026-08-31', lineCount: 3, sourceCount: 2, sourceAmount: 120, adjustmentAmount: -20, currency: 'CNY', status: 'DRAFT', version: 2, customerConfirmationStatus: 'NOT_REQUIRED', createdBy: '7', createdByName: '创建人A', updatedBy: '8', updatedByName: '更新人B' })
})
it('never serializes an empty customerId for optional finance list filters', async () => {
@@ -64,16 +77,16 @@ describe('finance api', () => {
it('uses the formal paged fee-source path and preserves independent business fields', async () => {
const get = vi.spyOn(http, 'get').mockResolvedValue({
code: '200', message: 'success', requestId: 'req-fees', traceId: 'trace-fees',
data: [{ feeSourceId: 42, sourceSystem: 'FIRSTLEG', sourceType: 'INBOUND_FEE', sourceBusinessId: 'IB-91', sourceFeeId: 81, sourceVersion: 3, direction: 'PAYABLE', customerId: 10001, customerName: '客户A', supplierId: 71, supplierName: '供应商A', orderId: 11, inboundId: 91, inboundCode: 'IB-91', containerId: 31, containerNo: 'CONT-31', businessDate: '2026-08-11', feeItemName: '运费', quantity: 2, unitPrice: 50, amount: 100, currency: 'CNY', reviewStatus: 'APPROVED', settlementIncluded: true, sourceStatus: 'AVAILABLE', statementGenerated: false }],
data: [{ feeSourceId: 42, sourceSystem: 'FIRSTLEG', sourceType: 'INBOUND_FEE', sourceBusinessId: 'IB-91', sourceFeeId: 81, sourceVersion: 3, direction: 'PAYABLE', customerId: 10001, customerName: '客户A', supplierId: 71, supplierName: '供应商A', orderId: 11, inboundId: 91, inboundCode: 'IB-91', containerId: 31, containerNo: 'CONT-31', businessDate: '2026-08-11', feeItemName: '运费', quantity: 2, unitPrice: 50, amount: 100, currency: 'CNY', reviewStatus: 'APPROVED', sourceStatus: 'AVAILABLE', statementGenerated: false }],
page: { page: 2, pageSize: 50, total: 51 },
})
const result = await listFinanceFeeSources({ page: 2, size: 50, customerId: '10001', direction: 'PAYABLE', status: 'AVAILABLE', reviewStatus: 'APPROVED', settlementIncluded: true, statementGenerated: false, businessDateFrom: '2026-08-01', businessDateTo: '2026-08-31', keyword: ' IB-91 ' })
const result = await listFinanceFeeSources({ page: 2, size: 50, customerId: '10001', direction: 'PAYABLE', status: 'AVAILABLE', reviewStatus: 'APPROVED', statementGenerated: false, businessDateFrom: '2026-08-01', businessDateTo: '2026-08-31', keyword: ' IB-91 ' })
expect(get).toHaveBeenCalledWith('/platform/finance/v1/fee-sources', { params: {
page: 2, pageSize: 50, customerId: '10001', direction: 'PAYABLE', status: 'AVAILABLE', reviewStatus: 'APPROVED', settlementIncluded: true, statementGenerated: false, businessDateFrom: '2026-08-01', businessDateTo: '2026-08-31', keyword: 'IB-91',
page: 2, pageSize: 50, customerId: '10001', direction: 'PAYABLE', status: 'AVAILABLE', reviewStatus: 'APPROVED', statementGenerated: false, businessDateFrom: '2026-08-01', businessDateTo: '2026-08-31', keyword: 'IB-91',
} })
expect(result).toMatchObject({ total: 51, pages: 2, records: [{ feeSourceId: '42', sourceSystem: 'FIRSTLEG', sourceBusinessId: 'IB-91', supplierName: '供应商A', inboundCode: 'IB-91', containerNo: 'CONT-31', businessDate: '2026-08-11', reviewStatus: 'APPROVED', settlementIncluded: true, statementGenerated: false }] })
expect(result).toMatchObject({ total: 51, pages: 2, records: [{ feeSourceId: '42', sourceSystem: 'FIRSTLEG', sourceBusinessId: 'IB-91', supplierName: '供应商A', inboundCode: 'IB-91', containerNo: 'CONT-31', businessDate: '2026-08-11', reviewStatus: 'APPROVED', statementGenerated: false }] })
})
it('uses fixed read-model paths for all five finance pages', async () => {
@@ -114,24 +127,24 @@ describe('finance api', () => {
expect(result.map((item) => item.feeSourceId)).toEqual(['42', null])
})
it('uses the formal G2 detail, review and settlement-inclusion contracts', async () => {
const detail = { feeSourceId: 42, sourceType: 'INBOUND_FEE', sourceFeeId: 81, sourceVersion: 3, direction: 'RECEIVABLE', customerId: 10001, customerName: '客户A', inboundId: 91, inboundCode: 'IB-91', businessDate: '2026-08-11', feeItemName: '运费', quantity: 2, unitPrice: 50, amount: 100, currency: 'CNY', reviewStatus: 'APPROVED', settlementIncluded: true, sourceStatus: 'AVAILABLE', statementGenerated: false, operationVersion: 4, operationLogs: [{ occurredAt: '2026-08-11T12:00:00', action: 'FEE_SOURCE_REVIEW_APPROVE', operatorName: '财务A', result: 'SUCCESS' }] }
it('uses the formal fee-source detail, review and edit contracts', async () => {
const detail = { feeSourceId: 42, sourceType: 'INBOUND_FEE', sourceFeeId: 81, sourceVersion: 3, direction: 'RECEIVABLE', customerId: 10001, customerName: '客户A', inboundId: 91, inboundCode: 'IB-91', businessDate: '2026-08-11', feeItemName: '运费', quantity: 2, unitPrice: 50, amount: 100, currency: 'CNY', reviewStatus: 'APPROVED', sourceStatus: 'AVAILABLE', statementGenerated: false, operationVersion: 4, operationLogs: [{ occurredAt: '2026-08-11T12:00:00', action: 'FEE_SOURCE_REVIEW_APPROVE', operatorName: '财务A', result: 'SUCCESS' }] }
const get = vi.spyOn(http, 'get').mockResolvedValue({ code: '200', message: 'success', requestId: 'req-g2', traceId: 'trace-g2', data: detail })
const post = vi.spyOn(http, 'post').mockResolvedValue({ code: '200', message: 'success', data: detail })
const put = vi.spyOn(http, 'put').mockResolvedValue({ code: '200', message: 'success', data: detail })
const result = await getFinanceFeeSource('42')
await reviewFinanceFeeSource('42', { action: 'APPROVE', reason: '核对无误', expectedVersion: 3 })
await updateFinanceFeeSourceSettlementInclusion('42', { included: true, reason: '本期结算', expectedVersion: 4 })
await editFinanceFeeSource('42', { amount: 120, reason: '更正金额', expectedVersion: 4 })
expect(get).toHaveBeenCalledWith('/platform/finance/v1/fee-sources/42')
expect(post).toHaveBeenCalledWith('/platform/finance/v1/fee-sources/42/review', { action: 'APPROVE', reason: '核对无误', expectedVersion: 3 }, { headers: { 'Idempotency-Key': expect.any(String) } })
expect(put).toHaveBeenCalledWith('/platform/finance/v1/fee-sources/42/settlement-inclusion', { included: true, reason: '本期结算', expectedVersion: 4 }, { headers: { 'Idempotency-Key': expect.any(String) } })
expect(result).toMatchObject({ feeSourceId: '42', sourceId: '81', customerId: '10001', inboundId: '91', reviewStatus: 'APPROVED', settlementIncluded: true, operationVersion: 4, currency: 'CNY' })
expect(put).toHaveBeenCalledWith('/platform/finance/v1/fee-sources/42', { amount: 120, reason: '更正金额', expectedVersion: 4 }, { headers: { 'Idempotency-Key': expect.any(String) } })
expect(result).toMatchObject({ feeSourceId: '42', sourceId: '81', customerId: '10001', inboundId: '91', reviewStatus: 'APPROVED', operationVersion: 4, currency: 'CNY' })
})
it('uses the frozen G3 reconciliation create and action contracts', async () => {
const detail = { statementId: 501, statementNo: 'REC-501', customerId: 10001, customerName: '客户A', periodStart: '2026-08-01', periodEnd: '2026-08-31', creditDays: 30, currency: 'CNY', totalReceivable: 100, receivedAmount: 0, outstandingAmount: 100, settled: false, status: 'DRAFT', version: 0, createdBy: 7, createdByName: '创建人A', createdAt: '2026-08-01T09:00:00', updatedBy: 8, updatedByName: '更新人B', updatedAt: '2026-08-02T10:00:00', lines: [{ statementLineId: 601, feeSourceId: 42, sourceVersion: 3, sourceHash: 'hash-42', direction: 'RECEIVABLE', sourceType: 'CONTAINER_FEE', sourceNo: 'CONT-31', orderId: 11, inboundId: 21, inboundCode: 'IN-21', containerId: 31, containerNo: 'CONT-31', businessDate: '2026-08-15', quantity: 4, unitPrice: 30, feeItemName: '运费', sourceAmount: 120, statementAmount: 100, adjustmentReason: '优惠调整' }], paymentHistory: [{ transactionId: 701, transactionType: 'RECEIPT', transactionDate: '2026-08-02', amount: 10, method: 'BANK_TRANSFER', referenceNo: 'PAY-701', voucherFileIds: [9001], createdBy: 9, createdByName: '收款登记人', createdAt: '2026-08-02T11:00:00' }], revisions: [{ revisionNo: 1, action: 'ADJUST', beforeAmount: 120, afterAmount: 100, reason: '优惠', operatorId: 10, operatorName: '调整人', occurredAt: '2026-08-02T12:00:00' }] }
const detail = { statementId: 501, statementNo: 'REC-501', customerId: 10001, customerName: '客户A', periodStart: '2026-08-01', periodEnd: '2026-08-31', creditDays: 30, currency: 'CNY', totalReceivable: 100, receivedAmount: 0, outstandingAmount: 100, settled: false, status: 'ISSUED', version: 4, customerConfirmationStatus: 'CONFIRMED', customerConfirmedBy: 17, customerConfirmedByName: '客户确认人', customerConfirmedAt: '2026-08-05T10:00:00', createdBy: 7, createdByName: '创建人A', createdAt: '2026-08-01T09:00:00', updatedBy: 8, updatedByName: '更新人B', updatedAt: '2026-08-02T10:00:00', lines: [{ statementLineId: 601, feeSourceId: 42, sourceVersion: 3, sourceHash: 'hash-42', direction: 'RECEIVABLE', sourceType: 'CONTAINER_FEE', sourceNo: 'CONT-31', orderId: 11, inboundId: 21, inboundCode: 'IN-21', containerId: 31, containerNo: 'CONT-31', businessDate: '2026-08-15', quantity: 4, unitPrice: 30, feeItemName: '运费', sourceAmount: 120, statementAmount: 100, adjustmentReason: '优惠调整' }], paymentHistory: [{ transactionId: 701, transactionType: 'RECEIPT', transactionDate: '2026-08-02', amount: 10, method: 'BANK_TRANSFER', referenceNo: 'PAY-701', voucherFileIds: [9001], createdBy: 9, createdByName: '收款登记人', createdAt: '2026-08-02T11:00:00' }], revisions: [{ revisionNo: 1, action: 'ADJUST', beforeAmount: 120, afterAmount: 100, reason: '优惠', operatorId: 10, operatorName: '调整人', occurredAt: '2026-08-02T12:00:00' }] }
const post = vi.spyOn(http, 'post').mockResolvedValue({ code: '200', message: 'success', data: detail })
const get = vi.spyOn(http, 'get').mockResolvedValue({ code: '200', message: 'success', data: detail })
@@ -144,6 +157,7 @@ describe('finance api', () => {
expect(get).toHaveBeenCalledWith('/platform/finance/v1/reconciliation-statements/501')
expect(created).toMatchObject({ id: '501', billNo: 'REC-501', customerId: '10001', totalAmount: 100, createdBy: '7', createdByName: '创建人A', createdAt: '2026-08-01T09:00:00', updatedBy: '8', updatedByName: '更新人B', updatedAt: '2026-08-02T10:00:00', lines: [{ statementLineId: '601', sourceFeeId: '42', sourceHash: 'hash-42', containerId: '31', sourceAmount: 120, amount: 100, adjustmentReason: '优惠调整' }] })
expect(loaded.paymentHistory[0]).toMatchObject({ voucherFileIds: ['9001'], createdBy: '9', createdByName: '收款登记人' })
expect(loaded).toMatchObject({ customerConfirmationStatus: 'CONFIRMED', customerConfirmedBy: '17', customerConfirmedByName: '客户确认人', customerConfirmedAt: '2026-08-05T10:00:00' })
expect(loaded.revisions[0]).toMatchObject({ operatorId: '10', operatorName: '调整人' })
})
@@ -241,11 +255,4 @@ describe('finance api', () => {
expect(post).toHaveBeenNthCalledWith(2, '/platform/finance/v1/bills/81/void', { expectedVersion: 3, reason: '重复账单' }, { headers: { 'Idempotency-Key': expect.any(String) } })
})
it('issues a bill with optimistic version and an idempotency key', async () => {
const post = vi.spyOn(http, 'post').mockResolvedValue({ code: '200', message: 'ok', data: {} })
await issueFinanceBill('81001', 3)
expect(post).toHaveBeenCalledWith('/platform/finance/v1/bills/81001/issue', { expectedVersion: 3 }, { headers: { 'Idempotency-Key': expect.any(String) } })
})
})