优化
This commit is contained in:
@@ -1,5 +1,42 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { listOceanBookings, performOceanBookingAction, createOceanBooking } from './platformOceanBookings'
|
||||
import { createOceanBooking, listOceanBookings, performOceanBookingAction, requestOceanBookingChange } from './platformOceanBookings'
|
||||
import { http } from '@/shared/api/http'
|
||||
describe('ocean bookings api', () => { it('normalizes pagination and long ids', async () => { vi.spyOn(http, 'get').mockResolvedValueOnce({ code: '200', message: 'ok', data: { records: [{ id: 123, customerId: 456, containers: [] }], page: '2', size: '20', total: '21', pages: '2' } }); const result = await listOceanBookings({ page: 1, size: 10 }); expect(result.page).toBe(2); expect(result.total).toBe(21); expect(result.records[0]?.id).toBe('123'); expect(result.records[0]?.customerId).toBe('456') })
|
||||
it('wraps writes with idempotency and expected version action payload', async () => { const post = vi.spyOn(http, 'post').mockResolvedValue({ code: '200', message: 'ok', data: 'id-1' }); await createOceanBooking({ expectedVersion: null, data: { customerId: 'c1', customerName: '客户', parties: [], containerRequirements: [] } }); expect(post.mock.calls[0]?.[2]).toMatchObject({ headers: { 'Idempotency-Key': expect.any(String) } }); await performOceanBookingAction('id-1', 'confirm', { expectedVersion: 3, reason: 'ok' }); expect(post.mock.calls[1]?.[0]).toContain('/id-1/confirm'); expect(post.mock.calls[1]?.[1]).toEqual({ expectedVersion: 3, reason: 'ok' }) }) })
|
||||
|
||||
const detailResponse = (version = 0) => ({
|
||||
oceanBooking: {
|
||||
id: 123, bookingNo: 'OB123', status: 'DRAFT', version, customerId: 456, customerName: '客户',
|
||||
containers: [],
|
||||
},
|
||||
parties: [], containerRequirements: [], containerAllocations: [], histories: [],
|
||||
})
|
||||
|
||||
describe('ocean bookings api', () => {
|
||||
it('normalizes pagination and long ids', async () => {
|
||||
vi.spyOn(http, 'get').mockResolvedValueOnce({ code: '200', message: 'ok', data: { records: [{ id: 123, customerId: 456, containers: [] }], page: '2', size: '20', total: '21', pages: '2' } })
|
||||
const result = await listOceanBookings({ page: 1, size: 10 })
|
||||
expect(result.page).toBe(2)
|
||||
expect(result.total).toBe(21)
|
||||
expect(result.records[0]?.id).toBe('123')
|
||||
expect(result.records[0]?.customerId).toBe('456')
|
||||
})
|
||||
|
||||
it('uses version zero for create and extracts the id from the returned Detail', async () => {
|
||||
const post = vi.spyOn(http, 'post').mockResolvedValue({ code: '200', message: 'ok', data: detailResponse() })
|
||||
const created = await createOceanBooking({ expectedVersion: 0, data: { customerId: '456', customerName: '客户', parties: [], containerRequirements: [] } })
|
||||
expect(post.mock.calls[0]?.[1]).toMatchObject({ expectedVersion: 0 })
|
||||
expect(post.mock.calls[0]?.[2]).toMatchObject({ headers: { 'Idempotency-Key': expect.any(String) } })
|
||||
expect(created.id).toBe('123')
|
||||
expect(created.version).toBe(0)
|
||||
})
|
||||
|
||||
it('uses action and change-specific request contracts', async () => {
|
||||
const post = vi.spyOn(http, 'post').mockResolvedValue({ code: '200', message: 'ok', data: detailResponse(4) })
|
||||
await performOceanBookingAction('123', 'confirm', { expectedVersion: 3, reason: 'ok' })
|
||||
expect(post.mock.calls[0]?.[0]).toContain('/123/confirm')
|
||||
expect(post.mock.calls[0]?.[1]).toEqual({ expectedVersion: 3, reason: 'ok' })
|
||||
|
||||
await requestOceanBookingChange('123', { expectedVersion: 4, vesselName: 'VESSEL', voyageNo: 'V001', reason: '调整船期' })
|
||||
expect(post.mock.calls[1]?.[0]).toContain('/123/change-request')
|
||||
expect(post.mock.calls[1]?.[1]).toMatchObject({ expectedVersion: 4, vesselName: 'VESSEL', voyageNo: 'V001', reason: '调整船期' })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,20 +1,51 @@
|
||||
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
|
||||
import type { OceanBookingDetail, OceanBookingPage, OceanBookingQuery, OceanBookingResource, OceanBookingSaveRequest, OceanBookingStatus } from '../types'
|
||||
import type { OceanBookingActionRequest, OceanBookingChangeRequest, OceanBookingDetail, OceanBookingPage, OceanBookingQuery, OceanBookingResource, OceanBookingSaveRequest, OceanBookingStatus } from '../types'
|
||||
|
||||
const basePath = '/platform/v1/ocean-bookings'
|
||||
function pageNumber(value: number | string, fallback: number) { const n = Number(value); return Number.isFinite(n) ? Math.trunc(n) : fallback }
|
||||
function booking(raw: OceanBookingResource): OceanBookingResource { const item = raw as OceanBookingResource & { departurePortName?: string | null; destinationPortName?: string | null; shippingCompanyName?: string | null; estimatedSailingAt?: string | null; estimatedArrivalAt?: string | null }; return { ...raw, id: String(raw.id), customerId: raw.customerId == null ? null : String(raw.customerId), pol: raw.pol ?? item.departurePortName ?? null, pod: raw.pod ?? item.destinationPortName ?? null, carrierName: raw.carrierName ?? item.shippingCompanyName ?? null, etd: raw.etd ?? item.estimatedSailingAt ?? null, eta: raw.eta ?? item.estimatedArrivalAt ?? null, containers: (raw.containers || []).map((container) => ({ ...container, id: String(container.id ?? '') })) } }
|
||||
function booking(raw: OceanBookingResource): OceanBookingResource {
|
||||
const item = raw as OceanBookingResource & { version?: number | string | null }
|
||||
return {
|
||||
...raw,
|
||||
id: String(raw.id),
|
||||
version: Number(item.version ?? 0),
|
||||
customerId: raw.customerId == null ? null : String(raw.customerId),
|
||||
shippingCompanyId: raw.shippingCompanyId == null ? null : String(raw.shippingCompanyId),
|
||||
bookingAgentId: raw.bookingAgentId == null ? null : String(raw.bookingAgentId),
|
||||
pol: raw.pol ?? raw.departurePortName ?? null,
|
||||
pod: raw.pod ?? raw.destinationPortName ?? null,
|
||||
carrierName: raw.carrierName ?? raw.shippingCompanyName ?? null,
|
||||
bookingAgent: raw.bookingAgent ?? raw.bookingAgentName ?? null,
|
||||
etd: raw.etd ?? raw.estimatedSailingAt ?? null,
|
||||
eta: raw.eta ?? raw.estimatedArrivalAt ?? null,
|
||||
containers: (raw.containers || []).map((container) => ({ ...container, id: String(container.id ?? '') })),
|
||||
}
|
||||
}
|
||||
function detail(raw: OceanBookingDetail): OceanBookingDetail {
|
||||
const source = booking(raw.oceanBooking || raw)
|
||||
const histories = (raw.histories || raw.statusHistory || []).map((event) => ({ ...event, id: String(event.id) }))
|
||||
return {
|
||||
...raw,
|
||||
...source,
|
||||
oceanBooking: source,
|
||||
parties: (raw.parties || []).map((party) => ({ ...party, id: party.id == null ? undefined : String(party.id) })),
|
||||
containerRequirements: (raw.containerRequirements || []).map((container) => ({ ...container, id: container.id == null ? undefined : String(container.id) })),
|
||||
containerAllocations: raw.containerAllocations || [],
|
||||
histories,
|
||||
statusHistory: histories,
|
||||
}
|
||||
}
|
||||
export async function listOceanBookings(query: OceanBookingQuery) {
|
||||
const response = await http.get<ApiEnvelope<OceanBookingPage>, ApiEnvelope<OceanBookingPage>>(basePath, { params: query })
|
||||
const result = unwrapApiResponse(response)
|
||||
return { ...result, records: (result.records || []).map(booking), page: pageNumber(result.page, query.page), size: pageNumber(result.size, query.size), total: pageNumber(result.total, 0), pages: pageNumber(result.pages, 0) } satisfies OceanBookingPage
|
||||
}
|
||||
export async function getOceanBooking(id: string) { const response = await http.get<ApiEnvelope<OceanBookingDetail>, ApiEnvelope<OceanBookingDetail>>(`${basePath}/${encodeURIComponent(id)}`); const raw = unwrapApiResponse(response); const source = raw.oceanBooking || raw; const normalized = booking(source); const histories = (raw.histories || raw.statusHistory || []).map((event) => ({ ...event, id: String(event.id) })); return { ...raw, ...normalized, oceanBooking: normalized, parties: raw.parties || [], containerRequirements: raw.containerRequirements || [], containerAllocations: raw.containerAllocations || [], histories, statusHistory: histories } }
|
||||
export async function getOceanBooking(id: string) { const response = await http.get<ApiEnvelope<OceanBookingDetail>, ApiEnvelope<OceanBookingDetail>>(`${basePath}/${encodeURIComponent(id)}`); return detail(unwrapApiResponse(response)) }
|
||||
function idempotencyKey() { return `ocean-booking-${Date.now()}-${Math.random().toString(36).slice(2)}` }
|
||||
export async function createOceanBooking(payload: OceanBookingSaveRequest) { const response = await http.post<ApiEnvelope<string>, ApiEnvelope<string>>(basePath, payload, { headers: { 'Idempotency-Key': idempotencyKey() } }); return unwrapApiResponse(response) }
|
||||
export async function updateOceanBooking(id: string, payload: OceanBookingSaveRequest) { const response = await http.put<ApiEnvelope<void>, ApiEnvelope<void>>(`${basePath}/${encodeURIComponent(id)}`, payload, { headers: { 'Idempotency-Key': idempotencyKey() } }); return unwrapApiResponse(response) }
|
||||
export async function createOceanBooking(payload: OceanBookingSaveRequest) { const response = await http.post<ApiEnvelope<OceanBookingDetail>, ApiEnvelope<OceanBookingDetail>>(basePath, payload, { headers: { 'Idempotency-Key': idempotencyKey() } }); return detail(unwrapApiResponse(response)) }
|
||||
export async function updateOceanBooking(id: string, payload: OceanBookingSaveRequest) { const response = await http.put<ApiEnvelope<OceanBookingDetail>, ApiEnvelope<OceanBookingDetail>>(`${basePath}/${encodeURIComponent(id)}`, payload, { headers: { 'Idempotency-Key': idempotencyKey() } }); return detail(unwrapApiResponse(response)) }
|
||||
export type OceanBookingAction = 'submit' | 'confirm' | 'release' | 'fail' | 'cancel' | 'change-request' | 'change-confirm' | 'change-fail'
|
||||
export interface OceanBookingActionRequest { expectedVersion: number | null; reason?: string | null }
|
||||
export async function performOceanBookingAction(id: string, action: OceanBookingAction, request: OceanBookingActionRequest) { const response = await http.post<ApiEnvelope<void>, ApiEnvelope<void>>(`${basePath}/${encodeURIComponent(id)}/${action}`, request, { headers: { 'Idempotency-Key': idempotencyKey() } }); return unwrapApiResponse(response) }
|
||||
export async function performOceanBookingAction(id: string, action: Exclude<OceanBookingAction, 'change-request'>, request: OceanBookingActionRequest) { const response = await http.post<ApiEnvelope<OceanBookingDetail>, ApiEnvelope<OceanBookingDetail>>(`${basePath}/${encodeURIComponent(id)}/${action}`, request, { headers: { 'Idempotency-Key': idempotencyKey() } }); return detail(unwrapApiResponse(response)) }
|
||||
export async function requestOceanBookingChange(id: string, request: OceanBookingChangeRequest) { const response = await http.post<ApiEnvelope<OceanBookingDetail>, ApiEnvelope<OceanBookingDetail>>(`${basePath}/${encodeURIComponent(id)}/change-request`, request, { headers: { 'Idempotency-Key': idempotencyKey() } }); return detail(unwrapApiResponse(response)) }
|
||||
export async function printOceanBooking(id: string, type: 'SHIPPING_INSTRUCTION' | 'BILL_CONFIRMATION') { return http.get<Blob, Blob>(`${basePath}/${encodeURIComponent(id)}/print`, { params: { type }, responseType: 'blob' }) }
|
||||
export type { OceanBookingResource, OceanBookingStatus }
|
||||
|
||||
Reference in New Issue
Block a user