feat(procurement): add sample fulfillment operation workspace

This commit is contained in:
2026-10-10 02:19:39 +08:00
parent 2f23e30e20
commit 302cc50a4b
30 changed files with 2107 additions and 301 deletions
@@ -4,9 +4,9 @@ const overseas = vi.hoisted(() => ({ get: vi.fn(), patch: vi.fn(), post: vi.fn()
vi.mock('@/shared/api/http', async (importOriginal) => ({ ...(await importOriginal<typeof import('@/shared/api/http')>()), http: overseas }))
import {
createLocalLooseboardOrder, createUsTruckOrder, getLocalLooseboardOrder, getOverseasPurchase, getUsTruckOrder,
createLocalLooseboardOrder, createUsTruckOrder, getLocalLooseboardOrder, getOverseasPurchase, getOverseasPurchaseEditor, getUsTruckOrder,
listLocalLooseboardOrders, listOverseasPurchaseCustomerOptions, listOverseasPurchases,
listOverseasPurchaseWarehouseOptions, listUsTruckOrders, reviewOverseasPurchase, updateLocalLooseboardOrder, updateOverseasPurchase,
listOverseasPurchaseWarehouseOptions, listUsTruckOrders, updateLocalLooseboardOrder, updateOverseasPurchase,
updateUsTruckOrder,
} from './platformOverseasBusiness'
import type { UpdateLocalTransportOrderRequest } from '../types'
@@ -15,7 +15,7 @@ const envelope = <T>(data: T) => ({ code: '200', message: 'success', data })
const purchaseSummary = {
id: '9007199254740993', orderNo: 'OP-1', customerId: '10001', customerName: '客户一', purchaseCategory: '服装', productName: '外套',
productSpecification: null, purchaseQuantity: 10, budgetUnitPrice: 12.34, currency: 'USD', purchaseWarehouseId: '9007199254740995',
purchaseWarehouseName: '采购仓', inboundQuantity: 5, outboundQuantity: 0, inventoryQuantity: 5, status: 'PENDING_OUTBOUND', version: 4,
purchaseWarehouseName: '采购仓', status: 'WAIT_PACKING', version: 4,
createdAt: '2026-08-01T00:00:00', updatedAt: '2026-08-02T00:00:00',
}
const truckSummary = {
@@ -48,23 +48,24 @@ describe('platform overseas business API adapter', () => {
})
it('loads and patches procurement detail with three-state null payloads', async () => {
const detail = { summary: purchaseSummary, productUrl: null, histories: [{ operationType: 'UPDATE', fromStatus: 'APPROVED', toStatus: 'APPROVED', operatorId: '7', changeDetail: '{}', remark: null, createdAt: '2026-08-02T00:00:00' }] }
overseas.get.mockResolvedValue(envelope(detail))
expect((await getOverseasPurchase('OP/1', '10001')).histories[0]?.operationType).toBe('UPDATE')
const detail = { summary: purchaseSummary, productUrl: null, histories: [{ operationType: 'UPDATE', fromStatus: 'WAIT_PURCHASE', toStatus: 'WAIT_PURCHASE', operatorId: '7', changeDetail: '{}', remark: null, createdAt: '2026-08-02T00:00:00' }] }
overseas.get.mockResolvedValue(envelope({ customerView: { order: purchaseSummary }, reviewHistory: detail.histories, physical: { dispositions: [], receipts: [], cartons: [] }, fees: [], feeCorrections: [], controls: { notes: [], remaining: [], closures: [], closureItems: [] }, files: [], transport: { overseasCargo: [], domesticCargo: [], overseasLabels: null, domesticLabels: null, shipments: [] } }))
expect((await getOverseasPurchase('OP/1', '10001')).reviewHistory[0]?.operationType).toBe('UPDATE')
overseas.patch.mockResolvedValue(envelope(detail))
const payload = { version: 4, productUrl: null, estimatedArrivalAt: null }
const payload = { version: 4, remark: null }
await updateOverseasPurchase('OP-1', '10001', payload)
expect(overseas.patch).toHaveBeenCalledWith('/platform/v1/last-leg/overseas-procurements/OP-1', payload, { params: { customerId: '10001' } })
})
it('submits procurement review with version and idempotency data', async () => {
const detail = { summary: { ...purchaseSummary, status: 'APPROVED', version: 5 }, histories: [] }
overseas.post.mockResolvedValue(envelope(detail))
const payload = { version: 4, decision: 'APPROVE' as const, idempotencyKey: 'review-key', remark: '资料齐全' }
const result = await reviewOverseasPurchase('OP/1', '10001', payload)
expect(overseas.post).toHaveBeenCalledWith('/platform/v1/last-leg/overseas-procurements/OP%2F1/review', payload, { params: { customerId: '10001' } })
expect(result.status).toBe('APPROVED')
expect(result.version).toBe(5)
it('reads the aggregate editor context and strips obsolete mutation fields', async () => {
const detail = { customerView: { order: { orderNo: 'OP/1', customerId: '10001', productName: '外套', purchaseQuantity: 10, version: 4 } }, internalRemark: '内部' }
overseas.get.mockResolvedValue(envelope(detail))
expect(await getOverseasPurchaseEditor('OP/1', '10001')).toEqual(detail)
expect(overseas.get).toHaveBeenCalledWith('/platform/v1/last-leg/overseas-procurements/OP%2F1', { params: { customerId: '10001' } })
overseas.patch.mockResolvedValue(envelope({ summary: purchaseSummary }))
const obsolete = { version: 4, remark: '内部', inboundQuantity: 200, purchaseWarehouseId: 'other', productName: 'changed' }
await updateOverseasPurchase('OP/1', '10001', obsolete)
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 () => {
@@ -1,12 +1,13 @@
import type { PlatformProcurement } from '../procurementTypes'
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import type {
CreateLocalLooseboardOrderRequest, CreateLocalTransportOrderRequest, LocalLooseboardDetail, LocalLooseboardOrder,
LocalLooseboardQuery, LocalTransportDetail, LocalTransportOrder, LocalTransportPallet, LocalTransportQuery,
OverseasBusinessHistory, OverseasBusinessPage, OverseasPurchaseCustomerOption, OverseasPurchaseDetail,
OverseasBusinessHistory, OverseasBusinessPage, OverseasPurchaseCustomerOption,
OverseasPurchaseQuery, OverseasPurchaseSummary, OverseasPurchaseWarehouseOption, UpdateLocalLooseboardOrderRequest,
UpdateLocalTransportOrderRequest, UpdateOverseasPurchaseRequest, OverseasPurchaseOutboundDetail,
OverseasPurchaseFile, OverseasPurchaseFee, ReviewOverseasPurchaseRequest,
UpdateLocalTransportOrderRequest, UpdateOverseasPurchaseRequest,
OverseasPurchaseEditorDetail,
} from '../types'
const procurementBasePath = '/platform/v1/last-leg/overseas-procurements'
@@ -15,13 +16,7 @@ const looseboardBasePath = '/platform/v1/last-leg/local-looseboard-orders'
type Numeric = number | string
interface PagePayload<T> { records?: T[]; page: Numeric; size: Numeric; total: Numeric; pages: Numeric }
interface ProcurementSummaryPayload extends Omit<OverseasPurchaseSummary, 'id' | 'purchaseWarehouseId' | 'budgetUnitPrice'> { id: Numeric; purchaseWarehouseId?: Numeric | null; budgetUnitPrice?: Numeric | null }
interface ProcurementDetailPayload {
summary: ProcurementSummaryPayload; productUrl?: string | null; receivingAddress?: string | null
contactName?: string | null; contactPhone?: string | null; domesticCarrier?: string | null; domesticTrackingNo?: string | null
overseasCarrier?: string | null; overseasTrackingNo?: string | null; estimatedArrivalAt?: string | null; remark?: string | null
createdBy?: string | null; reviewedBy?: string | null; reviewRemark?: string | null; reviewedAt?: string | null
histories?: OverseasBusinessHistory[]; outboundDetails?: OverseasPurchaseOutboundDetail[]; files?: any[]; fees?: any[]
}
interface ProcurementDetailPayload { summary: ProcurementSummaryPayload }
interface TruckSummaryPayload extends Omit<LocalTransportOrder, 'id' | 'budgetAmount' | 'totalWeightKg' | 'totalVolumeCbm'> { id: Numeric; budgetAmount?: Numeric | null; totalWeightKg: Numeric; totalVolumeCbm: Numeric }
interface TruckPalletPayload extends Omit<LocalTransportPallet, 'id' | 'warehousePalletId'> { id: Numeric; warehousePalletId?: Numeric | null }
interface TruckDetailPayload {
@@ -40,9 +35,6 @@ function normalizePage<TPayload, TResult>(payload: PagePayload<TPayload>, normal
function procurementSummary(value: ProcurementSummaryPayload): OverseasPurchaseSummary {
return { ...value, id: String(value.id), purchaseWarehouseId: value.purchaseWarehouseId == null ? null : String(value.purchaseWarehouseId), budgetUnitPrice: value.budgetUnitPrice == null ? null : String(value.budgetUnitPrice) }
}
function procurementDetail(value: ProcurementDetailPayload): OverseasPurchaseDetail {
return { ...procurementSummary(value.summary), productUrl: value.productUrl || null, receivingAddress: value.receivingAddress || null, contactName: value.contactName || null, contactPhone: value.contactPhone || null, domesticCarrier: value.domesticCarrier || null, domesticTrackingNo: value.domesticTrackingNo || null, overseasCarrier: value.overseasCarrier || null, overseasTrackingNo: value.overseasTrackingNo || null, estimatedArrivalAt: value.estimatedArrivalAt || null, remark: value.remark || null, createdBy: value.createdBy || null, reviewedBy: value.reviewedBy || null, reviewRemark: value.reviewRemark || null, reviewedAt: value.reviewedAt || null, histories: value.histories || [], outboundDetails: (value.outboundDetails || []).map((item) => ({ ...item, id: String(item.id), quantity: Number(item.quantity) })), files: (value.files || []).map((item) => ({ ...item, id: String(item.id), fileId: String(item.fileId), sizeBytes: Number(item.sizeBytes) })), fees: (value.fees || []).map((item) => ({ ...item, id: String(item.id), voucherFileId: String(item.voucherFileId), amount: String(item.amount) })) }
}
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) }
}
@@ -69,25 +61,33 @@ export async function listOverseasPurchases(query: OverseasPurchaseQuery = {}) {
return normalizePage(unwrapApiResponse(response), procurementSummary)
}
export async function getOverseasPurchase(orderNo: string, customerId: string) {
const response = await http.get<ApiEnvelope<ProcurementDetailPayload>, ApiEnvelope<ProcurementDetailPayload>>(`${procurementBasePath}/${encodeURIComponent(orderNo)}`, { params: { customerId } })
return procurementDetail(unwrapApiResponse(response))
const response = await http.get<ApiEnvelope<PlatformProcurement>, ApiEnvelope<PlatformProcurement>>(`${procurementBasePath}/${encodeURIComponent(orderNo)}`, { params: { customerId } })
const detail = unwrapApiResponse(response)
if (detail.purchaseFacts?.actual_warehouse_id != null) detail.purchaseFacts.actual_warehouse_id = String(detail.purchaseFacts.actual_warehouse_id)
detail.physical.dispositions = detail.physical.dispositions.map(row => ({ ...row, id: String(row.id), source_carton_id: row.source_carton_id == null ? null : String(row.source_carton_id), receipt_id: row.receipt_id == null ? null : String(row.receipt_id), voucher_file_id: String(row.voucher_file_id) }))
detail.physical.receipts = detail.physical.receipts.map(row => ({ ...row, id: String(row.id), sourceCartonId: row.sourceCartonId == null ? null : String(row.sourceCartonId) }))
detail.physical.cartons = detail.physical.cartons.map(row => ({ ...row, id: String(row.id), sources: row.sources.map(source => ({ ...source, receiptId: String(source.receiptId) })) }))
detail.fees = detail.fees.map(fee => ({ ...fee, id: String(fee.id), parentFeeId: fee.parentFeeId == null ? null : String(fee.parentFeeId), sourceFeeId: fee.sourceFeeId == null ? null : String(fee.sourceFeeId), voucherFileId: fee.voucherFileId == null ? null : String(fee.voucherFileId), walletLedgerId: fee.walletLedgerId == null ? null : String(fee.walletLedgerId) }))
detail.transport.shipments = detail.transport.shipments.map(row => ({ ...row, id: String(row.id) }))
detail.controls.notes = detail.controls.notes.map(row => ({ ...row, id: String(row.id), target_id: row.target_id == null ? null : String(row.target_id), voucher_file_id: String(row.voucher_file_id), resolution_voucher_id: row.resolution_voucher_id == null ? null : String(row.resolution_voucher_id) }))
detail.controls.remaining = detail.controls.remaining.map(row => ({ ...row, sourceId: row.sourceId == null ? null : String(row.sourceId) }))
detail.controls.closures = detail.controls.closures.map(row => ({ ...row, id: String(row.id) }))
detail.controls.closureItems = detail.controls.closureItems.map(row => ({ ...row, id: String(row.id), closure_id: String(row.closure_id), source_id: row.source_id == null ? null : String(row.source_id), voucher_file_id: String(row.voucher_file_id) }))
detail.files = detail.files.map(file => ({ ...file, id: String(file.id), fileId: String(file.fileId) }))
for (const leg of ['overseasCargo', 'domesticCargo'] as const) detail.transport[leg] = detail.transport[leg].map(row => ({ ...row, cartonId: String(row.cartonId), packagingRecordId: row.packagingRecordId == null ? null : String(row.packagingRecordId), sources: row.sources.map(source => ({ ...source, receiptId: String(source.receiptId) })) }))
for (const labels of [detail.transport.overseasLabels, detail.transport.domesticLabels]) if (labels) {
labels.id = String(labels.id); labels.manifestId = String(labels.manifestId)
labels.labels = labels.labels.map(row => ({ ...row, cartonId: String(row.cartonId), fileId: String(row.fileId) }))
}
return detail
}
export async function getOverseasPurchaseEditor(orderNo: string, customerId: string) {
const response = await http.get<ApiEnvelope<OverseasPurchaseEditorDetail>, ApiEnvelope<OverseasPurchaseEditorDetail>>(`${procurementBasePath}/${encodeURIComponent(orderNo)}`, { params: { customerId } })
return unwrapApiResponse(response)
}
export async function updateOverseasPurchase(orderNo: string, customerId: string, payload: UpdateOverseasPurchaseRequest) {
const response = await http.patch<ApiEnvelope<ProcurementDetailPayload>, ApiEnvelope<ProcurementDetailPayload>>(`${procurementBasePath}/${encodeURIComponent(orderNo)}`, payload, { params: { customerId } })
return procurementDetail(unwrapApiResponse(response))
}
export async function reviewOverseasPurchase(orderNo: string, customerId: string, payload: ReviewOverseasPurchaseRequest) {
const response = await http.post<ApiEnvelope<ProcurementDetailPayload>, ApiEnvelope<ProcurementDetailPayload>>(`${procurementBasePath}/${encodeURIComponent(orderNo)}/review`, payload, { params: { customerId } })
return procurementDetail(unwrapApiResponse(response))
}
export async function uploadOverseasPurchaseFile(orderNo: string, customerId: string, category: 'RECEIPT' | 'VOUCHER', file: File) {
const body = new FormData(); body.append('file', file)
const response = await http.post<ApiEnvelope<OverseasPurchaseFile>, ApiEnvelope<OverseasPurchaseFile>>(`${procurementBasePath}/${encodeURIComponent(orderNo)}/files/${category}`, body, { params: { customerId } })
return unwrapApiResponse(response)
}
export async function createOverseasPurchaseFee(orderNo: string, customerId: string, payload: { feeItemCode: string; feeItemName: string; amount: number; voucherFileId: string; remark?: string }) {
const response = await http.post<ApiEnvelope<OverseasPurchaseFee>, ApiEnvelope<OverseasPurchaseFee>>(`${procurementBasePath}/${encodeURIComponent(orderNo)}/fees`, payload, { params: { customerId } })
return unwrapApiResponse(response)
const response = await http.patch<ApiEnvelope<ProcurementDetailPayload>, ApiEnvelope<ProcurementDetailPayload>>(`${procurementBasePath}/${encodeURIComponent(orderNo)}`, { version: payload.version, remark: payload.remark }, { params: { customerId } })
return procurementSummary(unwrapApiResponse(response).summary)
}
export async function listUsTruckOrders(query: LocalTransportQuery = {}) {
const response = await http.get<ApiEnvelope<PagePayload<TruckSummaryPayload>>, ApiEnvelope<PagePayload<TruckSummaryPayload>>>(truckBasePath, { params: { ...query, orderNo: query.orderNo?.trim() || undefined, routeKeyword: query.routeKeyword?.trim() || undefined, status: query.status || undefined, transportType: query.transportType || undefined, customerId: query.customerId || undefined } })
@@ -0,0 +1,58 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const api = vi.hoisted(() => ({ get: vi.fn(), post: vi.fn() }))
vi.mock('@/shared/api/http', async importOriginal => ({ ...(await importOriginal<typeof import('@/shared/api/http')>()), http: api }))
import { submitProcurementCommand, uploadProcurementFile, downloadProcurementFile, type ProcurementCommand } from './procurementCommands'
const envelope = <T>(data: T) => ({ code: '200', message: 'success', data })
beforeEach(() => vi.clearAllMocks())
describe('procurement node HTTP contract', () => {
it.each<ProcurementCommand>(['review', 'purchase', 'purchase/confirm', 'budget-authorizations', 'planned-warehouse', 'cancel', 'overseas-receipts', 'domestic-receipts', 'overseas-receipts/complete', 'domestic-receipts/complete', 'packing', 'packing/confirm', 'packing/withdraw', 'dispositions', 'labels', 'international-estimate', 'labels/apply', 'domestic-repack', 'overseas-shipment', 'customer-shipment', 'charges/confirm', 'control-notes', 'actual-warehouse/correct', 'exception-close'])('sends %s to the scoped order endpoint without interpreting money success', async command => {
api.post.mockResolvedValue(envelope({ status: 'WAIT_OVERSEAS_RECEIPT', purchase: { walletStatus: 'FREEZE_PENDING' } }))
const payload = { version: 3, idempotencyKey: 'same-command', reason: '已核对', amount: '9007199254740993.01' }
await submitProcurementCommand('OP/1', '10001', command, payload)
expect(api.post).toHaveBeenCalledWith(`/platform/v1/last-leg/overseas-procurements/OP%2F1/${command}`, payload, { params: { customerId: '10001' } })
})
it('posts additional expenses without a fee route id or inherited correction fields', async () => {
api.post.mockResolvedValue(envelope({}))
const payload = { version: 3, idempotencyKey: 'additional', reason: 'invoice', name: 'Storage', amount: '3.25', currency: 'USD' as const, businessDate: '2020-01-03', voucherFileId: '501' }
await submitProcurementCommand('OP/1', '10001', 'fees/additional', { ...payload, feeId: '11', targetOriginalAmount: '99' })
expect(api.post).toHaveBeenCalledWith('/platform/v1/last-leg/overseas-procurements/OP%2F1/fees/additional', payload, { params: { customerId: '10001' } })
})
it('resolves the exact order-owned blocker and does not send the internal route reference as a mutable field', async () => {
api.post.mockResolvedValue(envelope({}))
await submitProcurementCommand('OP/1', '10001', 'control-notes/resolve', { version: 3, idempotencyKey: 'resolve-one', reason: '核对完成', noteId: '71', voucherFileId: '501' })
expect(api.post).toHaveBeenCalledWith('/platform/v1/last-leg/overseas-procurements/OP%2F1/control-notes/71/resolve', { version: 3, idempotencyKey: 'resolve-one', reason: '核对完成', voucherFileId: '501' }, { params: { customerId: '10001' } })
})
it('uses the exact fee endpoint and sends only fields belonging to that financial operation', async () => {
api.post.mockResolvedValue(envelope({}))
const base = { version: 3, idempotencyKey: 'finance-one', reason: '核对', feeId: '11' }
await submitProcurementCommand('OP/1', '10001', 'fees/correct-target', { ...base, targetOriginalAmount: '12.00', voucherFileId: '501', amount: 'unrelated' })
expect(api.post).toHaveBeenLastCalledWith('/platform/v1/last-leg/overseas-procurements/OP%2F1/fees/11/correct-target', { version: 3, idempotencyKey: 'finance-one', reason: '核对', targetOriginalAmount: '12.00', voucherFileId: '501' }, { params: { customerId: '10001' } })
await submitProcurementCommand('OP/1', '10001', 'fees/replace-failed', { ...base, targetOriginalAmount: '12.00', voucherFileId: '501' })
expect(api.post).toHaveBeenLastCalledWith('/platform/v1/last-leg/overseas-procurements/OP%2F1/fees/11/replace-failed', { version: 3, idempotencyKey: 'finance-one', reason: '核对', targetOriginalAmount: '12.00', voucherFileId: '501' }, { params: { customerId: '10001' } })
await submitProcurementCommand('OP/1', '10001', 'fees/refund', { ...base, amount: '1.01', voucherFileId: '501' })
expect(api.post).toHaveBeenLastCalledWith('/platform/v1/last-leg/overseas-procurements/OP%2F1/fees/11/refund', { version: 3, idempotencyKey: 'finance-one', reason: '核对', amount: '1.01', voucherFileId: '501' }, { params: { customerId: '10001' } })
for (const command of ['fees/retry', 'fees/terminate', 'fees/release-estimate'] as const) {
await submitProcurementCommand('OP/1', '10001', command, base)
expect(api.post).toHaveBeenLastCalledWith(`/platform/v1/last-leg/overseas-procurements/OP%2F1/fees/11/${command.slice(5)}`, { version: 3, idempotencyKey: 'finance-one', reason: '核对' }, { params: { customerId: '10001' } })
}
})
it('targets an original receipt and nests only the receipt contract plus correction evidence', async () => {
api.post.mockResolvedValue(envelope({}))
const receipt = { version: 3, idempotencyKey: 'correct-one', reason: '实收复核', leg: 'OVERSEAS' as const, warehouseId: '20', goodQuantity: 5, damagedQuantity: 1, extraQuantity: 0, receivedAt: '2020-01-02T10:00:00' }
await submitProcurementCommand('OP/1', '10001', 'receipts/correct', { ...receipt, receiptId: '11', voucherFileId: '901' })
expect(api.post).toHaveBeenCalledWith('/platform/v1/last-leg/overseas-procurements/OP%2F1/receipts/11/correct', { receipt: { ...receipt, sourceCartonId: undefined }, voucherFileId: '901' }, { params: { customerId: '10001' } })
await submitProcurementCommand('OP/1', '10001', 'dispositions/revoke', { version: 3, idempotencyKey: 'revoke-one', reason: '撤销错误处置', dispositionId: '71', voucherFileId: '901' })
expect(api.post).toHaveBeenLastCalledWith('/platform/v1/last-leg/overseas-procurements/OP%2F1/dispositions/71/revoke', { version: 3, idempotencyKey: 'revoke-one', reason: '撤销错误处置', voucherFileId: '901' }, { params: { customerId: '10001' } })
})
it('uses purchase-purpose multipart evidence and normalizes file references', async () => {
api.post.mockResolvedValue(envelope({ id: 11, fileId: '9007199254740993', category: 'PURCHASE', fileName: 'purchase.pdf' }))
const file = new File(['pdf'], 'purchase.pdf', { type: 'application/pdf' })
expect(await uploadProcurementFile('OP/1', '10001', 'PURCHASE', file)).toMatchObject({ id: '11', fileId: '9007199254740993' })
const call = api.post.mock.calls[0]; expect(call[0]).toBe('/platform/v1/last-leg/overseas-procurements/OP%2F1/files/PURCHASE'); expect(call[1].get('file')).toBe(file); expect(call[2]).toEqual({ params: { customerId: '10001' } })
})
it('downloads binary files through the authenticated order boundary', async () => {
const blob = new Blob(['pdf'], { type: 'application/pdf' }); api.get.mockResolvedValue(blob)
expect(await downloadProcurementFile('OP/1', '10001', '901')).toBe(blob)
expect(api.get).toHaveBeenCalledWith('/platform/v1/last-leg/overseas-procurements/OP%2F1/files/901/content', { params: { customerId: '10001' }, responseType: 'blob' })
})
})
@@ -0,0 +1,62 @@
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import type { ProcurementFile, ProcurementFileCategory, ProcurementRemainingKind } from '../procurementTypes'
export type ProcurementCommand = 'review' | 'purchase' | 'purchase/confirm' | 'budget-authorizations' | 'planned-warehouse' | 'cancel' | 'overseas-receipts' | 'domestic-receipts' | 'overseas-receipts/complete' | 'domestic-receipts/complete' | 'packing' | 'packing/confirm' | 'packing/withdraw' | 'dispositions' | 'receipts/correct' | 'dispositions/revoke' | 'labels' | 'international-estimate' | 'labels/apply' | 'domestic-repack' | 'overseas-shipment' | 'customer-shipment' | 'charges/confirm' | 'fees/retry' | 'fees/terminate' | 'fees/release-estimate' | 'fees/correct-target' | 'fees/refund' | 'fees/replace-failed' | 'fees/additional' | 'control-notes' | 'control-notes/resolve' | 'actual-warehouse/correct' | 'exception-close'
export interface ProcurementCommandBody {
name?: string
targetType?: 'PURCHASE' | 'WAREHOUSE' | 'RECEIPT' | 'SHIPMENT'; targetId?: string; blocking?: boolean; clientSummary?: string; noteId?: string
resolutions?: ProcurementResolution[]; customerVoucherFileId?: string; financialConclusion?: string
feeId?: string; targetOriginalAmount?: string; internationalFreight?: ProcurementFeeSelection; duty?: ProcurementFeeSelection; domesticFreight?: ProcurementFeeSelection
otherDetermination?: 'NOT_APPLICABLE' | 'ITEMIZED'; otherFees?: { code: string; name: string; selection: ProcurementFeeSelection }[]
version: number; idempotencyKey: string; reason?: string; decision?: 'APPROVE' | 'REJECT' | 'ACCEPT_SHORTAGE' | 'DISPOSE_DAMAGED' | 'RETURN_EXTRA'; remark?: string
purchaseWarehouseId?: string; warehouseId?: string; productName?: string; specification?: string
leg?: 'OVERSEAS' | 'DOMESTIC'; sourceCartonId?: string; receiptId?: string; dispositionId?: string
goodQuantity?: number; damagedQuantity?: number; extraQuantity?: number; receivedAt?: string
labelVersion?: number; receiver?: { name: string; phone: string; address: string }; addressVoucherFileId?: string
labels?: { cartonId: string; fileId: string; page: number; carrier: string; trackingNo: string }[]
cartonIds?: string[]; businessDate?: string; appliedAt?: string; shippedAt?: string; cartonId?: string
lengthCm?: string; widthCm?: string; heightCm?: string; grossWeightKg?: string
kind?: 'SHORTAGE' | 'DAMAGED' | 'EXTRA'; packingVersion?: number; cartons?: ProcurementPackingCarton[]
quantity?: number; amount?: string; currency?: 'CNY' | 'USD'; purchasedAt?: string; estimatedArrivalAt?: string; voucherFileId?: string
}
export interface ProcurementResolution { kind: ProcurementRemainingKind; sourceId: string | null; quantity: number; decision: string; destination: string; voucherFileId: string }
export interface ProcurementChargeAmount { determination: 'POSITIVE' | 'ZERO' | 'NOT_APPLICABLE'; amount: string; currency: 'CNY' | 'USD'; businessDate: string; voucherFileId?: string; reason: string }
export interface ProcurementFeeSelection { feeId?: string; value?: ProcurementChargeAmount }
export interface ProcurementPackingCarton { cartonNo: string; lengthCm: string; widthCm: string; heightCm: string; grossWeightKg: string; sources: { receiptId: string; quantity: number }[] }
const base = '/platform/v1/last-leg/overseas-procurements'
export async function submitProcurementCommand(orderNo: string, customerId: string, command: ProcurementCommand, payload: ProcurementCommandBody) {
let path: string = command; let body: unknown = payload
if (command === 'receipts/correct') {
if (!payload.receiptId || !/^[1-9]\d*$/.test(payload.receiptId)) throw new Error('缺少有效实收来源')
path = `receipts/${payload.receiptId}/correct`
body = { receipt: { version: payload.version, idempotencyKey: payload.idempotencyKey, reason: payload.reason, leg: payload.leg, warehouseId: payload.warehouseId, sourceCartonId: payload.sourceCartonId, goodQuantity: payload.goodQuantity, damagedQuantity: payload.damagedQuantity, extraQuantity: payload.extraQuantity, receivedAt: payload.receivedAt }, voucherFileId: payload.voucherFileId }
} else if (command === 'dispositions/revoke') {
if (!payload.dispositionId || !/^[1-9]\d*$/.test(payload.dispositionId)) throw new Error('缺少有效差异处置来源')
path = `dispositions/${payload.dispositionId}/revoke`
body = { version: payload.version, idempotencyKey: payload.idempotencyKey, reason: payload.reason, voucherFileId: payload.voucherFileId }
}
if (command === 'control-notes/resolve') {
if (!payload.noteId || !/^[1-9]\d*$/.test(payload.noteId)) throw new Error('缺少有效阻断异常来源')
path = `control-notes/${payload.noteId}/resolve`
body = { version: payload.version, idempotencyKey: payload.idempotencyKey, reason: payload.reason, voucherFileId: payload.voucherFileId }
}
if (command === 'fees/additional') body = { version: payload.version, idempotencyKey: payload.idempotencyKey, name: payload.name, amount: payload.amount, currency: payload.currency, businessDate: payload.businessDate, voucherFileId: payload.voucherFileId, reason: payload.reason }
if (command.startsWith('fees/') && command !== 'fees/additional') {
if (!payload.feeId || !/^[1-9]\d*$/.test(payload.feeId)) throw new Error('缺少有效费用来源')
path = `fees/${payload.feeId}/${command.slice(5)}`
body = { version: payload.version, idempotencyKey: payload.idempotencyKey, reason: payload.reason,
...(['fees/correct-target', 'fees/replace-failed'].includes(command) ? { targetOriginalAmount: payload.targetOriginalAmount, voucherFileId: payload.voucherFileId } : {}),
...(command === 'fees/refund' ? { amount: payload.amount, voucherFileId: payload.voucherFileId } : {}) }
}
const response = await http.post<ApiEnvelope<unknown>, ApiEnvelope<unknown>>(`${base}/${encodeURIComponent(orderNo)}/${path}`, body, { params: { customerId } })
unwrapApiResponse(response)
}
export async function uploadProcurementFile(orderNo: string, customerId: string, category: ProcurementFileCategory, file: File) {
const data = new FormData(); data.append('file', file)
const response = await http.post<ApiEnvelope<ProcurementFile>, ApiEnvelope<ProcurementFile>>(`${base}/${encodeURIComponent(orderNo)}/files/${category}`, data, { params: { customerId } })
const result = unwrapApiResponse(response)
return { ...result, id: String(result.id), fileId: String(result.fileId) }
}
export async function downloadProcurementFile(orderNo: string, customerId: string, fileId: string) {
return http.get<Blob, Blob>(`${base}/${encodeURIComponent(orderNo)}/files/${encodeURIComponent(fileId)}/content`, { params: { customerId }, responseType: 'blob' })
}