This commit is contained in:
2026-08-20 18:02:52 +08:00
parent 4ae5e7c672
commit 80947cd9ce
65 changed files with 1386 additions and 1083 deletions
@@ -17,9 +17,10 @@ const query = {
trackingType: 'CONTAINER_NO',
trackingNo: 'CSNU1234567',
status: 'MANUAL_REQUIRED',
triggerSource: 'OUTBOUND_CONFIRM',
latestStandardNode: 'DEPARTED_WAREHOUSE',
latestNodeObservedAt: '2026-08-20T10:00:00',
accessMode: 'DYNAMIC_IFRAME',
attemptCount: '2',
lastAttemptStatus: 'MANUAL_REQUIRED',
lastErrorCode: 'DYNAMIC_IFRAME_REQUIRED',
lastErrorMessage: '必须人工查询',
resultPortScheduleId: 404,
@@ -30,17 +31,25 @@ describe('platformCarrierScheduleQueries', () => {
beforeEach(() => vi.clearAllMocks())
it('filters by real booking/container relations and preserves long ids as strings', async () => {
vi.mocked(http.get).mockResolvedValue({ code: '200', data: { records: [{ query, expired: true, maxAttempts: '3', nextAllowedAt: null }], page: '1', size: '200', total: '1', pages: '1' } })
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', attemptCount: 2, lastAttemptStatus: 'MANUAL_REQUIRED', lastErrorCode: 'DYNAMIC_IFRAME_REQUIRED', lastErrorMessage: '必须人工查询', version: 3 }, expired: true, maxAttempts: 3 })
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 })
})
it('normalizes attempts and immutable history without creating query operations', async () => {
vi.mocked(http.get).mockResolvedValue({ code: '200', data: { query: { query, expired: false, maxAttempts: 3 }, attempts: [{ id: 1, queryId: query.id, windowAttemptNo: '2', carrierCode: 'COSCO', attemptStatus: 'FAILED', coverageGap: true }], histories: [{ id: 2, queryId: query.id, action: 'ATTEMPT', fromVersion: '1', toVersion: '2' }] } })
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 }] } })
const result = await getCarrierScheduleQuery('9007199254740993')
expect(vi.mocked(http.get)).toHaveBeenCalledWith('/platform/v1/carrier-schedule-queries/9007199254740993')
expect(result.attempts[0]).toMatchObject({ id: '1', queryId: '9007199254740993', windowAttemptNo: 2, attemptStatus: 'FAILED' })
expect(result.histories[0]).toMatchObject({ id: '2', fromVersion: 1, toVersion: 2 })
expect(result.nodes[0]).toMatchObject({ id: '1', standardNode: 'ARRIVED', nodeSequence: 50, actualAt: '2026-08-20T11:55:00', locationCode: 'USLAX' })
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 }] } })
const result = await getCarrierScheduleQuery('9007199254740993')
expect(result.query.query).toMatchObject({ containerBookingId: null, resultPortScheduleId: null, latestStandardNode: null, latestNodeObservedAt: 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 })
})
})
@@ -1,9 +1,9 @@
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import type {
CarrierScheduleQueryAttempt,
CarrierScheduleQueryDetail,
CarrierScheduleQueryFilter,
CarrierScheduleQueryHistory,
CarrierScheduleNodeFact,
CarrierScheduleQueryPage,
CarrierScheduleQueryResource,
CarrierScheduleQueryView,
@@ -22,21 +22,20 @@ function resource(raw: CarrierScheduleQueryResource): CarrierScheduleQueryResour
oceanBookingId: String(raw.oceanBookingId),
containerBookingId: optionalId(raw.containerBookingId),
resultPortScheduleId: optionalId(raw.resultPortScheduleId),
attemptCount: numberValue(raw.attemptCount),
version: numberValue(raw.version),
}
}
function view(raw: CarrierScheduleQueryView): CarrierScheduleQueryView {
return { ...raw, query: resource(raw.query), maxAttempts: numberValue(raw.maxAttempts), nextAllowedAt: raw.nextAllowedAt ?? raw.query.nextAllowedAt ?? null }
return { ...raw, query: resource(raw.query) }
}
function attempt(raw: CarrierScheduleQueryAttempt): CarrierScheduleQueryAttempt {
return { ...raw, id: String(raw.id), queryId: String(raw.queryId), windowAttemptNo: numberValue(raw.windowAttemptNo), coverageGap: Boolean(raw.coverageGap) }
function node(raw: CarrierScheduleNodeFact): CarrierScheduleNodeFact {
return { ...raw, id: String(raw.id), nodeSequence: numberValue(raw.nodeSequence) }
}
function history(raw: CarrierScheduleQueryHistory): CarrierScheduleQueryHistory {
return { ...raw, id: String(raw.id), queryId: String(raw.queryId), fromVersion: raw.fromVersion == null ? null : numberValue(raw.fromVersion), toVersion: raw.toVersion == null ? null : numberValue(raw.toVersion) }
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) }
}
export async function listCarrierScheduleQueries(filter: CarrierScheduleQueryFilter): Promise<CarrierScheduleQueryPage> {
@@ -48,5 +47,5 @@ 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), attempts: (result.attempts || []).map(attempt), histories: (result.histories || []).map(history) }
return { ...result, query: view(result.query), nodes: (result.nodes || []).map(node), histories: (result.histories || []).map(history) }
}