feat(appointment): integrate work calendar and original business actions

This commit is contained in:
2026-10-15 17:11:03 +08:00
parent af2e1481a6
commit b769d2479c
59 changed files with 2604 additions and 941 deletions
@@ -9,7 +9,24 @@ import {
recordTerminalDrayageEmptyReturn,
scheduleTerminalDrayagePickup,
selectTerminalDrayageFleet,
getDirectDeliveries, recordDirectDelivery, recordTerminalPickup,
} from './terminalDrayages'
it('records only the original actual PICKED_UP node with an immutable observed time and idempotency header', async () => {
const command = { standardNode: 'PICKED_UP' as const, observedAt: '2026-10-15T09:00:00', estimatedAt: null, actualAt: '2026-10-14T23:00:00', locationCode: 'TEST-PORT', locationName: '测试码头', reason: '司机确认已提柜' }
vi.mocked(http.post).mockResolvedValue({ data: {} })
await recordTerminalPickup('9007199254740993', command, 'same-pickup-key')
expect(http.post).toHaveBeenCalledWith('/platform/v1/container-shipping-nodes/9007199254740993', command, { headers: { 'Idempotency-Key': 'same-pickup-key' } })
})
it('uses the actual direct-delivery source and stable idempotency key without coercing long IDs', async () => {
vi.mocked(http.get).mockResolvedValue({ data: [] }); await getDirectDeliveries('9007199254740993')
expect(http.get).toHaveBeenCalledWith('/platform/v1/terminal-drayages/9007199254740993/direct-deliveries')
const request = { expectedVersion: 0, actualAt: '2026-10-16T01:00:00', timezone: 'America/Los_Angeles', receivedBy: '测试签收人', evidenceNo: 'POD-001', remark: null }
vi.mocked(http.post).mockResolvedValue({ data: { requirementKey: 'INBOUND:9007199254740999', version: 1 } })
await recordDirectDelivery('9007199254740993', 'INBOUND:9007199254740999', request, 'retry-same-key')
expect(http.post).toHaveBeenCalledWith('/platform/v1/terminal-drayages/9007199254740993/direct-deliveries/9007199254740999', request, { headers: { 'Idempotency-Key': 'retry-same-key' } })
await expect(recordDirectDelivery('1', 'ORDER:1', request, 'bad')).rejects.toThrow('标识无效')
})
vi.mock('@/shared/api/http', () => ({
http: { get: vi.fn(), post: vi.fn() },
@@ -108,21 +108,40 @@ export async function getTerminalDrayageHistory(containerBookingId: string) {
return (unwrapApiResponse(response) || []).map((item) => ({ ...item, operatorId: id(item.operatorId) }))
}
async function command(containerBookingId: string, action: string, body: object) {
async function command(containerBookingId: string, action: string, body: object, idempotencyKey = operationId(action)) {
const response = await http.post<ApiEnvelope<TerminalDrayageView>, ApiEnvelope<TerminalDrayageView>>(
`${path}/${encodeURIComponent(containerBookingId)}/${action}`,
body,
{ headers: { 'Idempotency-Key': operationId(action) } },
{ headers: { 'Idempotency-Key': idempotencyKey } },
)
return normalize(unwrapApiResponse(response))
}
export function selectTerminalDrayageFleet(containerBookingId: string, body: { expectedVersion: number; fleetSupplierId: string; fleetSupplierName?: string; reason?: string }) {
return command(containerBookingId, 'fleet', body)
export function selectTerminalDrayageFleet(containerBookingId: string, body: { expectedVersion: number; fleetSupplierId: string; fleetSupplierName?: string; reason?: string }, idempotencyKey?: string) {
return command(containerBookingId, 'fleet', body, idempotencyKey)
}
export function scheduleTerminalDrayagePickup(containerBookingId: string, body: { expectedVersion: number; pickupAppointmentAt: string; reason?: string }) {
return command(containerBookingId, 'pickup-appointment', body)
export function scheduleTerminalDrayagePickup(containerBookingId: string, body: { expectedVersion: number; pickupAppointmentAt: string; reason?: string }, idempotencyKey?: string) {
return command(containerBookingId, 'pickup-appointment', body, idempotencyKey)
}
export function recordTerminalDrayageEmptyReturn(containerBookingId: string, body: { expectedVersion: number; actualAt: string; remark?: string }) {
return command(containerBookingId, 'empty-return', body)
export function recordTerminalDrayageEmptyReturn(containerBookingId: string, body: { expectedVersion: number; actualAt: string; remark?: string }, idempotencyKey?: string) {
return command(containerBookingId, 'empty-return', body, idempotencyKey)
}
export interface DirectDelivery {
requirementKey: string; inboundCode: string | null; orderNo: string | null; destinationName: string | null
packageCount: number | null; version: number; actualAt: string | null; timezone: string | null
receivedBy: string | null; evidenceNo: string | null; remark: string | null
}
export interface DirectDeliveryCommand { expectedVersion: number; actualAt: string; timezone: string; receivedBy: string; evidenceNo: string; remark: string | null }
export async function getDirectDeliveries(containerBookingId: string) {
return unwrapApiResponse(await http.get<ApiEnvelope<DirectDelivery[]>, ApiEnvelope<DirectDelivery[]>>(`${path}/${encodeURIComponent(containerBookingId)}/direct-deliveries`))
}
export async function recordDirectDelivery(containerBookingId: string, requirementKey: string, body: DirectDeliveryCommand, idempotencyKey: string) {
if (!/^INBOUND:\d+$/.test(requirementKey)) throw new Error('服务端入仓来源标识无效,请重新读取清单')
const inboundId = requirementKey.slice('INBOUND:'.length)
return unwrapApiResponse(await http.post<ApiEnvelope<DirectDelivery>, ApiEnvelope<DirectDelivery>>(`${path}/${encodeURIComponent(containerBookingId)}/direct-deliveries/${encodeURIComponent(inboundId)}`, body, { headers: { 'Idempotency-Key': idempotencyKey } }))
}
export interface TerminalPickupCommand { standardNode: 'PICKED_UP'; observedAt: string; estimatedAt: null; actualAt: string; locationCode: string | null; locationName: string | null; reason: string }
export async function recordTerminalPickup(containerBookingId: string, body: TerminalPickupCommand, idempotencyKey: string) {
unwrapApiResponse(await http.post<ApiEnvelope<unknown>, ApiEnvelope<unknown>>(`/platform/v1/container-shipping-nodes/${encodeURIComponent(containerBookingId)}`, body, { headers: { 'Idempotency-Key': idempotencyKey } }))
}