import { beforeEach, describe, expect, it, vi } from 'vitest' import { http } from '@/shared/api/http' import { getOwnedBusiness, listOwnedBusiness, listOwnedBusinessHistory, saveOwnedBusinessOperations, uploadOwnedBusinessPhoto, bindOwnedBusinessPhoto, getOwnedBusinessPhotoContent } from './ownedBusiness' import { ownedBusinessFixture } from '../components/OwnedBusinessFixtures' import type { OwnedBusinessPhoto, OwnedBusinessSave } from '../ownedBusinessTypes' vi.mock('@/shared/api/http', async importOriginal => { const actual = await importOriginal() 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/owned-business' const photo: OwnedBusinessPhoto = { slot: 'FUEL_START', category: 'FUEL', fileId: '9007199254740993', fileName: 'fuel.png', contentType: 'image/png', sizeBytes: '10', uploadedAt: '2026-10-20T10:00:00Z', savedAt: null } describe('owned business HTTP contract', () => { it('uses one list response for matched statistics and keeps physical count independent of cargo rows', async () => { const detail = ownedBusinessFixture() 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 listOwnedBusiness(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 = ownedBusinessFixture(); 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 getOwnedBusiness('DS/1')).row.dispatchRecordVersion).toBe('9007199254740993') expect(http.get).toHaveBeenCalledWith(`${base}/DS%2F1`, { params: undefined }) detail.dispatch.dispatchNo = 'another'; await expect(getOwnedBusiness('DS/1')).rejects.toThrow('不一致') }) it('does not convert money text, calculate a client unit price or submit a dispatch version', async () => { const body: OwnedBusinessSave = { recordVersion: '0', idempotencyKey: 'original-key', operations: { ...ownedBusinessFixture().row.operations, money: { currency: 'USD', amount: '9999999999999999.99' }, fuelConsumptionLitresPer100Km: '18.600' }, reason: null } const detail = ownedBusinessFixture(); detail.row.operations.money = body.operations.money vi.mocked(http.put).mockResolvedValue({ data: detail }) const saved = await saveOwnedBusinessOperations('DS-OWN-1', body) expect(http.put).toHaveBeenCalledExactlyOnceWith(`${base}/DS-OWN-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'], 'fuel.png', { type: 'image/png' }) await uploadOwnedBusinessPhoto('DS/1', 'FUEL_START', file, 'upload-key') const [url, form] = vi.mocked(http.post).mock.calls[0]! expect(url).toBe(`${base}/DS%2F1/photos/FUEL_START/uploads`); expect((form as FormData).get('file')).toBe(file); expect((form as FormData).get('idempotencyKey')).toBe('upload-key') const detail = ownedBusinessFixture(); detail.row.dispatchNo = 'DS/1'; detail.dispatch.dispatchNo = 'DS/1' vi.mocked(http.put).mockResolvedValue({ data: detail }) await bindOwnedBusinessPhoto('DS/1', 'FUEL_START', { recordVersion: '0', idempotencyKey: 'bind-key', fileId: photo.fileId }) expect(http.put).toHaveBeenCalledExactlyOnceWith(`${base}/DS%2F1/photos/FUEL_START`, { 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: ownedBusinessFixture().row.operations, photos: [] }], page: '1', size: '20', total: '2' } }) const value = await listOwnedBusinessHistory('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 getOwnedBusinessPhotoContent('DS/1', photo, '1') expect(http.get).toHaveBeenLastCalledWith(`${base}/DS%2F1/photos/FUEL_START/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(getOwnedBusinessPhotoContent('DS-OWN-1', photo)).rejects.toMatchObject({ code: 'A0403', requestId: 'request', traceId: 'trace' }) vi.mocked(http.get).mockResolvedValue(new Blob([], { type: 'image/png' })); await expect(getOwnedBusinessPhotoContent('DS-OWN-1', photo)).rejects.toThrow('为空') await expect(uploadOwnedBusinessPhoto('DS-OWN-1','FUEL_START',new File(['text'],'bad.txt',{type:'text/plain'}),'key')).rejects.toThrow('JPG') }) })