diff --git a/src/modules/finance/api/finance.ts b/src/modules/finance/api/finance.ts index ab96ca7e..e928104e 100644 --- a/src/modules/finance/api/finance.ts +++ b/src/modules/finance/api/finance.ts @@ -1,9 +1,26 @@ import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http' -import type { FinanceAccountEntry, FinanceBillDetail, FinanceBillLine, FinanceBillResource, FinanceBillRevision, FinanceCostResource, FinanceCustomerAccountReadModel, FinanceCustomerTerms, FinanceFeePoolItem, FinancePayableSettlement, FinanceReceivableSettlement, FinanceReceiptResource, FinanceStatementReadModel, FinanceVoucherResource } from '../types' +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: false, + accountLedger: false, + 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 +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) } @@ -56,6 +73,36 @@ export async function listFinanceFeePool(customerId: string, direction?: 'RECEIV 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 response = await http.get, ApiEnvelope>(`${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 } +} + +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>(`${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 } +} + +export async function getFinanceFeeSource(feeSourceId: string): Promise { + const response = await http.get, ApiEnvelope>(`${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) } +} + +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) { throw new FinanceCapabilityBlockedError('createReconciliationStatement') } +export async function actionReconciliationStatement(statementId: string, payload: Record) { throw new FinanceCapabilityBlockedError('actionReconciliationStatement') } +export async function createReceivableSettlement(payload: Record) { throw new FinanceCapabilityBlockedError('createReceivableSettlement') } +export async function createPayableSettlement(payload: Record) { throw new FinanceCapabilityBlockedError('createPayableSettlement') } +export async function getFinanceSettlement(settlementId: string): Promise { throw new FinanceCapabilityBlockedError('getSettlement') } +export async function createFinanceSettlementPayment(settlementId: string, payload: Record) { throw new FinanceCapabilityBlockedError('createSettlementPayment') } + export async function listFinanceStatements(customerId: string): Promise { const response = await http.get, ApiEnvelope>(`${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) })) diff --git a/src/modules/finance/types.ts b/src/modules/finance/types.ts index 27aa76d7..121c9767 100644 --- a/src/modules/finance/types.ts +++ b/src/modules/finance/types.ts @@ -1,6 +1,45 @@ export type FinanceBillStatus = 'DRAFT' | 'ISSUED' | 'PARTIALLY_PAID' | 'PAID' | 'VOIDED' export type FinanceCostType = 'SINGLE_ORDER' | 'CONTAINER_SHARED' export type FinancePaymentMethod = 'BANK_TRANSFER' | 'CASH' | 'OTHER' +export type FinanceFeeDirection = 'RECEIVABLE' | 'PAYABLE' +export type FinanceReviewStatus = 'PENDING' | 'APPROVED' | 'REJECTED' +export type FinanceReconciliationStatus = 'DRAFT' | 'PENDING_REVIEW' | 'APPROVED' | 'REJECTED' | 'ISSUED' | 'VOIDED' +export type FinanceSettlementStatus = 'ISSUED' | 'VOIDED' +export type FinancePaymentStatus = 'UNPAID' | 'PARTIALLY_PAID' | 'PAID' + +export interface FinancePageMeta { page: number; pageSize: number; total: number } +export interface FinanceAccountSummary { + customerId: string; customerCode: string; customerName: string; contactName?: string; contactPhone?: string + outstandingReceivable: number; pendingBilling: number; receivedInPeriod: number; outstandingPayable: number + status: 'ACTIVE' | 'DISABLED'; lastChangedAt: string; email?: string; creditDays?: number +} +export interface FinanceLedgerEntry { + ledgerId: string; occurredAt: string; businessType: string; documentType: string; documentId: string; documentNo?: string + receivableChange: number; payableChange: number; outstandingReceivableAfter: number; operatorName: string; remark?: string +} +export interface FinanceFeeSource { + feeSourceId: string; sourceType: string; sourceFeeId: string; sourceVersion: number; direction: FinanceFeeDirection + customerId: string; customerName?: string; supplierId?: string; supplierName?: string; inboundId?: string; inboundCode?: string + containerId?: string; containerNo?: string; businessDate: string; feeItemName: string; quantity?: number; unitPrice?: number; amount: number + currency: 'CNY'; reviewStatus: FinanceReviewStatus; settlementIncluded: boolean; sourceStatus: string; statementGenerated: boolean +} +export interface FinanceFeeSourceDetail extends FinanceFeeSource { reviewReason?: string; settlementReason?: string; sourceHash?: string; operationLogs?: FinanceOperationLog[] } +export interface FinanceOperationLog { occurredAt: string; action: string; operatorName: string; result: string; remark?: string } +export interface FinanceReconciliationSummary { + statementId: string; statementNo: string; customerId: string; customerName: string; periodStart: string; periodEnd: string; creditDays: number + currency: 'CNY'; totalReceivable: number; receivedAmount: number; outstandingAmount: number; lastReceiptAt?: string; settled: boolean + status: FinanceReconciliationStatus; version: number; createdAt: string; updatedAt: string +} +export interface FinanceReconciliationLine { statementLineId: string; feeSourceId: string; sourceVersion: number; sourceHash?: string; inboundId?: string; containerId?: string; feeItemName: string; sourceAmount: number; statementAmount: number; currency: 'CNY' } +export interface FinanceReconciliationDetail extends FinanceReconciliationSummary { remark?: string; lines: FinanceReconciliationLine[]; paymentHistory: FinanceSettlementTransaction[]; revisions: FinanceReconciliationRevision[] } +export interface FinanceReconciliationRevision { revisionNo: number; action: string; beforeAmount: number; afterAmount: number; reason?: string; operatorId: string; occurredAt: string } +export interface FinanceSettlementSummary { + settlementId: string; settlementNo: string; direction: FinanceFeeDirection; customerId?: string; customerName?: string; supplierId?: string; supplierName?: string + reconciliationStatementId?: string; businessPeriod?: string; currency: 'CNY'; totalAmount: number; paidAmount: number; outstandingAmount: number + generatedDate: string; dueDate: string; paymentStatus: FinancePaymentStatus; status: FinanceSettlementStatus; version: number +} +export interface FinanceSettlementTransaction { transactionId: string; transactionType: 'RECEIPT' | 'PAYMENT'; transactionDate: string; amount: number; method: FinancePaymentMethod; referenceNo?: string; voucherFileIds?: string[]; createdBy: string; createdAt: string; remark?: string } +export interface FinanceSettlementDetail extends FinanceSettlementSummary { lines: FinanceFeeSource[]; transactions: FinanceSettlementTransaction[]; revisions: FinanceReconciliationRevision[] } export interface FinanceVoucherResource { fileId: string @@ -102,6 +141,58 @@ export interface FinancePayableSettlement { version: number } +export interface FinanceAccountSummary { + customerId: string + customerName: string + customerStatus: string + currency: 'CNY' + totalReceivable: number + receivedAmount: number + outstandingAmount: number + creditDays: number +} + +export interface FinanceLedgerEntry { + entryId: string + businessType: string + resourceId: string + resourceNo: string + occurredAt: string + debitAmount: number + creditAmount: number + balance: number + currency: 'CNY' + status: string +} + +export interface FinanceFeeSourceDetail extends FinanceFeePoolItem { + reviewStatus: string + settlementIncluded: boolean + sourceStatus: string + reviewReason?: string + settlementReason?: string + sourceHash?: string + operationLogs?: Array<{ occurredAt: string; action: string; operatorName: string; result: string; remark?: string }> + version?: number +} + +export interface FinanceSettlementDetail { + id: string + settlementNo: string + customerId: string + supplierId?: string + direction: 'RECEIVABLE' | 'PAYABLE' + amount: number + paidAmount: number + outstandingAmount: number + currency: 'CNY' + status: string + version: number + lines: FinanceFeeSourceDetail[] + transactions: Array<{ transactionId: string; transactionType: string; transactionDate: string; amount: number; method: string; voucherFileIds: string[]; remark?: string }> + revisions: Array<{ revisionNo: number; action: string; beforeAmount: number; afterAmount: number; reason?: string; occurredAt: string }> +} + export interface FinanceBillResource { id: string billNo: string