2026-08-11 10:45:59 +08:00
|
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
|
import { http } from '@/shared/api/http'
|
2026-08-12 09:25:55 +08:00
|
|
|
import { actionReconciliationStatement, adjustFinanceBill, createFinanceCost, createFinanceReceipt, createFinanceSettlementPayment, createPayableSettlement, createReceivableSettlement, createReconciliationStatement, getFinanceBill, getFinanceCustomerAccount, getFinanceCustomerTerms, getFinanceFeeSource, getFinanceSettlement, getReconciliationStatement, issueFinanceBill, listFinanceAccountLedger, listFinanceAccounts, listFinanceBillRevisions, listFinanceFeePool, listFinancePayableSettlements, listFinanceReceivableSettlements, listFinanceStatements, reviewFinanceFeeSource, updateFinanceBill, updateFinanceCustomerTerms, updateFinanceFeeSourceSettlementInclusion, uploadFinanceVoucher, voidFinanceBill } from './finance'
|
2026-08-11 10:45:59 +08:00
|
|
|
|
|
|
|
|
describe('finance api', () => {
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
vi.restoreAllMocks()
|
|
|
|
|
})
|
|
|
|
|
|
2026-08-11 18:06:35 +08:00
|
|
|
it('uses the customer statement read-model path', async () => {
|
2026-08-11 10:45:59 +08:00
|
|
|
const get = vi.spyOn(http, 'get').mockResolvedValue({
|
2026-08-11 18:06:35 +08:00
|
|
|
code: '200', message: 'ok', data: [{ id: 1, customerId: 10001, billNo: 'B-1', customerName: '客户A', billDate: '2026-08-11', dueDate: '2026-09-10', totalAmount: 100, receivedAmount: 0, outstandingAmount: 100, status: 'DRAFT', version: 2 }],
|
2026-08-11 10:45:59 +08:00
|
|
|
})
|
|
|
|
|
|
2026-08-11 18:06:35 +08:00
|
|
|
const result = await listFinanceStatements('10001')
|
2026-08-11 10:45:59 +08:00
|
|
|
|
2026-08-11 18:06:35 +08:00
|
|
|
expect(get).toHaveBeenCalledWith('/platform/finance/v1/statements', { params: { customerId: '10001' } })
|
|
|
|
|
expect(result[0]).toMatchObject({ id: '1', customerId: '10001', billNo: 'B-1', currency: 'CNY', status: 'DRAFT', version: 2 })
|
2026-08-11 10:45:59 +08:00
|
|
|
})
|
|
|
|
|
|
2026-08-12 17:29:35 +08:00
|
|
|
it('never serializes an empty customerId for optional finance list filters', async () => {
|
|
|
|
|
const get = vi.spyOn(http, 'get').mockResolvedValue({ code: '200', message: 'ok', data: [] })
|
|
|
|
|
|
|
|
|
|
await listFinanceFeePool('')
|
|
|
|
|
await listFinanceStatements(' ')
|
|
|
|
|
await listFinanceReceivableSettlements()
|
|
|
|
|
await listFinancePayableSettlements('')
|
|
|
|
|
|
|
|
|
|
expect(get.mock.calls).toEqual([
|
|
|
|
|
['/platform/finance/v1/fee-pool', { params: { customerId: undefined, direction: undefined } }],
|
|
|
|
|
['/platform/finance/v1/statements', { params: { customerId: undefined } }],
|
|
|
|
|
['/platform/finance/v1/receivable-settlements', { params: { customerId: undefined } }],
|
|
|
|
|
['/platform/finance/v1/payable-settlements', { params: { customerId: undefined } }],
|
|
|
|
|
])
|
|
|
|
|
})
|
|
|
|
|
|
2026-08-11 18:43:56 +08:00
|
|
|
it('uses the formal G1 account list path and fixed page DTO', async () => {
|
|
|
|
|
const get = vi.spyOn(http, 'get').mockResolvedValue({ code: '200', message: 'success', requestId: 'req-g1', traceId: 'trace-g1', data: [{ customerId: 10001, customerCode: 'C-10001', customerName: '客户A', contactName: '张三', contactPhone: '13800000000', outstandingReceivable: 300, pendingBilling: 50, receivedInPeriod: 100, outstandingPayable: 80, status: 'ACTIVE', lastChangedAt: '2026-08-11T10:00:00' }], page: { page: 2, pageSize: 20, total: 21 } })
|
|
|
|
|
|
|
|
|
|
const result = await listFinanceAccounts({ page: 2, size: 20, keyword: '客户A', status: 'ACTIVE' })
|
|
|
|
|
|
|
|
|
|
expect(get).toHaveBeenCalledWith('/platform/finance/v1/accounts', { params: { page: 2, pageSize: 20, keyword: '客户A', status: 'ACTIVE' } })
|
|
|
|
|
expect(result).toEqual({ records: [{ customerId: '10001', customerCode: 'C-10001', customerName: '客户A', contactName: '张三', contactPhone: '13800000000', outstandingReceivable: 300, pendingBilling: 50, receivedInPeriod: 100, outstandingPayable: 80, status: 'ACTIVE', lastChangedAt: '2026-08-11T10:00:00' }], total: 21, pages: 2 })
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('uses the formal G1 account ledger path and fixed page DTO', async () => {
|
|
|
|
|
const get = vi.spyOn(http, 'get').mockResolvedValue({ code: '200', message: 'success', requestId: 'req-ledger', traceId: 'trace-ledger', data: [{ ledgerId: 81, occurredAt: '2026-08-11T12:00:00', businessType: 'RECEIPT', documentType: 'SETTLEMENT', documentId: 91, documentNo: 'AR-91', receivableChange: -100, payableChange: 0, outstandingReceivableAfter: 200, operatorName: '财务A', remark: '人工收款' }], page: { page: 1, pageSize: 50, total: 1 } })
|
|
|
|
|
|
|
|
|
|
const result = await listFinanceAccountLedger('10001', { page: 1, size: 50, businessType: 'RECEIPT', startDate: '2026-08-01', endDate: '2026-08-31' })
|
|
|
|
|
|
|
|
|
|
expect(get).toHaveBeenCalledWith('/platform/finance/v1/accounts/10001/ledger', { params: { page: 1, pageSize: 50, businessType: 'RECEIPT', startDate: '2026-08-01', endDate: '2026-08-31' } })
|
|
|
|
|
expect(result.records[0]).toEqual({ ledgerId: '81', occurredAt: '2026-08-11T12:00:00', businessType: 'RECEIPT', documentType: 'SETTLEMENT', documentId: '91', documentNo: 'AR-91', receivableChange: -100, payableChange: 0, outstandingReceivableAfter: 200, operatorName: '财务A', remark: '人工收款' })
|
|
|
|
|
})
|
|
|
|
|
|
2026-08-11 18:06:35 +08:00
|
|
|
it('uses fixed read-model paths for all five finance pages', async () => {
|
|
|
|
|
const get = vi.spyOn(http, 'get')
|
|
|
|
|
.mockResolvedValueOnce({ code: '200', message: 'ok', data: { customerId: 10001, totalReceivable: 300, totalReceived: 100, outstandingAmount: 200, entries: [] } })
|
2026-08-12 17:29:35 +08:00
|
|
|
.mockResolvedValueOnce({ code: '200', message: 'ok', data: [{ poolItemId: 9, customerId: 10001, direction: 'PAYABLE', sourceType: 'CONTAINER_COST', sourceId: 81, sourceVersion: 2, containerId: 81, feeItemName: '拖车费', amount: 680.5, status: 'OPEN' }] })
|
2026-08-11 18:06:35 +08:00
|
|
|
.mockResolvedValueOnce({ code: '200', message: 'ok', data: [] })
|
|
|
|
|
.mockResolvedValueOnce({ code: '200', message: 'ok', data: [{ id: 81, customerId: 10001, settlementNo: 'AR-81', receivableAmount: 300, receivedAmount: 100, outstandingAmount: 200, status: 'PARTIALLY_PAID', version: 2 }] })
|
|
|
|
|
.mockResolvedValueOnce({ code: '200', message: 'ok', data: [{ id: 71, customerId: 10001, settlementNo: 'AP-71', costType: 'CONTAINER_SHARED', containerId: 81, feeItemName: '拖车费', payableAmount: 680.5, paidAmount: 0, outstandingAmount: 680.5, status: 'UNPAID', version: 1 }] })
|
|
|
|
|
|
|
|
|
|
const account = await getFinanceCustomerAccount('10001')
|
|
|
|
|
const pool = await listFinanceFeePool('10001', 'PAYABLE')
|
|
|
|
|
await listFinanceStatements('10001')
|
|
|
|
|
const receivables = await listFinanceReceivableSettlements('10001')
|
|
|
|
|
const payables = await listFinancePayableSettlements('10001')
|
|
|
|
|
|
|
|
|
|
expect(get.mock.calls.map(([path]) => path)).toEqual([
|
|
|
|
|
'/platform/finance/v1/customer-accounts/10001',
|
|
|
|
|
'/platform/finance/v1/fee-pool',
|
|
|
|
|
'/platform/finance/v1/statements',
|
|
|
|
|
'/platform/finance/v1/receivable-settlements',
|
|
|
|
|
'/platform/finance/v1/payable-settlements',
|
|
|
|
|
])
|
|
|
|
|
expect(account).toMatchObject({ customerId: '10001', currency: 'CNY', accountMode: 'MANUAL_RECEIVABLE_LEDGER', outstandingAmount: 200 })
|
2026-08-12 17:29:35 +08:00
|
|
|
expect(pool[0]).toMatchObject({ poolItemId: '9', customerId: '10001', sourceId: '81', sourceVersion: 2, containerId: '81', feeItemName: '拖车费', amount: 680.5 })
|
2026-08-11 18:06:35 +08:00
|
|
|
expect(receivables[0]).toMatchObject({ id: '81', settlementNo: 'AR-81', currency: 'CNY' })
|
|
|
|
|
expect(payables[0]).toMatchObject({ id: '71', settlementNo: 'AP-71', feeItemName: '拖车费', payableAmount: 680.5, currency: 'CNY' })
|
|
|
|
|
})
|
|
|
|
|
|
2026-08-11 19:35:28 +08:00
|
|
|
it('derives the numeric fee source id without treating manual costs as reviewable sources', async () => {
|
|
|
|
|
vi.spyOn(http, 'get').mockResolvedValue({ code: '200', message: 'ok', data: [
|
2026-08-12 17:29:35 +08:00
|
|
|
{ poolItemId: 'RECEIVABLE:42', customerId: 10001, direction: 'RECEIVABLE', sourceType: 'INBOUND_FEE', sourceId: 81, sourceVersion: 3, feeItemName: '运费', amount: 100, status: 'AVAILABLE' },
|
|
|
|
|
{ poolItemId: 'PAYABLE:43', customerId: 10002, direction: 'PAYABLE', sourceType: 'MANUAL_COST', sourceId: 43, sourceVersion: 1, feeItemName: '拖车费', amount: 200, status: 'POSTED' },
|
2026-08-11 19:35:28 +08:00
|
|
|
] })
|
|
|
|
|
|
|
|
|
|
const result = await listFinanceFeePool('10001')
|
|
|
|
|
|
|
|
|
|
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' }] }
|
|
|
|
|
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 })
|
|
|
|
|
|
|
|
|
|
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' })
|
|
|
|
|
})
|
|
|
|
|
|
2026-08-12 09:25:55 +08:00
|
|
|
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, lines: [{ statementLineId: 601, feeSourceId: 42, sourceVersion: 3, feeItemName: '运费', statementAmount: 100 }], paymentHistory: [], revisions: [] }
|
|
|
|
|
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 })
|
|
|
|
|
|
|
|
|
|
const created = await createReconciliationStatement({ customerId: '10001', currency: 'CNY', generationMode: 'BY_CUTOFF', periodStart: '2026-08-01', periodEnd: '2026-08-31' })
|
|
|
|
|
const loaded = await getReconciliationStatement('501')
|
|
|
|
|
await actionReconciliationStatement('501', { action: 'SUBMIT', expectedVersion: 0 })
|
|
|
|
|
|
|
|
|
|
expect(post.mock.calls[0]).toEqual(['/platform/finance/v1/statements', expect.objectContaining({ customerId: '10001', currency: 'CNY', generationMode: 'BY_CUTOFF' }), { headers: { 'Idempotency-Key': expect.any(String) } }])
|
|
|
|
|
expect(post.mock.calls[1]).toEqual(['/platform/finance/v1/reconciliation-statements/501/actions', { action: 'SUBMIT', expectedVersion: 0 }, { headers: { 'Idempotency-Key': expect.any(String) } }])
|
|
|
|
|
expect(get).toHaveBeenCalledWith('/platform/finance/v1/reconciliation-statements/501')
|
|
|
|
|
expect(created).toMatchObject({ id: '501', billNo: 'REC-501', customerId: '10001', totalAmount: 100, lines: [{ statementLineId: '601', sourceFeeId: '42', amount: 100 }] })
|
|
|
|
|
expect(loaded.revisions).toEqual([])
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('uses the frozen G4 settlement create, detail and payment contracts', async () => {
|
|
|
|
|
const detail = { summary: { settlementId: 701, settlementNo: 'AP-701', customerId: 10001, supplierId: 81, reconciliationStatementId: 501, direction: 'PAYABLE', totalAmount: 200, paidAmount: 0, outstandingAmount: 200, currency: 'CNY', status: 'ISSUED', version: 0 }, lines: [], transactions: [], revisions: [] }
|
|
|
|
|
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 })
|
|
|
|
|
|
|
|
|
|
await createReceivableSettlement({ reconciliationStatementId: '501', currency: 'CNY' })
|
|
|
|
|
await createPayableSettlement({ customerId: '10001', supplierId: '81', feeSourceIds: ['42'], dueDate: '2026-09-10', currency: 'CNY' })
|
|
|
|
|
const loaded = await getFinanceSettlement('701')
|
|
|
|
|
await createFinanceSettlementPayment('701', { transactionDate: '2026-08-11', amount: 200, currency: 'CNY', method: 'BANK_TRANSFER', voucherFileIds: ['9001'], expectedVersion: 0 })
|
|
|
|
|
|
|
|
|
|
expect(post.mock.calls[0]?.[0]).toBe('/platform/finance/v1/receivable-settlements')
|
|
|
|
|
expect(post.mock.calls[1]?.[0]).toBe('/platform/finance/v1/payable-settlements')
|
|
|
|
|
expect(get).toHaveBeenCalledWith('/platform/finance/v1/settlements/701')
|
|
|
|
|
expect(post.mock.calls[2]).toEqual(['/platform/finance/v1/settlements/701/transactions', expect.objectContaining({ amount: 200, voucherFileIds: ['9001'], expectedVersion: 0 }), { headers: { 'Idempotency-Key': expect.any(String) } }])
|
|
|
|
|
expect(loaded).toMatchObject({ id: '701', settlementNo: 'AP-701', customerId: '10001', supplierId: '81', reconciliationStatementId: '501', direction: 'PAYABLE', amount: 200, status: 'ISSUED' })
|
|
|
|
|
})
|
|
|
|
|
|
2026-08-11 18:06:35 +08:00
|
|
|
it('keeps receipt and cost commands on CNY with real voucher ids', async () => {
|
2026-08-11 10:45:59 +08:00
|
|
|
const post = vi.spyOn(http, 'post').mockResolvedValue({ code: '200', message: 'ok', data: {} })
|
|
|
|
|
|
|
|
|
|
await createFinanceReceipt({ customerId: '10001', amount: 100 }, ['90001'])
|
|
|
|
|
await createFinanceCost({ customerId: '10001', amount: 100 }, ['90001'])
|
|
|
|
|
|
|
|
|
|
expect(post.mock.calls[0]?.[1]).toMatchObject({ currency: 'CNY', voucherFileIds: ['90001'] })
|
|
|
|
|
expect(post.mock.calls[1]?.[1]).toMatchObject({ currency: 'CNY', voucherFileIds: ['90001'] })
|
|
|
|
|
expect(post.mock.calls[0]?.[2]).toMatchObject({ headers: { 'Idempotency-Key': expect.any(String) } })
|
|
|
|
|
expect(post.mock.calls[1]?.[2]).toMatchObject({ headers: { 'Idempotency-Key': expect.any(String) } })
|
|
|
|
|
})
|
|
|
|
|
|
2026-08-11 13:55:23 +08:00
|
|
|
it('uploads a real voucher through the Finance Controller proxy', async () => {
|
|
|
|
|
const post = vi.spyOn(http, 'post').mockResolvedValue({ code: '200', message: 'ok', data: { fileId: 90001, tenantId: 10001, fileName: 'receipt.png', contentType: 'image/png', sizeBytes: 128 } })
|
|
|
|
|
const file = new File(['image'], 'receipt.png', { type: 'image/png' })
|
|
|
|
|
|
|
|
|
|
const result = await uploadFinanceVoucher('10001', file)
|
|
|
|
|
|
|
|
|
|
expect(result).toEqual({ fileId: '90001', customerId: '10001', fileName: 'receipt.png', contentType: 'image/png', sizeBytes: 128 })
|
|
|
|
|
expect(post.mock.calls[0]?.[0]).toBe('/platform/finance/v1/vouchers')
|
|
|
|
|
expect(post.mock.calls[0]?.[1]).toBeInstanceOf(FormData)
|
|
|
|
|
expect((post.mock.calls[0]?.[1] as FormData).get('file')).toBe(file)
|
|
|
|
|
expect(post.mock.calls[0]?.[2]).toEqual({ params: { customerId: '10001' }, headers: { 'Content-Type': 'multipart/form-data' } })
|
|
|
|
|
})
|
|
|
|
|
|
2026-08-11 18:06:35 +08:00
|
|
|
it('lets Finance backend obtain the container snapshot from Firstleg', async () => {
|
2026-08-11 13:55:23 +08:00
|
|
|
const post = vi.spyOn(http, 'post').mockResolvedValue({ code: '200', message: 'ok', data: {} })
|
2026-08-11 18:06:35 +08:00
|
|
|
const payload = { customerId: '10001', costType: 'CONTAINER_SHARED', containerId: '81', expenseName: '拖车费', amount: '600.000000' }
|
2026-08-11 13:55:23 +08:00
|
|
|
|
|
|
|
|
await createFinanceCost(payload, ['90001'])
|
|
|
|
|
|
2026-08-11 18:06:35 +08:00
|
|
|
expect(post.mock.calls[0]?.[1]).toEqual({ ...payload, currency: 'CNY', voucherFileIds: ['90001'] })
|
|
|
|
|
expect(post.mock.calls[0]?.[1]).not.toHaveProperty('containerOrders')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('gets and updates the customer credit terms', async () => {
|
|
|
|
|
const get = vi.spyOn(http, 'get').mockResolvedValue({ code: '200', message: 'ok', data: { tenantId: 10001, customerName: '客户A', creditDays: 45, status: 'ACTIVE' } })
|
|
|
|
|
const put = vi.spyOn(http, 'put').mockResolvedValue({ code: '200', message: 'ok', data: { tenantId: 10001, customerName: '客户A', creditDays: 60, status: 'ACTIVE' } })
|
|
|
|
|
|
|
|
|
|
const terms = await getFinanceCustomerTerms('10001')
|
|
|
|
|
const updated = await updateFinanceCustomerTerms('10001', { customerName: '客户A', creditDays: 60 })
|
|
|
|
|
|
|
|
|
|
expect(get).toHaveBeenCalledWith('/platform/finance/v1/customers/10001/terms')
|
|
|
|
|
expect(put).toHaveBeenCalledWith('/platform/finance/v1/customers/10001/terms', { customerName: '客户A', creditDays: 60 }, { headers: { 'Idempotency-Key': expect.any(String) } })
|
|
|
|
|
expect(terms.creditDays).toBe(45)
|
|
|
|
|
expect(updated.creditDays).toBe(60)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('queries and normalizes real bill revision snapshots', async () => {
|
|
|
|
|
const get = vi.spyOn(http, 'get').mockResolvedValue({ code: '200', message: 'ok', data: [{ id: 301, bill_id: 81, version: 2, reason: '金额修正', bill_snapshot: JSON.stringify({ status: 'ISSUED', total_amount: '120.00', received_amount: '20.00', outstanding_amount: '100.00' }), line_snapshot: JSON.stringify([{ source_id: 91, source_version: 2, fee_item_name: '运费', amount: '120.00' }]), created_by: 7, created_at: '2026-08-11T12:00:00' }] })
|
|
|
|
|
|
|
|
|
|
const revisions = await listFinanceBillRevisions('81', '10001')
|
|
|
|
|
|
|
|
|
|
expect(get).toHaveBeenCalledWith('/platform/finance/v1/bills/81/revisions', { params: { customerId: '10001' } })
|
|
|
|
|
expect(revisions[0]).toMatchObject({ id: '301', billId: '81', version: 2, reason: '金额修正', createdBy: '7', status: 'ISSUED', totalAmount: 120, receivedAmount: 20, outstandingAmount: 100, lines: [{ sourceFeeId: '91', sourceFeeVersion: 2, feeItemName: '运费', amount: 120 }] })
|
2026-08-11 13:55:23 +08:00
|
|
|
})
|
|
|
|
|
|
2026-08-11 11:41:47 +08:00
|
|
|
it('normalizes bill details and uses update, adjust and void contracts', async () => {
|
2026-08-11 18:06:35 +08:00
|
|
|
const statement = { id: 81, customerId: 10001, billNo: 'B-81', customerName: '客户A', billDate: '2026-08-11', dueDate: '2026-09-10', totalAmount: 100, receivedAmount: 0, outstandingAmount: 100, status: 'DRAFT', version: 3 }
|
|
|
|
|
const raw = { statement, lines: [{ sourceId: 91, sourceVersion: 2, orderId: 92, inboundId: 93, feeItemName: '运费', amount: 100 }] }
|
2026-08-11 11:41:47 +08:00
|
|
|
vi.spyOn(http, 'get').mockResolvedValue({ code: '200', message: 'ok', data: raw })
|
|
|
|
|
const put = vi.spyOn(http, 'put').mockResolvedValue({ code: '200', message: 'ok', data: raw })
|
|
|
|
|
const post = vi.spyOn(http, 'post').mockResolvedValue({ code: '200', message: 'ok', data: raw })
|
|
|
|
|
|
|
|
|
|
const detail = await getFinanceBill('81')
|
|
|
|
|
await updateFinanceBill('81', { customerId: '10001', expectedVersion: 3, lines: [] })
|
|
|
|
|
await adjustFinanceBill('81', { expectedVersion: 3, reason: '金额修正', lines: [] })
|
|
|
|
|
await voidFinanceBill('81', 3, '重复账单')
|
|
|
|
|
|
2026-08-11 18:06:35 +08:00
|
|
|
expect(detail).toMatchObject({ id: '81', customerId: '10001', version: 3, lines: [{ sourceFeeId: '91', sourceFeeVersion: 2, orderId: '92', inboundId: '93', feeItemName: '运费', amount: 100 }] })
|
2026-08-11 11:41:47 +08:00
|
|
|
expect(put).toHaveBeenCalledWith('/platform/finance/v1/bills/81', expect.objectContaining({ expectedVersion: 3 }), { headers: { 'Idempotency-Key': expect.any(String) } })
|
|
|
|
|
expect(post).toHaveBeenNthCalledWith(1, '/platform/finance/v1/bills/81/adjust', expect.objectContaining({ expectedVersion: 3, reason: '金额修正' }), { headers: { 'Idempotency-Key': expect.any(String) } })
|
|
|
|
|
expect(post).toHaveBeenNthCalledWith(2, '/platform/finance/v1/bills/81/void', { expectedVersion: 3, reason: '重复账单' }, { headers: { 'Idempotency-Key': expect.any(String) } })
|
|
|
|
|
})
|
|
|
|
|
|
2026-08-11 10:45:59 +08:00
|
|
|
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) } })
|
|
|
|
|
})
|
|
|
|
|
})
|