import axios from 'axios' import { http, unwrapApiResponse, ApiResponseError, type ApiEnvelope } from '@/shared/api/http' import type { PageResult } from './overseasMasterData' export const companyCostPermissions = { list: 'platform:finance:vehicle-cost:list', approve: 'platform:finance:vehicle-cost:approve', void: 'platform:finance:vehicle-cost:void', pay: 'platform:finance:vehicle-cost:pay', upload: 'platform:finance:vehicle-cost:upload', } as const export type CompanyCostState = 'PENDING' | 'APPROVED' | 'PARTIALLY_PAID' | 'PAID' | 'VOIDED' export interface CompanyCost { costNo: string; subjectCode: 'PLATFORM_DEFAULT'; supplierName: string; currency: 'CNY'; status: CompanyCostState recordVersion: string; paidAmount: number; outstandingAmount: number | null source: { sourceType: string; sourceNo: string; sourceVersion: string; vehicleId: string; vehiclePlate: string; tripNo: string | null; supplierId: string; feeName: string; businessDate: string; originalCurrency: 'CNY' | 'USD'; originalAmount: number; exchangeRate: number; exchangeRateMonth: string; amount: number; remark: string | null } } export interface CompanyCostCommand { expectedVersion: string; reason: string | null } export interface CompanyCostPayment { expectedVersion: string; amount: number; paidAt: string; method: 'ONLINE' | 'BANK_TRANSFER' | 'CASH' | 'OTHER' referenceNo: string | null; voucherFileIds: string[]; remark: string | null } export interface CompanyCostHistory { recordVersion: string; action: string; reason: string | null; operatorName: string; occurredAt: string; snapshot: CompanyCost } export interface CompanyCostPaid { paymentNo: string; currency: 'CNY'; amount: number; paidAt: string; method: string; referenceNo: string | null; voucherFileIds: string[]; operatorName: string; recordedAt: string; remark: string | null } export interface CompanyCostVoucher { fileId: string; fileName: string; contentType: string; sizeBytes: number } type Numeric = string | number type CostWire = Omit & { recordVersion: Numeric; paidAmount: Numeric; outstandingAmount?: Numeric | null source: Omit & { sourceVersion: Numeric; supplierId: Numeric; originalAmount: Numeric; exchangeRate: Numeric; amount: Numeric } } const base = (no: string) => `/platform/finance/v1/vehicle-costs/${encodeURIComponent(no)}` function version(value: Numeric): string { if (!/^(0|[1-9]\d*)$/.test(String(value)) || (typeof value === 'number' && !Number.isSafeInteger(value))) throw new Error('公司成本版本或标识无效') return String(value) } function money(value: Numeric): number { if ((typeof value !== 'string' && typeof value !== 'number') || !/^\d+(\.\d{1,2})?$/.test(String(value))) throw new Error('公司成本金额无效') const [whole, fraction = ''] = String(value).split('.') const cents = BigInt(whole!) * 100n + BigInt(fraction.padEnd(2, '0')) if (cents > BigInt(Number.MAX_SAFE_INTEGER) || BigInt(Number(value).toFixed(2).replace('.', '')) !== cents) throw new Error('公司成本金额超出精确展示范围') return Number(value) } function integer(value: Numeric): number { const result = Number(version(value)) if (!Number.isSafeInteger(result)) throw new Error('公司成本分页数值无效') return result } function cost(value: CostWire, expected?: string): CompanyCost { if ((expected && value.costNo !== expected) || value.subjectCode !== 'PLATFORM_DEFAULT' || value.currency !== 'CNY' || !['PENDING', 'APPROVED', 'PARTIALLY_PAID', 'PAID', 'VOIDED'].includes(value.status)) throw new Error('公司成本响应归属或状态无效') const rate = Number(value.source.exchangeRate) if (!Number.isFinite(rate) || rate <= 0 || !['CNY', 'USD'].includes(value.source.originalCurrency)) throw new Error('公司成本汇率或原币无效') return { ...value, recordVersion: version(value.recordVersion), paidAmount: money(value.paidAmount), outstandingAmount: value.outstandingAmount == null ? null : money(value.outstandingAmount), source: { ...value.source, sourceVersion: version(value.source.sourceVersion), supplierId: version(value.source.supplierId), originalAmount: money(value.source.originalAmount), exchangeRate: rate, amount: money(value.source.amount), tripNo: value.source.tripNo ?? null, remark: value.source.remark ?? null } } } type PageWire = { records: T[]; page: Numeric; size: Numeric; total: Numeric; pages: Numeric } function page(value: PageWire, convert: (value: T) => R): PageResult { return { records: value.records.map(convert), page: integer(value.page), size: integer(value.size), total: integer(value.total), pages: integer(value.pages) } } export async function getCompanyCost(no: string): Promise { return cost(unwrapApiResponse(await http.get, ApiEnvelope>(base(no))), no) } export async function commandCompanyCost(no: string, action: 'approve' | 'void', key: string, request: CompanyCostCommand): Promise { return cost(unwrapApiResponse(await http.post, ApiEnvelope>(`${base(no)}/${action}`, request, { headers: { 'Idempotency-Key': key } })), no) } export async function payCompanyCost(no: string, key: string, request: CompanyCostPayment): Promise { return cost(unwrapApiResponse(await http.post, ApiEnvelope>(`${base(no)}/payments`, request, { headers: { 'Idempotency-Key': key } })), no) } export async function listCompanyCostHistory(no: string, currentPage = 1): Promise> { type HistoryWire = Omit & { recordVersion: Numeric; snapshot: CostWire } const value = unwrapApiResponse(await http.get>, ApiEnvelope>>(`${base(no)}/history`, { params: { page: currentPage, size: 10 } })) return page(value, row => ({ ...row, recordVersion: version(row.recordVersion), snapshot: cost(row.snapshot, no) })) } export async function listCompanyCostPayments(no: string, currentPage = 1): Promise> { type PaidWire = Omit & { amount: Numeric; voucherFileIds: Numeric[] } const value = unwrapApiResponse(await http.get>, ApiEnvelope>>(`${base(no)}/payments`, { params: { page: currentPage, size: 10 } })) return page(value, row => { if (row.currency !== 'CNY') throw new Error('公司付款币种无效') return { ...row, amount: money(row.amount), voucherFileIds: row.voucherFileIds.map(version) } }) } export async function uploadCompanyCostVoucher(no: string, file: File): Promise { type Wire = { fileId: Numeric; fileName: string; contentType: string; sizeBytes: Numeric; ownerScope: string; tenantId: Numeric; businessType: string } const form = new FormData(); form.append('file', file) const value = unwrapApiResponse(await http.post, ApiEnvelope>(`${base(no)}/vouchers`, form)) if (value.ownerScope !== 'PLATFORM' || String(value.tenantId) !== '0' || value.businessType !== 'FINANCE_VEHICLE_VOUCHER' || !['image/jpeg', 'image/png'].includes(value.contentType) || integer(value.sizeBytes) <= 0) throw new Error('公司付款凭证返回内容无效') return { fileId: version(value.fileId), fileName: value.fileName, contentType: value.contentType, sizeBytes: integer(value.sizeBytes) } } async function rejectFileError(value: unknown, status: number): Promise { if (value instanceof Blob && value.type.includes('json')) { try { value = JSON.parse(await value.text()) as unknown } catch { throw new Error('付款凭证返回了无法识别的错误') } } if (value && typeof value === 'object' && 'code' in value) { const error = value as ApiEnvelope throw new ApiResponseError(error.message || '付款凭证读取失败', error.code, status, error.data, error.requestId, error.traceId) } } export async function downloadCompanyCostVoucher(no: string, fileId: string): Promise { try { const value = await http.get(`${base(no)}/vouchers/${encodeURIComponent(fileId)}/content`, { responseType: 'blob' }) await rejectFileError(value, 200) if (!(value instanceof Blob) || !value.size || !['image/png', 'image/jpeg'].includes(value.type)) throw new Error('付款凭证为空或格式无效') return value } catch (cause) { if (axios.isAxiosError(cause)) await rejectFileError(cause.response?.data, cause.response?.status || 0) throw cause } }