This commit is contained in:
2026-10-21 14:30:33 +08:00
parent 3953453090
commit 8498798a9c
47 changed files with 791 additions and 409 deletions
@@ -2,7 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import { http } from '@/shared/api/http'
import * as api from './dispatches'
import { candidate, command, detail, loadingPreview, page } from '../dispatchTestFixtures'
import type { ArrangeDelivery, CreateDispatch, DispatchBatchAllocation, DraftSelection, LoadingComplete, ReasonCommand, ReviewCommand, UpdateDispatch, Version, VersionCommand } from '../dispatchContracts'
import type { CreateDispatch, DispatchBatchAllocation, DraftSelection, LoadingComplete, ReasonCommand, ReviewCommand, UpdateDispatch, Version, VersionCommand } from '../dispatchContracts'
vi.mock('@/shared/api/http', () => ({ http: { get: vi.fn(), post: vi.fn(), put: vi.fn(), delete: vi.fn() }, unwrapApiResponse: (value: { data: unknown }) => value.data }))
const body = { recordVersion: 0, idempotencyKey: 'original-key', reason: '核对原因' }
const draft = () => ({ draftToken: 'server-token', recordVersion: 0, status: 'ACTIVE', warehouseId: '321', containers: [], sources: [candidate()], expiresAt: '' })
@@ -74,7 +74,7 @@ describe('native whole-vehicle dispatch API', () => {
const request: CreateDispatch = { idempotencyKey: 'create', warehouseId: '321', vehicle: null, driver: null, plannedDepartureAt: null, estimatedArrivalAt: null, remark: null, cargo: [], batchAllocations: [] }
await api.createDispatch(request); expect(http.post).toHaveBeenCalledWith('/platform/v1/last-leg/dispatches', request)
})
it.each(['cancel', 'remove', 'review', 'loading-review', 'return', 'loading', 'depart', 'arrange', 'add', 'submit', 'draft-container', 'draft-select', 'draft-release', 'draft-convert', 'draft-snapshot'])('rejects missing/null/negative versions before %s requests and accepts zero', async action => {
it.each(['cancel', 'remove', 'review', 'loading-review', 'return', 'loading', 'add', 'submit', 'draft-container', 'draft-select', 'draft-release', 'draft-convert', 'draft-snapshot'])('rejects missing/null/negative versions before %s requests and accepts zero', async action => {
const invoke = (value: unknown) => {
const request = { ...body, recordVersion: value as Version }
switch (action) {
@@ -84,8 +84,6 @@ describe('native whole-vehicle dispatch API', () => {
case 'loading-review': return api.reviewDispatchLoading('PC/1', request as unknown as ReviewCommand)
case 'return': return api.returnDispatchToReview('PC/1', request)
case 'loading': return api.completeDispatchLoading('PC/1', request as unknown as LoadingComplete)
case 'depart': return api.departDispatch('PC/1', { ...request, remark: null })
case 'arrange': return api.transferDispatchToDelivery('PC/1', request as unknown as ArrangeDelivery)
case 'add': return api.addDispatchCargo('PC/1', { ...request, cargo: [], batchAllocations: [] })
case 'submit': return api.submitDispatchReview('PC/1', { ...request, remark: null })
case 'draft-container': return api.createDispatchDraftContainer({ ...request, draftToken: 'draft', capacity: 30 })
@@ -107,14 +105,6 @@ describe('native whole-vehicle dispatch API', () => {
await api.cancelDispatch('PC/1', body); await api.removeDispatchItem('PC/1', 'item/1', body)
expect(vi.mocked(http.post).mock.calls.map(call => call[0])).toEqual(['/platform/v1/last-leg/dispatches/PC%2F1/cancel', '/platform/v1/last-leg/dispatches/PC%2F1/items/item%2F1/remove']); expect(http.delete).not.toHaveBeenCalled()
})
it('preserves complete grouped arrangement evidence and returns an immutable command envelope', async () => {
const outbound = (customerId: string) => ({ customerId, customerName: `客户${customerId}`, outboundNo: `OUT-${customerId}`, status: 'WAIT_OUTBOUND', packageCount: 2, palletCount: 1 })
const delivery = (customerId: string) => ({ ...outbound(customerId), groupKey: customerId, deliveryNo: `DEL-${customerId}`, destination: candidate().destination })
const data = { ...command('ARRANGE_DELIVERY'), result: { dispatchNo: 'PC/1', recordVersion: '9007199254740993', arrangementKey: 'arrange', cargoHash: 'cargo', arrangedAt: '2026-10-19T10:00:00Z', outbounds: [outbound('A'), outbound('B')], deliveries: [delivery('A'), delivery('B')] } }
vi.mocked(http.post).mockResolvedValueOnce({ data })
const request: ArrangeDelivery = { ...body, recordVersion: '9007199254740993', cargoHash: 'cargo', loadingCompletionHash: 'loaded', groups: [] }
expect(await api.transferDispatchToDelivery('PC/1', request)).toEqual(data); expect(http.post).toHaveBeenCalledWith('/platform/v1/last-leg/dispatches/PC%2F1/transfer-delivery', request)
})
it('keeps draft version, server token and source evidence and converts explicit groups atomically', async () => {
const source = candidate().selection
await api.lockDispatchDraftPallet({ ...body, draftToken: 'server-token', draftContainerId: '190000000000000011', source })
@@ -1,7 +1,7 @@
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import type { DispatchEvidenceKind, DispatchRoute, DispatchRouteCommand } from '../dispatchContracts'
import { requireVersion, requireBatchVersions, type AddCargo, type ArrangeDelivery, 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 { normalizeArrangementPreview, normalizeCandidateList, normalizeConvertedDraft, normalizeDispatchCommand, normalizeDispatchDetail, normalizeDispatchDraft, normalizeDispatchItems, normalizeDispatchPage, normalizeEvidenceFile, normalizeLoadingPreview, type ArrangementPreviewWire, type CandidateListWire, type CommandResultWire, type ConvertedDraftWire, type DispatchDetailWire, type DispatchDraftWire, type DispatchEvidenceFileWire, type DispatchItemsWire, type DispatchPageWire, type LoadingPreviewWire } from '../dispatchWire'
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'
export type { DispatchBatchReference, DispatchBatchAllocation } from '../dispatchContracts'
const base = '/platform/v1/last-leg/dispatches'
@@ -38,9 +38,6 @@ export const reviewDispatch = (no: string, body: ReviewCommand) => postCommand(`
export const reviewDispatchLoading = (no: string, body: ReviewCommand) => postCommand(`${path(no)}/loading-review`, versioned(body))
export const returnDispatchToReview = (no: string, body: ReasonCommand) => postCommand(`${path(no)}/return-to-review`, versioned(body))
export const completeDispatchLoading = (no: string, body: LoadingComplete) => postCommand(`${path(no)}/loading-complete`, versioned(body))
export const departDispatch = (no: string, body: VersionCommand & { remark: string | null }) => postCommand(`${path(no)}/depart`, versioned(body))
export const getDispatchArrangement = (no: string) => get<ArrangementPreviewWire>(`${path(no)}/delivery-arrangement`).then(normalizeArrangementPreview)
export const transferDispatchToDelivery = (no: string, body: ArrangeDelivery) => postCommand(`${path(no)}/transfer-delivery`, versioned(body))
export const getDispatchBolPdf = (no: string) => http.get<Blob, Blob>(`${path(no)}/prints/bol`, { responseType: 'blob' })
export async function listDispatchCandidates(query: CandidateQuery) {