优化
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { http } from '@/shared/api/http'
|
||||
import { getCarrierScheduleQuery, listCarrierScheduleQueries } from './platformCarrierScheduleQueries'
|
||||
import { appendCarrierScheduleManualNode, getCarrierScheduleQuery, listCarrierScheduleQueries } from './platformCarrierScheduleQueries'
|
||||
|
||||
vi.mock('@/shared/api/http', async () => {
|
||||
const actual = await vi.importActual<typeof import('@/shared/api/http')>('@/shared/api/http')
|
||||
return { ...actual, http: { get: vi.fn() } }
|
||||
return { ...actual, http: { get: vi.fn(), post: vi.fn() } }
|
||||
})
|
||||
|
||||
const query = {
|
||||
@@ -18,8 +18,8 @@ const query = {
|
||||
trackingNo: 'CSNU1234567',
|
||||
status: 'MANUAL_REQUIRED',
|
||||
triggerSource: 'OUTBOUND_CONFIRM',
|
||||
latestStandardNode: 'DEPARTED_WAREHOUSE',
|
||||
latestNodeObservedAt: '2026-08-20T10:00:00',
|
||||
latestActualNode: 'DEPARTED_WAREHOUSE',
|
||||
latestActualAt: '2026-08-20T10:00:00',
|
||||
accessMode: 'DYNAMIC_IFRAME',
|
||||
lastErrorCode: 'DYNAMIC_IFRAME_REQUIRED',
|
||||
lastErrorMessage: '必须人工查询',
|
||||
@@ -34,22 +34,31 @@ describe('platformCarrierScheduleQueries', () => {
|
||||
vi.mocked(http.get).mockResolvedValue({ code: '200', data: { records: [{ query, expired: true }], page: '1', size: '200', total: '1', pages: '1' } })
|
||||
const result = await listCarrierScheduleQueries({ page: 1, size: 200, oceanBookingId: '202', containerBookingId: '303' })
|
||||
expect(vi.mocked(http.get)).toHaveBeenCalledWith('/platform/v1/carrier-schedule-queries', { params: { page: 1, size: 200, oceanBookingId: '202', containerBookingId: '303' } })
|
||||
expect(result.records[0]).toMatchObject({ query: { id: '9007199254740993', customerId: '101', oceanBookingId: '202', containerBookingId: '303', resultPortScheduleId: '404', accessMode: 'DYNAMIC_IFRAME', triggerSource: 'OUTBOUND_CONFIRM', latestStandardNode: 'DEPARTED_WAREHOUSE', latestNodeObservedAt: '2026-08-20T10:00:00', lastErrorCode: 'DYNAMIC_IFRAME_REQUIRED', lastErrorMessage: '必须人工查询', version: 3 }, expired: true })
|
||||
expect(result.records[0]).toMatchObject({ query: { id: '9007199254740993', customerId: '101', oceanBookingId: '202', containerBookingId: '303', resultPortScheduleId: '404', accessMode: 'DYNAMIC_IFRAME', triggerSource: 'OUTBOUND_CONFIRM', latestActualNode: 'DEPARTED_WAREHOUSE', latestActualAt: '2026-08-20T10:00:00', lastErrorCode: 'DYNAMIC_IFRAME_REQUIRED', lastErrorMessage: '必须人工查询', version: 3 }, expired: true })
|
||||
})
|
||||
|
||||
it('normalizes standard node facts and immutable business history', async () => {
|
||||
vi.mocked(http.get).mockResolvedValue({ code: '200', data: { query: { query: { ...query, status: 'ARRIVED', latestStandardNode: 'ARRIVED' }, expired: false }, nodes: [{ id: 1, providerCode: 'COSCO', providerNodeCode: 'ARR', standardNode: 'ARRIVED', nodeSequence: '50', observedAt: '2026-08-20T12:00:00', estimatedAt: null, actualAt: '2026-08-20T11:55:00', locationCode: 'USLAX', locationName: 'Los Angeles', sourceReference: 'provider-event-1', rawPayloadHash: 'a'.repeat(64), createdAt: '2026-08-20T12:01:00' }], histories: [{ id: 2, action: 'NODE_OBSERVED', fromVersion: '1', toVersion: '2', reason: 'ARRIVED', operatorId: 808 }] } })
|
||||
vi.mocked(http.get).mockResolvedValue({ code: '200', data: { query: { query: { ...query, status: 'READY', latestActualNode: 'ARRIVED_DESTINATION_PORT' }, expired: false }, nodes: [{ id: 1, providerCode: 'COSCO', providerEventId: 'provider-event-1', providerNodeCode: 'ARR', standardNode: 'ARRIVED_DESTINATION_PORT', nodeSequence: '60', observedAt: '2026-08-20T12:00:00', estimatedAt: null, actualAt: '2026-08-20T11:55:00', locationCode: 'USLAX', locationName: 'Los Angeles', sourceReference: 'provider-event-1', rawPayloadHash: 'a'.repeat(64), createdAt: '2026-08-20T12:01:00' }], exceptions: [{ id: 3, providerCode: 'COSCO', providerEventId: 'hold-1', providerStatus: 'CUSTOMS HOLD', exceptionCode: 'CUSTOMS_HOLD', active: true, observedAt: '2026-08-20T12:05:00', locationCode: 'USLAX', locationName: 'Los Angeles', sourceReference: 'provider-hold-1', rawPayloadHash: 'c'.repeat(64), createdAt: '2026-08-20T12:06:00' }], histories: [{ id: 2, action: 'NODE_OBSERVED', fromVersion: '1', toVersion: '2', reason: 'ARRIVED_DESTINATION_PORT', operatorId: 808 }] } })
|
||||
const result = await getCarrierScheduleQuery('9007199254740993')
|
||||
expect(vi.mocked(http.get)).toHaveBeenCalledWith('/platform/v1/carrier-schedule-queries/9007199254740993')
|
||||
expect(result.nodes[0]).toMatchObject({ id: '1', standardNode: 'ARRIVED', nodeSequence: 50, actualAt: '2026-08-20T11:55:00', locationCode: 'USLAX' })
|
||||
expect(result.nodes[0]).toMatchObject({ id: '1', standardNode: 'ARRIVED_DESTINATION_PORT', nodeSequence: 60, actualAt: '2026-08-20T11:55:00', locationCode: 'USLAX' })
|
||||
expect(result.exceptions[0]).toMatchObject({ id: '3', providerEventId: 'hold-1', providerStatus: 'CUSTOMS HOLD', active: true })
|
||||
expect(result.histories[0]).toMatchObject({ id: '2', action: 'NODE_OBSERVED', fromVersion: 1, toVersion: 2, operatorId: '808' })
|
||||
})
|
||||
|
||||
it('preserves nullable DTO fields instead of fabricating false or zero values', async () => {
|
||||
vi.mocked(http.get).mockResolvedValue({ code: '200', data: { query: { query: { ...query, containerBookingId: null, resultPortScheduleId: null, latestStandardNode: null, latestNodeObservedAt: null, stoppedAt: null, stopReason: null }, expired: true }, nodes: [{ id: 1, providerCode: 'FIRSTLEG', providerNodeCode: 'OUTBOUND_CONFIRM', standardNode: 'DEPARTED_WAREHOUSE', nodeSequence: 10, observedAt: '2026-08-20T10:00:00', estimatedAt: null, actualAt: null, locationCode: null, locationName: null, sourceReference: null, rawPayloadHash: 'b'.repeat(64), createdAt: null }], histories: [{ id: 2, action: 'AUTO_CREATE', fromVersion: null, toVersion: 0, reason: null, operatorId: null, operatorName: null, createdAt: null }] } })
|
||||
vi.mocked(http.get).mockResolvedValue({ code: '200', data: { query: { query: { ...query, containerBookingId: null, resultPortScheduleId: null, latestActualNode: null, latestActualAt: null, stoppedAt: null, stopReason: null }, expired: true }, nodes: [{ id: 1, providerCode: 'FIRSTLEG', providerEventId: 'outbound-1', providerNodeCode: 'OUTBOUND_CONFIRM', standardNode: 'DEPARTED_WAREHOUSE', nodeSequence: 10, observedAt: '2026-08-20T10:00:00', estimatedAt: null, actualAt: null, locationCode: null, locationName: null, sourceReference: null, rawPayloadHash: 'b'.repeat(64), createdAt: null }], exceptions: [], histories: [{ id: 2, action: 'AUTO_CREATE', fromVersion: null, toVersion: 0, reason: null, operatorId: null, operatorName: null, createdAt: null }] } })
|
||||
const result = await getCarrierScheduleQuery('9007199254740993')
|
||||
expect(result.query.query).toMatchObject({ containerBookingId: null, resultPortScheduleId: null, latestStandardNode: null, latestNodeObservedAt: null, stoppedAt: null, stopReason: null })
|
||||
expect(result.query.query).toMatchObject({ containerBookingId: null, resultPortScheduleId: null, latestActualNode: null, latestActualAt: null, stoppedAt: null, stopReason: null })
|
||||
expect(result.nodes[0]).toMatchObject({ estimatedAt: null, actualAt: null, locationCode: null, locationName: null, sourceReference: null, createdAt: null })
|
||||
expect(result.histories[0]).toMatchObject({ fromVersion: null, operatorId: null, operatorName: null, createdAt: null })
|
||||
})
|
||||
|
||||
it('passes the dialog-scoped idempotency key to the frozen manual-node endpoint', async () => {
|
||||
const detail = { query: { query: { ...query, status: 'READY' }, expired: false }, nodes: [], exceptions: [], histories: [] }
|
||||
vi.mocked(http.post).mockResolvedValue({ code: '200', data: detail })
|
||||
const request = { expectedVersion: 3, standardNode: 'IN_TRANSIT' as const, observedAt: '2026-08-21T10:00:00', estimatedAt: '2026-08-22T10:00:00', actualAt: null, locationCode: 'PACIFIC', locationName: 'Pacific Ocean', reason: '船司官网人工核验' }
|
||||
await appendCarrierScheduleManualNode('9007199254740993', 'manual-session-key', request)
|
||||
expect(vi.mocked(http.post)).toHaveBeenCalledWith('/platform/v1/carrier-schedule-queries/9007199254740993/manual-nodes', request, { headers: { 'Idempotency-Key': 'manual-session-key' } })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
|
||||
import type {
|
||||
CarrierScheduleQueryDetail,
|
||||
CarrierScheduleExceptionFact,
|
||||
CarrierScheduleQueryFilter,
|
||||
CarrierScheduleQueryHistory,
|
||||
CarrierScheduleNodeFact,
|
||||
CarrierScheduleQueryPage,
|
||||
CarrierScheduleQueryResource,
|
||||
CarrierScheduleQueryView,
|
||||
CarrierScheduleManualNodeRequest,
|
||||
} from '../types'
|
||||
|
||||
const basePath = '/platform/v1/carrier-schedule-queries'
|
||||
@@ -34,6 +36,10 @@ function node(raw: CarrierScheduleNodeFact): CarrierScheduleNodeFact {
|
||||
return { ...raw, id: String(raw.id), nodeSequence: numberValue(raw.nodeSequence) }
|
||||
}
|
||||
|
||||
function exception(raw: CarrierScheduleExceptionFact): CarrierScheduleExceptionFact {
|
||||
return { ...raw, id: String(raw.id), active: Boolean(raw.active) }
|
||||
}
|
||||
|
||||
function history(raw: CarrierScheduleQueryHistory): CarrierScheduleQueryHistory {
|
||||
return { ...raw, id: String(raw.id), fromVersion: raw.fromVersion == null ? null : numberValue(raw.fromVersion), toVersion: raw.toVersion == null ? null : numberValue(raw.toVersion), operatorId: optionalId(raw.operatorId) }
|
||||
}
|
||||
@@ -47,5 +53,15 @@ export async function listCarrierScheduleQueries(filter: CarrierScheduleQueryFil
|
||||
export async function getCarrierScheduleQuery(queryId: string): Promise<CarrierScheduleQueryDetail> {
|
||||
const response = await http.get<ApiEnvelope<CarrierScheduleQueryDetail>, ApiEnvelope<CarrierScheduleQueryDetail>>(`${basePath}/${encodeURIComponent(queryId)}`)
|
||||
const result = unwrapApiResponse(response)
|
||||
return { ...result, query: view(result.query), nodes: (result.nodes || []).map(node), histories: (result.histories || []).map(history) }
|
||||
return { ...result, query: view(result.query), nodes: (result.nodes || []).map(node), exceptions: (result.exceptions || []).map(exception), histories: (result.histories || []).map(history) }
|
||||
}
|
||||
|
||||
export async function appendCarrierScheduleManualNode(queryId: string, idempotencyKey: string, request: CarrierScheduleManualNodeRequest): Promise<CarrierScheduleQueryDetail> {
|
||||
const response = await http.post<ApiEnvelope<CarrierScheduleQueryDetail>, ApiEnvelope<CarrierScheduleQueryDetail>>(
|
||||
`${basePath}/${encodeURIComponent(queryId)}/manual-nodes`,
|
||||
request,
|
||||
{ headers: { 'Idempotency-Key': idempotencyKey } },
|
||||
)
|
||||
const result = unwrapApiResponse(response)
|
||||
return { ...result, query: view(result.query), nodes: (result.nodes || []).map(node), exceptions: (result.exceptions || []).map(exception), histories: (result.histories || []).map(history) }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user