fix(appointments): normalize inbound plan counts before arrival date projection
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { http } from '@/shared/api/http'
|
||||
import { projectContainerSchedule } from './schedule'
|
||||
import { getCalendarSources, getWarehouseArrivals } from './scheduleApi'
|
||||
|
||||
vi.mock('@/shared/api/http', async original => ({
|
||||
...await original<typeof import('@/shared/api/http')>(),
|
||||
http: { get: vi.fn(), post: vi.fn() },
|
||||
}))
|
||||
|
||||
const range = { from: '2026-09-28', to: '2026-11-02' }
|
||||
const bookingId = '9007199254740993'
|
||||
// 使用真实接口的结构:Long 数量序列化为字符串,空时间字段不输出。
|
||||
const wire = {
|
||||
sourceContainerBookingId: bookingId, containerNo: 'CALENDAR-CONT',
|
||||
warehouseName: '接仓仓库', warehouseCode: 'US-W', version: 0, planCount: '0', canEdit: true,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(http.get).mockResolvedValue({ code: '200', message: 'OK', data: {
|
||||
containers: [{ containerBookingId: bookingId, cabinetNo: 'D-CALENDAR', containerNo: 'CALENDAR-CONT',
|
||||
vesselName: null, voyageNo: null, destinationPortName: null, destinationWarehouseCode: null,
|
||||
arrivalAt: '2026-10-20T23:00:00', arrivalType: 'ESTIMATED' }],
|
||||
emptyReturns: [], emptyReturnsReadable: true,
|
||||
} })
|
||||
})
|
||||
|
||||
async function readSchedule(overrides: Record<string, unknown> = {}) {
|
||||
vi.mocked(http.post).mockResolvedValue({ code: '200', message: 'OK', data: [{ ...wire, ...overrides }] })
|
||||
const sources = await getCalendarSources(range)
|
||||
const warehouses = await getWarehouseArrivals(range, [bookingId])
|
||||
return { warehouses, event: projectContainerSchedule(sources, warehouses, range, true).find(row => row.type === 'warehouse') }
|
||||
}
|
||||
|
||||
describe('warehouse calendar response to schedule', () => {
|
||||
it.each(['0', 0])('uses zero plan count %j to show arrival plus two days without inventing a time', async planCount => {
|
||||
const { warehouses, event } = await readSchedule({ planCount })
|
||||
expect(warehouses[0]).toMatchObject({ sourceContainerBookingId: bookingId, planCount: 0, plannedAt: null, actualAt: null })
|
||||
expect(event).toMatchObject({ date: '2026-10-22', time: '', canEdit: true, subtitle: '到港日期+2个自然日(默认)' })
|
||||
expect(event?.fields).toContainEqual({ label: '计划到仓', value: '2026-10-22' })
|
||||
expect(event?.fields).toContainEqual({ label: '实际到仓', value: '—' })
|
||||
})
|
||||
|
||||
it('keeps the authoritative frozen plan instead of recalculating from arrival', async () => {
|
||||
const { warehouses, event } = await readSchedule({ planCount: '2', plannedAt: '2026-10-25T09:30:00', canEdit: false })
|
||||
expect(warehouses[0]?.planCount).toBe(2)
|
||||
expect(event).toMatchObject({ date: '2026-10-25', time: '09:30', canEdit: false, subtitle: '已有入库计划,日期已冻结' })
|
||||
})
|
||||
|
||||
it('keeps a manually maintained time when there is no inbound plan', async () => {
|
||||
const { event } = await readSchedule({ version: 1, plannedAt: '2026-10-24T08:00:00' })
|
||||
expect(event).toMatchObject({ date: '2026-10-24', time: '08:00', subtitle: '人工维护' })
|
||||
})
|
||||
|
||||
it('does not calculate a default when existing plans have no unified time', async () => {
|
||||
const { event } = await readSchedule({ planCount: '2', canEdit: false })
|
||||
expect(event?.date).toBeNull()
|
||||
})
|
||||
|
||||
it('keeps conflicting authoritative versions unresolved even with zero plans', async () => {
|
||||
const { event } = await readSchedule({ version: -1, canEdit: false })
|
||||
expect(event).toMatchObject({ date: null, canEdit: false, subtitle: '柜内计划时间不一致,待核实' })
|
||||
})
|
||||
|
||||
it.each([undefined, null, '', 'invalid', '-1', '1.5', '9007199254740992', -1, Infinity])(
|
||||
'rejects invalid or unsafe plan count %j rather than treating it as no plans', async planCount => {
|
||||
await expect(readSchedule({ planCount })).rejects.toThrow('入库计划数量无效')
|
||||
},
|
||||
)
|
||||
})
|
||||
@@ -31,15 +31,35 @@ export interface WarehouseArrival {
|
||||
canEdit: boolean
|
||||
}
|
||||
export interface ScheduleRange { from: string; to: string }
|
||||
type WarehouseArrivalWire = Omit<WarehouseArrival, 'planCount' | 'plannedAt' | 'actualAt'> & {
|
||||
planCount: string | number
|
||||
plannedAt?: string | null
|
||||
actualAt?: string | null
|
||||
}
|
||||
|
||||
/** 在接口边界统一长整型数量,未知数量不能被当成零触发默认日期。 */
|
||||
function normalizeWarehouseArrival(row: WarehouseArrivalWire): WarehouseArrival {
|
||||
const count = row.planCount
|
||||
if (typeof count !== 'number' && (typeof count !== 'string' || !/^(0|[1-9]\d*)$/.test(count))) {
|
||||
throw new Error('到仓记录的入库计划数量无效,请刷新后重试')
|
||||
}
|
||||
const planCount = Number(count)
|
||||
if (!Number.isSafeInteger(planCount) || planCount < 0) {
|
||||
throw new Error('到仓记录的入库计划数量无效,请刷新后重试')
|
||||
}
|
||||
return { ...row, planCount, plannedAt: row.plannedAt ?? null, actualAt: row.actualAt ?? null }
|
||||
}
|
||||
|
||||
export async function getCalendarSources(range: ScheduleRange, signal?: AbortSignal) {
|
||||
return unwrapApiResponse(await http.get<ApiEnvelope<CalendarSources>, ApiEnvelope<CalendarSources>>(
|
||||
'/platform/v1/terminal-drayages/calendar-sources', { params: range, signal },
|
||||
))
|
||||
}
|
||||
export async function getWarehouseArrivals(range: ScheduleRange, sourceContainerBookingIds: string[], signal?: AbortSignal) {
|
||||
return unwrapApiResponse(await http.post<ApiEnvelope<WarehouseArrival[]>, ApiEnvelope<WarehouseArrival[]>>(
|
||||
const rows = unwrapApiResponse(await http.post<ApiEnvelope<WarehouseArrivalWire[]>, ApiEnvelope<WarehouseArrivalWire[]>>(
|
||||
'/platform/v1/last-leg/appointment-calendar/warehouse-arrivals/query', { ...range, sourceContainerBookingIds }, { signal },
|
||||
))
|
||||
return rows.map(normalizeWarehouseArrival)
|
||||
}
|
||||
export interface WarehouseArrivalChange { estimatedWarehouseArrivalAt: string; expectedVersion: number }
|
||||
export async function saveWarehouseArrival(containerId: string, body: WarehouseArrivalChange, key: string) {
|
||||
|
||||
Reference in New Issue
Block a user