fix(admin): integrate workbench contracts and dropship review states

This commit is contained in:
2026-09-09 11:50:42 +08:00
79 changed files with 2288 additions and 448 deletions
@@ -6,7 +6,7 @@ import {
adjustSmartLoadingItem, assignPendingReviewSmartLoadingOrder, assignSmartLoadingGroup, assignSmartLoadingItem, assignSmartLoadingPallet, getContainer,
listContainers, listSmartLoadingCandidates, listSmartLoadingWorkbenchContainers, removeAllUnpickedSmartLoadingItems, removeSmartLoadingGroup, removeSmartLoadingItem,
getContainerClearance, downloadContainerClearance, uploadContainerClearance, completeContainerClearance,
getContainerOverview, markContainerException, cancelContainerException, uploadContainerDocument, downloadContainerDocument, downloadContainerPdaPhoto,
getContainerOverview, updateContainerPickupCode, markContainerException, cancelContainerException, uploadContainerDocument, downloadContainerDocument, downloadContainerPdaPhoto,
getCustomerContainerTransitSummary,
createContainerSimulation, linkContainerOceanBooking, unlinkContainerOceanBooking,
updateContainer,
@@ -17,7 +17,7 @@ vi.mock('@/shared/api/http', () => ({
unwrapApiResponse: <T>(response: { data: T }) => response.data,
}))
const container = { id: 9007199254740993, planId: 9007199254740994, planNo: 'LP-001', warehouseId: 9007199254740995, shippingCompanyId: 9007199254740996, terminalTruckingCompanyId: '9007199254740998', terminalTruckingCompany: '测试拖车供应商', sourceType: 'SIMULATION', recordStatus: 'ACTIVE', mergedIntoContainerBookingId: null, channelCode: 'NY_EXX', channelName: '纽约EXX海铁', exceptionActive: true, destinationClearanceDocumentStatus: 'COMPLETED', customsDeclarationDocumentStatus: 'UPLOADED', deliveryOrderDocumentStatus: 'UPLOADED', pickupOrderDocumentStatus: 'MISSING', unloadingCompletedAt: '2026-08-20T10:00:00', unloadingPhotoUploadedAt: '2026-08-20T10:15:00', estimatedEmptyReportedAt: '2026-08-21T09:00:00', actualEmptyReportedAt: null, emptyReportSourceVersion: '9007199254740997', version: '2', capacityCbm: '76', maxWeightKg: '28000' }
const container = { id: 9007199254740993, planId: 9007199254740994, planNo: 'LP-001', warehouseId: 9007199254740995, shippingCompanyId: 9007199254740996, terminalTruckingCompanyId: '9007199254740998', terminalTruckingCompany: '测试拖车供应商', sourceType: 'SIMULATION', recordStatus: 'ACTIVE', mergedIntoContainerBookingId: null, channelCode: 'NY_EXX', channelName: '纽约EXX海铁', exceptionActive: true, destinationClearanceDocumentStatus: 'COMPLETED', customsDeclarationDocumentStatus: 'UPLOADED', deliveryOrderDocumentStatus: 'UPLOADED', pickupCode: '00123456', pickupCodeUpdatedBy: 9007199254740999, unloadingCompletedAt: '2026-08-20T10:00:00', unloadingPhotoUploadedAt: '2026-08-20T10:15:00', estimatedEmptyReportedAt: '2026-08-21T09:00:00', actualEmptyReportedAt: null, emptyReportSourceVersion: '9007199254740997', version: '2', capacityCbm: '76', maxWeightKg: '28000' }
const plan = {
id: 9007199254740993, bookingId: 9007199254740994, version: '3', status: 'LOADING',
canAssign: true, canEdit: true, blockedReason: null, finalCompletionStatus: 'NOT_STARTED',
@@ -38,10 +38,10 @@ describe('platform container loading API', () => {
})
it('normalizes paged container ids and numeric metadata', async () => {
vi.mocked(http.get).mockResolvedValueOnce({ code: '200', data: { records: [container], page: '1', size: '20', total: '1', pages: '1' } })
vi.mocked(http.get).mockResolvedValueOnce({ code: '200', data: { records: [{ container, workbenchStage: 'PRE_LOADING', statusSummary: { loadingStatus: 'WAIT_LOADING' }, allowedActions: ['EDIT_CONTAINER'], actualFacts: {}, terminalDrayage: null }], page: '1', size: '20', total: '1', pages: '1' } })
const result = await listContainers({ page: 1, size: 20 })
expect(result.records[0]).toMatchObject({ id: String(container.id), planId: String(container.planId), planNo: 'LP-001', warehouseId: String(container.warehouseId), terminalTruckingCompanyId: String(container.terminalTruckingCompanyId), terminalTruckingCompany: '测试拖车供应商', channelCode: 'NY_EXX', channelName: '纽约EXX海铁', exceptionActive: true, destinationClearanceDocumentStatus: 'COMPLETED', customsDeclarationDocumentStatus: 'UPLOADED', deliveryOrderDocumentStatus: 'UPLOADED', pickupOrderDocumentStatus: 'MISSING', unloadingCompletedAt: '2026-08-20T10:00:00', unloadingPhotoUploadedAt: '2026-08-20T10:15:00', estimatedEmptyReportedAt: '2026-08-21T09:00:00', actualEmptyReportedAt: null, emptyReportSourceVersion: '9007199254740997', version: 2, capacityCbm: 76 })
expect(http.get).toHaveBeenCalledWith('/platform/v1/containers', { params: { page: 1, size: 20 } })
expect(result.records[0]).toMatchObject({ id: String(container.id), planId: String(container.planId), planNo: 'LP-001', warehouseId: String(container.warehouseId), terminalTruckingCompanyId: String(container.terminalTruckingCompanyId), terminalTruckingCompany: '测试拖车供应商', channelCode: 'NY_EXX', channelName: '纽约EXX海铁', exceptionActive: true, destinationClearanceDocumentStatus: 'COMPLETED', customsDeclarationDocumentStatus: 'UPLOADED', deliveryOrderDocumentStatus: 'UPLOADED', pickupCode: '00123456', pickupCodeUpdatedBy: String(container.pickupCodeUpdatedBy), unloadingCompletedAt: '2026-08-20T10:00:00', unloadingPhotoUploadedAt: '2026-08-20T10:15:00', estimatedEmptyReportedAt: '2026-08-21T09:00:00', actualEmptyReportedAt: null, emptyReportSourceVersion: '9007199254740997', version: 2, capacityCbm: 76 })
expect(http.get).toHaveBeenCalledWith('/platform/v1/containers/domestic', { params: { page: 1, size: 20, scope: 'ACTIVE' } })
})
it('uses the full-edit request shape without retaining the retired container transport endpoint', async () => {
@@ -52,6 +52,35 @@ describe('platform container loading API', () => {
expect(readFileSync(resolve(process.cwd(), 'src/modules/container-loading/api/platformContainerLoading.ts'), 'utf8')).not.toContain('/transport')
})
it.each(['DOMESTIC', 'OVERSEAS'] as const)('reads the %s detail envelope and exact long identifiers in one request', async (workbench) => {
vi.mocked(http.get).mockResolvedValueOnce({ code: '200', data: {
container: { ...container, id: '9007199254740993', planId: '9007199254740995' },
workbenchStage: 'PRE_LOADING', allowedActions: ['EDIT_CONTAINER'], statusSummary: {}, actualFacts: null, terminalDrayage: null,
} })
expect(await getContainer('9007199254740993', workbench)).toMatchObject({ id: '9007199254740993', planId: '9007199254740995', allowedActions: ['EDIT_CONTAINER'] })
expect(http.get).toHaveBeenCalledExactlyOnceWith(`/platform/v1/containers/${workbench.toLowerCase()}/9007199254740993`)
})
it('keeps empty overseas history pages and query filters without fetching individual details', async () => {
vi.mocked(http.get).mockResolvedValueOnce({ code: '200', data: { records: [], page: 2, size: 20, total: 0, pages: 0 } })
const query = { page: 2, size: 20, warehouseId: '9007199254740993', cabinetNo: 'CB-TEST', scope: 'HISTORY' as const }
expect(await listContainers(query, 'OVERSEAS')).toEqual({ records: [], page: 2, size: 20, total: 0, pages: 0 })
expect(http.get).toHaveBeenCalledExactlyOnceWith('/platform/v1/containers/overseas', { params: query })
})
it('propagates a failed workbench request without retrying retired routes', async () => {
const failure = new Error('workbench unavailable')
vi.mocked(http.get).mockRejectedValueOnce(failure)
await expect(listContainers({ page: 1, size: 20 })).rejects.toBe(failure)
expect(http.get).toHaveBeenCalledTimes(1)
})
it('rejects a retired flat response instead of rendering incorrect container rows', async () => {
vi.mocked(http.get).mockResolvedValueOnce({ code: '200', data: { records: [container], page: 1, size: 20, total: 1, pages: 1 } })
await expect(listContainers({ page: 1, size: 20 })).rejects.toThrow()
expect(http.get).toHaveBeenCalledTimes(1)
})
it('uses the frozen simulation create and ocean booking link contracts', async () => {
vi.mocked(http.post).mockResolvedValue({ code: '200', data: { ...container, oceanBookingId: 101, oceanBookingRequirementId: 202 } })
vi.mocked(http.delete).mockResolvedValue({ code: '200', data: { ...container, oceanBookingId: null, oceanBookingRequirementId: null } })
@@ -65,7 +94,7 @@ describe('platform container loading API', () => {
it('normalizes every long id in candidates and plans', async () => {
vi.mocked(http.get)
.mockResolvedValueOnce({ code: '200', data: container })
.mockResolvedValueOnce({ code: '200', data: { container, workbenchStage: 'PRE_LOADING', statusSummary: {}, allowedActions: [], actualFacts: {}, terminalDrayage: null } })
.mockResolvedValueOnce({ code: '200', data: { records: [{ accountId: 1, inboundId: 2, orderId: 4, orderNo: 'ORD-1', inboundCode: 'IN-1', orderStatusSnapshot: 'WAIT_SPLIT', expectedArrivalAt: '2026-09-10T09:30:00', warehouseId: 5, destinationWarehouseId: 6, channelCode: 'NY_EXX', channelName: '纽约EXX海铁', customerName: '示例企业', goodsSummary: '服装、鞋类', availablePackageCount: '7', unitGrossWeightKg: '12.5', unitNetWeightKg: '11.8', averageUnitVolumeCbm: '0.187', availableVolumeCbm: '1.309', estimatedVolumeCbm: '2.5', coLoadGroupId: 9007199254740999n, groupNo: 'CLG-9007199254740999', memberCount: '3', groupTotalBoxes: '21', locked: false, blockReason: null, groupMembers: [] }], page: '1', size: '20', total: '1', pages: '1' } })
expect((await getContainer('9')).id).toBe(String(container.id))
expect(await listSmartLoadingCandidates({ channelGroupCodes: ['EXX', 'HEDE'], warehouseId: '5', orderStatuses: ['PENDING_REVIEW', 'WAIT_MEASURE'], page: 1, size: 20 })).toMatchObject({ records: [{ accountId: '1', customerId: '1', inboundId: '2', inboundCode: 'IN-1', orderStatusSnapshot: 'WAIT_SPLIT', expectedArrivalAt: '2026-09-10T09:30:00', destinationWarehouseId: '6', channelCode: 'NY_EXX', channelName: '纽约EXX海铁', goodsSummary: '服装、鞋类', availablePackageCount: 7, unitGrossWeightKg: 12.5, unitNetWeightKg: 11.8, averageUnitVolumeCbm: 0.187, availableVolumeCbm: 1.309, estimatedVolumeCbm: 2.5, coLoadGroupId: '9007199254740999', groupNo: 'CLG-9007199254740999', memberCount: 3, groupTotalBoxes: 21, locked: false, blockReason: null }], total: 1 })
@@ -214,6 +243,13 @@ describe('platform container loading API', () => {
expect(http.get).toHaveBeenCalledWith('/platform/v1/containers/7/documents/DELIVERY_ORDER/2/download', { responseType: 'blob' })
})
it('saves the PU numeric code as a string with the container version', async () => {
vi.mocked(http.put).mockResolvedValueOnce({ code: '200', data: { container: { id: 7, warehouseId: 8, pickupCode: '00123456', pickupCodeUpdatedBy: 9, version: 3 }, documents: [], exception: {}, exceptionHistory: [], timeline: [] } })
const result = await updateContainerPickupCode('7', '00123456', 2)
expect(http.put).toHaveBeenCalledWith('/platform/v1/containers/7/pickup-code', { pickupCode: '00123456', expectedVersion: 2 })
expect(result.container).toMatchObject({ pickupCode: '00123456', pickupCodeUpdatedBy: '9', version: 3 })
})
it('normalizes the gateway /api prefix once and accepts only exact protected container photo paths', async () => {
vi.mocked(http.get).mockResolvedValue(new Blob(['photo']))
const longPath = '/api/platform/v1/containers/2092437461590446082/pda/photos/2092439032600797185/download'