From 68fca1bd9fdbb75949846a2a0998e1c7c468fa69 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Sat, 5 Sep 2026 08:34:40 +0800 Subject: [PATCH] feat(finance): manage customer wallet balances --- src/modules/finance/AGENTS.md | 7 +- src/modules/finance/api/finance.test.ts | 34 ++- src/modules/finance/api/finance.ts | 62 ++++- src/modules/finance/financeDelivery.test.ts | 11 +- .../pages/FinanceCustomerAccountsView.test.ts | 121 ++++++-- .../pages/FinanceCustomerAccountsView.vue | 263 +++++++++++++++--- src/modules/finance/types.ts | 60 +++- 7 files changed, 487 insertions(+), 71 deletions(-) diff --git a/src/modules/finance/AGENTS.md b/src/modules/finance/AGENTS.md index 08148976..1f4e2778 100644 --- a/src/modules/finance/AGENTS.md +++ b/src/modules/finance/AGENTS.md @@ -4,7 +4,12 @@ - 本文件适用于 `src/modules/finance/`。产品范围和状态读取 `../../../../../docs/财务管理/` 的产品确认与验收契约,后端读取 `../../../../../baiyue-firstleg/finance/AGENTS.md`。 - 页面从 `pages/` 进入,统一 API 从 `api/finance.ts` 进入;费用来源和柜次成本快照联查 `firstleg/`,凭证与异步导出联查 `file/`。 -- 正式范围包括客户账户、客户应收应付、应收款统计、客户对账单、应收结算单和应付结算单;不要从历史原型自动扩大财务功能。 +- 正式范围包括客户资金账户、客户应收应付、应收款统计、客户对账单、应收结算单和应付结算单。资金账户当前只接充值申请审核和平台手工充值/扣款,冻结/解冻、消费及支付回调后续接入;不要从历史原型自动扩大其他财务功能。 + +## 客户资金账户 + +- `pages/FinanceCustomerAccountsView.vue` 的列表和详情展示实际余额、冻结金额、可用余额;操作列固定第一列。客户上传的充值申请在详情内按待审核优先展示,审核、驳回及凭证预览以服务端申请版本和客户归属为准。 +- 平台手工充值和扣款必须提交账户版本与幂等键;扣款不得超过服务端可用余额。页面只展示服务端余额和流水结果,不在浏览器补算或回退静态数据。 ## 客户筛选与统计 diff --git a/src/modules/finance/api/finance.test.ts b/src/modules/finance/api/finance.test.ts index 54d30f27..9cc5e3c0 100644 --- a/src/modules/finance/api/finance.test.ts +++ b/src/modules/finance/api/finance.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { http } from '@/shared/api/http' -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' +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, updateFinanceCustomerTerms, uploadFinanceVoucher, voidFinanceBill } from './finance' describe('finance api', () => { beforeEach(() => { @@ -79,21 +79,45 @@ describe('finance api', () => { }) 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' }], page: { page: 2, pageSize: 20, total: 21 } }) + 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: '118080.50', 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' }], total: 21, pages: 2 }) + 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: '118080.50', 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: '人工收款' }], page: { page: 1, pageSize: 50, total: 1 } }) + 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]).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: '人工收款' }) + 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('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 () => { diff --git a/src/modules/finance/api/finance.ts b/src/modules/finance/api/finance.ts index 28e466b4..7371f2c2 100644 --- a/src/modules/finance/api/finance.ts +++ b/src/modules/finance/api/finance.ts @@ -1,5 +1,5 @@ import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http' -import type { CreatePayableSettlementPayload, CreateReceivableSettlementPayload, CreateReconciliationPayload, CreateSettlementPaymentPayload, FinanceAccountEntry, FinanceAccountSummary, FinanceBillDetail, FinanceBillLine, FinanceBillResource, FinanceBillRevision, FinanceCostResource, FinanceCustomerAccountReadModel, FinanceCustomerTerms, FinanceFeePoolItem, FinanceFeeSourceDetail, FinanceFeeSourceSummary, FinanceLedgerEntry, FinancePayableSettlement, FinanceReceivableSettlement, FinanceReceiptResource, FinanceReconciliationDetail, FinanceSettlementDetail, FinanceSettlementLine, FinanceStatementReadModel, FinanceVoucherResource, ReceivableStatisticsResult, ReconciliationActionPayload } from '../types' +import type { CreatePayableSettlementPayload, CreateReceivableSettlementPayload, CreateReconciliationPayload, CreateSettlementPaymentPayload, FinanceAccountAdjustmentPayload, FinanceAccountEntry, FinanceAccountSummary, FinanceBillDetail, FinanceBillLine, FinanceBillResource, FinanceBillRevision, FinanceCostResource, FinanceCustomerAccountReadModel, FinanceCustomerTerms, FinanceFeePoolItem, FinanceFeeSourceDetail, FinanceFeeSourceSummary, FinanceLedgerEntry, FinancePayableSettlement, FinanceRechargeRequest, FinanceRechargeReviewPayload, FinanceReceivableSettlement, FinanceReceiptResource, FinanceReconciliationDetail, FinanceSettlementDetail, FinanceSettlementLine, FinanceStatementReadModel, FinanceVoucherResource, ReceivableStatisticsResult, ReconciliationActionPayload } from '../types' const basePath = '/platform/finance/v1' export interface FinanceCustomerAccountSummaryResource { @@ -53,6 +53,7 @@ function idempotencyKey(operation: string) { return `finance-${operation}-${Date type RawResource = Record function stringValue(value: unknown) { return value == null ? '' : String(value) } function numberValue(value: unknown) { return Number(value ?? 0) } +function nullableStringValue(value: unknown) { return value == null ? null : String(value) } function jsonRecord(value: unknown): RawResource { if (value && typeof value === 'object' && !Array.isArray(value)) return value as RawResource if (typeof value !== 'string' || !value.trim()) return {} @@ -126,22 +127,77 @@ export async function listFinanceAccounts(params: { page?: number; size?: number const query = { page: params.page ?? 1, pageSize: params.size ?? 20, customerId: params.customerId || undefined, status: params.status || undefined } const response = await http.get & { page?: RawResource }, ApiEnvelope & { page?: RawResource }>(`${basePath}/accounts`, { params: query }) const data = unwrapApiResponse(response) - const records: FinanceAccountSummary[] = data.map((item) => ({ customerId: stringValue(item.customerId), customerCode: stringValue(item.customerCode), customerName: stringValue(item.customerName), contactName: stringValue(item.contactName), contactPhone: stringValue(item.contactPhone), outstandingReceivable: numberValue(item.outstandingReceivable), pendingBilling: numberValue(item.pendingBilling), receivedInPeriod: numberValue(item.receivedInPeriod), totalCost: numberValue(item.totalCost), paidCost: numberValue(item.paidCost), outstandingPayable: numberValue(item.outstandingPayable), status: stringValue(item.status), lastChangedAt: stringValue(item.lastChangedAt) })) + const records: FinanceAccountSummary[] = data.map(normalizeFinanceAccount) const total = numberValue(response.page?.total ?? records.length) const pageSize = numberValue(response.page?.pageSize ?? query.pageSize) || query.pageSize return { records, total, pages: Math.max(1, Math.ceil(total / pageSize)) } } +function normalizeFinanceAccount(item: RawResource): FinanceAccountSummary { + return { + customerId: stringValue(item.customerId), customerCode: stringValue(item.customerCode), customerName: stringValue(item.customerName), + contactName: stringValue(item.contactName), contactPhone: stringValue(item.contactPhone), outstandingReceivable: numberValue(item.outstandingReceivable), + pendingBilling: numberValue(item.pendingBilling), receivedInPeriod: numberValue(item.receivedInPeriod), totalCost: numberValue(item.totalCost), + paidCost: numberValue(item.paidCost), outstandingPayable: numberValue(item.outstandingPayable), status: stringValue(item.status), + lastChangedAt: stringValue(item.lastChangedAt), actualBalance: stringValue(item.actualBalance), frozenBalance: stringValue(item.frozenBalance), + availableBalance: stringValue(item.availableBalance), accountVersion: numberValue(item.accountVersion), pendingRechargeCount: numberValue(item.pendingRechargeCount), + } +} + export async function listFinanceAccountLedger(customerId: string, params: { page?: number; size?: number; businessType?: string; startDate?: string; endDate?: string } = {}): Promise<{ records: FinanceLedgerEntry[]; total: number; pages: number }> { const query = { page: params.page ?? 1, pageSize: params.size ?? 20, businessType: params.businessType || undefined, startDate: params.startDate || undefined, endDate: params.endDate || undefined } const response = await http.get & { page?: RawResource }, ApiEnvelope & { page?: RawResource }>(`${basePath}/accounts/${customerId}/ledger`, { params: query }) const data = unwrapApiResponse(response) - const records = data.map((item) => ({ ledgerId: stringValue(item.ledgerId), occurredAt: stringValue(item.occurredAt), businessType: stringValue(item.businessType), documentType: stringValue(item.documentType), documentId: stringValue(item.documentId), documentNo: stringValue(item.documentNo), receivableChange: numberValue(item.receivableChange), payableChange: numberValue(item.payableChange), outstandingReceivableAfter: numberValue(item.outstandingReceivableAfter), operatorName: stringValue(item.operatorName), remark: stringValue(item.remark) })) + const records: FinanceLedgerEntry[] = data.map((item) => ({ + ledgerId: stringValue(item.ledgerId), occurredAt: stringValue(item.occurredAt), businessType: stringValue(item.businessType), + documentType: stringValue(item.documentType), documentId: stringValue(item.documentId), documentNo: stringValue(item.documentNo), + receivableChange: numberValue(item.receivableChange), payableChange: numberValue(item.payableChange), + outstandingReceivableAfter: numberValue(item.outstandingReceivableAfter), operatorName: stringValue(item.operatorName), remark: stringValue(item.remark), + businessNo: nullableStringValue(item.businessNo), changeType: nullableStringValue(item.changeType), amount: nullableStringValue(item.amount), + actualBalanceAfter: nullableStringValue(item.actualBalanceAfter), frozenBalanceAfter: nullableStringValue(item.frozenBalanceAfter), + availableBalanceAfter: nullableStringValue(item.availableBalanceAfter), sourceType: nullableStringValue(item.sourceType), + sourceNo: nullableStringValue(item.sourceNo), occurredDate: nullableStringValue(item.occurredDate), + paymentMethod: nullableStringValue(item.paymentMethod), referenceNo: nullableStringValue(item.referenceNo), + createdAt: nullableStringValue(item.createdAt), + })) const total = numberValue(response.page?.total ?? records.length) const pageSize = numberValue(response.page?.pageSize ?? query.pageSize) || query.pageSize return { records, total, pages: Math.max(1, Math.ceil(total / pageSize)) } } +function normalizeRechargeRequest(item: RawResource): FinanceRechargeRequest { + return { + requestId: stringValue(item.requestId), requestNo: stringValue(item.requestNo), amount: stringValue(item.amount), + paymentMethod: stringValue(item.paymentMethod), voucherFileId: stringValue(item.voucherFileId), voucherFileName: stringValue(item.voucherFileName), + status: stringValue(item.status), version: numberValue(item.version), submittedByName: stringValue(item.submittedByName), + reviewedByName: stringValue(item.reviewedByName), reviewRemark: stringValue(item.reviewRemark), createdAt: stringValue(item.createdAt), + reviewedAt: stringValue(item.reviewedAt), + } +} + +export async function listFinanceRechargeRequests(customerId: string, params: { page?: number; size?: number } = {}): Promise<{ records: FinanceRechargeRequest[]; total: number; pages: number }> { + const query = { page: params.page ?? 1, pageSize: params.size ?? 200 } + const response = await http.get & { page?: RawResource }, ApiEnvelope & { page?: RawResource }>(`${basePath}/accounts/${customerId}/recharge-requests`, { params: query }) + const records = unwrapApiResponse(response).map(normalizeRechargeRequest) + const total = numberValue(response.page?.total ?? records.length) + const pageSize = numberValue(response.page?.pageSize ?? query.pageSize) || query.pageSize + return { records, total, pages: Math.max(1, Math.ceil(total / pageSize)) } +} + +export async function reviewFinanceRechargeRequest(requestId: string, payload: FinanceRechargeReviewPayload) { + const response = await http.post, ApiEnvelope>(`${basePath}/recharge-requests/${requestId}/review`, payload, mutationHeaders('recharge-review')) + return normalizeRechargeRequest(unwrapApiResponse(response)) +} + +export function downloadFinanceRechargeVoucher(requestId: string) { + return http.get(`${basePath}/recharge-requests/${encodeURIComponent(requestId)}/voucher`, { responseType: 'blob' }) +} + +export async function adjustFinanceAccount(customerId: string, payload: FinanceAccountAdjustmentPayload) { + const response = await http.post, ApiEnvelope>(`${basePath}/accounts/${customerId}/adjustments`, payload, mutationHeaders('account-adjustment')) + return unwrapApiResponse(response) +} + export async function listFinanceFeeSources(params: { page?: number size?: number diff --git a/src/modules/finance/financeDelivery.test.ts b/src/modules/finance/financeDelivery.test.ts index 0effe20b..6b1daa0f 100644 --- a/src/modules/finance/financeDelivery.test.ts +++ b/src/modules/finance/financeDelivery.test.ts @@ -63,11 +63,12 @@ describe('finance six-page delivery contract', () => { expect(page).toContain('options: customers.value.filter') expect(page).toContain('label: item.tenantName') expect(page).not.toContain('tenantCode') - expect(types).toContain('export interface FinanceOperatingFundsSummary') - expect(types).toContain('export interface FinanceAccountSummary extends FinanceOperatingFundsSummary') - expect(api).toContain('const records: FinanceAccountSummary[]') - for (const metric of ['待收客户款', '待付供应商款', '已线下支付供应商款']) expect(page).toContain(metric) - for (const forbidden of ['客户线下支付', '已收客户款', '已付成本']) expect(page).not.toContain(forbidden) + expect(types).toContain('export interface FinanceAccountSummary') + expect(types).toContain('actualBalance: string') + expect(types).toContain('export interface FinanceRechargeRequest') + expect(api).toContain('data.map(normalizeFinanceAccount)') + for (const metric of ['实际余额', '冻结金额', '可用余额']) expect(page).toContain(metric) + for (const operation of ['listFinanceRechargeRequests', 'reviewFinanceRechargeRequest', 'adjustFinanceAccount']) expect(page).toContain(operation) const columns = page.slice(page.indexOf('const columns: DataTableColumn[]')) expect(columns.indexOf("key: 'actions'")).toBeLessThan(columns.indexOf("key: 'customerName'")) for (const state of ['客户账户加载失败', '暂无客户账户', '无权访问客户账户', '账户流水加载失败']) expect(page).toContain(state) diff --git a/src/modules/finance/pages/FinanceCustomerAccountsView.test.ts b/src/modules/finance/pages/FinanceCustomerAccountsView.test.ts index fcc49164..0f1be3c0 100644 --- a/src/modules/finance/pages/FinanceCustomerAccountsView.test.ts +++ b/src/modules/finance/pages/FinanceCustomerAccountsView.test.ts @@ -10,12 +10,16 @@ import { useAuthStore } from '@/core/auth/auth' import { createPlatformExportTask } from '@/modules/downloads' import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam' import { showExportTaskCreatedDialog } from '@/shared/components/exportTaskCreatedDialog' -import { listFinanceAccountLedger, listFinanceAccounts } from '../api/finance' +import { adjustFinanceAccount, downloadFinanceRechargeVoucher, listFinanceAccountLedger, listFinanceAccounts, listFinanceRechargeRequests, reviewFinanceRechargeRequest } from '../api/finance' import FinanceCustomerAccountsView from './FinanceCustomerAccountsView.vue' vi.mock('../api/finance', () => ({ listFinanceAccounts: vi.fn(), listFinanceAccountLedger: vi.fn(), + listFinanceRechargeRequests: vi.fn(), + reviewFinanceRechargeRequest: vi.fn(), + adjustFinanceAccount: vi.fn(), + downloadFinanceRechargeVoucher: vi.fn(), getFinanceCustomerTerms: vi.fn(), updateFinanceCustomerTerms: vi.fn(), })) @@ -30,6 +34,13 @@ function memoryStorage(): Storage { return { get length() { return values.size }, clear: () => values.clear(), getItem: (key) => values.get(key) ?? null, key: (index) => [...values.keys()][index] ?? null, removeItem: (key) => values.delete(key), setItem: (key, value) => values.set(key, value) } } +const accountFixture = { + customerId: '10001', customerCode: 'C-001', customerName: '客户A', contactName: '张三', contactPhone: '13800000000', + outstandingReceivable: 100, pendingBilling: 20, receivedInPeriod: 30, totalCost: 90, paidCost: 50, outstandingPayable: 40, + actualBalance: '126580.50', frozenBalance: '8500.00', availableBalance: '118080.50', accountVersion: 7, pendingRechargeCount: 1, + status: 'ACTIVE', lastChangedAt: '2026-08-13 10:00:00', +} + async function mountAccountsView(permissions: string[], initialPath = '/finance/customer-accounts') { const pinia = createPinia(); setActivePinia(pinia) const auth = useAuthStore(); auth.token = 'token'; auth.permissions = permissions @@ -54,7 +65,7 @@ describe('FinanceCustomerAccountsView delivery', () => { expect(page).toContain('返回客户账户') expect(page).toContain('