2026-08-06 13:56:04 +08:00
|
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
|
import { http } from '@/shared/api/http'
|
|
|
|
|
import {
|
2026-08-10 15:36:53 +08:00
|
|
|
adjustSmartLoadingItem,
|
|
|
|
|
assignSmartLoadingItem,
|
2026-08-06 13:56:04 +08:00
|
|
|
completeLoading,
|
2026-08-10 15:36:53 +08:00
|
|
|
deleteSmartLoadingItem,
|
2026-08-11 11:15:44 +08:00
|
|
|
downloadEvidenceFile,
|
2026-08-10 15:36:53 +08:00
|
|
|
getContainerBooking,
|
|
|
|
|
getOutboundDispatch,
|
2026-08-06 13:56:04 +08:00
|
|
|
getPicking,
|
|
|
|
|
getPreload,
|
2026-08-10 15:36:53 +08:00
|
|
|
listSmartLoadingCandidates,
|
|
|
|
|
recommendSmartLoading,
|
2026-08-06 13:56:04 +08:00
|
|
|
smartSplit,
|
2026-08-10 12:05:20 +08:00
|
|
|
uploadEvidenceFile,
|
|
|
|
|
voidEvidence,
|
2026-08-06 13:56:04 +08:00
|
|
|
} from './api'
|
|
|
|
|
|
|
|
|
|
vi.mock('@/shared/api/http', () => ({
|
2026-08-10 15:36:53 +08:00
|
|
|
http: { get: vi.fn(), post: vi.fn(), patch: vi.fn(), delete: vi.fn() },
|
2026-08-06 13:56:04 +08:00
|
|
|
unwrapApiResponse: <T>(response: { data: T }) => response.data,
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
describe('unified order workflow API contract', () => {
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
vi.mocked(http.get).mockReset()
|
|
|
|
|
vi.mocked(http.post).mockReset()
|
2026-08-10 15:36:53 +08:00
|
|
|
vi.mocked(http.patch).mockReset()
|
|
|
|
|
vi.mocked(http.delete).mockReset()
|
2026-08-06 13:56:04 +08:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('uses the C03 preload endpoints and expected version', async () => {
|
|
|
|
|
vi.mocked(http.get).mockResolvedValue({ data: { id: 1, inboundId: 8, tenantId: 2, status: 'PENDING', version: 3, items: [] } })
|
|
|
|
|
vi.mocked(http.post).mockResolvedValue({ data: { mainStatus: 'WAIT_SPLIT', preloadPlan: null } })
|
|
|
|
|
await getPreload('2', '8')
|
|
|
|
|
await smartSplit('2', '8', { expectedVersion: 3, items: [] })
|
|
|
|
|
expect(http.get).toHaveBeenCalledWith('/platform/v1/inbounds/8/preload', { params: { tenantId: '2' } })
|
|
|
|
|
expect(http.post).toHaveBeenCalledWith(
|
|
|
|
|
'/platform/v1/inbounds/8/split/smart', { expectedVersion: 3, items: [] },
|
|
|
|
|
expect.objectContaining({ params: { tenantId: '2' }, headers: { 'Idempotency-Key': expect.any(String) } }),
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('uses C04 picking and loading paths without a generic action endpoint', async () => {
|
|
|
|
|
vi.mocked(http.get).mockResolvedValue({ data: { id: 1, inboundId: 8, status: 'PICKING', version: 4 } })
|
|
|
|
|
vi.mocked(http.post).mockResolvedValue({ data: { mainStatus: 'LOADED', evidences: null } })
|
|
|
|
|
await getPicking('2', '8')
|
|
|
|
|
await completeLoading('2', '8', { expectedVersion: 7 })
|
|
|
|
|
expect(http.get).toHaveBeenCalledWith('/platform/v1/inbounds/8/loading/picking', { params: { tenantId: '2' } })
|
|
|
|
|
expect(http.post).toHaveBeenCalledWith(
|
|
|
|
|
'/platform/v1/inbounds/8/loading/complete', { expectedVersion: 7 },
|
|
|
|
|
expect.objectContaining({ params: { tenantId: '2' }, headers: { 'Idempotency-Key': expect.any(String) } }),
|
|
|
|
|
)
|
|
|
|
|
expect(vi.mocked(http.post).mock.calls.some(([url]) => String(url).includes('/actions/'))).toBe(false)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('does not swallow non-success responses', async () => {
|
|
|
|
|
vi.mocked(http.get).mockRejectedValue(new Error('无权限'))
|
|
|
|
|
await expect(getPreload('2', '8')).rejects.toThrow('无权限')
|
|
|
|
|
})
|
|
|
|
|
|
2026-08-10 12:05:20 +08:00
|
|
|
it('uploads a real evidence file and voids evidence through audited endpoints', async () => {
|
|
|
|
|
vi.mocked(http.post)
|
|
|
|
|
.mockResolvedValueOnce({ data: { fileId: '88', fileName: 'empty.jpg' } })
|
|
|
|
|
.mockResolvedValueOnce({ data: { mainStatus: 'LOADING', evidences: { inboundId: '8', evidences: [] } } })
|
|
|
|
|
await uploadEvidenceFile('2', '8', 'PHOTO_STEP_1', new File(['photo'], 'empty.jpg', { type: 'image/jpeg' }))
|
|
|
|
|
await voidEvidence('2', '8', '33', { expectedVersion: 2, reason: '照片错误' })
|
|
|
|
|
expect(http.post).toHaveBeenNthCalledWith(1,
|
|
|
|
|
'/platform/v1/files/loading-evidence/2/8', expect.any(FormData),
|
|
|
|
|
{ headers: { 'Content-Type': 'multipart/form-data' } },
|
|
|
|
|
)
|
|
|
|
|
expect(http.post).toHaveBeenNthCalledWith(2,
|
|
|
|
|
'/platform/v1/inbounds/8/loading/evidence/33/void', { expectedVersion: 2, reason: '照片错误' },
|
|
|
|
|
expect.objectContaining({ params: { tenantId: '2' }, headers: { 'Idempotency-Key': expect.any(String) } }),
|
|
|
|
|
)
|
|
|
|
|
})
|
2026-08-10 15:36:53 +08:00
|
|
|
|
2026-08-11 11:15:44 +08:00
|
|
|
it('downloads loading evidence through the tenant and inbound scoped file endpoint', async () => {
|
|
|
|
|
const blob = new Blob(['photo'], { type: 'image/jpeg' })
|
|
|
|
|
vi.mocked(http.get).mockResolvedValue(blob)
|
|
|
|
|
await expect(downloadEvidenceFile('2', '8', '88')).resolves.toBe(blob)
|
|
|
|
|
expect(http.get).toHaveBeenCalledWith('/platform/v1/files/loading-evidence/2/8/88', { responseType: 'blob' })
|
|
|
|
|
})
|
|
|
|
|
|
2026-08-10 15:36:53 +08:00
|
|
|
it('normalizes smart-loading long identifiers as strings', async () => {
|
|
|
|
|
vi.mocked(http.get)
|
|
|
|
|
.mockResolvedValueOnce({ data: { id: '2086085512165634050', warehouseId: '2086085512165634051', capacityCbm: '68', maxWeightKg: '25000', version: 2 } })
|
|
|
|
|
.mockResolvedValueOnce({ data: [{
|
|
|
|
|
accountId: '2086085512165634052', inboundId: '2086085512165634053', inboundDetailId: '2086085512165634054',
|
|
|
|
|
orderId: '2086085512165634055', warehouseId: '2086085512165634051', availablePackageCount: '12',
|
|
|
|
|
unitWeightKg: '10.5', unitVolumeCbm: '0.25',
|
|
|
|
|
}] })
|
|
|
|
|
const container = await getContainerBooking('2086085512165634050')
|
2026-08-12 18:43:54 +08:00
|
|
|
const candidates = await listSmartLoadingCandidates(container.id, container.warehouseId!)
|
|
|
|
|
expect(http.get).toHaveBeenLastCalledWith('/platform/v1/first-leg/smart-loading/candidates', { params: { bookingId: container.id, warehouseId: container.warehouseId } })
|
2026-08-10 15:36:53 +08:00
|
|
|
expect(container).toMatchObject({ id: '2086085512165634050', warehouseId: '2086085512165634051', capacityCbm: 68 })
|
|
|
|
|
expect(candidates[0]).toMatchObject({
|
|
|
|
|
accountId: '2086085512165634052', inboundId: '2086085512165634053',
|
|
|
|
|
inboundDetailId: '2086085512165634054', orderId: '2086085512165634055', availablePackageCount: 12,
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('normalizes preload and outbound aggregate identifiers as strings', async () => {
|
|
|
|
|
vi.mocked(http.get)
|
|
|
|
|
.mockResolvedValueOnce({ data: {
|
|
|
|
|
id: '2086085512165634050', inboundId: '2086085512165634051', tenantId: '2086085512165634052',
|
|
|
|
|
status: 'PENDING', version: 1, items: [],
|
|
|
|
|
} })
|
|
|
|
|
.mockResolvedValueOnce({ data: [{
|
|
|
|
|
orderId: '2086085512165634053', inboundId: '2086085512165634051',
|
|
|
|
|
containerBookingId: '2086085512165634054', loadingPlanId: '2086085512165634055',
|
|
|
|
|
oceanBookingId: '2086085512165634056', truckAppointmentId: '2086085512165634057', gate: null,
|
|
|
|
|
}] })
|
|
|
|
|
const preload = await getPreload('2086085512165634052', '2086085512165634051')
|
|
|
|
|
const dispatch = await getOutboundDispatch('2086085512165634052', '2086085512165634053', '2086085512165634051')
|
|
|
|
|
expect(preload).toMatchObject({ id: '2086085512165634050', inboundId: '2086085512165634051', tenantId: '2086085512165634052' })
|
|
|
|
|
expect(dispatch).toMatchObject({
|
|
|
|
|
orderId: '2086085512165634053', containerBookingId: '2086085512165634054',
|
|
|
|
|
loadingPlanId: '2086085512165634055', oceanBookingId: '2086085512165634056', truckAppointmentId: '2086085512165634057',
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('uses optimistic locks and operation identifiers for smart-loading mutations', async () => {
|
|
|
|
|
const plan = { id: '91', bookingId: '81', status: 'EDITING', version: 4, items: [] }
|
|
|
|
|
vi.mocked(http.post).mockResolvedValue({ data: plan })
|
|
|
|
|
vi.mocked(http.patch).mockResolvedValue({ data: plan })
|
|
|
|
|
vi.mocked(http.delete).mockResolvedValue({ data: plan })
|
|
|
|
|
await assignSmartLoadingItem('91', { inboundId: '71', inboundDetailId: '72', packageCount: 3, expectedVersion: 4 })
|
|
|
|
|
await recommendSmartLoading('91', 4)
|
|
|
|
|
await adjustSmartLoadingItem('91', '92', { packageCount: 2, expectedPlanVersion: 4, expectedItemVersion: 1 })
|
|
|
|
|
await deleteSmartLoadingItem('91', '92', { expectedPlanVersion: 4, expectedItemVersion: 1 })
|
|
|
|
|
expect(http.post).toHaveBeenNthCalledWith(1, '/platform/v1/first-leg/smart-loading/plans/91/assignments', {
|
|
|
|
|
inboundId: '71', inboundDetailId: '72', packageCount: 3, expectedVersion: 4,
|
|
|
|
|
operationId: expect.stringContaining('smart-loading-assign'),
|
|
|
|
|
})
|
|
|
|
|
expect(http.post).toHaveBeenNthCalledWith(2, '/platform/v1/first-leg/smart-loading/plans/91/recommend', {
|
|
|
|
|
expectedVersion: 4, operationId: expect.stringContaining('smart-loading-recommend'),
|
|
|
|
|
})
|
|
|
|
|
expect(http.patch).toHaveBeenCalledWith('/platform/v1/first-leg/smart-loading/plans/91/items/92', expect.objectContaining({
|
|
|
|
|
expectedPlanVersion: 4, expectedItemVersion: 1, operationId: expect.stringContaining('smart-loading-adjust'),
|
|
|
|
|
}))
|
|
|
|
|
expect(http.delete).toHaveBeenCalledWith('/platform/v1/first-leg/smart-loading/plans/91/items/92', {
|
|
|
|
|
data: expect.objectContaining({ expectedPlanVersion: 4, expectedItemVersion: 1, operationId: expect.stringContaining('smart-loading-delete') }),
|
|
|
|
|
})
|
|
|
|
|
})
|
2026-08-06 13:56:04 +08:00
|
|
|
})
|