Files
admin-web/src/modules/finance/api/finance.ts
T
2026-08-11 18:43:56 +08:00

211 lines
19 KiB
TypeScript

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'
const basePath = '/platform/finance/v1'
export const financePhase1Capabilities = {
accountList: true,
accountLedger: true,
feeSourceDetail: false,
feeSourceReview: false,
feeSourceSettlementInclusion: false,
reconciliationCreate: false,
reconciliationActions: false,
receivableSettlementCreate: false,
payableSettlementCreate: false,
settlementDetail: false,
settlementPayment: false,
} 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 {
if (value && typeof value === 'object' && !Array.isArray(value)) return value as RawResource
if (typeof value !== 'string' || !value.trim()) return {}
try { const parsed = JSON.parse(value) as unknown; return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed as RawResource : {} } catch { return {} }
}
function jsonRecords(value: unknown): RawResource[] {
if (Array.isArray(value)) return value.filter((item): item is RawResource => Boolean(item) && typeof item === 'object' && !Array.isArray(item))
if (typeof value !== 'string' || !value.trim()) return []
try { const parsed = JSON.parse(value) as unknown; return Array.isArray(parsed) ? parsed.filter((item): item is RawResource => Boolean(item) && typeof item === 'object' && !Array.isArray(item)) : [] } catch { return [] }
}
function normalizeBill(item: RawResource, customerId = stringValue(item.tenant_id ?? item.customerId)): FinanceBillResource {
return {
id: stringValue(item.id), billNo: stringValue(item.bill_no ?? item.billNo), customerId,
customerName: stringValue(item.customer_name_snapshot ?? item.customerName) || `客户 ${customerId}`,
billDate: stringValue(item.bill_date ?? item.billDate), dueDate: stringValue(item.due_date ?? item.dueDate), currency: 'CNY',
totalAmount: numberValue(item.total_amount ?? item.totalAmount), paidAmount: numberValue(item.received_amount ?? item.receivedAmount ?? item.paidAmount),
outstandingAmount: numberValue(item.outstanding_amount ?? item.outstandingAmount), status: stringValue(item.status) as FinanceBillResource['status'],
version: numberValue(item.version),
}
}
function normalizeBillLine(item: RawResource): FinanceBillLine {
return {
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),
feeItemName: stringValue(item.fee_item_name ?? item.feeItemName), amount: numberValue(item.amount),
}
}
function normalizeBillDetail(item: RawResource): FinanceBillDetail {
const statement = item.statement && typeof item.statement === 'object' ? item.statement as RawResource : item
return { ...normalizeBill(statement), remark: stringValue(statement.remark), lines: Array.isArray(item.lines) ? item.lines.map((line) => normalizeBillLine(line as RawResource)) : [] }
}
function mutationHeaders(operation: string) { return { headers: { 'Idempotency-Key': idempotencyKey(operation) } } }
export async function getFinanceCustomerAccount(customerId: string): Promise<FinanceCustomerAccountReadModel> {
const response = await http.get<ApiEnvelope<RawResource>, ApiEnvelope<RawResource>>(`${basePath}/customer-accounts/${customerId}`)
const item = unwrapApiResponse(response)
return {
customerId: stringValue(item.customerId), currency: 'CNY', accountMode: 'MANUAL_RECEIVABLE_LEDGER',
totalReceivable: numberValue(item.totalReceivable), totalReceived: numberValue(item.totalReceived), outstandingAmount: numberValue(item.outstandingAmount),
entries: (Array.isArray(item.entries) ? item.entries : []).map((entry) => { const row = entry as RawResource; return { entryType: stringValue(row.entryType) as FinanceAccountEntry['entryType'], resourceId: stringValue(row.resourceId), resourceNo: stringValue(row.resourceNo), occurredAt: stringValue(row.occurredAt), debitAmount: numberValue(row.debitAmount), creditAmount: numberValue(row.creditAmount), currency: 'CNY', status: stringValue(row.status) } }),
}
}
export async function listFinanceFeePool(customerId: string, direction?: 'RECEIVABLE' | 'PAYABLE'): Promise<FinanceFeePoolItem[]> {
const response = await http.get<ApiEnvelope<RawResource[]>, ApiEnvelope<RawResource[]>>(`${basePath}/fee-pool`, { params: { customerId, direction } })
return unwrapApiResponse(response).map((item) => ({ poolItemId: stringValue(item.poolItemId), direction: stringValue(item.direction) as FinanceFeePoolItem['direction'], sourceType: stringValue(item.sourceType), sourceId: stringValue(item.sourceId), sourceVersion: numberValue(item.sourceVersion), orderId: item.orderId == null ? null : stringValue(item.orderId), inboundId: item.inboundId == null ? null : stringValue(item.inboundId), containerId: item.containerId == null ? null : stringValue(item.containerId), feeItemName: stringValue(item.feeItemName), amount: numberValue(item.amount), currency: 'CNY', status: stringValue(item.status), createdAt: stringValue(item.createdAt) }))
}
export async function listFinanceAccounts(params: { page?: number; size?: number; keyword?: string; status?: string } = {}): Promise<{ records: FinanceAccountSummary[]; total: number; pages: number }> {
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 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> {
throw new FinanceCapabilityBlockedError('getFinanceFeeSource')
}
export async function reviewFinanceFeeSource(feeSourceId: string, payload: { action: 'APPROVE' | 'REJECT'; reason?: string; expectedVersion: number }) { throw new FinanceCapabilityBlockedError('reviewFinanceFeeSource') }
export async function updateFinanceFeeSourceSettlementInclusion(feeSourceId: string, payload: { included: boolean; reason: string; expectedVersion: number }) { throw new FinanceCapabilityBlockedError('updateFeeSourceSettlementInclusion') }
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') }
export async function listFinanceStatements(customerId: string): Promise<FinanceStatementReadModel[]> {
const response = await http.get<ApiEnvelope<RawResource[]>, ApiEnvelope<RawResource[]>>(`${basePath}/statements`, { params: { customerId } })
return unwrapApiResponse(response).map((item) => ({ id: stringValue(item.id), customerId: stringValue(item.customerId), billNo: stringValue(item.billNo), customerName: stringValue(item.customerName), billDate: stringValue(item.billDate), dueDate: stringValue(item.dueDate), totalAmount: numberValue(item.totalAmount), receivedAmount: numberValue(item.receivedAmount), outstandingAmount: numberValue(item.outstandingAmount), currency: 'CNY', status: stringValue(item.status) as FinanceStatementReadModel['status'], version: numberValue(item.version) }))
}
export async function listFinanceReceivableSettlements(customerId: string): Promise<FinanceReceivableSettlement[]> {
const response = await http.get<ApiEnvelope<RawResource[]>, ApiEnvelope<RawResource[]>>(`${basePath}/receivable-settlements`, { params: { customerId } })
return unwrapApiResponse(response).map((item) => ({ id: stringValue(item.id), customerId: stringValue(item.customerId), settlementNo: stringValue(item.settlementNo), billDate: stringValue(item.billDate), dueDate: stringValue(item.dueDate), receivableAmount: numberValue(item.receivableAmount), receivedAmount: numberValue(item.receivedAmount), outstandingAmount: numberValue(item.outstandingAmount), currency: 'CNY', status: stringValue(item.status) as FinanceReceivableSettlement['status'], version: numberValue(item.version) }))
}
export async function listFinancePayableSettlements(customerId: string): Promise<FinancePayableSettlement[]> {
const response = await http.get<ApiEnvelope<RawResource[]>, ApiEnvelope<RawResource[]>>(`${basePath}/payable-settlements`, { params: { customerId } })
return unwrapApiResponse(response).map((item) => ({ id: stringValue(item.id), customerId: stringValue(item.customerId), settlementNo: stringValue(item.settlementNo), costType: stringValue(item.costType) as FinancePayableSettlement['costType'], orderId: item.orderId == null ? null : stringValue(item.orderId), inboundId: item.inboundId == null ? null : stringValue(item.inboundId), containerId: item.containerId == null ? null : stringValue(item.containerId), supplierName: stringValue(item.supplierName), feeItemName: stringValue(item.feeItemName), payableAmount: numberValue(item.payableAmount), paidAmount: numberValue(item.paidAmount), outstandingAmount: numberValue(item.outstandingAmount), currency: 'CNY', status: stringValue(item.status), createdAt: stringValue(item.createdAt), version: numberValue(item.version) }))
}
function normalizeCustomerTerms(item: RawResource, customerId: string): FinanceCustomerTerms {
return {
customerId: stringValue(item.tenant_id ?? item.tenantId ?? customerId),
customerName: stringValue(item.customer_name_snapshot ?? item.customerName),
creditDays: numberValue(item.credit_days ?? item.creditDays ?? 30),
status: stringValue(item.status) || 'ACTIVE',
}
}
export async function getFinanceCustomerTerms(customerId: string) {
const response = await http.get<ApiEnvelope<RawResource>, ApiEnvelope<RawResource>>(`${basePath}/customers/${customerId}/terms`)
return normalizeCustomerTerms(unwrapApiResponse(response), customerId)
}
export async function updateFinanceCustomerTerms(customerId: string, payload: { customerName: string; creditDays: number }) {
const response = await http.put<ApiEnvelope<RawResource>, ApiEnvelope<RawResource>>(`${basePath}/customers/${customerId}/terms`, payload, mutationHeaders('customer-terms'))
return normalizeCustomerTerms(unwrapApiResponse(response), customerId)
}
export async function createFinanceBill(payload: Record<string, unknown>) {
const response = await http.post<ApiEnvelope<FinanceBillResource>, ApiEnvelope<FinanceBillResource>>(`${basePath}/bills`, { ...payload }, mutationHeaders('bill'))
return unwrapApiResponse(response)
}
export async function getFinanceBill(billId: string) {
const response = await http.get<ApiEnvelope<RawResource>, ApiEnvelope<RawResource>>(`${basePath}/bills/${billId}`)
return normalizeBillDetail(unwrapApiResponse(response))
}
export async function listFinanceBillRevisions(billId: string, customerId: string): Promise<FinanceBillRevision[]> {
const response = await http.get<ApiEnvelope<RawResource[]>, ApiEnvelope<RawResource[]>>(`${basePath}/bills/${billId}/revisions`, { params: { customerId } })
return unwrapApiResponse(response).map((item) => {
const bill = jsonRecord(item.bill_snapshot ?? item.billSnapshot)
const lines = jsonRecords(item.line_snapshot ?? item.lineSnapshot)
return {
id: stringValue(item.id), billId: stringValue(item.bill_id ?? item.billId ?? billId), version: numberValue(item.version),
reason: stringValue(item.reason), createdBy: stringValue(item.created_by ?? item.createdBy), createdAt: stringValue(item.created_at ?? item.createdAt),
status: stringValue(bill.status) as FinanceBillRevision['status'], totalAmount: numberValue(bill.total_amount ?? bill.totalAmount),
receivedAmount: numberValue(bill.received_amount ?? bill.receivedAmount), outstandingAmount: numberValue(bill.outstanding_amount ?? bill.outstandingAmount),
lines: lines.map(normalizeBillLine),
}
})
}
export async function updateFinanceBill(billId: string, payload: Record<string, unknown>) {
const response = await http.put<ApiEnvelope<RawResource>, ApiEnvelope<RawResource>>(`${basePath}/bills/${billId}`, payload, mutationHeaders('bill-update'))
return normalizeBillDetail(unwrapApiResponse(response))
}
export async function adjustFinanceBill(billId: string, payload: Record<string, unknown>) {
const response = await http.post<ApiEnvelope<RawResource>, ApiEnvelope<RawResource>>(`${basePath}/bills/${billId}/adjust`, payload, mutationHeaders('bill-adjust'))
return normalizeBillDetail(unwrapApiResponse(response))
}
export async function voidFinanceBill(billId: string, expectedVersion: number, reason: string) {
const response = await http.post<ApiEnvelope<RawResource>, ApiEnvelope<RawResource>>(`${basePath}/bills/${billId}/void`, { expectedVersion, reason }, mutationHeaders('bill-void'))
return normalizeBillDetail(unwrapApiResponse(response))
}
export async function createFinanceReceipt(payload: Record<string, unknown>, voucherFileIds: string[]) {
const response = await http.post<ApiEnvelope<FinanceReceiptResource>, ApiEnvelope<FinanceReceiptResource>>(`${basePath}/receipts`, { ...payload, currency: 'CNY', voucherFileIds }, { headers: { 'Idempotency-Key': idempotencyKey('receipt') } })
return unwrapApiResponse(response)
}
export async function uploadFinanceVoucher(customerId: string, file: File): Promise<FinanceVoucherResource> {
const body = new FormData()
body.append('file', file)
const response = await http.post<ApiEnvelope<RawResource>, ApiEnvelope<RawResource>>(`${basePath}/vouchers`, body, {
params: { customerId }, headers: { 'Content-Type': 'multipart/form-data' },
})
const item = unwrapApiResponse(response)
return {
fileId: stringValue(item.fileId ?? item.file_id), customerId: stringValue(item.tenantId ?? item.customerId ?? customerId),
fileName: stringValue(item.fileName ?? item.file_name), contentType: stringValue(item.contentType ?? item.content_type),
sizeBytes: numberValue(item.sizeBytes ?? item.size_bytes),
}
}
export async function createFinanceCost(payload: Record<string, unknown>, voucherFileIds: string[]) {
const response = await http.post<ApiEnvelope<FinanceCostResource>, ApiEnvelope<FinanceCostResource>>(`${basePath}/cost-entries`, { ...payload, currency: 'CNY', voucherFileIds }, { headers: { 'Idempotency-Key': idempotencyKey('cost') } })
return unwrapApiResponse(response)
}
export async function issueFinanceBill(billId: string, expectedVersion: number) {
const response = await http.post<ApiEnvelope<FinanceBillResource>, ApiEnvelope<FinanceBillResource>>(`${basePath}/bills/${billId}/issue`, { expectedVersion }, mutationHeaders('bill-issue'))
return unwrapApiResponse(response)
}