This commit is contained in:
2026-10-20 11:33:39 +08:00
parent d2f2ecf57a
commit 41a498657c
33 changed files with 909 additions and 88 deletions
@@ -7,10 +7,11 @@ import {
createLocalLooseboardOrder, createUsTruckOrder, getLocalLooseboardOrder, getOverseasPurchase, getOverseasPurchaseEditor, getUsTruckOrder,
listLocalLooseboardOrders, listOverseasPurchaseCustomerOptions, listOverseasPurchases,
listOverseasPurchaseWarehouseOptions, listUsTruckOrders, updateLocalLooseboardOrder, updateOverseasPurchase,
updateUsTruckOrder,
updateUsTruckOrder, executeUsTruckCommand,
} from './platformOverseasBusiness'
import type { UpdateLocalTransportOrderRequest } from '../types'
import { readPendingProcurement } from '../procurementPending'
import type { TruckOperation } from '../truckWorkflow'
const envelope = <T>(data: T) => ({ code: '200', message: 'success', data })
const purchaseSummary = {
@@ -92,7 +93,7 @@ describe('platform overseas business API adapter', () => {
expect(overseas.patch).toHaveBeenCalledWith('/platform/v1/last-leg/overseas-procurements/OP%2F1', { version: 4, remark: '内部' }, { params: { customerId: '10001' } })
})
it('supports platform truck list/detail/create/update without status endpoints', async () => {
it('supports platform truck list/detail/create/update', async () => {
const detail = { summary: truckSummary, pallets: [{ id: '9007199254741001', warehousePalletId: '9007199254741003', palletNo: 'P-1' }], histories: [] }
overseas.get.mockResolvedValueOnce(envelope({ records: [truckSummary], page: '1', size: '20', total: '1', pages: '1' })).mockResolvedValueOnce(envelope(detail))
expect((await listUsTruckOrders({ routeKeyword: ' LA ' })).records[0]?.totalWeightKg).toBe('123.45')
@@ -106,6 +107,32 @@ describe('platform overseas business API adapter', () => {
expect(overseas.patch).toHaveBeenCalledWith('/platform/v1/last-leg/us-truck-orders/TR-1', update, { params: { customerId: '9007199254741999' } })
})
it.each<TruckOperation>([
{ command: 'review', payload: { version: 2, idempotencyKey: 'review-key', decision: 'APPROVE' } },
{ command: 'review', payload: { version: 2, idempotencyKey: 'reject-key', decision: 'REJECT', remark: '资料不全' } },
{ command: 'receive-pallets', payload: { version: 2, idempotencyKey: 'receive-key', actualPalletCount: 3, packageCount: 10 } },
{ command: 'review-pallet-overage', payload: { version: 2, idempotencyKey: 'overage-key', decision: 'APPROVE' } },
{ command: 'allocate-pallets', payload: { version: 2, idempotencyKey: 'allocate-key', palletIds: ['9007199254741001'] } },
...(['prepare-dispatch', 'dispatch', 'start-transit', 'complete'] as const).map(command => ({ command, payload: { version: 2, idempotencyKey: command } })),
{ command: 'cancel', payload: { version: 2, idempotencyKey: 'cancel-key', status: 'CANCELLED', remark: '客户取消' } },
])('sends the scoped named truck command $command with version and unchanged replay key', async operation => {
const response = envelope({ summary: { ...truckSummary, version: '3', palletCount: '2', packageCount: '10', receivedPalletCount: '1', allocatedPalletCount: '0' }, pallets: [{ id: '9007199254741001', warehousePalletId: null }], histories: [] })
overseas.post.mockResolvedValue(response); overseas.patch.mockResolvedValue(response)
const result = await executeUsTruckCommand('TR/1', '9007199254741999', operation)
const mutation = operation.command === 'cancel' ? overseas.patch : overseas.post
expect(mutation).toHaveBeenCalledWith(`/platform/v1/last-leg/us-truck-orders/TR%2F1/${operation.command === 'cancel' ? 'status' : operation.command}`, operation.payload, { params: { customerId: '9007199254741999' } })
expect(result).toMatchObject({ version: 3, palletCount: 2, receivedPalletCount: 1, allocatedPalletCount: 0 })
expect(result.pallets[0].id).toBe('9007199254741001')
expect(operation.command === 'cancel' ? overseas.post : overseas.patch).not.toHaveBeenCalled()
})
it('propagates a truck command version conflict without fallback status writes', async () => {
overseas.post.mockResolvedValue({ code: 'A0409', message: '版本冲突', data: null, requestId: 'TRUCK-409' })
await expect(executeUsTruckCommand('TR-1', '10002', { command: 'complete', payload: { version: 2, idempotencyKey: 'original' } })).rejects.toMatchObject({ code: 'A0409', requestId: 'TRUCK-409' })
expect(overseas.post).toHaveBeenCalledTimes(1)
expect(overseas.patch).not.toHaveBeenCalled()
})
it('supports looseboard list/detail/create and PUT update', async () => {
const detail = { summary: looseboardSummary, originAddress: null, destinationAddress: null, remark: null, createdBy: '7', histories: [] }
overseas.get.mockResolvedValueOnce(envelope({ records: [looseboardSummary], page: 1, size: 20, total: 1, pages: 1 })).mockResolvedValueOnce(envelope(detail))
@@ -1,4 +1,5 @@
import type { PlatformProcurement } from '../procurementTypes'
import type { TruckOperation } from '../truckWorkflow'
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import type {
@@ -32,15 +33,15 @@ interface LooseboardDetailPayload {
function normalizePage<TPayload, TResult>(payload: PagePayload<TPayload>, normalize: (value: TPayload) => TResult): OverseasBusinessPage<TResult> {
return { records: (payload.records || []).map(normalize), page: Number(payload.page), size: Number(payload.size), total: Number(payload.total), pages: Number(payload.pages) }
}
function procurementInteger(value: unknown): number {
if ((typeof value !== 'number' && typeof value !== 'string') || !/^\d+$/.test(String(value)) || !Number.isSafeInteger(Number(value))) throw new Error('采购数据中的数量或版本无效,请刷新后重试')
function procurementInteger(value: unknown, business = '采购'): number {
if ((typeof value !== 'number' && typeof value !== 'string') || !/^\d+$/.test(String(value)) || !Number.isSafeInteger(Number(value))) throw new Error(`${business}数据中的数量或版本无效,请刷新后重试`)
return Number(value)
}
function procurementSummary(value: ProcurementSummaryPayload): OverseasPurchaseSummary {
return { ...value, version: procurementInteger(value.version), purchaseQuantity: procurementInteger(value.purchaseQuantity), id: String(value.id), purchaseWarehouseId: value.purchaseWarehouseId == null ? null : String(value.purchaseWarehouseId), budgetUnitPrice: value.budgetUnitPrice == null ? null : String(value.budgetUnitPrice) }
}
function truckSummary(value: TruckSummaryPayload): LocalTransportOrder {
return { ...value, id: String(value.id), totalWeightKg: String(value.totalWeightKg), totalVolumeCbm: String(value.totalVolumeCbm), budgetAmount: value.budgetAmount == null ? null : String(value.budgetAmount) }
return { ...value, id: String(value.id), customerId: String(value.customerId), version: procurementInteger(value.version, '卡车'), palletCount: procurementInteger(value.palletCount, '卡车'), packageCount: procurementInteger(value.packageCount, '卡车'), receivedPalletCount: procurementInteger(value.receivedPalletCount, '卡车'), allocatedPalletCount: procurementInteger(value.allocatedPalletCount, '卡车'), totalWeightKg: String(value.totalWeightKg), totalVolumeCbm: String(value.totalVolumeCbm), budgetAmount: value.budgetAmount == null ? null : String(value.budgetAmount) }
}
function truckDetail(value: TruckDetailPayload): LocalTransportDetail {
return { ...truckSummary(value.summary), originAddress: value.originAddress || null, destinationAddress: value.destinationAddress || null, remark: value.remark || null, createdBy: value.createdBy || null, pallets: (value.pallets || []).map((item) => ({ ...item, id: String(item.id), warehousePalletId: item.warehousePalletId == null ? null : String(item.warehousePalletId) })), histories: value.histories || [] }
@@ -130,6 +131,14 @@ export async function updateUsTruckOrder(orderNo: string, customerId: string, pa
const response = await http.patch<ApiEnvelope<TruckDetailPayload>, ApiEnvelope<TruckDetailPayload>>(`${truckBasePath}/${encodeURIComponent(orderNo)}`, payload, { params: { customerId } })
return truckDetail(unwrapApiResponse(response))
}
/** 正向履约使用具名命令,通用状态接口仅用于取消。 */
export async function executeUsTruckCommand(orderNo: string, customerId: string, operation: TruckOperation) {
const path = `${truckBasePath}/${encodeURIComponent(orderNo)}/${operation.command === 'cancel' ? 'status' : operation.command}`
const response = operation.command === 'cancel'
? await http.patch<ApiEnvelope<TruckDetailPayload>, ApiEnvelope<TruckDetailPayload>>(path, operation.payload, { params: { customerId } })
: await http.post<ApiEnvelope<TruckDetailPayload>, ApiEnvelope<TruckDetailPayload>>(path, operation.payload, { params: { customerId } })
return truckDetail(unwrapApiResponse(response))
}
export async function listLocalLooseboardOrders(query: LocalLooseboardQuery = {}) {
const response = await http.get<ApiEnvelope<PagePayload<LooseboardSummaryPayload>>, ApiEnvelope<PagePayload<LooseboardSummaryPayload>>>(looseboardBasePath, { params: { ...query, customerId: query.customerId || undefined, orderNo: query.orderNo?.trim() || undefined, routeKeyword: query.routeKeyword?.trim() || undefined, status: query.status || undefined } })
return normalizePage(unwrapApiResponse(response), looseboardSummary)