112 lines
8.6 KiB
TypeScript
112 lines
8.6 KiB
TypeScript
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<CompanyCost, 'recordVersion' | 'paidAmount' | 'outstandingAmount' | 'source'> & {
|
|
recordVersion: Numeric; paidAmount: Numeric; outstandingAmount?: Numeric | null
|
|
source: Omit<CompanyCost['source'], 'sourceVersion' | 'supplierId' | 'originalAmount' | 'exchangeRate' | 'amount'> & {
|
|
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<T> = { records: T[]; page: Numeric; size: Numeric; total: Numeric; pages: Numeric }
|
|
function page<T, R>(value: PageWire<T>, convert: (value: T) => R): PageResult<R> {
|
|
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<CompanyCost> {
|
|
return cost(unwrapApiResponse(await http.get<ApiEnvelope<CostWire>, ApiEnvelope<CostWire>>(base(no))), no)
|
|
}
|
|
export async function commandCompanyCost(no: string, action: 'approve' | 'void', key: string, request: CompanyCostCommand): Promise<CompanyCost> {
|
|
return cost(unwrapApiResponse(await http.post<ApiEnvelope<CostWire>, ApiEnvelope<CostWire>>(`${base(no)}/${action}`, request, { headers: { 'Idempotency-Key': key } })), no)
|
|
}
|
|
export async function payCompanyCost(no: string, key: string, request: CompanyCostPayment): Promise<CompanyCost> {
|
|
return cost(unwrapApiResponse(await http.post<ApiEnvelope<CostWire>, ApiEnvelope<CostWire>>(`${base(no)}/payments`, request, { headers: { 'Idempotency-Key': key } })), no)
|
|
}
|
|
export async function listCompanyCostHistory(no: string, currentPage = 1): Promise<PageResult<CompanyCostHistory>> {
|
|
type HistoryWire = Omit<CompanyCostHistory, 'recordVersion' | 'snapshot'> & { recordVersion: Numeric; snapshot: CostWire }
|
|
const value = unwrapApiResponse(await http.get<ApiEnvelope<PageWire<HistoryWire>>, ApiEnvelope<PageWire<HistoryWire>>>(`${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<PageResult<CompanyCostPaid>> {
|
|
type PaidWire = Omit<CompanyCostPaid, 'amount' | 'voucherFileIds'> & { amount: Numeric; voucherFileIds: Numeric[] }
|
|
const value = unwrapApiResponse(await http.get<ApiEnvelope<PageWire<PaidWire>>, ApiEnvelope<PageWire<PaidWire>>>(`${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<CompanyCostVoucher> {
|
|
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<Wire>, ApiEnvelope<Wire>>(`${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<void> {
|
|
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<unknown>
|
|
throw new ApiResponseError(error.message || '付款凭证读取失败', error.code, status, error.data, error.requestId, error.traceId)
|
|
}
|
|
}
|
|
export async function downloadCompanyCostVoucher(no: string, fileId: string): Promise<Blob> {
|
|
try {
|
|
const value = await http.get<Blob, Blob>(`${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
|
|
}
|
|
}
|