import { http, unwrapApiResponse, ApiResponseError, type ApiEnvelope } from '@/shared/api/http' import type { PageResult } from './overseasMasterData' export type FleetQuoteKind = 'PURCHASE' | 'SALE' export interface FleetQuoteDraft { kind: FleetQuoteKind; partyId: string; originCity: string; destinationCode: string; distanceMiles: number currency: 'USD' | 'CNY'; selfPickupPrice: number | null; fullDeliveryPrice: number | null; looseDeliveryPrice: number | null validFrom: string; validTo: string; remark: string | null } export interface FleetQuote { quoteNo: string; details: FleetQuoteDraft; partyName: string; status: 'ACTIVE' | 'INACTIVE' | 'DELETED'; recordVersion: string; updatedAt: string } export interface FleetQuoteQuery { page?: number; size?: number; keyword?: string; kind?: FleetQuoteKind; status?: string; effectiveOn?: string } type Numeric = string | number type WireQuote = Omit & { recordVersion: Numeric; details: Omit & { distanceMiles: Numeric; selfPickupPrice: Numeric | null; fullDeliveryPrice: Numeric | null; looseDeliveryPrice: Numeric | null } } export function fleetNumber(value: Numeric): number { if ((typeof value !== 'string' && typeof value !== 'number') || String(value).trim() === '' || !Number.isFinite(Number(value)) || Number(value) < 0) throw new Error('接口返回了无效数值') return Number(value) } export function fleetMoney(value: Numeric): number { const text = String(value) if (!/^\d+(\.\d{1,2})?$/.test(text)) throw new Error('金额格式无效') const [whole, fraction = ''] = text.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) } export function fleetVersion(value: Numeric): string { if (!/^(0|[1-9]\d*)$/.test(String(value)) || (typeof value === 'number' && !Number.isSafeInteger(value))) throw new Error('接口版本无效') return String(value) } export function isFleetCommandRejected(error: unknown): boolean { return error instanceof ApiResponseError && ((error.status != null && error.status >= 400 && error.status < 500) || /^A04\d\d$/.test(error.code) || ['400', '409', '422', 'VALIDATION_FAILED', 'CONFLICT'].includes(error.code)) } export function fleetPage(value: { records: T[]; page: Numeric; size: Numeric; total: Numeric; pages: Numeric }): PageResult { const integer = (n: Numeric) => { const result = fleetNumber(n); if (!Number.isSafeInteger(result)) throw new Error('接口分页超出有效范围'); return result } return { ...value, page: integer(value.page), size: integer(value.size), total: integer(value.total), pages: integer(value.pages) } } const base = '/platform/v1/last-leg/fleet/quotes' const path = (no: string) => `${base}/${encodeURIComponent(no)}` const quote = (v: WireQuote): FleetQuote => ({ ...v, recordVersion: fleetVersion(v.recordVersion), details: { ...v.details, partyId: String(v.details.partyId), distanceMiles: fleetNumber(v.details.distanceMiles), selfPickupPrice: v.details.selfPickupPrice == null ? null : fleetNumber(v.details.selfPickupPrice), fullDeliveryPrice: v.details.fullDeliveryPrice == null ? null : fleetNumber(v.details.fullDeliveryPrice), looseDeliveryPrice: v.details.looseDeliveryPrice == null ? null : fleetNumber(v.details.looseDeliveryPrice) } }) export async function listFleetQuotes(query: FleetQuoteQuery) { const result = unwrapApiResponse(await http.get>, ApiEnvelope>>(base, { params: query })) return fleetPage({ ...result, records: result.records.map(quote) }) } export async function createFleetQuote(idempotencyKey: string, details: FleetQuoteDraft) { return quote(unwrapApiResponse(await http.post, ApiEnvelope>(base, { idempotencyKey, details }))) } export async function updateFleetQuote(value: FleetQuote, details: FleetQuoteDraft) { return quote(unwrapApiResponse(await http.put, ApiEnvelope>(path(value.quoteNo), { recordVersion: value.recordVersion, details }))) } export async function setFleetQuoteStatus(value: FleetQuote, status: string) { return quote(unwrapApiResponse(await http.patch, ApiEnvelope>(`${path(value.quoteNo)}/status`, { recordVersion: value.recordVersion, status }))) } export async function deleteFleetQuote(value: FleetQuote) { return quote(unwrapApiResponse(await http.delete, ApiEnvelope>(path(value.quoteNo), { params: { recordVersion: value.recordVersion } }))) } export async function importFleetQuotes(idempotencyKey: string, rows: FleetQuoteDraft[]) { return unwrapApiResponse(await http.post, ApiEnvelope<{ importedRows: number }>>(`${base}/import`, { idempotencyKey, rows })) } export async function fleetQuoteCustomers(keyword: string) { return unwrapApiResponse(await http.get, ApiEnvelope<{ id: string; name: string; code: string }[]>>(`${base}/customer-options`, { params: { keyword } })) } export async function fleetQuoteSuppliers(keyword: string) { const result = unwrapApiResponse(await http.get>, ApiEnvelope>>('/platform/v1/suppliers', { params: { page: 1, size: 30, keyword, status: 'ACTIVE', category: 'TRUCKING', truckingScope: 'OVERSEAS' } })) return result.records.map(row => ({ ...row, id: String(row.id) })) } export function saveFleetCsv(blob: Blob, name: string) { const url = URL.createObjectURL(blob); const anchor = document.createElement('a'); anchor.href = url; anchor.download = name; anchor.click(); setTimeout(() => URL.revokeObjectURL(url), 1000) }