feat(fleet): integrate trip management and fee coverage workflows

This commit is contained in:
2026-10-03 04:35:55 +08:00
parent bca08f8749
commit dc35288a1b
87 changed files with 3774 additions and 233 deletions
@@ -65,9 +65,9 @@ export interface DeliveryOrderRow {
}
export interface DeliverySkuRow { inboundCode?: string | null; customerName?: string | null; destinationWarehouseCode?: string | null; receivingAddress?: string | null; shipmentNo?: string | null; fbaTrackingNo?: string | null; skuCode?: string | null; productName?: string | null; packageCount: number; productQuantity?: number | null; weightKg?: number | null; volumeCbm?: number | null }
export interface DeliveryFeeRow { id: string; feeDirection: string; feeType: string; currency: string; amount: number; remark?: string | null }
export interface DeliveryFeeRow { id: string; feeDirection: string; feeType: string; currency: string; amount: number; remark?: string | null; reviewStatus?: string | null; sourceStatus?: string | null; voidStatus?: string | null }
export interface DeliveryOperationRow { id: string; operatedAt: string; operationNode: string; operatorName?: string | null; result: string }
export interface DeliveryOrderDetail { order: DeliveryOrderRow; skus: DeliverySkuRow[]; fees: DeliveryFeeRow[]; operations: DeliveryOperationRow[] }
export interface DeliveryOrderDetail { order: DeliveryOrderRow; skus: DeliverySkuRow[]; fees: DeliveryFeeRow[]; operations: DeliveryOperationRow[]; feeCoverage?: import('../feeCoverageTypes').FeeCoverageEvidence | null }
export interface DeliveryReturnSource {
outboundPalletItemId: string
@@ -1,10 +1,21 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { http } from '@/shared/api/http'
import { cancelDispatch, createDispatchDraftContainer, deleteDispatchDraftContainer, getDispatch, getDispatchBolPdf, listDispatchCandidates, listDispatchDrafts, listDispatches, lockDispatchDraftPallet, preArrangeDispatch, releaseDispatchDraft, transferDispatchToDelivery, unlockDispatchDraftPallet, updateDispatch } from './dispatches'
import { cancelDispatch, createDispatchDraftContainer, deleteDispatchDraftContainer, getDispatch, getDispatchBolPdf, getDispatchExternalFreight, getDispatchFeeCoverage, listDispatchCandidates, listDispatchDrafts, listDispatches, lockDispatchDraftPallet, preArrangeDispatch, releaseDispatchDraft, transferDispatchToDelivery, unlockDispatchDraftPallet, updateDispatch } from './dispatches'
vi.mock('@/shared/api/http', async (importOriginal) => ({ ...(await importOriginal<typeof import('@/shared/api/http')>()), http: { get: vi.fn(), post: vi.fn(), put: vi.fn(), delete: vi.fn() } }))
describe('dispatch overseas API', () => {
it('reads external freight from the scoped dispatch without a supplied customer', async () => {
vi.mocked(http.get).mockResolvedValue({ code: '200', data: { snapshotHash: 'hash', requiresDeliveryFee: false, quotes: [] } })
expect(await getDispatchExternalFreight('DP/1')).toEqual({ snapshotHash: 'hash', requiresDeliveryFee: false, quotes: [] })
expect(http.get).toHaveBeenCalledWith('/platform/v1/last-leg/dispatches/DP%2F1/external-freight')
})
it('loads per-shipment coverage without exposing a caller-supplied customer or losing long IDs', async () => {
vi.mocked(http.get).mockResolvedValue({ code: '200', data: { dispatchNo: 'DP/1', sourceComplete: true, blockReason: null, cargoHash: 'hash', cargo: [], feeSources: [{ financeFeeId: '190000000000000099', sourceFeeId: '190000000000000100', customerId: '101', sourceInboundId: '11', sourceVersion: '2', amount: '100.25' }] } })
const result = await getDispatchFeeCoverage('DP/1')
expect(http.get).toHaveBeenCalledWith('/platform/v1/last-leg/dispatches/DP%2F1/fee-coverage')
expect(result.feeSources[0]).toMatchObject({ financeFeeId: '190000000000000099', sourceVersion: 2, amount: 100.25 })
})
beforeEach(() => {
vi.mocked(http.get).mockReset()
vi.mocked(http.post).mockReset()
@@ -235,6 +235,9 @@ export async function releaseDispatchDraft(draftToken: string) {
export interface TransferDeliveryItem { deliveryNo: string; deliveryStatus: string; destinationWarehouseId?: string | null; destinationWarehouseCode?: string | null; destinationWarehouseName?: string | null; palletCount: number; packageCount: number }
export interface TransferDeliveryResult { dispatchNo: string; outboundNo: string; outboundStatus: string; palletCount: number; packageCount: number; deliveries: TransferDeliveryItem[]; replayed: boolean }
export interface TransferDeliveryPayload {
feeCoverageCargoHash?: string
feeCoverage?: import('../feeCoverageTypes').FeeCoverageSelection[]
externalFreightHash?: string
idempotencyKey: string
plannedDeliveryAt: string
deliveryMethod: string
@@ -262,3 +265,24 @@ export async function transferDispatchToDelivery(dispatchNo: string, payload: Tr
)
return unwrapApiResponse(response)
}
export interface ExternalFreightPreview {
snapshotHash: string
requiresDeliveryFee: boolean
quotes: Array<{ bookingNo: string; recordVersion: string | number; incomingNo: string; inboundCode: string;
customerName: string; destinationKey: string; currency: string; amount: number; quantity: number; quantityUnit: string; unitPrice: number }>
}
export async function getDispatchExternalFreight(dispatchNo: string): Promise<ExternalFreightPreview> {
const response = await http.get<ApiEnvelope<ExternalFreightPreview>, ApiEnvelope<ExternalFreightPreview>>(
`/platform/v1/last-leg/dispatches/${encodeURIComponent(dispatchNo)}/external-freight`,
)
return unwrapApiResponse(response)
}
export async function getDispatchFeeCoverage(dispatchNo: string): Promise<import('../feeCoverageTypes').FeeCoveragePreview> {
const response = await http.get<ApiEnvelope<import('../feeCoverageTypes').FeeCoveragePreview>, ApiEnvelope<import('../feeCoverageTypes').FeeCoveragePreview>>(
`/platform/v1/last-leg/dispatches/${encodeURIComponent(dispatchNo)}/fee-coverage`,
)
const data = unwrapApiResponse(response)
return { ...data, cargo: data.cargo.map(row => ({ ...row, dispatchItemId: String(row.dispatchItemId), warehousePalletId: String(row.warehousePalletId), orderId: row.orderId == null ? null : String(row.orderId), sourceInboundId: row.sourceInboundId == null ? null : String(row.sourceInboundId), packages: Number(row.packages) })), feeSources: data.feeSources.map(row => ({ ...row, financeFeeId: String(row.financeFeeId), sourceFeeId: String(row.sourceFeeId), customerId: String(row.customerId), sourceInboundId: String(row.sourceInboundId), sourceVersion: Number(row.sourceVersion), amount: Number(row.amount) })) }
}