优化
This commit is contained in:
@@ -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, getFinanceFeeSource, issueFinanceBill, listFinanceAccountLedger, listFinanceAccounts, listFinanceBillRevisions, listFinanceFeePool, listFinancePayableSettlements, listFinanceReceivableSettlements, listFinanceStatements, reviewFinanceFeeSource, updateFinanceBill, updateFinanceCustomerTerms, updateFinanceFeeSourceSettlementInclusion, uploadFinanceVoucher, voidFinanceBill } from './finance'
|
||||
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'
|
||||
|
||||
describe('finance api', () => {
|
||||
beforeEach(() => {
|
||||
@@ -90,6 +90,39 @@ describe('finance api', () => {
|
||||
expect(result).toMatchObject({ feeSourceId: '42', sourceId: '81', customerId: '10001', inboundId: '91', reviewStatus: 'APPROVED', settlementIncluded: true, 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, 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' })
|
||||
})
|
||||
|
||||
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: {} })
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
|
||||
import type { FinanceAccountEntry, FinanceAccountSummary, FinanceBillDetail, FinanceBillLine, FinanceBillResource, FinanceBillRevision, FinanceCostResource, FinanceCustomerAccountReadModel, FinanceCustomerTerms, FinanceFeePoolItem, FinanceFeeSourceDetail, FinanceLedgerEntry, FinancePayableSettlement, FinanceReceivableSettlement, FinanceReceiptResource, FinanceSettlementDetail, FinanceStatementReadModel, FinanceVoucherResource } from '../types'
|
||||
import type { CreatePayableSettlementPayload, CreateReceivableSettlementPayload, CreateReconciliationPayload, CreateSettlementPaymentPayload, FinanceAccountEntry, FinanceAccountSummary, FinanceBillDetail, FinanceBillLine, FinanceBillResource, FinanceBillRevision, FinanceCostResource, FinanceCustomerAccountReadModel, FinanceCustomerTerms, FinanceFeePoolItem, FinanceFeeSourceDetail, FinanceLedgerEntry, FinancePayableSettlement, FinanceReceivableSettlement, FinanceReceiptResource, FinanceReconciliationDetail, FinanceSettlementDetail, FinanceStatementReadModel, FinanceVoucherResource, ReconciliationActionPayload } from '../types'
|
||||
|
||||
const basePath = '/platform/finance/v1'
|
||||
export const financePhase1Capabilities = {
|
||||
@@ -8,20 +8,15 @@ export const financePhase1Capabilities = {
|
||||
feeSourceDetail: true,
|
||||
feeSourceReview: true,
|
||||
feeSourceSettlementInclusion: true,
|
||||
reconciliationCreate: false,
|
||||
reconciliationActions: false,
|
||||
receivableSettlementCreate: false,
|
||||
payableSettlementCreate: false,
|
||||
settlementDetail: false,
|
||||
settlementPayment: false,
|
||||
reconciliationCreate: true,
|
||||
reconciliationActions: true,
|
||||
receivableSettlementCreate: true,
|
||||
payableSettlementCreate: true,
|
||||
settlementDetail: true,
|
||||
settlementPayment: true,
|
||||
} as const
|
||||
function idempotencyKey(operation: string) { return `finance-${operation}-${Date.now()}-${Math.random().toString(36).slice(2)}` }
|
||||
type RawResource = Record<string, unknown>
|
||||
export class FinanceCapabilityBlockedError extends Error {
|
||||
readonly code = 'FINANCE_NOT_IMPLEMENTED'
|
||||
constructor(operation: string) { super(`接口尚未提供,暂不可操作:${operation}`); this.name = 'FinanceCapabilityBlockedError' }
|
||||
}
|
||||
|
||||
function stringValue(value: unknown) { return value == null ? '' : String(value) }
|
||||
function numberValue(value: unknown) { return Number(value ?? 0) }
|
||||
function jsonRecord(value: unknown): RawResource {
|
||||
@@ -46,6 +41,7 @@ function normalizeBill(item: RawResource, customerId = stringValue(item.tenant_i
|
||||
}
|
||||
function normalizeBillLine(item: RawResource): FinanceBillLine {
|
||||
return {
|
||||
statementLineId: stringValue(item.id ?? item.statementLineId),
|
||||
sourceFeeId: stringValue(item.source_id ?? item.sourceId ?? item.sourceFeeId), sourceFeeVersion: numberValue(item.source_version ?? item.sourceVersion ?? item.sourceFeeVersion) || 1,
|
||||
orderId: item.order_id == null && item.orderId == null ? null : stringValue(item.order_id ?? item.orderId),
|
||||
inboundId: item.inbound_id == null && item.inboundId == null ? null : stringValue(item.inbound_id ?? item.inboundId),
|
||||
@@ -129,12 +125,59 @@ export async function updateFinanceFeeSourceSettlementInclusion(feeSourceId: str
|
||||
const response = await http.put<ApiEnvelope<RawResource>, ApiEnvelope<RawResource>>(`${basePath}/fee-sources/${feeSourceId}/settlement-inclusion`, payload, mutationHeaders('fee-source-settlement'))
|
||||
return normalizeFeeSourceDetail(unwrapApiResponse(response))
|
||||
}
|
||||
export async function createReconciliationStatement(payload: Record<string, unknown>) { throw new FinanceCapabilityBlockedError('createReconciliationStatement') }
|
||||
export async function actionReconciliationStatement(statementId: string, payload: Record<string, unknown>) { throw new FinanceCapabilityBlockedError('actionReconciliationStatement') }
|
||||
export async function createReceivableSettlement(payload: Record<string, unknown>) { throw new FinanceCapabilityBlockedError('createReceivableSettlement') }
|
||||
export async function createPayableSettlement(payload: Record<string, unknown>) { throw new FinanceCapabilityBlockedError('createPayableSettlement') }
|
||||
export async function getFinanceSettlement(settlementId: string): Promise<FinanceSettlementDetail> { throw new FinanceCapabilityBlockedError('getSettlement') }
|
||||
export async function createFinanceSettlementPayment(settlementId: string, payload: Record<string, unknown>) { throw new FinanceCapabilityBlockedError('createSettlementPayment') }
|
||||
function normalizeReconciliationDetail(item: RawResource): FinanceReconciliationDetail {
|
||||
const lines = (Array.isArray(item.lines) ? item.lines : []).map((entry) => {
|
||||
const row = entry as RawResource
|
||||
return { statementLineId: stringValue(row.statementLineId), sourceFeeId: stringValue(row.feeSourceId), sourceFeeVersion: numberValue(row.sourceVersion), orderId: null, inboundId: row.inboundId == null ? null : stringValue(row.inboundId), feeItemName: stringValue(row.feeItemName), amount: numberValue(row.statementAmount) }
|
||||
})
|
||||
return {
|
||||
id: stringValue(item.statementId), billNo: stringValue(item.statementNo), customerId: stringValue(item.customerId), customerName: stringValue(item.customerName),
|
||||
billDate: stringValue(item.periodEnd), dueDate: '', periodStart: stringValue(item.periodStart), periodEnd: stringValue(item.periodEnd), creditDays: numberValue(item.creditDays), currency: 'CNY',
|
||||
totalAmount: numberValue(item.totalReceivable), paidAmount: numberValue(item.receivedAmount), outstandingAmount: numberValue(item.outstandingAmount), settled: Boolean(item.settled), status: stringValue(item.status) as FinanceBillResource['status'], version: numberValue(item.version), remark: stringValue(item.remark), lines,
|
||||
paymentHistory: (Array.isArray(item.paymentHistory) ? item.paymentHistory : []).map((entry) => { const row = entry as RawResource; return { transactionId: stringValue(row.transactionId), transactionType: stringValue(row.transactionType), transactionDate: stringValue(row.transactionDate), amount: numberValue(row.amount), method: stringValue(row.method), referenceNo: stringValue(row.referenceNo), createdBy: stringValue(row.createdBy), occurredAt: stringValue(row.occurredAt ?? row.createdAt), remark: stringValue(row.remark) || undefined } }),
|
||||
revisions: (Array.isArray(item.revisions) ? item.revisions : []).map((entry) => { const row = entry as RawResource; return { revisionNo: numberValue(row.revisionNo), action: stringValue(row.action), beforeAmount: numberValue(row.beforeAmount), afterAmount: numberValue(row.afterAmount), reason: stringValue(row.reason) || undefined, operatorId: stringValue(row.operatorId), occurredAt: stringValue(row.occurredAt) } }),
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeSettlementDetail(item: RawResource): FinanceSettlementDetail {
|
||||
const summary = item.summary && typeof item.summary === 'object' ? item.summary as RawResource : item
|
||||
return {
|
||||
id: stringValue(summary.settlementId ?? summary.id), settlementNo: stringValue(summary.settlementNo), customerId: stringValue(summary.customerId), supplierId: summary.supplierId == null ? undefined : stringValue(summary.supplierId), reconciliationStatementId: summary.reconciliationStatementId == null ? undefined : stringValue(summary.reconciliationStatementId), direction: stringValue(summary.direction) as FinanceSettlementDetail['direction'],
|
||||
amount: numberValue(summary.totalAmount ?? summary.amount), paidAmount: numberValue(summary.paidAmount), outstandingAmount: numberValue(summary.outstandingAmount), currency: 'CNY', status: stringValue(summary.status), version: numberValue(summary.version),
|
||||
lines: (Array.isArray(item.lines) ? item.lines : []).map((line) => normalizeFeeSourceDetail(line as RawResource)),
|
||||
transactions: (Array.isArray(item.transactions) ? item.transactions : []).map((entry) => { const row = entry as RawResource; return { transactionId: stringValue(row.transactionId), transactionType: stringValue(row.transactionType), transactionDate: stringValue(row.transactionDate), amount: numberValue(row.amount), method: stringValue(row.method), voucherFileIds: Array.isArray(row.voucherFileIds) ? row.voucherFileIds.map(stringValue) : [], remark: stringValue(row.remark) || undefined } }),
|
||||
revisions: (Array.isArray(item.revisions) ? item.revisions : []).map((entry) => { const row = entry as RawResource; return { revisionNo: numberValue(row.revisionNo), action: stringValue(row.action), beforeAmount: numberValue(row.beforeAmount), afterAmount: numberValue(row.afterAmount), reason: stringValue(row.reason) || undefined, occurredAt: stringValue(row.occurredAt) } }),
|
||||
}
|
||||
}
|
||||
|
||||
export async function createReconciliationStatement(payload: CreateReconciliationPayload) {
|
||||
const response = await http.post<ApiEnvelope<RawResource>, ApiEnvelope<RawResource>>(`${basePath}/statements`, payload, mutationHeaders('reconciliation-create'))
|
||||
return normalizeReconciliationDetail(unwrapApiResponse(response))
|
||||
}
|
||||
export async function actionReconciliationStatement(statementId: string, payload: ReconciliationActionPayload) {
|
||||
const response = await http.post<ApiEnvelope<RawResource>, ApiEnvelope<RawResource>>(`${basePath}/reconciliation-statements/${statementId}/actions`, payload, mutationHeaders('reconciliation-action'))
|
||||
return normalizeReconciliationDetail(unwrapApiResponse(response))
|
||||
}
|
||||
export async function getReconciliationStatement(statementId: string): Promise<FinanceReconciliationDetail> {
|
||||
const response = await http.get<ApiEnvelope<RawResource>, ApiEnvelope<RawResource>>(`${basePath}/reconciliation-statements/${statementId}`)
|
||||
return normalizeReconciliationDetail(unwrapApiResponse(response))
|
||||
}
|
||||
export async function createReceivableSettlement(payload: CreateReceivableSettlementPayload) {
|
||||
const response = await http.post<ApiEnvelope<RawResource>, ApiEnvelope<RawResource>>(`${basePath}/receivable-settlements`, payload, mutationHeaders('settlement-ar-create'))
|
||||
return normalizeSettlementDetail(unwrapApiResponse(response))
|
||||
}
|
||||
export async function createPayableSettlement(payload: CreatePayableSettlementPayload) {
|
||||
const response = await http.post<ApiEnvelope<RawResource>, ApiEnvelope<RawResource>>(`${basePath}/payable-settlements`, payload, mutationHeaders('settlement-ap-create'))
|
||||
return normalizeSettlementDetail(unwrapApiResponse(response))
|
||||
}
|
||||
export async function getFinanceSettlement(settlementId: string): Promise<FinanceSettlementDetail> {
|
||||
const response = await http.get<ApiEnvelope<RawResource>, ApiEnvelope<RawResource>>(`${basePath}/settlements/${settlementId}`)
|
||||
return normalizeSettlementDetail(unwrapApiResponse(response))
|
||||
}
|
||||
export async function createFinanceSettlementPayment(settlementId: string, payload: CreateSettlementPaymentPayload) {
|
||||
const response = await http.post<ApiEnvelope<RawResource>, ApiEnvelope<RawResource>>(`${basePath}/settlements/${settlementId}/transactions`, payload, mutationHeaders('settlement-payment'))
|
||||
return normalizeSettlementDetail(unwrapApiResponse(response))
|
||||
}
|
||||
|
||||
export async function listFinanceStatements(customerId: string): Promise<FinanceStatementReadModel[]> {
|
||||
const response = await http.get<ApiEnvelope<RawResource[]>, ApiEnvelope<RawResource[]>>(`${basePath}/statements`, { params: { customerId } })
|
||||
|
||||
Reference in New Issue
Block a user