优化
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { http } from '@/shared/api/http'
|
||||
import {
|
||||
getConfirmedLoadingList,
|
||||
getPreloadPlan,
|
||||
listConfirmedLoadingLists,
|
||||
listPreloadPlans,
|
||||
updatePreloadTransport,
|
||||
} from './platformLoadingLists'
|
||||
|
||||
const preloadSummary = {
|
||||
planId: 1900000000000000001n as unknown as string,
|
||||
preloadNo: 'PL-001',
|
||||
containerNo: 'MSCU1234567',
|
||||
internalContainerNo: 'YG-001',
|
||||
customers: [{ accountId: 1900000000000000002n as unknown as string, customerName: '测试客户' }],
|
||||
warehouseId: 1900000000000000003n as unknown as string,
|
||||
warehouseName: '义乌仓',
|
||||
vesselName: 'MATSON',
|
||||
voyageNo: '001E',
|
||||
containerType: '40HC',
|
||||
status: 'LOADING',
|
||||
skuCount: '2',
|
||||
totalPackageCount: '10',
|
||||
totalVolumeCbm: 12.3,
|
||||
totalGrossWeightKg: 1200,
|
||||
createdAt: '2026-08-10T10:00:00',
|
||||
version: 3,
|
||||
}
|
||||
|
||||
const detail = {
|
||||
summary: preloadSummary,
|
||||
items: [{
|
||||
itemId: 11, accountId: 12, inboundId: 13, inboundDetailId: 14, productQuantity: 20,
|
||||
customerName: '测试客户', inboundCode: 'RK001', skuCode: 'SKU001', destinationWarehouse: 'ONT8', chineseName: '产品', englishName: null,
|
||||
goodsImageUrl: null, packageCount: 2, palletCount: 1, grossWeightKg: 10, netWeightKg: 9, volumeCbm: 1,
|
||||
dimensionalWeightKg: 2, unitsPerPackage: 10, fbaTrackingNo: null, shipmentNo: null, shippingMark: null,
|
||||
internalRemark: null, customerRemark: null, warehouseRemark: null, loadSequence: 1, frozenAt: null,
|
||||
}],
|
||||
histories: [{ historyId: 21, itemId: 11, actionType: 'UPDATE_TRANSPORT', fromStatus: 'LOADING', toStatus: 'LOADING', beforeSnapshot: null, afterSnapshot: null, operatorId: 22, operatorName: '操作员', operationReason: '调整', occurredAt: '2026-08-10T11:00:00' }],
|
||||
}
|
||||
|
||||
describe('platform loading lists API', () => {
|
||||
beforeEach(() => vi.restoreAllMocks())
|
||||
|
||||
it('queries preload plans with required warehouse and fixed LOADING status', async () => {
|
||||
const get = vi.spyOn(http, 'get').mockResolvedValue({ code: '200', message: 'ok', data: { records: [preloadSummary], page: '1', size: '20', total: '1', pages: '1' } })
|
||||
const result = await listPreloadPlans({ page: 1, size: 20, warehouseId: '300', keyword: 'PL' })
|
||||
|
||||
expect(get).toHaveBeenCalledWith('/platform/v1/first-leg/loading-plans', { params: { page: 1, size: 20, warehouseId: '300', keyword: 'PL', status: 'LOADING' } })
|
||||
expect(result.total).toBe(1)
|
||||
expect(result.records[0]?.planId).toBe('1900000000000000001')
|
||||
expect(result.records[0]?.customers[0]?.accountId).toBe('1900000000000000002')
|
||||
})
|
||||
|
||||
it('normalizes long ids in preload detail items and history', async () => {
|
||||
vi.spyOn(http, 'get').mockResolvedValue({ code: '200', message: 'ok', data: detail })
|
||||
const result = await getPreloadPlan('100', '300')
|
||||
|
||||
expect(result.items[0]).toMatchObject({ itemId: '11', accountId: '12', inboundId: '13', inboundDetailId: '14', productQuantity: '20' })
|
||||
expect(result.histories[0]).toMatchObject({ historyId: '21', itemId: '11', operatorId: '22' })
|
||||
})
|
||||
|
||||
it('updates transport with expectedVersion, warehouse and idempotency key', async () => {
|
||||
const patch = vi.spyOn(http, 'patch').mockResolvedValue({ code: '200', message: 'ok', data: detail })
|
||||
await updatePreloadTransport('100', '300', { containerNo: 'MSCU1234567', voyageNo: '001E', expectedVersion: 3, operationReason: '调整' })
|
||||
|
||||
expect(patch.mock.calls[0]?.[0]).toBe('/platform/v1/first-leg/loading-plans/100/transport')
|
||||
expect(patch.mock.calls[0]?.[1]).toMatchObject({ expectedVersion: 3, voyageNo: '001E' })
|
||||
expect(patch.mock.calls[0]?.[2]).toMatchObject({ params: { warehouseId: '300' }, headers: { 'Idempotency-Key': expect.any(String) } })
|
||||
})
|
||||
|
||||
it('uses distinct completed list and detail endpoints', async () => {
|
||||
const confirmed = { ...preloadSummary, loadingListNo: 'LL-001', totalNetWeightKg: 1100, totalQuantity: '20', remark: null, createdBy: 9, createdByName: '操作员', status: 'COMPLETED' }
|
||||
const get = vi.spyOn(http, 'get')
|
||||
.mockResolvedValueOnce({ code: '200', message: 'ok', data: { records: [confirmed], page: '1', size: '20', total: '1', pages: '1' } })
|
||||
.mockResolvedValueOnce({ code: '200', message: 'ok', data: { ...detail, summary: confirmed } })
|
||||
|
||||
const page = await listConfirmedLoadingLists({ page: 1, size: 20, warehouseId: '300' })
|
||||
const loaded = await getConfirmedLoadingList('100', '300')
|
||||
|
||||
expect(get.mock.calls[0]?.[0]).toBe('/platform/v1/first-leg/loading-lists')
|
||||
expect(get.mock.calls[1]?.[0]).toBe('/platform/v1/first-leg/loading-lists/100')
|
||||
expect(page.records[0]?.createdBy).toBe('9')
|
||||
expect(loaded.summary.status).toBe('COMPLETED')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,257 @@
|
||||
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
|
||||
|
||||
export type LoadingListMode = 'preload' | 'confirmed'
|
||||
|
||||
export interface LoadingCustomer {
|
||||
accountId: string
|
||||
customerName: string | null
|
||||
}
|
||||
|
||||
export interface PreloadSummary {
|
||||
planId: string
|
||||
preloadNo: string
|
||||
containerNo: string | null
|
||||
internalContainerNo: string | null
|
||||
customers: LoadingCustomer[]
|
||||
warehouseId: string | null
|
||||
warehouseName: string | null
|
||||
vesselName: string | null
|
||||
voyageNo: string | null
|
||||
containerType: string | null
|
||||
status: string
|
||||
skuCount: string
|
||||
totalPackageCount: string
|
||||
totalVolumeCbm: number
|
||||
totalGrossWeightKg: number
|
||||
createdAt: string
|
||||
version: number
|
||||
}
|
||||
|
||||
export interface ConfirmedSummary {
|
||||
planId: string
|
||||
loadingListNo: string
|
||||
warehouseId: string | null
|
||||
warehouseName: string | null
|
||||
containerNo: string | null
|
||||
internalContainerNo: string | null
|
||||
status: string
|
||||
totalPackageCount: string
|
||||
totalGrossWeightKg: number
|
||||
totalNetWeightKg: number
|
||||
totalVolumeCbm: number
|
||||
totalQuantity: string
|
||||
remark: string | null
|
||||
createdBy: string | null
|
||||
createdByName: string | null
|
||||
createdAt: string
|
||||
version: number
|
||||
}
|
||||
|
||||
export type LoadingListSummary = PreloadSummary | ConfirmedSummary
|
||||
|
||||
export interface LoadingItem {
|
||||
itemId: string
|
||||
accountId: string
|
||||
customerName: string | null
|
||||
inboundId: string
|
||||
inboundDetailId: string
|
||||
inboundCode: string | null
|
||||
skuCode: string | null
|
||||
destinationWarehouse: string | null
|
||||
chineseName: string | null
|
||||
englishName: string | null
|
||||
goodsImageUrl: string | null
|
||||
packageCount: number
|
||||
palletCount: number
|
||||
grossWeightKg: number
|
||||
netWeightKg: number
|
||||
volumeCbm: number
|
||||
dimensionalWeightKg: number
|
||||
productQuantity: string
|
||||
unitsPerPackage: number
|
||||
fbaTrackingNo: string | null
|
||||
shipmentNo: string | null
|
||||
shippingMark: string | null
|
||||
internalRemark: string | null
|
||||
customerRemark: string | null
|
||||
warehouseRemark: string | null
|
||||
loadSequence: number
|
||||
frozenAt: string | null
|
||||
}
|
||||
|
||||
export interface LoadingHistory {
|
||||
historyId: string
|
||||
itemId: string | null
|
||||
actionType: string
|
||||
fromStatus: string | null
|
||||
toStatus: string | null
|
||||
beforeSnapshot: string | null
|
||||
afterSnapshot: string | null
|
||||
operatorId: string
|
||||
operatorName: string | null
|
||||
operationReason: string | null
|
||||
occurredAt: string
|
||||
}
|
||||
|
||||
export interface LoadingDetail<T extends LoadingListSummary = LoadingListSummary> {
|
||||
summary: T
|
||||
items: LoadingItem[]
|
||||
histories: LoadingHistory[]
|
||||
}
|
||||
|
||||
export interface LoadingListPage<T extends LoadingListSummary = LoadingListSummary> {
|
||||
records: T[]
|
||||
page: number
|
||||
size: number
|
||||
total: number
|
||||
pages: number
|
||||
}
|
||||
|
||||
export interface LoadingListQuery {
|
||||
page: number
|
||||
size: number
|
||||
warehouseId: string
|
||||
keyword?: string
|
||||
createdFrom?: string
|
||||
createdTo?: string
|
||||
}
|
||||
|
||||
export interface TransportUpdateRequest {
|
||||
containerNo: string | null
|
||||
voyageNo: string | null
|
||||
expectedVersion: number
|
||||
operationReason: string | null
|
||||
}
|
||||
|
||||
type RawId = string | number | null | undefined
|
||||
type RawPage<T extends LoadingListSummary> = Omit<LoadingListPage<T>, 'page' | 'size' | 'total' | 'pages'> & {
|
||||
page: string | number
|
||||
size: string | number
|
||||
total: string | number
|
||||
pages: string | number
|
||||
}
|
||||
|
||||
const basePath = '/platform/v1/first-leg'
|
||||
|
||||
function requiredId(value: RawId) {
|
||||
return String(value ?? '')
|
||||
}
|
||||
|
||||
function optionalId(value: RawId) {
|
||||
return value === null || value === undefined ? null : String(value)
|
||||
}
|
||||
|
||||
function normalizePreloadSummary(raw: PreloadSummary): PreloadSummary {
|
||||
return {
|
||||
...raw,
|
||||
planId: requiredId(raw.planId),
|
||||
warehouseId: optionalId(raw.warehouseId),
|
||||
skuCount: String(raw.skuCount ?? '0'),
|
||||
totalPackageCount: String(raw.totalPackageCount ?? '0'),
|
||||
customers: (raw.customers || []).map((customer) => ({
|
||||
...customer,
|
||||
accountId: requiredId(customer.accountId),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeConfirmedSummary(raw: ConfirmedSummary): ConfirmedSummary {
|
||||
return {
|
||||
...raw,
|
||||
planId: requiredId(raw.planId),
|
||||
warehouseId: optionalId(raw.warehouseId),
|
||||
createdBy: optionalId(raw.createdBy),
|
||||
totalPackageCount: String(raw.totalPackageCount ?? '0'),
|
||||
totalQuantity: String(raw.totalQuantity ?? '0'),
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeItem(raw: LoadingItem): LoadingItem {
|
||||
return {
|
||||
...raw,
|
||||
itemId: requiredId(raw.itemId),
|
||||
accountId: requiredId(raw.accountId),
|
||||
inboundId: requiredId(raw.inboundId),
|
||||
inboundDetailId: requiredId(raw.inboundDetailId),
|
||||
productQuantity: String(raw.productQuantity ?? '0'),
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeHistory(raw: LoadingHistory): LoadingHistory {
|
||||
return {
|
||||
...raw,
|
||||
historyId: requiredId(raw.historyId),
|
||||
itemId: optionalId(raw.itemId),
|
||||
operatorId: requiredId(raw.operatorId),
|
||||
}
|
||||
}
|
||||
|
||||
function normalizePage<T extends LoadingListSummary>(raw: RawPage<T>, normalize: (row: T) => T): LoadingListPage<T> {
|
||||
return {
|
||||
records: (raw.records || []).map(normalize),
|
||||
page: Number(raw.page),
|
||||
size: Number(raw.size),
|
||||
total: Number(raw.total),
|
||||
pages: Number(raw.pages),
|
||||
}
|
||||
}
|
||||
|
||||
function idempotencyKey(planId: string) {
|
||||
return `admin-preload-transport-${planId}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
||||
}
|
||||
|
||||
export async function listPreloadPlans(query: LoadingListQuery) {
|
||||
const response = await http.get<ApiEnvelope<RawPage<PreloadSummary>>, ApiEnvelope<RawPage<PreloadSummary>>>(
|
||||
`${basePath}/loading-plans`,
|
||||
{ params: { ...query, status: 'LOADING' } },
|
||||
)
|
||||
return normalizePage(unwrapApiResponse(response), normalizePreloadSummary)
|
||||
}
|
||||
|
||||
export async function getPreloadPlan(planId: string, warehouseId: string) {
|
||||
const response = await http.get<ApiEnvelope<LoadingDetail<PreloadSummary>>, ApiEnvelope<LoadingDetail<PreloadSummary>>>(
|
||||
`${basePath}/loading-plans/${planId}`,
|
||||
{ params: { warehouseId } },
|
||||
)
|
||||
const result = unwrapApiResponse(response)
|
||||
return {
|
||||
summary: normalizePreloadSummary(result.summary),
|
||||
items: (result.items || []).map(normalizeItem),
|
||||
histories: (result.histories || []).map(normalizeHistory),
|
||||
}
|
||||
}
|
||||
|
||||
export async function updatePreloadTransport(planId: string, warehouseId: string, request: TransportUpdateRequest) {
|
||||
const response = await http.patch<ApiEnvelope<LoadingDetail<PreloadSummary>>, ApiEnvelope<LoadingDetail<PreloadSummary>>>(
|
||||
`${basePath}/loading-plans/${planId}/transport`,
|
||||
request,
|
||||
{ params: { warehouseId }, headers: { 'Idempotency-Key': idempotencyKey(planId) } },
|
||||
)
|
||||
const result = unwrapApiResponse(response)
|
||||
return {
|
||||
summary: normalizePreloadSummary(result.summary),
|
||||
items: (result.items || []).map(normalizeItem),
|
||||
histories: (result.histories || []).map(normalizeHistory),
|
||||
}
|
||||
}
|
||||
|
||||
export async function listConfirmedLoadingLists(query: LoadingListQuery) {
|
||||
const response = await http.get<ApiEnvelope<RawPage<ConfirmedSummary>>, ApiEnvelope<RawPage<ConfirmedSummary>>>(
|
||||
`${basePath}/loading-lists`,
|
||||
{ params: query },
|
||||
)
|
||||
return normalizePage(unwrapApiResponse(response), normalizeConfirmedSummary)
|
||||
}
|
||||
|
||||
export async function getConfirmedLoadingList(planId: string, warehouseId: string) {
|
||||
const response = await http.get<ApiEnvelope<LoadingDetail<ConfirmedSummary>>, ApiEnvelope<LoadingDetail<ConfirmedSummary>>>(
|
||||
`${basePath}/loading-lists/${planId}`,
|
||||
{ params: { warehouseId } },
|
||||
)
|
||||
const result = unwrapApiResponse(response)
|
||||
return {
|
||||
summary: normalizeConfirmedSummary(result.summary),
|
||||
items: (result.items || []).map(normalizeItem),
|
||||
histories: (result.histories || []).map(normalizeHistory),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user