This commit is contained in:
2026-08-11 18:43:56 +08:00
parent 6456278ec8
commit 6622815d6c
37 changed files with 739 additions and 205 deletions
+19 -1
View File
@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { http } from '@/shared/api/http'
import { adjustFinanceBill, createFinanceCost, createFinanceReceipt, getFinanceBill, getFinanceCustomerAccount, getFinanceCustomerTerms, issueFinanceBill, listFinanceBillRevisions, listFinanceFeePool, listFinancePayableSettlements, listFinanceReceivableSettlements, listFinanceStatements, updateFinanceBill, updateFinanceCustomerTerms, uploadFinanceVoucher, voidFinanceBill } from './finance'
import { adjustFinanceBill, createFinanceCost, createFinanceReceipt, getFinanceBill, getFinanceCustomerAccount, getFinanceCustomerTerms, issueFinanceBill, listFinanceAccountLedger, listFinanceAccounts, listFinanceBillRevisions, listFinanceFeePool, listFinancePayableSettlements, listFinanceReceivableSettlements, listFinanceStatements, updateFinanceBill, updateFinanceCustomerTerms, uploadFinanceVoucher, voidFinanceBill } from './finance'
describe('finance api', () => {
beforeEach(() => {
@@ -18,6 +18,24 @@ describe('finance api', () => {
expect(result[0]).toMatchObject({ id: '1', customerId: '10001', billNo: 'B-1', currency: 'CNY', status: 'DRAFT', version: 2 })
})
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: '人工收款' })
})
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: [] } })
+17 -14
View File
@@ -3,8 +3,8 @@ import type { FinanceAccountEntry, FinanceAccountSummary, FinanceBillDetail, Fin
const basePath = '/platform/finance/v1'
export const financePhase1Capabilities = {
accountList: false,
accountLedger: false,
accountList: true,
accountLedger: true,
feeSourceDetail: false,
feeSourceReview: false,
feeSourceSettlementInclusion: false,
@@ -74,24 +74,27 @@ export async function listFinanceFeePool(customerId: string, direction?: 'RECEIV
}
export async function listFinanceAccounts(params: { page?: number; size?: number; keyword?: string; status?: string } = {}): Promise<{ records: FinanceAccountSummary[]; total: number; pages: number }> {
const response = await http.get<ApiEnvelope<RawResource>, ApiEnvelope<RawResource>>(`${basePath}/accounts`, { params })
const data = unwrapApiResponse(response) as RawResource
const records = (Array.isArray(data.records) ? data.records : Array.isArray(data.items) ? data.items : []).map((item) => ({ customerId: stringValue(item.customerId ?? item.customer_id), customerName: stringValue(item.customerName ?? item.customer_name), customerStatus: stringValue(item.customerStatus ?? item.status), currency: 'CNY' as const, totalReceivable: numberValue(item.totalReceivable ?? item.total_receivable), receivedAmount: numberValue(item.receivedAmount ?? item.totalReceived ?? item.received_amount), outstandingAmount: numberValue(item.outstandingAmount ?? item.outstanding_amount), creditDays: numberValue(item.creditDays ?? item.credit_days ?? 30) }))
return { records, total: numberValue(data.total ?? records.length), pages: numberValue(data.pages ?? 1) || 1 }
const query = { page: params.page ?? 1, pageSize: params.size ?? 20, keyword: params.keyword || undefined, status: params.status || undefined }
const response = await http.get<ApiEnvelope<RawResource[]> & { page?: RawResource }, ApiEnvelope<RawResource[]> & { page?: RawResource }>(`${basePath}/accounts`, { params: query })
const data = unwrapApiResponse(response)
const records = 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), outstandingPayable: numberValue(item.outstandingPayable), status: stringValue(item.status), lastChangedAt: stringValue(item.lastChangedAt) }))
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 listFinanceAccountLedger(customerId: string, params: { page?: number; size?: number; businessType?: string; startDate?: string; endDate?: string } = {}): Promise<{ records: FinanceLedgerEntry[]; total: number; pages: number }> {
const response = await http.get<ApiEnvelope<RawResource>, ApiEnvelope<RawResource>>(`${basePath}/accounts/${customerId}/ledger`, { params })
const data = unwrapApiResponse(response) as RawResource
const records = (Array.isArray(data.records) ? data.records : Array.isArray(data.items) ? data.items : []).map((item) => ({ entryId: stringValue(item.entryId ?? item.id), businessType: stringValue(item.businessType), resourceId: stringValue(item.resourceId), resourceNo: stringValue(item.resourceNo), occurredAt: stringValue(item.occurredAt), debitAmount: numberValue(item.debitAmount), creditAmount: numberValue(item.creditAmount), balance: numberValue(item.balance), currency: 'CNY' as const, status: stringValue(item.status) }))
return { records, total: numberValue(data.total ?? records.length), pages: numberValue(data.pages ?? 1) || 1 }
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<ApiEnvelope<RawResource[]> & { page?: RawResource }, ApiEnvelope<RawResource[]> & { 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 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 getFinanceFeeSource(feeSourceId: string): Promise<FinanceFeeSourceDetail> {
const response = await http.get<ApiEnvelope<RawResource>, ApiEnvelope<RawResource>>(`${basePath}/fee-sources/${feeSourceId}`)
const item = unwrapApiResponse(response)
const pool = (await Promise.resolve([item])).map((raw) => ({ poolItemId: stringValue(raw.poolItemId ?? raw.id), direction: stringValue(raw.direction) as FinanceFeePoolItem['direction'], sourceType: stringValue(raw.sourceType), sourceId: stringValue(raw.sourceId), sourceVersion: numberValue(raw.sourceVersion) || 1, orderId: raw.orderId == null ? null : stringValue(raw.orderId), inboundId: raw.inboundId == null ? null : stringValue(raw.inboundId), containerId: raw.containerId == null ? null : stringValue(raw.containerId), feeItemName: stringValue(raw.feeItemName), amount: numberValue(raw.amount), currency: 'CNY' as const, status: stringValue(raw.status), createdAt: stringValue(raw.createdAt) }))[0]
return { ...pool, reviewStatus: stringValue(item.reviewStatus), settlementIncluded: Boolean(item.settlementIncluded), sourceStatus: stringValue(item.sourceStatus), reviewReason: stringValue(item.reviewReason), settlementReason: stringValue(item.settlementReason), sourceHash: stringValue(item.sourceHash), operationLogs: Array.isArray(item.operationLogs) ? item.operationLogs as FinanceFeeSourceDetail['operationLogs'] : [], version: numberValue(item.version) }
throw new FinanceCapabilityBlockedError('getFinanceFeeSource')
}
export async function reviewFinanceFeeSource(feeSourceId: string, payload: { action: 'APPROVE' | 'REJECT'; reason?: string; expectedVersion: number }) { throw new FinanceCapabilityBlockedError('reviewFinanceFeeSource') }