import { beforeEach, describe, expect, it, vi } from 'vitest' import { ApiResponseError, http } from '@/shared/api/http' 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/http', async (importOriginal) => ({ ...(await importOriginal()), http: { 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', maxPalletSpaces: 30, maxLoadKg: 18000, maxVolumeCbm: 90, 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('normalizes quote page/detail decimals and preserves long string IDs', async () => { vi.mocked(http.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(http.get).toHaveBeenNthCalledWith(1, '/platform/v1/last-leg/master-data/overseas-quotes', { params: { page: 1, size: 20, keyword: 'V1' } }) expect(http.get).toHaveBeenNthCalledWith(2, `/platform/v1/last-leg/master-data/overseas-quotes/${longId}`) expect(http.get).toHaveBeenNthCalledWith(3, '/platform/v1/last-leg/master-data/overseas-quotes/warehouse-options') }) 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(http.post).mockResolvedValue(ok(quoteDetail)) vi.mocked(http.put).mockResolvedValue(ok({ ...quoteDetail, summary: { ...quoteSummary, recordVersion: '8' } })) vi.mocked(http.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(http.post).toHaveBeenCalledWith('/platform/v1/last-leg/master-data/overseas-quotes', create) expect(http.put).toHaveBeenCalledWith(`/platform/v1/last-leg/master-data/overseas-quotes/${longId}`, update) expect(http.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('normalizes exchange-rate decimal/version and uses month as the stable key', async () => { vi.mocked(http.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(http.post).mockResolvedValue(ok(exchangeRate)) vi.mocked(http.put).mockResolvedValue(ok({ ...exchangeRate, recordVersion: '4' })) vi.mocked(http.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(http.patch).toHaveBeenLastCalledWith('/platform/v1/last-leg/master-data/exchange-rates/2026-08/status', { status: 'INACTIVE', recordVersion: 4, remark: '停用' }) }) it('normalizes vehicle decimals/version while preserving long string IDs', async () => { vi.mocked(http.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', maxPalletSpaces: 30, maxLoadKg: 18000, maxVolumeCbm: 90, fuelConsumptionPer100Km: 12.5, mileageKm: 101, driverOrDispatchCompany: null, phone: null, region: 'OVERSEAS' as const, ownership: 'OWNED' as const, remark: null } vi.mocked(http.post).mockResolvedValue(ok(vehicle)) vi.mocked(http.put).mockResolvedValue(ok({ ...vehicle, recordVersion: '5' })) vi.mocked(http.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(http.put).toHaveBeenCalledWith(`/platform/v1/last-leg/master-data/vehicles/${longId}`, { ...editable, recordVersion: 4 }) expect(http.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(http.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(http.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(http.get).toHaveBeenNthCalledWith(1, '/platform/v1/last-leg/master-data/overseas-quotes/export', { params: { status: 'ACTIVE' }, responseType: 'arraybuffer' }) expect(http.get).toHaveBeenNthCalledWith(2, '/platform/v1/last-leg/master-data/exchange-rates/export', { params: {}, responseType: 'arraybuffer' }) expect(http.get).toHaveBeenNthCalledWith(3, '/platform/v1/last-leg/master-data/vehicles/export', { params: { ownership: 'OWNED' }, responseType: 'arraybuffer' }) }) })