feat(phase4): automate admin integration acceptance
This commit is contained in:
@@ -1,55 +1,143 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ApiResponseError } from '@/shared/api/http'
|
||||
import { overseasHttp } from '@/shared/api/overseasHttp'
|
||||
import {
|
||||
changeExchangeRateStatus, changeQuoteStatus, changeVehicleStatus, createExchangeRate, createQuote, createVehicle,
|
||||
exportExchangeRates, exportQuotes, exportVehicles, getExchangeRate, getExchangeRateSummary, getQuote, getVehicle,
|
||||
listExchangeRates, listQuotes, listQuoteWarehouseOptions, listVehicles, updateExchangeRate, updateQuote, updateVehicle,
|
||||
type QuoteCreate, type QuoteUpdate,
|
||||
} from './overseasMasterData'
|
||||
|
||||
vi.mock('@/shared/api/overseasHttp', () => ({ overseasHttp: { get: vi.fn(), post: vi.fn(), put: vi.fn(), patch: vi.fn() } }))
|
||||
const ok = (data: unknown) => ({ code: '200', message: 'success', data })
|
||||
const longId = '9223372036854775807'
|
||||
const quoteSummary = {
|
||||
id: longId, name: '报价', warehouseId: '9223372036854775806', warehouseCode: 'US-LAX', warehouseName: '洛杉矶合作仓',
|
||||
countryCode: 'US', providerName: '服务商', currency: 'USD', effectiveStart: '2026-08-01', effectiveEnd: '2026-12-31',
|
||||
versionCode: 'V1', status: 'INACTIVE', recordVersion: '7', updatedBy: 'admin', updatedAt: '2026-08-24T10:00:00', remark: null,
|
||||
}
|
||||
const quoteDetail = {
|
||||
summary: quoteSummary,
|
||||
warehouseFees: [{ item: '入库', billingUnit: 'PALLET', unitPrice: '1.2500', minimumCharge: '2.5000', remark: null }],
|
||||
transitPrices: [{ fbaWarehouseCode: 'ONT8', deliveryMethod: 'TRUCK', estimatedMiles: '12.50', price: '18.7500', minimumCharge: null, remark: null }],
|
||||
createdBy: 'admin', createdAt: '2026-08-24T09:00:00',
|
||||
}
|
||||
const exchangeRate = { month: '2026-08', baseCurrency: 'USD', quoteCurrency: 'CNY', usdToCnyAverage: '7.12345678', status: 'ACTIVE', recordVersion: '3', updatedBy: 'admin', updatedAt: '2026-08-24T10:00:00', remark: null }
|
||||
const vehicle = { id: longId, plateNumber: 'CA-01', model: 'Truck', fuelConsumptionPer100Km: '12.50', mileageKm: '101', driverOrDispatchCompany: null, phone: null, region: 'OVERSEAS', ownership: 'OWNED', status: 'IN_SERVICE', remark: null, recordVersion: '4', updatedBy: 'admin', updatedAt: '2026-08-24T10:00:00' }
|
||||
|
||||
beforeEach(() => vi.clearAllMocks())
|
||||
|
||||
describe('overseas master data API', () => {
|
||||
it('uses the overseas quote contract including server-owned partner options', async () => {
|
||||
vi.mocked(overseasHttp.get).mockResolvedValue(ok({ records: [], page: '1', size: '20', total: '0', pages: '0' }))
|
||||
await listQuotes({ page: 1, size: 20, keyword: 'V1' })
|
||||
expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/master-data/overseas-quotes', { params: { page: 1, size: 20, keyword: 'V1' } })
|
||||
vi.mocked(overseasHttp.get).mockResolvedValue(ok([])); await listQuoteWarehouseOptions()
|
||||
expect(overseasHttp.get).toHaveBeenLastCalledWith('/platform/v1/last-leg/master-data/overseas-quotes/warehouse-options')
|
||||
vi.mocked(overseasHttp.get).mockResolvedValue(ok({ summary: { id: '9223372036854775807' }, warehouseFees: [], transitPrices: [] })); await getQuote('9223372036854775807')
|
||||
expect(overseasHttp.get).toHaveBeenLastCalledWith('/platform/v1/last-leg/master-data/overseas-quotes/9223372036854775807')
|
||||
const payload = { name: '报价', warehouseId: '9', providerName: '服务商', currency: 'USD' as const, effectiveStart: '2026-08-01', effectiveEnd: '2026-12-31', versionCode: 'V1', warehouseFees: [], transitPrices: [], remark: null }
|
||||
vi.mocked(overseasHttp.post).mockResolvedValue(ok({})); await createQuote(payload)
|
||||
vi.mocked(overseasHttp.put).mockResolvedValue(ok({})); await updateQuote('9', { ...payload, recordVersion: 2 })
|
||||
vi.mocked(overseasHttp.patch).mockResolvedValue(ok({})); await changeQuoteStatus('9', 'INACTIVE', 2)
|
||||
expect(overseasHttp.patch).toHaveBeenCalledWith('/platform/v1/last-leg/master-data/overseas-quotes/9/status', { status: 'INACTIVE', recordVersion: 2 })
|
||||
it('normalizes quote page/detail decimals and preserves long string IDs', async () => {
|
||||
vi.mocked(overseasHttp.get)
|
||||
.mockResolvedValueOnce(ok({ records: [quoteSummary], page: '1', size: '20', total: '1', pages: '1' }))
|
||||
.mockResolvedValueOnce(ok(quoteDetail))
|
||||
.mockResolvedValueOnce(ok([{ warehouseId: quoteSummary.warehouseId, warehouseCode: 'US-LAX', warehouseName: '洛杉矶合作仓', countryCode: 'US' }]))
|
||||
|
||||
const result = await listQuotes({ page: 1, size: 20, keyword: 'V1' })
|
||||
expect(result).toMatchObject({ page: 1, size: 20, total: 1, pages: 1 })
|
||||
expect(result.records[0]).toMatchObject({ id: longId, warehouseId: quoteSummary.warehouseId, recordVersion: 7 })
|
||||
const detail = await getQuote(longId)
|
||||
expect(detail.summary.id).toBe(longId)
|
||||
expect(detail.summary.recordVersion).toBe(7)
|
||||
expect(detail.warehouseFees[0]).toMatchObject({ unitPrice: 1.25, minimumCharge: 2.5 })
|
||||
expect(detail.transitPrices[0]).toMatchObject({ estimatedMiles: 12.5, price: 18.75, minimumCharge: null })
|
||||
await listQuoteWarehouseOptions()
|
||||
|
||||
expect(overseasHttp.get).toHaveBeenNthCalledWith(1, '/platform/v1/last-leg/master-data/overseas-quotes', { params: { page: 1, size: 20, keyword: 'V1' } })
|
||||
expect(overseasHttp.get).toHaveBeenNthCalledWith(2, `/platform/v1/last-leg/master-data/overseas-quotes/${longId}`)
|
||||
expect(overseasHttp.get).toHaveBeenNthCalledWith(3, '/platform/v1/last-leg/master-data/overseas-quotes/warehouse-options')
|
||||
})
|
||||
|
||||
it('uses the monthly exchange-rate contract', async () => {
|
||||
vi.mocked(overseasHttp.get).mockResolvedValue(ok({ records: [], page: 1, size: 20, total: 0, pages: 0 })); await listExchangeRates({ page: 1, size: 20, monthFrom: '2026-08', monthTo: '2026-08' })
|
||||
vi.mocked(overseasHttp.get).mockResolvedValue(ok({ latestMonth: '2026-08' })); await getExchangeRateSummary()
|
||||
vi.mocked(overseasHttp.get).mockResolvedValue(ok({ month: '2026-08' })); await getExchangeRate('2026-08')
|
||||
vi.mocked(overseasHttp.post).mockResolvedValue(ok({})); await createExchangeRate({ month: '2026-08', usdToCnyAverage: 7.18, remark: null })
|
||||
vi.mocked(overseasHttp.put).mockResolvedValue(ok({})); await updateExchangeRate('2026-08', { recordVersion: 1, usdToCnyAverage: 7.19, remark: null })
|
||||
vi.mocked(overseasHttp.patch).mockResolvedValue(ok({})); await changeExchangeRateStatus('2026-08', 'INACTIVE', 1, '停用')
|
||||
expect(overseasHttp.patch).toHaveBeenLastCalledWith('/platform/v1/last-leg/master-data/exchange-rates/2026-08/status', { status: 'INACTIVE', recordVersion: 1, remark: '停用' })
|
||||
it('keeps quote create/update DTOs distinct and sends recordVersion on writes', async () => {
|
||||
const create: QuoteCreate = {
|
||||
name: '报价', warehouseId: quoteSummary.warehouseId, providerName: '服务商', currency: 'USD',
|
||||
effectiveStart: '2026-08-01', effectiveEnd: '2026-12-31', versionCode: 'V1',
|
||||
warehouseFees: [{ item: '入库', billingUnit: 'PALLET', unitPrice: 1.25, minimumCharge: null, remark: null }],
|
||||
transitPrices: [], remark: null,
|
||||
}
|
||||
const update: QuoteUpdate = {
|
||||
name: '报价-编辑', providerName: '服务商', currency: 'USD', effectiveStart: '2026-08-01', effectiveEnd: '2026-12-31',
|
||||
warehouseFees: create.warehouseFees, transitPrices: [], remark: null, recordVersion: 7,
|
||||
}
|
||||
vi.mocked(overseasHttp.post).mockResolvedValue(ok(quoteDetail))
|
||||
vi.mocked(overseasHttp.put).mockResolvedValue(ok({ ...quoteDetail, summary: { ...quoteSummary, recordVersion: '8' } }))
|
||||
vi.mocked(overseasHttp.patch).mockResolvedValue(ok({ ...quoteDetail, summary: { ...quoteSummary, status: 'ACTIVE', recordVersion: '9' } }))
|
||||
|
||||
await createQuote(create)
|
||||
const updated = await updateQuote(longId, update)
|
||||
await changeQuoteStatus(longId, 'ACTIVE', 8)
|
||||
|
||||
expect(updated.summary.recordVersion).toBe(8)
|
||||
expect(overseasHttp.post).toHaveBeenCalledWith('/platform/v1/last-leg/master-data/overseas-quotes', create)
|
||||
expect(overseasHttp.put).toHaveBeenCalledWith(`/platform/v1/last-leg/master-data/overseas-quotes/${longId}`, update)
|
||||
expect(overseasHttp.patch).toHaveBeenCalledWith(`/platform/v1/last-leg/master-data/overseas-quotes/${longId}/status`, { status: 'ACTIVE', recordVersion: 8 })
|
||||
expect(update).not.toHaveProperty('warehouseId')
|
||||
expect(update).not.toHaveProperty('versionCode')
|
||||
})
|
||||
|
||||
it('uses the vehicle contract and preserves string IDs', async () => {
|
||||
vi.mocked(overseasHttp.get).mockResolvedValue(ok({ records: [], page: 1, size: 20, total: 0, pages: 0 })); await listVehicles({ page: 1, size: 20, ownership: 'OWNED' })
|
||||
vi.mocked(overseasHttp.get).mockResolvedValue(ok({ id: '9223372036854775807' })); await getVehicle('9223372036854775807')
|
||||
expect(overseasHttp.get).toHaveBeenLastCalledWith('/platform/v1/last-leg/master-data/vehicles/9223372036854775807')
|
||||
const payload = { plateNumber: 'CA-01', model: 'Truck', fuelConsumptionPer100Km: 12, mileageKm: 1, driverOrDispatchCompany: null, phone: null, region: 'OVERSEAS' as const, ownership: 'OWNED' as const, remark: null, status: 'IN_SERVICE' as const }
|
||||
vi.mocked(overseasHttp.post).mockResolvedValue(ok({})); await createVehicle(payload)
|
||||
vi.mocked(overseasHttp.put).mockResolvedValue(ok({})); await updateVehicle('9', { ...payload, recordVersion: 1 })
|
||||
vi.mocked(overseasHttp.patch).mockResolvedValue(ok({})); await changeVehicleStatus('9', 'INACTIVE', 1)
|
||||
expect(overseasHttp.patch).toHaveBeenLastCalledWith('/platform/v1/last-leg/master-data/vehicles/9/status', { status: 'INACTIVE', recordVersion: 1 })
|
||||
it('normalizes exchange-rate decimal/version and uses month as the stable key', async () => {
|
||||
vi.mocked(overseasHttp.get)
|
||||
.mockResolvedValueOnce(ok({ records: [exchangeRate], page: '1', size: '20', total: '1', pages: '1' }))
|
||||
.mockResolvedValueOnce(ok(exchangeRate))
|
||||
.mockResolvedValueOnce(ok({ latestMonth: '2026-08', latestUsdToCnyAverage: '7.12345678', updatedBy: 'admin', updatedAt: '2026-08-24T10:00:00' }))
|
||||
const page = await listExchangeRates({ page: 1, size: 20, monthFrom: '2026-08', monthTo: '2026-08' })
|
||||
const detail = await getExchangeRate('2026-08')
|
||||
const summary = await getExchangeRateSummary()
|
||||
expect(page.records[0]).toMatchObject({ usdToCnyAverage: 7.12345678, recordVersion: 3 })
|
||||
expect(detail).toMatchObject({ month: '2026-08', usdToCnyAverage: 7.12345678, recordVersion: 3 })
|
||||
expect(summary.latestUsdToCnyAverage).toBe(7.12345678)
|
||||
|
||||
vi.mocked(overseasHttp.post).mockResolvedValue(ok(exchangeRate))
|
||||
vi.mocked(overseasHttp.put).mockResolvedValue(ok({ ...exchangeRate, recordVersion: '4' }))
|
||||
vi.mocked(overseasHttp.patch).mockResolvedValue(ok({ ...exchangeRate, status: 'INACTIVE', recordVersion: '5' }))
|
||||
await createExchangeRate({ month: '2026-08', usdToCnyAverage: 7.12345678, remark: null })
|
||||
await updateExchangeRate('2026-08', { recordVersion: 3, usdToCnyAverage: 7.2, remark: null })
|
||||
await changeExchangeRateStatus('2026-08', 'INACTIVE', 4, '停用')
|
||||
expect(overseasHttp.patch).toHaveBeenLastCalledWith('/platform/v1/last-leg/master-data/exchange-rates/2026-08/status', { status: 'INACTIVE', recordVersion: 4, remark: '停用' })
|
||||
})
|
||||
|
||||
it('downloads each CSV through overseasHttp', async () => {
|
||||
const blob = new Blob(['csv']); vi.mocked(overseasHttp.get).mockResolvedValue(blob)
|
||||
await exportQuotes({ status: 'ACTIVE' }); await exportExchangeRates({}); await exportVehicles({ ownership: 'OWNED' })
|
||||
it('normalizes vehicle decimals/version while preserving long string IDs', async () => {
|
||||
vi.mocked(overseasHttp.get)
|
||||
.mockResolvedValueOnce(ok({ records: [vehicle], page: '1', size: '20', total: '1', pages: '1' }))
|
||||
.mockResolvedValueOnce(ok(vehicle))
|
||||
const result = await listVehicles({ page: 1, size: 20, ownership: 'OWNED' })
|
||||
const detail = await getVehicle(longId)
|
||||
expect(result.records[0]).toMatchObject({ id: longId, fuelConsumptionPer100Km: 12.5, mileageKm: 101, recordVersion: 4 })
|
||||
expect(detail.id).toBe(longId)
|
||||
|
||||
const editable = { plateNumber: 'CA-01', model: 'Truck', fuelConsumptionPer100Km: 12.5, mileageKm: 101, driverOrDispatchCompany: null, phone: null, region: 'OVERSEAS' as const, ownership: 'OWNED' as const, remark: null }
|
||||
vi.mocked(overseasHttp.post).mockResolvedValue(ok(vehicle))
|
||||
vi.mocked(overseasHttp.put).mockResolvedValue(ok({ ...vehicle, recordVersion: '5' }))
|
||||
vi.mocked(overseasHttp.patch).mockResolvedValue(ok({ ...vehicle, status: 'INACTIVE', recordVersion: '6' }))
|
||||
await createVehicle({ ...editable, status: 'IN_SERVICE' })
|
||||
await updateVehicle(longId, { ...editable, recordVersion: 4 })
|
||||
await changeVehicleStatus(longId, 'INACTIVE', 5)
|
||||
expect(overseasHttp.put).toHaveBeenCalledWith(`/platform/v1/last-leg/master-data/vehicles/${longId}`, { ...editable, recordVersion: 4 })
|
||||
expect(overseasHttp.patch).toHaveBeenCalledWith(`/platform/v1/last-leg/master-data/vehicles/${longId}/status`, { status: 'INACTIVE', recordVersion: 5 })
|
||||
})
|
||||
|
||||
it('preserves A0409 metadata instead of translating it into success', async () => {
|
||||
vi.mocked(overseasHttp.put).mockResolvedValue({ code: 'A0409', message: '记录版本已变化', data: null, requestId: 'request-conflict', traceId: 'trace-conflict' })
|
||||
const error = await updateExchangeRate('2026-08', { recordVersion: 1, usdToCnyAverage: 7.2, remark: null }).catch((reason: unknown) => reason)
|
||||
expect(error).toBeInstanceOf(ApiResponseError)
|
||||
expect(error).toMatchObject({ code: 'A0409', message: '记录版本已变化', requestId: 'request-conflict', traceId: 'trace-conflict' })
|
||||
})
|
||||
|
||||
it('downloads real CSV Blobs and rejects JSON error Blobs with trace metadata', async () => {
|
||||
const csv = new Blob(['id,status\n1,ACTIVE\n'], { type: 'text/csv;charset=UTF-8' })
|
||||
vi.mocked(overseasHttp.get)
|
||||
.mockResolvedValueOnce(csv)
|
||||
.mockResolvedValueOnce(csv)
|
||||
.mockResolvedValueOnce(csv)
|
||||
.mockResolvedValueOnce(new Blob([JSON.stringify({ code: 'A0409', message: '导出条件已变化', data: null, requestId: 'request-export' })], { type: 'application/json' }))
|
||||
|
||||
await expect(exportQuotes({ status: 'ACTIVE' })).resolves.toBe(csv)
|
||||
await expect(exportExchangeRates({})).resolves.toBe(csv)
|
||||
await expect(exportVehicles({ ownership: 'OWNED' })).resolves.toBe(csv)
|
||||
const error = await exportVehicles({}).catch((reason: unknown) => reason)
|
||||
expect(error).toBeInstanceOf(ApiResponseError)
|
||||
expect(error).toMatchObject({ code: 'A0409', message: '导出条件已变化', requestId: 'request-export' })
|
||||
expect(overseasHttp.get).toHaveBeenNthCalledWith(1, '/platform/v1/last-leg/master-data/overseas-quotes/export', { params: { status: 'ACTIVE' }, responseType: 'blob' })
|
||||
expect(overseasHttp.get).toHaveBeenNthCalledWith(2, '/platform/v1/last-leg/master-data/exchange-rates/export', { params: {}, responseType: 'blob' })
|
||||
expect(overseasHttp.get).toHaveBeenNthCalledWith(3, '/platform/v1/last-leg/master-data/vehicles/export', { params: { ownership: 'OWNED' }, responseType: 'blob' })
|
||||
|
||||
@@ -1,42 +1,99 @@
|
||||
import axios from 'axios'
|
||||
import { overseasHttp } from '@/shared/api/overseasHttp'
|
||||
import { unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
|
||||
import { ApiResponseError, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
|
||||
import type {
|
||||
MasterDataStatus, MonthlyExchangeRate, OverseasWarehouseQuote, OverseasWarehouseQuoteDetail, PartnerWarehouseOption,
|
||||
MasterDataStatus, MonthlyExchangeRate, OverseasTransitPrice, OverseasWarehouseFee, OverseasWarehouseQuote, OverseasWarehouseQuoteDetail, PartnerWarehouseOption,
|
||||
VehicleDetail, VehicleOwnership, VehicleRegion, VehicleStatus,
|
||||
} from '../types'
|
||||
|
||||
export interface PageResult<T> { records: T[]; page: number; size: number; total: number; pages: number }
|
||||
type PagePayload<T> = Omit<PageResult<T>, 'page' | 'size' | 'total' | 'pages'> & { page: number | string; size: number | string; total: number | string; pages: number | string }
|
||||
const base = '/platform/v1/last-leg/master-data'
|
||||
function page<T>(value: PagePayload<T>): PageResult<T> { return { ...value, page: Number(value.page), size: Number(value.size), total: Number(value.total), pages: Number(value.pages) } }
|
||||
async function getPage<T>(path: string, params: object) { return page(unwrapApiResponse(await overseasHttp.get<ApiEnvelope<PagePayload<T>>, ApiEnvelope<PagePayload<T>>>(path, { params }))) }
|
||||
async function download(path: string, params: object) { return overseasHttp.get<Blob, Blob>(path, { params, responseType: 'blob' }) }
|
||||
type Numeric = number | string
|
||||
type QuoteWire = Omit<OverseasWarehouseQuote, 'recordVersion'> & { recordVersion: Numeric }
|
||||
type FeeWire = Omit<OverseasWarehouseFee, 'unitPrice' | 'minimumCharge'> & { unitPrice: Numeric; minimumCharge: Numeric | null }
|
||||
type TransitPriceWire = Omit<OverseasTransitPrice, 'estimatedMiles' | 'price' | 'minimumCharge'> & { estimatedMiles: Numeric | null; price: Numeric; minimumCharge: Numeric | null }
|
||||
type QuoteDetailWire = Omit<OverseasWarehouseQuoteDetail, 'summary' | 'warehouseFees' | 'transitPrices'> & { summary: QuoteWire; warehouseFees: FeeWire[]; transitPrices: TransitPriceWire[] }
|
||||
type ExchangeRateWire = Omit<MonthlyExchangeRate, 'usdToCnyAverage' | 'recordVersion'> & { usdToCnyAverage: Numeric; recordVersion: Numeric }
|
||||
type ExchangeRateSummary = { latestMonth: string | null; latestUsdToCnyAverage: number | null; updatedBy: string | null; updatedAt: string | null }
|
||||
type ExchangeRateSummaryWire = Omit<ExchangeRateSummary, 'latestUsdToCnyAverage'> & { latestUsdToCnyAverage: Numeric | null }
|
||||
type VehicleWire = Omit<VehicleDetail, 'fuelConsumptionPer100Km' | 'mileageKm' | 'recordVersion'> & { fuelConsumptionPer100Km: Numeric; mileageKm: Numeric; recordVersion: Numeric }
|
||||
|
||||
function page<T, W>(value: PagePayload<W>, normalize: (record: W) => T): PageResult<T> {
|
||||
return { ...value, records: value.records.map(normalize), page: Number(value.page), size: Number(value.size), total: Number(value.total), pages: Number(value.pages) }
|
||||
}
|
||||
async function getPage<T, W>(path: string, params: object, normalize: (record: W) => T) {
|
||||
return page(unwrapApiResponse(await overseasHttp.get<ApiEnvelope<PagePayload<W>>, ApiEnvelope<PagePayload<W>>>(path, { params })), normalize)
|
||||
}
|
||||
const numeric = (value: Numeric) => Number(value)
|
||||
const nullableNumeric = (value: Numeric | null) => value == null ? null : Number(value)
|
||||
const normalizeQuote = (value: QuoteWire): OverseasWarehouseQuote => ({ ...value, recordVersion: numeric(value.recordVersion) })
|
||||
const normalizeFee = (value: FeeWire): OverseasWarehouseFee => ({ ...value, unitPrice: numeric(value.unitPrice), minimumCharge: nullableNumeric(value.minimumCharge) })
|
||||
const normalizeTransitPrice = (value: TransitPriceWire): OverseasTransitPrice => ({ ...value, estimatedMiles: nullableNumeric(value.estimatedMiles), price: numeric(value.price), minimumCharge: nullableNumeric(value.minimumCharge) })
|
||||
const normalizeQuoteDetail = (value: QuoteDetailWire): OverseasWarehouseQuoteDetail => ({ ...value, summary: normalizeQuote(value.summary), warehouseFees: value.warehouseFees.map(normalizeFee), transitPrices: value.transitPrices.map(normalizeTransitPrice) })
|
||||
const normalizeExchangeRate = (value: ExchangeRateWire): MonthlyExchangeRate => ({ ...value, usdToCnyAverage: numeric(value.usdToCnyAverage), recordVersion: numeric(value.recordVersion) })
|
||||
const normalizeVehicle = (value: VehicleWire): VehicleDetail => ({ ...value, fuelConsumptionPer100Km: numeric(value.fuelConsumptionPer100Km), mileageKm: numeric(value.mileageKm), recordVersion: numeric(value.recordVersion) })
|
||||
|
||||
async function csvError(blob: Blob, status: number) {
|
||||
if (!blob.type.toLowerCase().includes('json')) return null
|
||||
try {
|
||||
const payload = JSON.parse(await blob.text()) as Partial<ApiEnvelope<unknown>>
|
||||
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) throw new Error('invalid envelope')
|
||||
return new ApiResponseError(payload.message || 'CSV 导出失败', payload.code || 'DOWNLOAD_FAILED', status,
|
||||
payload.data, payload.requestId, payload.traceId)
|
||||
} catch (error) {
|
||||
if (error instanceof ApiResponseError) return error
|
||||
return new ApiResponseError('CSV 导出接口返回了无法识别的 JSON 响应', 'INVALID_DOWNLOAD_RESPONSE', status)
|
||||
}
|
||||
}
|
||||
async function download(path: string, params: object) {
|
||||
try {
|
||||
const blob = await overseasHttp.get<Blob, Blob>(path, { params, responseType: 'blob' })
|
||||
const error = await csvError(blob, 200)
|
||||
if (error) throw error
|
||||
return blob
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error) && error.response?.data instanceof Blob) {
|
||||
const responseError = await csvError(error.response.data, error.response.status)
|
||||
if (responseError) throw responseError
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export interface QuoteQuery { page: number; size: number; keyword?: string; warehouseId?: string; providerName?: string; currency?: string; status?: MasterDataStatus }
|
||||
export type QuoteSave = Pick<OverseasWarehouseQuote, 'name' | 'providerName' | 'currency' | 'effectiveStart' | 'effectiveEnd' | 'warehouseFees' | 'transitPrices' | 'remark'>
|
||||
& { warehouseId?: string; versionCode?: string; recordVersion?: number }
|
||||
export const listQuotes = (query: QuoteQuery) => getPage<OverseasWarehouseQuote>(`${base}/overseas-quotes`, query)
|
||||
export async function getQuote(id: string) { return unwrapApiResponse(await overseasHttp.get<ApiEnvelope<OverseasWarehouseQuoteDetail>, ApiEnvelope<OverseasWarehouseQuoteDetail>>(`${base}/overseas-quotes/${encodeURIComponent(id)}`)) }
|
||||
type QuoteDetailFields = { warehouseFees: OverseasWarehouseFee[]; transitPrices: OverseasTransitPrice[] }
|
||||
type QuoteEditableFields = Pick<OverseasWarehouseQuote, 'name' | 'providerName' | 'currency' | 'effectiveStart' | 'remark'>
|
||||
& { effectiveEnd: string } & QuoteDetailFields
|
||||
export type QuoteCreate = QuoteEditableFields & Pick<OverseasWarehouseQuote, 'warehouseId' | 'versionCode'>
|
||||
export type QuoteUpdate = QuoteEditableFields & Pick<OverseasWarehouseQuote, 'recordVersion'>
|
||||
export const listQuotes = (query: QuoteQuery) => getPage<OverseasWarehouseQuote, QuoteWire>(`${base}/overseas-quotes`, query, normalizeQuote)
|
||||
export async function getQuote(id: string) { return normalizeQuoteDetail(unwrapApiResponse(await overseasHttp.get<ApiEnvelope<QuoteDetailWire>, ApiEnvelope<QuoteDetailWire>>(`${base}/overseas-quotes/${encodeURIComponent(id)}`))) }
|
||||
export async function listQuoteWarehouseOptions() { return unwrapApiResponse(await overseasHttp.get<ApiEnvelope<PartnerWarehouseOption[]>, ApiEnvelope<PartnerWarehouseOption[]>>(`${base}/overseas-quotes/warehouse-options`)) }
|
||||
export async function createQuote(payload: QuoteSave) { return unwrapApiResponse(await overseasHttp.post<ApiEnvelope<OverseasWarehouseQuoteDetail>, ApiEnvelope<OverseasWarehouseQuoteDetail>>(`${base}/overseas-quotes`, payload)) }
|
||||
export async function updateQuote(id: string, payload: QuoteSave) { return unwrapApiResponse(await overseasHttp.put<ApiEnvelope<OverseasWarehouseQuoteDetail>, ApiEnvelope<OverseasWarehouseQuoteDetail>>(`${base}/overseas-quotes/${encodeURIComponent(id)}`, payload)) }
|
||||
export async function changeQuoteStatus(id: string, status: MasterDataStatus, recordVersion: number) { return unwrapApiResponse(await overseasHttp.patch<ApiEnvelope<OverseasWarehouseQuoteDetail>, ApiEnvelope<OverseasWarehouseQuoteDetail>>(`${base}/overseas-quotes/${encodeURIComponent(id)}/status`, { status, recordVersion })) }
|
||||
export async function createQuote(payload: QuoteCreate) { return normalizeQuoteDetail(unwrapApiResponse(await overseasHttp.post<ApiEnvelope<QuoteDetailWire>, ApiEnvelope<QuoteDetailWire>>(`${base}/overseas-quotes`, payload))) }
|
||||
export async function updateQuote(id: string, payload: QuoteUpdate) { return normalizeQuoteDetail(unwrapApiResponse(await overseasHttp.put<ApiEnvelope<QuoteDetailWire>, ApiEnvelope<QuoteDetailWire>>(`${base}/overseas-quotes/${encodeURIComponent(id)}`, payload))) }
|
||||
export async function changeQuoteStatus(id: string, status: MasterDataStatus, recordVersion: number) { return normalizeQuoteDetail(unwrapApiResponse(await overseasHttp.patch<ApiEnvelope<QuoteDetailWire>, ApiEnvelope<QuoteDetailWire>>(`${base}/overseas-quotes/${encodeURIComponent(id)}/status`, { status, recordVersion }))) }
|
||||
export const exportQuotes = (query: Omit<QuoteQuery, 'page' | 'size'>) => download(`${base}/overseas-quotes/export`, query)
|
||||
|
||||
export interface ExchangeRateQuery { page: number; size: number; monthFrom?: string; monthTo?: string; status?: MasterDataStatus }
|
||||
export const listExchangeRates = (query: ExchangeRateQuery) => getPage<MonthlyExchangeRate>(`${base}/exchange-rates`, query)
|
||||
export async function getExchangeRate(month: string) { return unwrapApiResponse(await overseasHttp.get<ApiEnvelope<MonthlyExchangeRate>, ApiEnvelope<MonthlyExchangeRate>>(`${base}/exchange-rates/${encodeURIComponent(month)}`)) }
|
||||
export async function getExchangeRateSummary() { return unwrapApiResponse(await overseasHttp.get<ApiEnvelope<{ latestMonth: string | null; latestUsdToCnyAverage: number | null; updatedBy: string | null; updatedAt: string | null }>, ApiEnvelope<{ latestMonth: string | null; latestUsdToCnyAverage: number | null; updatedBy: string | null; updatedAt: string | null }>>(`${base}/exchange-rates/summary`)) }
|
||||
export async function createExchangeRate(payload: { month: string; usdToCnyAverage: number; remark: string | null }) { return unwrapApiResponse(await overseasHttp.post<ApiEnvelope<MonthlyExchangeRate>, ApiEnvelope<MonthlyExchangeRate>>(`${base}/exchange-rates`, payload)) }
|
||||
export async function updateExchangeRate(month: string, payload: { recordVersion: number; usdToCnyAverage: number; remark: string | null }) { return unwrapApiResponse(await overseasHttp.put<ApiEnvelope<MonthlyExchangeRate>, ApiEnvelope<MonthlyExchangeRate>>(`${base}/exchange-rates/${encodeURIComponent(month)}`, payload)) }
|
||||
export async function changeExchangeRateStatus(month: string, status: MasterDataStatus, recordVersion: number, remark: string | null) { return unwrapApiResponse(await overseasHttp.patch<ApiEnvelope<MonthlyExchangeRate>, ApiEnvelope<MonthlyExchangeRate>>(`${base}/exchange-rates/${encodeURIComponent(month)}/status`, { status, recordVersion, remark })) }
|
||||
export const listExchangeRates = (query: ExchangeRateQuery) => getPage<MonthlyExchangeRate, ExchangeRateWire>(`${base}/exchange-rates`, query, normalizeExchangeRate)
|
||||
export async function getExchangeRate(month: string) { return normalizeExchangeRate(unwrapApiResponse(await overseasHttp.get<ApiEnvelope<ExchangeRateWire>, ApiEnvelope<ExchangeRateWire>>(`${base}/exchange-rates/${encodeURIComponent(month)}`))) }
|
||||
export async function getExchangeRateSummary(): Promise<ExchangeRateSummary> {
|
||||
const value = unwrapApiResponse(await overseasHttp.get<ApiEnvelope<ExchangeRateSummaryWire>, ApiEnvelope<ExchangeRateSummaryWire>>(`${base}/exchange-rates/summary`))
|
||||
return { ...value, latestUsdToCnyAverage: nullableNumeric(value.latestUsdToCnyAverage) }
|
||||
}
|
||||
export async function createExchangeRate(payload: { month: string; usdToCnyAverage: number; remark: string | null }) { return normalizeExchangeRate(unwrapApiResponse(await overseasHttp.post<ApiEnvelope<ExchangeRateWire>, ApiEnvelope<ExchangeRateWire>>(`${base}/exchange-rates`, payload))) }
|
||||
export async function updateExchangeRate(month: string, payload: { recordVersion: number; usdToCnyAverage: number; remark: string | null }) { return normalizeExchangeRate(unwrapApiResponse(await overseasHttp.put<ApiEnvelope<ExchangeRateWire>, ApiEnvelope<ExchangeRateWire>>(`${base}/exchange-rates/${encodeURIComponent(month)}`, payload))) }
|
||||
export async function changeExchangeRateStatus(month: string, status: MasterDataStatus, recordVersion: number, remark: string | null) { return normalizeExchangeRate(unwrapApiResponse(await overseasHttp.patch<ApiEnvelope<ExchangeRateWire>, ApiEnvelope<ExchangeRateWire>>(`${base}/exchange-rates/${encodeURIComponent(month)}/status`, { status, recordVersion, remark }))) }
|
||||
export const exportExchangeRates = (query: Omit<ExchangeRateQuery, 'page' | 'size'>) => download(`${base}/exchange-rates/export`, query)
|
||||
|
||||
export interface VehicleQuery { page: number; size: number; keyword?: string; ownership?: VehicleOwnership; region?: VehicleRegion; status?: VehicleStatus }
|
||||
export type VehicleSave = Omit<VehicleDetail, 'id' | 'recordVersion' | 'updatedBy' | 'updatedAt' | 'status'> & { status?: VehicleStatus; recordVersion?: number }
|
||||
export const listVehicles = (query: VehicleQuery) => getPage<VehicleDetail>(`${base}/vehicles`, query)
|
||||
export async function getVehicle(id: string) { return unwrapApiResponse(await overseasHttp.get<ApiEnvelope<VehicleDetail>, ApiEnvelope<VehicleDetail>>(`${base}/vehicles/${encodeURIComponent(id)}`)) }
|
||||
export async function createVehicle(payload: VehicleSave) { return unwrapApiResponse(await overseasHttp.post<ApiEnvelope<VehicleDetail>, ApiEnvelope<VehicleDetail>>(`${base}/vehicles`, payload)) }
|
||||
export async function updateVehicle(id: string, payload: VehicleSave) { return unwrapApiResponse(await overseasHttp.put<ApiEnvelope<VehicleDetail>, ApiEnvelope<VehicleDetail>>(`${base}/vehicles/${encodeURIComponent(id)}`, payload)) }
|
||||
export async function changeVehicleStatus(id: string, status: VehicleStatus, recordVersion: number) { return unwrapApiResponse(await overseasHttp.patch<ApiEnvelope<VehicleDetail>, ApiEnvelope<VehicleDetail>>(`${base}/vehicles/${encodeURIComponent(id)}/status`, { status, recordVersion })) }
|
||||
type VehicleEditableFields = Omit<VehicleDetail, 'id' | 'recordVersion' | 'updatedBy' | 'updatedAt' | 'status'>
|
||||
export type VehicleCreate = VehicleEditableFields & Pick<VehicleDetail, 'status'>
|
||||
export type VehicleUpdate = VehicleEditableFields & Pick<VehicleDetail, 'recordVersion'>
|
||||
export const listVehicles = (query: VehicleQuery) => getPage<VehicleDetail, VehicleWire>(`${base}/vehicles`, query, normalizeVehicle)
|
||||
export async function getVehicle(id: string) { return normalizeVehicle(unwrapApiResponse(await overseasHttp.get<ApiEnvelope<VehicleWire>, ApiEnvelope<VehicleWire>>(`${base}/vehicles/${encodeURIComponent(id)}`))) }
|
||||
export async function createVehicle(payload: VehicleCreate) { return normalizeVehicle(unwrapApiResponse(await overseasHttp.post<ApiEnvelope<VehicleWire>, ApiEnvelope<VehicleWire>>(`${base}/vehicles`, payload))) }
|
||||
export async function updateVehicle(id: string, payload: VehicleUpdate) { return normalizeVehicle(unwrapApiResponse(await overseasHttp.put<ApiEnvelope<VehicleWire>, ApiEnvelope<VehicleWire>>(`${base}/vehicles/${encodeURIComponent(id)}`, payload))) }
|
||||
export async function changeVehicleStatus(id: string, status: VehicleStatus, recordVersion: number) { return normalizeVehicle(unwrapApiResponse(await overseasHttp.patch<ApiEnvelope<VehicleWire>, ApiEnvelope<VehicleWire>>(`${base}/vehicles/${encodeURIComponent(id)}/status`, { status, recordVersion }))) }
|
||||
export const exportVehicles = (query: Omit<VehicleQuery, 'page' | 'size'>) => download(`${base}/vehicles/export`, query)
|
||||
|
||||
Reference in New Issue
Block a user