feat(fleet): complete exception processing and trip evidence interfaces
This commit is contained in:
@@ -1,10 +1,33 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { http } from '@/shared/api/http'
|
||||
import { createExceptionOrder, listExceptionOrders, resolveExceptionRelation } from './exceptionOrders'
|
||||
import { createExceptionOrder, listExceptionOrders, resolveExceptionRelation, getExceptionDetail, commandException, uploadExceptionEvidence, normalizeExceptionDetail, type ExceptionDetail } from './exceptionOrders'
|
||||
|
||||
vi.mock('@/shared/api/http', async (importOriginal) => ({ ...(await importOriginal<typeof import('@/shared/api/http')>()), http: { get: vi.fn(), post: vi.fn() } }))
|
||||
|
||||
describe('exception order API', () => {
|
||||
it('normalizes actual string versions and quantities without converting business references to numbers', () => {
|
||||
const raw = { order: { version: '7', evidenceFileIds: ['190000000000000099'] }, returnSources: [{ outboundPalletItemId: '190000000000000098', inventoryPositionId: '190000000000000097', maxReturnPackages: '2' }] } as unknown as ExceptionDetail
|
||||
const result = normalizeExceptionDetail(raw)
|
||||
expect(result.order.version).toBe(7); expect(result.returnSources[0]?.maxReturnPackages).toBe(2)
|
||||
expect(result.returnSources[0]?.outboundPalletItemId).toBe('190000000000000098')
|
||||
for (const version of ['', '-1', '1.5', '9007199254740993', undefined]) expect(() => normalizeExceptionDetail({ ...raw, order: { ...raw.order, version } } as unknown as ExceptionDetail)).toThrow()
|
||||
})
|
||||
it('loads authoritative detail and scopes every versioned command and evidence upload to its customer and business number', async () => {
|
||||
vi.mocked(http.get).mockResolvedValue({ code: '200', data: { order: { version: '7', allowedActions: ['COMPLETE_EXCEPTION'] }, returnSources: [] } })
|
||||
await getExceptionDetail('100', 'EX/1')
|
||||
expect(http.get).toHaveBeenCalledWith('/platform/v1/last-leg/exception-orders/EX%2F1', { params: { customerId: '100' } })
|
||||
vi.mocked(http.post).mockResolvedValue({ code: '200', data: { exceptionNo: 'EX/1', version: '8', restoredPackages: '0' } })
|
||||
for (const action of ['start-processing', 'cancel', 'complete'] as const) {
|
||||
const body = { expectedVersion: 7, idempotencyKey: `original-${action}` }
|
||||
await commandException('100', 'EX/1', action, body)
|
||||
expect(http.post).toHaveBeenLastCalledWith(`/platform/v1/last-leg/exception-orders/EX%2F1/${action}`, body, { params: { customerId: '100' } })
|
||||
}
|
||||
const file = new File(['evidence'], 'receipt.txt')
|
||||
await uploadExceptionEvidence('100', 'EX/1', file)
|
||||
const call = vi.mocked(http.post).mock.calls.at(-1)!
|
||||
expect(call[0]).toBe('/platform/v1/last-leg/exception-orders/EX%2F1/evidence')
|
||||
expect((call[1] as FormData).get('file')).toBe(file); expect(call[2]).toEqual({ params: { customerId: '100' } })
|
||||
})
|
||||
beforeEach(() => { vi.mocked(http.get).mockReset(); vi.mocked(http.post).mockReset() })
|
||||
|
||||
it('queries the real paginated exception endpoint', async () => {
|
||||
|
||||
@@ -10,6 +10,37 @@ export interface ExceptionOrderRow {
|
||||
deliveryOrderId?: string | null; deliveryNo?: string | null; outboundNo?: string | null
|
||||
skuCode?: string | null; packageCount?: number | null; returnDirection?: string | null; returnLogisticsNo?: string | null
|
||||
handlingAction?: string | null; handlerName?: string | null; exceptionAt: string; updatedAt: string
|
||||
version?: number; allowedActions?: string[]; evidenceFileIds?: string[]; handlingResult?: string | null; completedAt?: string | null
|
||||
}
|
||||
|
||||
export interface ExceptionReturnSource { outboundPalletItemId: string; inventoryPositionId: string; palletNo?: string | null; skuCode?: string | null; maxReturnPackages: number }
|
||||
export interface ExceptionDetail { order: ExceptionOrderRow; returnSources: ExceptionReturnSource[] }
|
||||
export interface ExceptionReceipt { outboundPalletItemId: string; inventoryPositionId: string; receivedPackages: number; targetPalletId: string; targetPalletNo: string; targetLocationId: string; targetLocationCode: string }
|
||||
export interface ExceptionCommand { expectedVersion: number; idempotencyKey: string; handlingAction?: string; reason?: string; handlingResult?: string; evidenceFileIds?: string[]; receipts?: ExceptionReceipt[] }
|
||||
export type ExceptionAction = 'start-processing' | 'complete' | 'cancel'
|
||||
export interface ExceptionCommandResult { exceptionNo: string; status: string; version: number; restoredPackages: number; replayed: boolean }
|
||||
const exceptionPath = (number: string) => `/platform/v1/last-leg/exception-orders/${encodeURIComponent(number)}`
|
||||
function safeInteger(value: unknown, label: string) {
|
||||
if ((typeof value !== 'number' && typeof value !== 'string') || !/^\d+$/.test(String(value)) || !Number.isSafeInteger(Number(value))) throw new Error(`${label}无效,请重新加载`)
|
||||
return Number(value)
|
||||
}
|
||||
export function normalizeExceptionDetail(value: ExceptionDetail): ExceptionDetail {
|
||||
return { ...value, order: { ...value.order, version: safeInteger(value.order.version, '异常版本'), evidenceFileIds: value.order.evidenceFileIds?.map(String) },
|
||||
returnSources: value.returnSources.map(row => ({ ...row, outboundPalletItemId: String(row.outboundPalletItemId), inventoryPositionId: String(row.inventoryPositionId), maxReturnPackages: safeInteger(row.maxReturnPackages, '剩余可收数量') })) }
|
||||
}
|
||||
export async function getExceptionDetail(customerId: string, exceptionNo: string) {
|
||||
const response = await http.get<ApiEnvelope<ExceptionDetail>, ApiEnvelope<ExceptionDetail>>(exceptionPath(exceptionNo), { params: { customerId } })
|
||||
return normalizeExceptionDetail(unwrapApiResponse(response))
|
||||
}
|
||||
export async function commandException(customerId: string, exceptionNo: string, action: ExceptionAction, payload: ExceptionCommand) {
|
||||
const response = await http.post<ApiEnvelope<ExceptionCommandResult>, ApiEnvelope<ExceptionCommandResult>>(`${exceptionPath(exceptionNo)}/${action}`, payload, { params: { customerId } })
|
||||
const result = unwrapApiResponse(response)
|
||||
return { ...result, version: safeInteger(result.version, '操作版本'), restoredPackages: safeInteger(result.restoredPackages, '实际入账数量') }
|
||||
}
|
||||
export async function uploadExceptionEvidence(customerId: string, exceptionNo: string, file: File) {
|
||||
const form = new FormData(); form.append('file', file)
|
||||
const response = await http.post<ApiEnvelope<{ fileId: string; fileName: string }>, ApiEnvelope<{ fileId: string; fileName: string }>>(`${exceptionPath(exceptionNo)}/evidence`, form, { params: { customerId } })
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
export interface ExceptionRelation {
|
||||
lastMileOrderId?: string | null; lastMileOrderNo?: string | null; inboundOrderId?: string | null; inboundCode?: string | null
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { expect, it, vi } from 'vitest'
|
||||
import { http } from '@/shared/api/http'
|
||||
import { getOverseasLocation } from './locations'
|
||||
vi.mock('@/shared/api/http', async original => ({ ...await original<typeof import('@/shared/api/http')>(), http: { get: vi.fn() } }))
|
||||
it('reads one location through its warehouse-scoped detail endpoint without scanning pages', async () => {
|
||||
vi.mocked(http.get).mockResolvedValue({ code: '200', data: { id: '40', warehouseId: '92', locationCode: 'LOC' } })
|
||||
expect(await getOverseasLocation('92', '40')).toEqual({ id: '40', warehouseId: '92', locationCode: 'LOC' })
|
||||
expect(http.get).toHaveBeenCalledExactlyOnceWith('/platform/v1/overseas-warehouse-resources/92/locations/40')
|
||||
})
|
||||
@@ -35,6 +35,11 @@ export interface OverseasLocationQuery { page?: number; size?: number; keyword?:
|
||||
|
||||
function base(warehouseId: string) { return `/platform/v1/overseas-warehouse-resources/${encodeURIComponent(warehouseId)}/locations` }
|
||||
|
||||
export async function getOverseasLocation(warehouseId: string, locationId: string) {
|
||||
const response = await http.get<ApiEnvelope<OverseasLocationDto>, ApiEnvelope<OverseasLocationDto>>(`${base(warehouseId)}/${encodeURIComponent(locationId)}`)
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
|
||||
export async function listOverseasLocations(warehouseId: string, query: OverseasLocationQuery = {}) {
|
||||
const response = await http.get<ApiEnvelope<OverseasPagePayload<OverseasLocationDto>>, ApiEnvelope<OverseasPagePayload<OverseasLocationDto>>>(base(warehouseId), { params: { page: query.page ?? 1, size: query.size ?? 20, ...query } })
|
||||
return normalizeOverseasPage(unwrapApiResponse(response))
|
||||
|
||||
Reference in New Issue
Block a user