Merge origin/release/test and preserve procurement direction with request unlock

This commit is contained in:
2026-10-26 13:51:37 +08:00
170 changed files with 3122 additions and 2458 deletions
@@ -15,10 +15,19 @@ beforeEach(() => {
vi.mocked(http.put).mockResolvedValue({ data: command('EDIT') })
vi.mocked(http.delete).mockResolvedValue({ data: draft() })
})
it('reads and writes original dispatch appointment allocations with lossless versions', async () => {
const value = { dispatchNo: 'PC/1', recordVersion: '9007199254740993', items: [], edit: { allowed: true, blockCode: null, blockReason: null } }
vi.mocked(http.get).mockResolvedValueOnce({ data: value })
expect(await api.getDispatchAppointmentAllocations('PC/1')).toEqual(value)
expect(http.get).toHaveBeenCalledWith('/platform/v1/last-leg/dispatches/PC%2F1/appointment-allocations')
const body = { recordVersion: 0, idempotencyKey: 'allocation-key', dispatchItemIds: ['11'], batchAllocations: [{ planPalletId: '21', batchKey: 'FBA_APPOINTMENT:9', customerId: '101', sourceKey: 'ORDER:1', destinationKey: 'FBA:ACY2', packageCount: 10, recordVersion: 0 }] }
await api.saveDispatchAppointmentAllocations('PC/1', body)
expect(http.put).toHaveBeenCalledWith('/platform/v1/last-leg/dispatches/PC%2F1/appointment-allocations', body)
})
describe('native whole-vehicle dispatch API', () => {
it('round-trips this trip maintenance data and its vehicle photo references without altering actual departure', async () => {
const maintenance = { ...blankVehicleMaintenance(), plannedDeliveryDate: '2026-11-02', deliveryMethod: '外配', fleetEmail: 'fleet@example.test', appointmentNo: 'APPT-1' }
const maintenance = { ...blankVehicleMaintenance(), plannedDeliveryDate: '2026-11-02', deliveryMethod: '外配', fleetEmail: 'fleet@example.test', destinationAppointments: [{ destinationKey: 'FBA:ONT8', appointmentNo: 'APPT-1' }] }
const value = detail()
value.vehicleInfo.maintenance = maintenance
value.vehicleInfo.vehicle!.evidenceFileIds = ['9007199254740993']
@@ -26,7 +35,7 @@ describe('native whole-vehicle dispatch API', () => {
const saved = await api.getDispatch('PC/1')
expect(saved.vehicleInfo.maintenance).toEqual(maintenance)
expect(saved.vehicleInfo.vehicle!.evidenceFileIds).toEqual(['9007199254740993'])
await api.updateDispatch('PC/1', { recordVersion: saved.recordVersion, idempotencyKey: 'maintenance-key', vehicle: saved.vehicleInfo.vehicle, driver: saved.vehicleInfo.driver, maintenance, plannedDepartureAt: null, estimatedArrivalAt: null, remark: null, publishLooseboardDemand: true })
await api.updateDispatch('PC/1', { recordVersion: saved.recordVersion, idempotencyKey: 'maintenance-key', vehicle: saved.vehicleInfo.vehicle, driver: saved.vehicleInfo.driver, maintenance, plannedDepartureAt: null, estimatedArrivalAt: null, remark: null })
expect(http.put).toHaveBeenCalledWith('/platform/v1/last-leg/dispatches/PC%2F1', expect.objectContaining({ maintenance, vehicle: expect.objectContaining({ evidenceFileIds: ['9007199254740993'] }) }))
expect(vi.mocked(http.put).mock.calls[0]![1]).not.toHaveProperty('departedAt')
})
@@ -111,7 +120,7 @@ describe('native whole-vehicle dispatch API', () => {
expect(http.post).not.toHaveBeenCalled(); await invoke(0); expect(vi.mocked(http.post).mock.calls[0]![1]).toEqual(expect.objectContaining({ recordVersion: 0 }))
})
it('updates vehicle resources and timing with the explicit dispatch version', async () => {
const request: UpdateDispatch = { publishLooseboardDemand: true, ...body, vehicle: null, driver: null, plannedDepartureAt: null, estimatedArrivalAt: null, remark: null }
const request: UpdateDispatch = { ...body, vehicle: null, driver: null, plannedDepartureAt: null, estimatedArrivalAt: null, remark: null }
await api.updateDispatch('PC/1', request); expect(http.put).toHaveBeenCalledWith('/platform/v1/last-leg/dispatches/PC%2F1', request)
await expect(api.updateDispatch('PC/1', { ...request, recordVersion: undefined as unknown as Version })).rejects.toThrow('版本')
})
@@ -1,4 +1,5 @@
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import type { AppointmentAllocations, AppointmentAllocationCommand } from '../dispatchContracts'
import type { MarkDispatchException, DispatchEvidenceKind, DispatchRoute, DispatchRouteCommand } from '../dispatchContracts'
import { requireVersion, requireBatchVersions, type AddCargo, type CandidateQuery, type ConvertDraft, type CreateDispatch, type DispatchQuery, type DraftContainer, type DraftSelection, type LoadingComplete, type ReasonCommand, type ReviewCommand, type UpdateDispatch, type VersionCommand, type DispatchBatchAllocation } from '../dispatchContracts'
import { normalizeCandidateList, normalizeConvertedDraft, normalizeDispatchCommand, normalizeDispatchDetail, normalizeDispatchDraft, normalizeDispatchItems, normalizeDispatchPage, normalizeEvidenceFile, normalizeLoadingPreview, type CandidateListWire, type CommandResultWire, type ConvertedDraftWire, type DispatchDetailWire, type DispatchDraftWire, type DispatchEvidenceFileWire, type DispatchItemsWire, type DispatchPageWire, type LoadingPreviewWire } from '../dispatchWire'
@@ -17,6 +18,19 @@ export async function listDispatches(query: DispatchQuery) {
}
export const getDispatch = (dispatchNo: string) => get<DispatchDetailWire>(path(dispatchNo)).then(normalizeDispatchDetail)
export const getDispatchItems = (no: string) => get<DispatchItemsWire>(`${path(no)}/items`).then(normalizeDispatchItems)
export async function getDispatchAppointmentAllocations(no: string) {
const value = await get<AppointmentAllocations>(`${path(no)}/appointment-allocations`)
requireVersion(value.recordVersion)
value.items.forEach(item => { requireBatchVersions(item.allocations); item.sources.forEach(source => source.options.forEach(option => requireVersion(option.recordVersion))) })
return value
}
export const saveDispatchAppointmentAllocations = (no: string, body: AppointmentAllocationCommand) => {
versioned(body); requireBatchVersions(body.batchAllocations)
return postAppointmentAllocations(no, body)
}
async function postAppointmentAllocations(no: string, body: AppointmentAllocationCommand) {
return normalizeDispatchCommand(unwrapApiResponse(await http.put<ApiEnvelope<CommandResultWire>, ApiEnvelope<CommandResultWire>>(`${path(no)}/appointment-allocations`, body)))
}
export const getDispatchRoute = (no: string) => get<DispatchRoute>(`${path(no)}/route`)
export async function reorderDispatchRoute(no: string, body: DispatchRouteCommand) { return normalizeDispatchCommand(unwrapApiResponse(await http.put<ApiEnvelope<CommandResultWire>, ApiEnvelope<CommandResultWire>>(`${path(no)}/route`, versioned(body)))) }
const evidencePath = (no: string, kind: DispatchEvidenceKind, id?: string) => `${path(no)}/evidence/${kind}${id ? `/${encodeURIComponent(id)}` : ''}`