feat: connect plan-level preload confirmation

This commit is contained in:
2026-08-15 17:04:00 +08:00
parent fc80d09faa
commit 835d1d0216
26 changed files with 398 additions and 267 deletions
@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import { http } from '@/shared/api/http'
import {
getConfirmedLoadingList,
confirmPreloadPlan,
uploadLoadingDeclaration,
completeLoadingDeclaration,
getPreloadPlan,
@@ -15,7 +16,7 @@ const preloadSummary = {
containerBookingId: 1900000000000000004n as unknown as string,
preloadNo: 'PL-001',
containerNo: 'MSCU1234567',
internalContainerNo: 'YG-001',
cabinetNo: 'YG-001',
customers: [{ accountId: 1900000000000000002n as unknown as string, customerName: '测试客户' }],
warehouseId: 1900000000000000003n as unknown as string,
warehouseName: '义乌仓',
@@ -29,6 +30,20 @@ const preloadSummary = {
totalGrossWeightKg: 1200,
createdAt: '2026-08-10T10:00:00',
version: 3,
readiness: {
preloadPushed: true,
containerNoReady: true,
preloadLinkCount: 1,
confirmedPreloadCount: 0,
oceanBookingId: 1900000000000000005n as unknown as string,
oceanBookingStatus: 'RELEASED',
pdaTaskId: null,
pdaTaskStatus: null,
confirmationStatus: 'PENDING',
canConfirm: true,
readyForPda: false,
blockingReasons: [],
},
}
const detail = {
@@ -54,6 +69,7 @@ describe('platform loading lists API', () => {
expect(result.total).toBe(1)
expect(result.records[0]?.planId).toBe('1900000000000000001')
expect(result.records[0]?.containerBookingId).toBe('1900000000000000004')
expect(result.records[0]?.readiness.oceanBookingId).toBe('1900000000000000005')
expect(result.records[0]?.customers[0]?.accountId).toBe('1900000000000000002')
})
@@ -92,6 +108,42 @@ describe('platform loading lists API', () => {
expect(loaded.summary.status).toBe('COMPLETED')
})
it('confirms the whole preload plan with expectedVersion and the caller operation id', async () => {
const post = vi.spyOn(http, 'post').mockResolvedValue({
code: '200', message: 'ok', data: {
planId: 1900000000000000001n,
containerBookingId: 1900000000000000004n,
confirmationStatus: 'CONFIRMED',
version: 4,
readiness: {
...preloadSummary.readiness,
confirmedPreloadCount: 1,
confirmationStatus: 'CONFIRMED',
canConfirm: false,
readyForPda: true,
pdaTaskId: 1900000000000000006n,
pdaTaskStatus: 'PENDING',
blockingReasons: ['PRELOAD_ALREADY_CONFIRMED'],
},
},
})
const result = await confirmPreloadPlan('1900000000000000001', 3, 'confirm-operation-1')
expect(post).toHaveBeenCalledWith(
'/platform/v1/first-leg/loading-plans/1900000000000000001/confirm',
{ expectedVersion: 3 },
{ headers: { 'Idempotency-Key': 'confirm-operation-1' } },
)
expect(result).toMatchObject({
planId: '1900000000000000001',
containerBookingId: '1900000000000000004',
confirmationStatus: 'CONFIRMED',
version: 4,
readiness: { pdaTaskId: '1900000000000000006', readyForPda: true },
})
})
it('uses stable plan and warehouse ids for customs document actions', async () => {
const post = vi.spyOn(http, 'post').mockResolvedValue({ code: '200', message: 'ok', data: { id: 6, documentType: 'CUSTOMS_DECLARATION', fileId: 9, fileName: 'declaration.pdf', version: 2 } })
const file = new File(['declaration'], 'declaration.pdf', { type: 'application/pdf' })
@@ -7,12 +7,38 @@ export interface LoadingCustomer {
customerName: string | null
}
export type PreloadConfirmationStatus = 'PENDING' | 'PARTIALLY_CONFIRMED' | 'CONFIRMED'
export type PreloadBlockingReason =
| 'PRELOAD_NOT_PUSHED'
| 'LOADING_PLAN_NOT_EDITING'
| 'CONTAINER_NO_MISSING'
| 'PRELOAD_LINK_MISSING'
| 'OCEAN_BOOKING_NOT_LINKED'
| 'OCEAN_BOOKING_NOT_RELEASED'
| 'PDA_TASK_ALREADY_CREATED'
| 'PRELOAD_ALREADY_CONFIRMED'
export interface PreloadReadiness {
preloadPushed: boolean
containerNoReady: boolean
preloadLinkCount: number
confirmedPreloadCount: number
oceanBookingId: string | null
oceanBookingStatus: 'CONFIRMED' | 'CHANGE_PENDING' | 'RELEASED' | 'CANCELLED' | null
pdaTaskId: string | null
pdaTaskStatus: string | null
confirmationStatus: PreloadConfirmationStatus
canConfirm: boolean
readyForPda: boolean
blockingReasons: PreloadBlockingReason[]
}
export interface PreloadSummary {
planId: string
containerBookingId: string
preloadNo: string
containerNo: string | null
internalContainerNo: string | null
cabinetNo: string | null
customers: LoadingCustomer[]
warehouseId: string | null
warehouseName: string | null
@@ -26,6 +52,7 @@ export interface PreloadSummary {
totalGrossWeightKg: number
createdAt: string
version: number
readiness: PreloadReadiness
}
export interface ConfirmedSummary {
@@ -35,7 +62,7 @@ export interface ConfirmedSummary {
warehouseId: string | null
warehouseName: string | null
containerNo: string | null
internalContainerNo: string | null
cabinetNo: string | null
status: string
totalPackageCount: string
totalGrossWeightKg: number
@@ -142,6 +169,14 @@ export interface TransportUpdateRequest {
operationReason: string | null
}
export interface PreloadConfirmationResponse {
planId: string
containerBookingId: string
confirmationStatus: 'CONFIRMED'
version: number
readiness: PreloadReadiness
}
type RawId = string | number | null | undefined
type RawPage<T extends LoadingListSummary> = Omit<LoadingListPage<T>, 'page' | 'size' | 'total' | 'pages'> & {
page: string | number
@@ -160,6 +195,15 @@ function optionalId(value: RawId) {
return value === null || value === undefined ? null : String(value)
}
function normalizePreloadReadiness(raw: PreloadReadiness): PreloadReadiness {
return {
...raw,
oceanBookingId: optionalId(raw.oceanBookingId),
pdaTaskId: optionalId(raw.pdaTaskId),
blockingReasons: raw.blockingReasons || [],
}
}
function normalizePreloadSummary(raw: PreloadSummary): PreloadSummary {
return {
...raw,
@@ -168,6 +212,7 @@ function normalizePreloadSummary(raw: PreloadSummary): PreloadSummary {
warehouseId: optionalId(raw.warehouseId),
skuCount: String(raw.skuCount ?? '0'),
totalPackageCount: String(raw.totalPackageCount ?? '0'),
readiness: normalizePreloadReadiness(raw.readiness),
customers: (raw.customers || []).map((customer) => ({
...customer,
accountId: requiredId(customer.accountId),
@@ -206,6 +251,7 @@ function normalizeHistory(raw: LoadingHistory): LoadingHistory {
operatorId: requiredId(raw.operatorId),
}
}
function normalizeDocument(raw: BusinessDocument | null | undefined): BusinessDocument | null {
if (!raw) return null
return { ...raw, id: raw.id == null ? null : requiredId(raw.id), fileId: requiredId(raw.fileId), uploadedBy: optionalId(raw.uploadedBy), completedBy: optionalId(raw.completedBy), version: Number(raw.version ?? 0) }
@@ -262,6 +308,21 @@ export async function updatePreloadTransport(planId: string, warehouseId: string
}
}
export async function confirmPreloadPlan(planId: string, expectedVersion: number, operationId: string) {
const response = await http.post<ApiEnvelope<PreloadConfirmationResponse>, ApiEnvelope<PreloadConfirmationResponse>>(
`${basePath}/loading-plans/${encodeURIComponent(planId)}/confirm`,
{ expectedVersion },
{ headers: { 'Idempotency-Key': operationId } },
)
const result = unwrapApiResponse(response)
return {
...result,
planId: requiredId(result.planId),
containerBookingId: requiredId(result.containerBookingId),
readiness: normalizePreloadReadiness(result.readiness),
}
}
export async function getLoadingDeclaration(planId: string, warehouseId: string) {
const response = await http.get<ApiEnvelope<BusinessDocument | null>, ApiEnvelope<BusinessDocument | null>>(`${basePath}/loading-lists/${encodeURIComponent(planId)}/customs`, { params: { warehouseId } })
return normalizeDocument(unwrapApiResponse(response))