import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http' import type { FinanceBillDetail, FinanceBillLine, FinanceBillResource, FinanceCostResource, FinancePaymentMethod, FinanceReceiptResource, FinanceVoucherResource } from '../types' const basePath = '/platform/finance/v1' function idempotencyKey(operation: string) { return `finance-${operation}-${Date.now()}-${Math.random().toString(36).slice(2)}` } type RawResource = Record interface PageResult { records: T[]; total: number; serverPaged: boolean } type RawPage = RawResource[] | { records?: RawResource[]; total?: number } function stringValue(value: unknown) { return value == null ? '' : String(value) } function numberValue(value: unknown) { return Number(value ?? 0) } function fileIds(value: unknown): string[] { if (Array.isArray(value)) return value.map(String) if (typeof value !== 'string' || !value.trim()) return [] try { const parsed = JSON.parse(value) as unknown; return Array.isArray(parsed) ? parsed.map(String) : [] } catch { return [] } } function normalizePage(payload: RawPage, map: (item: RawResource) => T): PageResult { const records = Array.isArray(payload) ? payload : payload.records || [] return { records: records.map(map), total: Array.isArray(payload) ? records.length : Number(payload.total ?? records.length), serverPaged: !Array.isArray(payload) } } 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.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.sourceFeeId), sourceFeeVersion: numberValue(item.source_version ?? 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 { return { ...normalizeBill(item), remark: stringValue(item.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 listFinanceBills(params: { customerId: string; status?: string; page: number; pageSize: number }) { const response = await http.get, ApiEnvelope>(`${basePath}/bills`, { params }) return normalizePage(unwrapApiResponse(response), (item) => normalizeBill(item, params.customerId)) } export async function createFinanceBill(payload: Record) { const response = await http.post, ApiEnvelope>(`${basePath}/bills`, { ...payload }, mutationHeaders('bill')) return unwrapApiResponse(response) } export async function getFinanceBill(billId: string) { const response = await http.get, ApiEnvelope>(`${basePath}/bills/${billId}`) return normalizeBillDetail(unwrapApiResponse(response)) } export async function updateFinanceBill(billId: string, payload: Record) { const response = await http.put, ApiEnvelope>(`${basePath}/bills/${billId}`, payload, mutationHeaders('bill-update')) return normalizeBillDetail(unwrapApiResponse(response)) } export async function adjustFinanceBill(billId: string, payload: Record) { const response = await http.post, ApiEnvelope>(`${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>(`${basePath}/bills/${billId}/void`, { expectedVersion, reason }, mutationHeaders('bill-void')) return normalizeBillDetail(unwrapApiResponse(response)) } export async function listFinanceReceipts(customerId?: string) { const response = await http.get, ApiEnvelope>(`${basePath}/receipts`, { params: { customerId: customerId || undefined } }) const payload = unwrapApiResponse(response) const normalized = normalizePage(payload, (item) => ({ id: stringValue(item.id), customerId: stringValue(item.tenant_id), customerName: `客户 ${stringValue(item.tenant_id)}`, receivedDate: stringValue(item.received_at).slice(0, 10), amount: numberValue(item.received_amount), currency: 'CNY', paymentMethod: stringValue(item.payment_method) as FinancePaymentMethod, voucherFileIds: fileIds(item.voucher_file_ids), allocationCount: item.bill_id == null ? 0 : 1, })) const grouped = new Map() for (const item of normalized.records) { const existing = grouped.get(item.id) if (existing) existing.allocationCount += item.allocationCount else grouped.set(item.id, { ...item }) } return { ...normalized, records: [...grouped.values()], total: normalized.serverPaged ? normalized.total : grouped.size } } export async function createFinanceReceipt(payload: Record, voucherFileIds: string[]) { const response = await http.post, ApiEnvelope>(`${basePath}/receipts`, { ...payload, currency: 'CNY', voucherFileIds }, { headers: { 'Idempotency-Key': idempotencyKey('receipt') } }) return unwrapApiResponse(response) } export async function uploadFinanceVoucher(customerId: string, file: File): Promise { const body = new FormData() body.append('file', file) const response = await http.post, ApiEnvelope>(`${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 listFinanceCosts(customerId?: string) { const response = await http.get, ApiEnvelope>(`${basePath}/cost-entries`, { params: { customerId: customerId || undefined } }) return normalizePage(unwrapApiResponse(response), (item) => ({ id: stringValue(item.id), customerId: stringValue(item.tenant_id), costType: stringValue(item.cost_type) as FinanceCostResource['costType'], orderId: item.order_id == null ? null : stringValue(item.order_id), containerId: item.container_id == null ? null : stringValue(item.container_id), expenseName: stringValue(item.fee_item_name ?? item.expense_name), amount: numberValue(item.cost_amount ?? item.amount), currency: 'CNY', voucherFileIds: fileIds(item.voucher_file_ids), createdAt: stringValue(item.created_at), })) } export async function createFinanceCost(payload: Record, voucherFileIds: string[]) { const response = await http.post, ApiEnvelope>(`${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>(`${basePath}/bills/${billId}/issue`, { expectedVersion }, mutationHeaders('bill-issue')) return unwrapApiResponse(response) }