import { beforeEach, describe, expect, it, vi } from 'vitest' import { http } from '@/shared/api/http' import { actionReconciliationStatement, adjustFinanceAccount, adjustFinanceBill, createFinanceCost, createFinanceReceipt, createFinanceSettlementPayment, createPayableSettlement, createReceivableSettlement, createReconciliationStatement, downloadFinanceRechargeVoucher, editFinanceFeeSource, getFinanceBill, getFinanceCustomerAccount, getFinanceCustomerAccountSummary, getFinanceCustomerTerms, getFinanceFeeSource, getFinanceSettlement, getReceivableStatistics, getReconciliationStatement, listFinanceAccountLedger, listFinanceAccounts, listFinanceBillRevisions, listFinanceFeePool, listFinanceFeeSources, listFinancePayableSettlements, listFinanceReceivableSettlements, listFinanceRechargeRequests, listFinanceStatements, reviewFinanceFeeSource, reviewFinanceRechargeRequest, updateFinanceBill, updateFinanceCreditLimit, 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', totalCost: '300.000000', paidCost: '200.000000', outstandingPayable: '100.000000', asOf: '2026-08-13T04:00:00Z', lastChangedAt: null } }) const result = await getFinanceCustomerAccountSummary('9007199254740993') expect(get).toHaveBeenCalledWith('/platform/finance/v1/customer-accounts/9007199254740993/summary') expect(result).toMatchObject({ customerId: '9007199254740993', currency: 'CNY', totalReceivable: '123456.780000', outstandingReceivable: '23456.780000' }) }) 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, 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, customerConfirmationStatus: 'NOT_REQUIRED', createdBy: '7', createdByName: '创建人A', updatedBy: '8', updatedByName: '更新人B' }) }) 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 } }], ]) }) it('includes a supplier filter only for payable fee-pool requests', async () => { const get = vi.spyOn(http, 'get').mockResolvedValue({ code: '200', message: 'ok', data: [] }) await listFinanceFeePool('10001', 'PAYABLE', '301') expect(get).toHaveBeenCalledWith('/platform/finance/v1/fee-pool', { params: { customerId: '10001', direction: 'PAYABLE', supplierId: '301' }, }) }) it('preserves settlement mode from fee-pool candidates for client-side eligibility checks', async () => { vi.spyOn(http, 'get').mockResolvedValue({ code: '200', message: 'ok', data: [{ poolItemId: 'PAYABLE:42', customerId: 10001, direction: 'PAYABLE', sourceType: 'INBOUND_FEE', sourceId: 42, sourceVersion: 1, feeItemName: '进仓费', amount: 33, currency: 'CNY', settlementMode: 'DIRECT_PAID', status: 'AVAILABLE', createdAt: '2026-08-26T00:00:00', }] }) const [candidate] = await listFinanceFeePool('10001', 'PAYABLE', '301') expect(candidate.settlementMode).toBe('DIRECT_PAID') }) 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, totalCost: 260, paidCost: 180, outstandingPayable: 80, status: 'ACTIVE', lastChangedAt: '2026-08-11T10:00:00', actualBalance: '126580.50', frozenBalance: '8500.00', availableBalance: '138080.50', creditLimit: '20000.00', creditUsed: '0.00', availableCredit: '20000.00', creditExpiresOn: '2028-09-10', creditActive: true, accountVersion: 7, pendingRechargeCount: 2 }], page: { page: 2, pageSize: 20, total: 21 } }) const result = await listFinanceAccounts({ page: 2, size: 20, customerId: '10001', status: 'ACTIVE' }) expect(get).toHaveBeenCalledWith('/platform/finance/v1/accounts', { params: { page: 2, pageSize: 20, customerId: '10001', status: 'ACTIVE' } }) expect(result).toEqual({ records: [{ customerId: '10001', customerCode: 'C-10001', customerName: '客户A', contactName: '张三', contactPhone: '13800000000', outstandingReceivable: 300, pendingBilling: 50, receivedInPeriod: 100, totalCost: 260, paidCost: 180, outstandingPayable: 80, status: 'ACTIVE', lastChangedAt: '2026-08-11T10:00:00', actualBalance: '126580.50', frozenBalance: '8500.00', availableBalance: '138080.50', creditLimit: '20000.00', creditUsed: '0.00', availableCredit: '20000.00', creditExpiresOn: '2028-09-10', creditActive: true, accountVersion: 7, pendingRechargeCount: 2 }], 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: '人工收款', businessNo: 'AC202608110001', changeType: 'RECHARGE', amount: '500.00', actualBalanceAfter: '1500.00', frozenBalanceAfter: '100.00', availableBalanceAfter: '1400.00', sourceType: 'MANUAL_ADJUSTMENT', sourceNo: 'RC-91', createdAt: '2026-08-11T12:00:00' }], 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]).toMatchObject({ ledgerId: '81', businessNo: 'AC202608110001', changeType: 'RECHARGE', amount: '500.00', actualBalanceAfter: '1500.00', frozenBalanceAfter: '100.00', availableBalanceAfter: '1400.00', sourceNo: 'RC-91' }) }) it('uses frozen recharge review and account adjustment contracts', async () => { const get = vi.spyOn(http, 'get').mockResolvedValue({ code: '200', message: 'ok', data: [{ requestId: 91, requestNo: 'RC-91', amount: '500.00', paymentMethod: 'WECHAT', voucherFileId: 81, voucherFileName: 'receipt.png', status: 'PENDING', version: 3, submittedByName: '客户A', createdAt: '2026-09-04T10:00:00' }], page: { page: 1, pageSize: 200, total: 1 } }) const post = vi.spyOn(http, 'post') .mockResolvedValueOnce({ code: '200', message: 'ok', data: { requestId: 91, requestNo: 'RC-91', amount: '500.00', paymentMethod: 'WECHAT', voucherFileId: 81, voucherFileName: 'receipt.png', status: 'APPROVED', version: 4 } }) .mockResolvedValueOnce({ code: '200', message: 'ok', data: { ledgerId: 101, businessNo: 'AC-101', changeType: 'DEDUCTION', amount: '-100.00', actualBalanceAfter: '1400.00', frozenBalanceAfter: '100.00', availableBalanceAfter: '1300.00' } }) const requests = await listFinanceRechargeRequests('10001') await reviewFinanceRechargeRequest('91', { decision: 'APPROVE', remark: '凭证有效', expectedVersion: 3 }) const ledger = await adjustFinanceAccount('10001', { type: 'DEDUCTION', amount: '100.00', occurredDate: '2026-09-04', paymentMethod: 'BANK_TRANSFER', referenceNo: 'OUT-1', remark: '贴牌费', expectedAccountVersion: 7 }) expect(get).toHaveBeenCalledWith('/platform/finance/v1/accounts/10001/recharge-requests', { params: { page: 1, pageSize: 200 } }) expect(requests.records[0]).toMatchObject({ requestId: '91', voucherFileId: '81', amount: '500.00', version: 3 }) expect(post).toHaveBeenNthCalledWith(1, '/platform/finance/v1/recharge-requests/91/review', { decision: 'APPROVE', remark: '凭证有效', expectedVersion: 3 }, { headers: { 'Idempotency-Key': expect.any(String) } }) expect(post).toHaveBeenNthCalledWith(2, '/platform/finance/v1/accounts/10001/adjustments', { type: 'DEDUCTION', amount: '100.00', occurredDate: '2026-09-04', paymentMethod: 'BANK_TRANSFER', referenceNo: 'OUT-1', remark: '贴牌费', expectedAccountVersion: 7 }, { headers: { 'Idempotency-Key': expect.any(String) } }) expect(ledger).toMatchObject({ ledgerId: 101, businessNo: 'AC-101', changeType: 'DEDUCTION', amount: '-100.00' }) }) it('updates the customer credit limit with account version and idempotency', async () => { const put = vi.spyOn(http, 'put').mockResolvedValue({ code: '200', message: 'ok', data: { ledgerId: 102, businessNo: 'AC-102', changeType: 'CREDIT_LIMIT', amount: '20000.00', creditLimitAfter: '20000.00', creditUsedAfter: '0.00', availableCreditAfter: '20000.00' } }) const result = await updateFinanceCreditLimit('10001', { creditLimit: '20000.00', creditExpiresOn: '2028-09-10', remark: '初始化授信', expectedAccountVersion: 7 }) expect(put).toHaveBeenCalledWith('/platform/finance/v1/accounts/10001/credit-limit', { creditLimit: '20000.00', creditExpiresOn: '2028-09-10', remark: '初始化授信', expectedAccountVersion: 7 }, { headers: { 'Idempotency-Key': expect.any(String) } }) expect(result).toMatchObject({ ledgerId: 102, changeType: 'CREDIT_LIMIT', creditLimitAfter: '20000.00', availableCreditAfter: '20000.00' }) }) it('downloads a recharge voucher as an authenticated blob', async () => { const blob = new Blob(['image'], { type: 'image/png' }) const get = vi.spyOn(http, 'get').mockResolvedValue(blob) await expect(downloadFinanceRechargeVoucher('91/2')).resolves.toBe(blob) expect(get).toHaveBeenCalledWith('/platform/finance/v1/recharge-requests/91%2F2/voucher', { responseType: 'blob' }) }) 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', 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', 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', 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', statementGenerated: false }] }) }) 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: [] } }) .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' }] }) .mockResolvedValueOnce({ code: '200', message: 'ok', data: [] }) .mockResolvedValueOnce({ code: '200', message: 'ok', data: [{ settlementId: 81, customerId: 10001, settlementNo: 'AR-81', direction: 'RECEIVABLE', totalAmount: 300, paidAmount: 100, outstandingAmount: 200, generatedDate: '2026-08-11', status: 'PARTIALLY_PAID', version: 2 }] }) .mockResolvedValueOnce({ code: '200', message: 'ok', data: [{ settlementId: 71, customerId: 10001, settlementNo: 'AP-71', direction: 'PAYABLE', supplierName: '供应商A', totalAmount: 680.5, paidAmount: 0, outstandingAmount: 680.5, generatedDate: '2026-08-11', status: 'ISSUED', 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 }) expect(pool[0]).toMatchObject({ poolItemId: '9', customerId: '10001', sourceId: '81', sourceVersion: 2, containerId: '81', feeItemName: '拖车费', amount: 680.5 }) expect(receivables[0]).toMatchObject({ id: '81', settlementNo: 'AR-81', currency: 'CNY' }) expect(payables[0]).toMatchObject({ id: '71', settlementNo: 'AP-71', supplierName: '供应商A', payableAmount: 680.5, currency: 'CNY' }) }) 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: [ { 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' }, ] }) const result = await listFinanceFeePool('10001') expect(result.map((item) => item.feeSourceId)).toEqual(['42', null]) }) 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 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', { 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: '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 }) 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, 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: '调整人' }) }) 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: [{ transactionId: 801, transactionType: 'PAYMENT', transactionDate: '2026-08-11', amount: 20, method: 'BANK_TRANSFER', referenceNo: 'PAY-801', voucherFileIds: [9001], createdBy: 11, createdByName: '付款登记人', createdAt: '2026-08-11T10:00:00' }], revisions: [{ revisionNo: 1, action: 'VOID', beforeAmount: 200, afterAmount: 200, reason: '测试', operatorId: 12, operatorName: '作废操作人', occurredAt: '2026-08-11T11: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 }) 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' }) expect(loaded.transactions[0]).toMatchObject({ referenceNo: 'PAY-801', createdBy: '11', createdByName: '付款登记人', createdAt: '2026-08-11T10:00:00' }) expect(loaded.revisions[0]).toMatchObject({ operatorId: '12', operatorName: '作废操作人' }) }) it('keeps receipt and cost commands on CNY with real voucher ids', async () => { 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) } }) }) 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' } }) }) it('lets Finance backend obtain the container snapshot from Firstleg', async () => { const post = vi.spyOn(http, 'post').mockResolvedValue({ code: '200', message: 'ok', data: {} }) const payload = { customerId: '10001', costType: 'CONTAINER_SHARED', containerId: '81', expenseName: '拖车费', amount: '600.000000' } await createFinanceCost(payload, ['90001']) 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 }] }) }) it('normalizes bill details and uses update, adjust and void contracts', async () => { 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 }] } 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, '重复账单') expect(detail).toMatchObject({ id: '81', customerId: '10001', version: 3, lines: [{ sourceFeeId: '91', sourceFeeVersion: 2, orderId: '92', inboundId: '93', feeItemName: '运费', amount: 100 }] }) 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) } }) }) })