Files
admin-web/src/modules/overseas-master-data/api/outsourcedBusiness.test.ts
T

99 lines
9.2 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
import { http } from '@/shared/api/http'
import { getOutsourcedBusiness, listOutsourcedBusiness, listOutsourcedBusinessHistory, saveOutsourcedBusinessOperations, uploadOutsourcedBusinessPhoto, bindOutsourcedBusinessPhoto, getOutsourcedBusinessPhotoContent } from './outsourcedBusiness'
import { outsourcedBusinessFixture } from '../components/OutsourcedBusinessFixtures'
import type { OutsourcedBusinessPhoto, OutsourcedBusinessPhotoSlot, OutsourcedBusinessSave } from '../outsourcedBusinessTypes'
vi.mock('@/shared/api/http', async importOriginal => {
const actual = await importOriginal<typeof import('@/shared/api/http')>()
return { ...actual, http: { get: vi.fn(), put: vi.fn(), post: vi.fn() }, unwrapApiResponse: (value: { data: unknown }) => value.data }
})
beforeEach(() => vi.clearAllMocks())
const base = '/platform/v1/last-leg/fleet/outsourced-business'
const photo: OutsourcedBusinessPhoto = { slot: 'LOADING_SCENE', category: 'LOADING', fileId: '9007199254740993', fileName: 'loading.png', contentType: 'image/png', sizeBytes: '10', uploadedAt: '2026-10-20T10:00:00Z', savedAt: null }
describe('outsourced business HTTP contract', () => {
it.each(['OWNED', 'DISPATCHED'] as const)('accepts server-classified pooled %s vehicles in lists, details and write responses', async ownership => {
const detail = outsourcedBusinessFixture(); detail.row.vehicle.ownership = ownership
vi.mocked(http.get).mockResolvedValueOnce({ data: { records: [detail.row], page: 1, size: 20, total: 1, summary: { dispatchCount: 1, afterLoadingCount: 1, deliveringCount: 0, deliveredCount: 0 } } }).mockResolvedValue({ data: detail })
expect((await listOutsourcedBusiness({})).records[0]!.vehicle).toEqual(detail.row.vehicle)
expect((await getOutsourcedBusiness(detail.row.dispatchNo)).row.vehicle.ownership).toBe(ownership)
vi.mocked(http.put).mockResolvedValue({ data: detail })
expect((await saveOutsourcedBusinessOperations(detail.row.dispatchNo, { recordVersion: detail.row.recordVersion, idempotencyKey: 'pool-ownership', operations: detail.row.operations, reason: null })).row.vehicle.ownership).toBe(ownership)
})
it.each(['UNKNOWN', null, undefined])('rejects invalid vehicle ownership %s', async ownership => {
const detail = outsourcedBusinessFixture()
vi.mocked(http.get).mockResolvedValue({ data: { ...detail, row: { ...detail.row, vehicle: { ...detail.row.vehicle, ownership } } } })
await expect(getOutsourcedBusiness(detail.row.dispatchNo)).rejects.toThrow('来源或阶段')
})
it('uses one list response for matched statistics and keeps physical count independent of cargo rows', async () => {
const detail = outsourcedBusinessFixture()
vi.mocked(http.get).mockResolvedValue({ data: { records: [detail.row], page: '1', size: '20', total: '41', summary: { dispatchCount: '41', afterLoadingCount: '40', deliveringCount: '1', deliveredCount: '0' } } })
const query = { page: 1, size: 20, plannedDepartureFrom: '2026-10-20T00:00:00Z', plannedDepartureTo: '2026-10-21T00:00:00Z', keyword: 'FBA-1' }
const value = await listOutsourcedBusiness(query)
expect(http.get).toHaveBeenCalledExactlyOnceWith(base, { params: query })
expect(value.summary.dispatchCount).toBe(41); expect(value.records).toHaveLength(1); expect(value.records[0]!.actualPalletCount).toBe(1)
})
it('encodes the dispatch business number and preserves auxiliary/main versions independently', async () => {
const detail = outsourcedBusinessFixture(); detail.row.recordVersion = '0'; detail.row.dispatchRecordVersion = '9007199254740993'
detail.row.dispatchNo = 'DS/1'; detail.dispatch.dispatchNo = 'DS/1'
vi.mocked(http.get).mockResolvedValue({ data: detail })
expect((await getOutsourcedBusiness('DS/1')).row.dispatchRecordVersion).toBe('9007199254740993')
expect(http.get).toHaveBeenCalledWith(`${base}/DS%2F1`, { params: undefined })
detail.dispatch.dispatchNo = 'another'; await expect(getOutsourcedBusiness('DS/1')).rejects.toThrow('不一致')
})
it('does not convert money text, calculate a client unit price or submit a dispatch version', async () => {
const body: OutsourcedBusinessSave = { recordVersion: '0', idempotencyKey: 'original-key', operations: { ...outsourcedBusinessFixture().row.operations, money: { currency: 'USD', amount: '9999999999999999.99' } }, reason: null }
const detail = outsourcedBusinessFixture(); detail.row.operations.money = body.operations.money
vi.mocked(http.put).mockResolvedValue({ data: detail })
const saved = await saveOutsourcedBusinessOperations('DS-OUT-1', body)
expect(http.put).toHaveBeenCalledExactlyOnceWith(`${base}/DS-OUT-1/operations`, body)
expect(body.operations.money!.amount).toBe('9999999999999999.99'); expect('dispatchRecordVersion' in body).toBe(false)
expect(saved.row.operations.money!.amount).toBe('9999999999999999.99')
})
it('keeps upload and versioned file binding separate with the same prepared file identity', async () => {
vi.mocked(http.post).mockResolvedValue({ data: photo })
const file = new File(['image'], 'loading.png', { type: 'image/png' })
await uploadOutsourcedBusinessPhoto('DS/1', 'LOADING_SCENE', file, 'upload-key')
const [url, form] = vi.mocked(http.post).mock.calls[0]!
expect(url).toBe(`${base}/DS%2F1/photos/LOADING_SCENE/uploads`); expect((form as FormData).get('file')).toBe(file); expect((form as FormData).get('idempotencyKey')).toBe('upload-key')
const detail = outsourcedBusinessFixture(); detail.row.dispatchNo = 'DS/1'; detail.dispatch.dispatchNo = 'DS/1'
vi.mocked(http.put).mockResolvedValue({ data: detail })
await bindOutsourcedBusinessPhoto('DS/1', 'LOADING_SCENE', { recordVersion: '0', idempotencyKey: 'bind-key', fileId: photo.fileId })
expect(http.put).toHaveBeenCalledExactlyOnceWith(`${base}/DS%2F1/photos/LOADING_SCENE`, { recordVersion: '0', idempotencyKey: 'bind-key', fileId: photo.fileId })
})
it('retains the two history streams and reads historical files only at their explicit revision', async () => {
vi.mocked(http.get).mockResolvedValue({ data: { records: [{ eventSource: 'DISPATCH', recordVersion: null, dispatchRecordVersion: '9', operations: null, photos: [] }, { eventSource: 'OPERATIONS', recordVersion: '1', dispatchRecordVersion: null, operations: outsourcedBusinessFixture().row.operations, photos: [] }], page: '1', size: '20', total: '2' } })
const value = await listOutsourcedBusinessHistory('DS/1', 1, 20)
expect(value.records.map(record => [record.eventSource, record.recordVersion, record.dispatchRecordVersion])).toEqual([['DISPATCH',null,'9'],['OPERATIONS','1',null]])
vi.mocked(http.get).mockResolvedValue(new Blob(['image'], { type: 'image/png' }))
await getOutsourcedBusinessPhotoContent('DS/1', photo, '1')
expect(http.get).toHaveBeenLastCalledWith(`${base}/DS%2F1/photos/LOADING_SCENE/files/${photo.fileId}/content`, { responseType: 'blob', params: { revisionVersion: '1' } })
})
it('never renders JSON errors or empty bytes as a photo', async () => {
vi.mocked(http.get).mockResolvedValue(new Blob([JSON.stringify({ code: 'A0403', message: '拒绝读取', requestId: 'request', traceId: 'trace' })], { type: 'application/json' }))
await expect(getOutsourcedBusinessPhotoContent('DS-OUT-1', photo)).rejects.toMatchObject({ code: 'A0403', requestId: 'request', traceId: 'trace' })
vi.mocked(http.get).mockResolvedValue(new Blob([], { type: 'image/png' })); await expect(getOutsourcedBusinessPhotoContent('DS-OUT-1', photo)).rejects.toThrow('为空')
await expect(uploadOutsourcedBusinessPhoto('DS-OUT-1','LOADING_SCENE',new File(['text'],'bad.txt',{type:'text/plain'}),'key')).rejects.toThrow('JPG')
})
it.each(['2', null])('accepts authoritative external vehicle identity %s without looking up the current master', async vehicleId => {
const detail = outsourcedBusinessFixture(); detail.row.vehicle.vehicleId = vehicleId; detail.row.vehicle.carrierName = null
vi.mocked(http.get).mockResolvedValue({ data: detail })
const value = await getOutsourcedBusiness('DS-OUT-1')
expect(value.row.vehicle).toMatchObject({ vehicleId, ownership: 'DISPATCHED', carrierName: null })
expect(http.get).toHaveBeenCalledTimes(1)
})
it('rejects unsupported fuel or mileage writes before any request', async () => {
const detail = outsourcedBusinessFixture()
for (const field of ['actualDistanceMiles','fuelConsumptionLitresPer100Km','actualFuelLitres']) {
const body = { recordVersion:'0', idempotencyKey:'original-key', operations:{ ...detail.row.operations, [field]:'1' }, reason:null }
await expect(saveOutsourcedBusinessOperations('DS-OUT-1', body)).rejects.toThrow('不支持里程或油耗')
}
expect(http.put).not.toHaveBeenCalled()
for (const slot of ['FUEL_START','FUEL_END']) {
await expect(uploadOutsourcedBusinessPhoto('DS-OUT-1',slot as OutsourcedBusinessPhotoSlot,new File(['image'],'loading.png',{type:'image/png'}),'upload-key')).rejects.toThrow('不支持该照片分类')
await expect(bindOutsourcedBusinessPhoto('DS-OUT-1',slot as OutsourcedBusinessPhotoSlot,{recordVersion:'0',idempotencyKey:'bind-key',fileId:photo.fileId})).rejects.toThrow('不支持该照片分类')
}
expect(http.post).not.toHaveBeenCalled(); expect(http.put).not.toHaveBeenCalled()
})
})