Files
client-web/src/modules/first-leg/api/firstLeg.ts
T

591 lines
18 KiB
TypeScript

import { ApiError, apiBlobRequest, apiBlobResponseRequest, apiRequest } from '@/api/http'
export type ApiFirstLegStatus = 'DRAFT' | 'SUBMITTED' | 'APPROVED' | 'ACCEPTED' | 'REJECTED' | 'CANCELLED' | 'PROCESSING' | 'COMPLETED'
export type ApiTransportMode = 'SEA' | 'AIR' | 'RAIL' | 'TRUCK'
export type ApiPickupMethod = 'SELF_EXPRESS' | 'PICKUP' | 'SELF_TRUCK'
export type ApiTaxMode = 'BUY_DECLARATION' | 'TAX_REFUND'
export type ApiContainerMode = 'FULL_CONTAINER' | 'LCL'
export type ApiDestinationType = 'FBA' | 'OVERSEAS' | 'PRIVATE'
export type ApiPricingTaxMode = 'TAX_INCLUDED' | 'TAX_EXCLUDED'
export type ApiPriceModel = 'FBA_WAREHOUSE_RATE' | 'REGION_WEIGHT_TIER'
export type CustomerOrderAddressType = 'PICKUP' | 'PRIVATE_DESTINATION'
export type MerchantBatchImportMode = 'ATOMIC' | 'PARTIAL'
export interface PageResult<T> {
records: T[]
page: number
size: number
total: number
pages: number
}
export type MerchantWarehouseType = 'DOMESTIC' | 'FBA' | 'OVERSEAS'
export interface MerchantWarehouseQuery {
page?: number
size?: number
status?: 'ACTIVE' | 'INACTIVE'
warehouseType?: MerchantWarehouseType
}
export interface MerchantWarehouse {
id: number | string
warehouseCode: string
warehouseName: string
warehouseType: string
countryCode: string
timezone: string | null
contactName: string | null
contactPhone: string | null
contactEmail: string | null
status: 'ACTIVE' | 'INACTIVE'
remark: string | null
updatedAt: string | null
}
export interface MerchantWarehouseAddress {
countryCode: string
stateProvince: string | null
city: string | null
district: string | null
postalCode: string | null
addressLine1: string
addressLine2: string | null
}
export interface MerchantWarehouseCapability {
capabilityCode: string
capabilityName: string
enabled: boolean
cutoffTime: string | null
description: string | null
}
export interface MerchantWarehouseDetail {
warehouse: MerchantWarehouse
addresses: MerchantWarehouseAddress[]
capabilities: MerchantWarehouseCapability[]
}
export interface FirstLegOrderPartyPayload {
companyName: string
contactName: string
phone: string
email?: string
countryCode: string
province?: string
city: string
district?: string
addressLine: string
postalCode?: string
}
export interface FirstLegOrderCargoPayload {
cargoName: string
hsCode?: string
packageType: string
packageCount: number
grossWeightKg: number
volumeCbm: number
lengthCm?: number
widthCm?: number
heightCm?: number
declaredValue?: number
currency?: string
dangerousGoods?: boolean
woodenProduct: boolean
cargoRemark?: string
skuNo?: string
upcNo?: string
cargoNameEn?: string
customsHsCode?: string
unitsPerPackage?: number
totalQuantity?: number
netWeightKg?: number
unitDeclaredValue?: number
material?: string
usage?: string
model?: string
brand?: string
productLengthCm?: number
productWidthCm?: number
productHeightCm?: number
shipmentId?: string
referenceId?: string
productImageFileId?: number | string
packingGroupNo?: string
}
export interface ProductImageUploadResult {
fileId: number | string
fileName: string
contentType: string
sizeBytes: number
}
export interface FirstLegOrderSaveRequest {
inboundCode?: string
customerName: string
fulfillmentType?: 'STANDARD' | 'ONE_CLICK_DROPSHIP'
lxCode?: string
transportMode: ApiTransportMode
tradeTerm?: string
destinationCountryCode: string
destinationWarehouseId?: number | string
destinationWarehouseName: string
expectedShipDate?: string
customerRemark?: string
senderAddressId?: number | string
privateDestinationAddressId?: number | string
cargoes: FirstLegOrderCargoPayload[]
pickupMethod: ApiPickupMethod
expressTrackingNo?: string
receivingWarehouseId: number | string
receivingWarehouseName: string
receivingWarehouseAddress?: string | null
receivingTime?: string | null
channelCode: string
channelName: string
pricingTaxMode: ApiPricingTaxMode
taxMode: ApiTaxMode
containerMode?: ApiContainerMode
customsInspection: boolean | null
destinationType: ApiDestinationType
amazonWarehouseCode?: string
shipmentId?: string
referenceId?: string
}
export interface CustomerOrderAddressSaveRequest {
addressType: CustomerOrderAddressType
addressName: string
companyName: string
contactName: string
phone: string
email?: string
countryCode: string
province?: string
city: string
district?: string
addressLine: string
postalCode?: string
defaultAddress: boolean
}
export interface CustomerOrderAddress extends CustomerOrderAddressSaveRequest {
id: number | string
}
export interface InboundCodePolicy {
mode: 'AUTO' | 'LEGACY_MANUAL'
manualEntryRequired: boolean
}
export interface MerchantBatchDraftItem {
clientRowId: string
draft: FirstLegOrderSaveRequest
}
export interface MerchantBatchDraftResult {
index: number
clientRowId: string
success: boolean
orderId: number | string | null
orderNo: string | null
inboundCode: string | null
errorCode: string | null
errorMessage: string | null
}
export interface MerchantBatchDraftResponse {
total: number
succeeded: number
failed: number
items: MerchantBatchDraftResult[]
}
export interface MerchantBatchImportResponse {
batchNo: string
mode: MerchantBatchImportMode
sourceFileId: number | string
errorReportFileId: number | string | null
result: MerchantBatchDraftResponse
}
export interface MerchantFirstLegOrder {
id?: number | string
orderId?: number | string
orderNo: string | null
customerId: number | string | null
customerName: string
fulfillmentType?: 'STANDARD' | 'ONE_CLICK_DROPSHIP' | null
lxCode?: string | null
status: ApiFirstLegStatus
transportMode?: ApiTransportMode
tradeTerm: string | null
destinationCountryCode: string
destinationWarehouseId: number | string | null
destinationWarehouseName: string
expectedShipDate: string | null
customerRemark: string | null
pickupMethod: ApiPickupMethod
receivingWarehouseId: number | string
receivingWarehouseName: string
receivingWarehouseAddress?: string | null
receivingTime?: string | null
channelCode: string
channelName: string
pricingTaxMode?: ApiPricingTaxMode | null
carrierId?: number | string | null
carrierName?: string | null
taxMode: ApiTaxMode
containerMode: ApiContainerMode | null
customsInspection: boolean | null
destinationType: ApiDestinationType
amazonWarehouseCode: string | null
shipmentId: string | null
referenceId: string | null
expressCompany?: string | null
expressTrackingNo?: string | null
skuNos?: string[] | null
totalPackageCount?: number | null
totalGrossWeightKg?: number | null
totalNetWeightKg?: number | null
totalVolumeCbm?: number | null
totalQuantity?: number | null
chargeableWeightKg?: number | null
chargeableVolumeCbm?: number | null
inboundCode?: string | null
inboundedAt?: string | null
businessDate?: string | null
internationalOrderNo?: string | null
submittedAt?: string | null
completedAt?: string | null
version: number
createdAt: string
updatedAt: string
}
export interface MerchantFirstLegOrderRow {
orderId: number | string
orderNo: string | null
packageCount: number | null
weightKg: number | null
inboundId: number | string | null
inboundCode: string | null
channelName: string | null
destinationType: ApiDestinationType | null
pickupMethod: ApiPickupMethod | null
customerName: string | null
receivingCompanyName: string | null
specificationModel: string | null
status: ApiFirstLegStatus
totalQuantity: number | null
totalGrossWeightKg: number | null
totalNetWeightKg: number | null
totalVolumeCbm: number | null
trackingNo: string | null
internationalOrderNo: string | null
referenceId: string | null
remark: string | null
destinationWarehouseName: string | null
expressCompany: string | null
skuNos: string | string[] | null
chargeableWeightKg: number | null
chargeableVolumeCbm: number | null
billingUnitPrice: number | null
inboundedAt: string | null
businessDate: string | null
shipmentId: string | null
fulfillmentType?: 'STANDARD' | 'ONE_CLICK_DROPSHIP' | null
lxCode?: string | null
createdAt: string
cargoes?: Array<{ skuNo?: string | null }>
}
export interface MerchantFirstLegOrderCargo extends FirstLegOrderCargoPayload {
id: number | string
itemNo: number
productUsage?: string
}
export interface MerchantFirstLegOrderParty extends FirstLegOrderPartyPayload {
id: number | string
partyType: 'SENDER' | 'RECEIVER' | string
}
export interface MerchantOrderStatusHistory {
id: number | string
fromStatus: ApiFirstLegStatus | null
toStatus: ApiFirstLegStatus
action: string
reason: string | null
operatorName: string | null
occurredAt: string
}
export interface MerchantFirstLegOrderDetail {
order: MerchantFirstLegOrder
cargoes: MerchantFirstLegOrderCargo[]
parties: MerchantFirstLegOrderParty[]
internalNotes: []
statusHistory: MerchantOrderStatusHistory[]
}
export function uploadMerchantProductImage(file: File) {
const data = new FormData()
data.append('file', file)
return apiRequest<ProductImageUploadResult>({ method: 'POST', url: '/merchant/v1/files/product-images', data })
}
export function downloadMerchantProductImage(fileId: number | string) {
return apiBlobRequest({ method: 'GET', url: `/merchant/v1/files/product-images/${fileId}` })
}
export function downloadMerchantFirstLegBatchImportErrorReport(batchNo: string, fileId: number | string) {
return apiBlobRequest({
method: 'GET',
url: `/merchant/v1/files/order-batch-imports/${encodeURIComponent(batchNo)}/error-reports/${encodeURIComponent(String(fileId))}`,
})
}
export async function downloadMerchantInboundCartonLabels(inboundId: number | string) {
return downloadMerchantCartonLabelPdf(
`/merchant/v1/first-leg/inbounds/${encodeURIComponent(String(inboundId))}/carton-label-pdf`,
)
}
export async function downloadMerchantOrderCartonLabels(orderId: number | string) {
return downloadMerchantCartonLabelPdf(
`/merchant/v1/first-leg/orders/${encodeURIComponent(String(orderId))}/carton-label-pdf`,
)
}
export async function downloadMerchantOrderCartonLabelsBatch(orderIds: Array<number | string>) {
const response = await apiBlobResponseRequest({
method: 'POST',
url: '/merchant/v1/first-leg/orders/carton-label-pdf',
data: orderIds.map(String),
timeout: 120_000,
})
await requirePdfBlob(response.blob)
return {
blob: response.blob,
failureCount: Number(response.headers['x-carton-label-failure-count'] ?? 0),
failures: String(response.headers['x-carton-label-failures'] ?? ''),
}
}
async function downloadMerchantCartonLabelPdf(url: string) {
const blob = await apiBlobRequest({
method: 'POST',
url,
data: null,
timeout: 60_000,
})
await requirePdfBlob(blob)
return blob
}
async function requirePdfBlob(blob: Blob) {
const signature = new TextDecoder().decode(await blob.slice(0, 5).arrayBuffer())
if (signature === '%PDF-') return
const text = await blob.text()
try {
const error = JSON.parse(text) as { code?: unknown; message?: unknown; traceId?: unknown }
if (typeof error.message === 'string') {
throw new ApiError(
error.message || '箱唛生成失败',
typeof error.code === 'string' ? error.code : 'INVALID_PDF',
undefined,
undefined,
typeof error.traceId === 'string' ? error.traceId : null,
)
}
} catch (error) {
if (error instanceof ApiError) throw error
}
throw new ApiError('箱唛服务未返回有效的 PDF 文件', 'INVALID_PDF')
}
export interface FirstLegOrderQuery {
keyword?: string
status?: ApiFirstLegStatus
customerId?: number | string
destinationWarehouseId?: number | string
createdFrom?: string
createdTo?: string
page?: number
size?: number
}
export interface MerchantPricingChannel {
priceBookVersionId: number | string
channelCode: string
channelName: string
priceModel: ApiPriceModel
pricingTaxModes: ApiPricingTaxMode[]
estimatedDepartureDate: string | null
estimatedDepartureText: string | null
latestWarehouseEntry: string | null
referenceTransit: string | null
surchargeDescription: string | null
remark: string | null
}
export interface MerchantPriceEstimateRequest {
originWarehouseId: number | string
channelCode: string
destinationCode?: string
destinationType: Exclude<ApiDestinationType, 'PRIVATE'>
destinationWarehouseId?: number | string
destinationName?: string
pricingTaxMode: ApiPricingTaxMode
chargeMode?: 'KG' | 'CBM'
actualWeightKg: number
actualVolumeCbm: number
}
export interface MerchantPriceEstimate {
priceBookVersionId: number | string
channelCode: string
priceModel: ApiPriceModel
pricingTaxMode: ApiPricingTaxMode
chargeableQuantity: number
chargeUnit: string
unitPrice: number
estimatedAmount: number
currency: string
disclaimer: string
}
export function getMerchantWarehouses(params: MerchantWarehouseQuery = {}) {
const request = apiRequest<PageResult<MerchantWarehouse>>({
method: 'GET',
url: '/merchant/v1/warehouses',
params: { page: 1, size: 200, status: 'ACTIVE', ...params },
})
if (params.warehouseType !== 'OVERSEAS') return request
return request.then(async (result) => {
const usableRecords = result.records.filter((item) => item.status === 'ACTIVE' && item.warehouseType === 'OVERSEAS')
if (usableRecords.length > 0) return { ...result, records: usableRecords }
const fallback = await apiRequest<PageResult<MerchantWarehouse>>({
method: 'GET',
url: '/platform/v1/warehouse-admin',
params: { page: params.page || 1, size: params.size || 200, tenantId: 0, warehouseType: 'OVERSEAS', status: 'ACTIVE' },
})
return {
...fallback,
records: fallback.records.filter((item) => item.status === 'ACTIVE' && item.warehouseType === 'OVERSEAS'),
}
})
}
export function getMerchantWarehouseDetail(id: number | string) {
return apiRequest<MerchantWarehouseDetail>({ method: 'GET', url: `/merchant/v1/warehouses/${id}` }).catch(async () => {
const fallback = await apiRequest<MerchantWarehouseDetail>({ method: 'GET', url: `/platform/v1/warehouse-admin/${encodeURIComponent(id)}`, params: { tenantId: 0 } })
return fallback
})
}
export function getMerchantPricingChannels(originWarehouseId: number | string) {
return apiRequest<MerchantPricingChannel[]>({
method: 'GET',
url: '/merchant/v1/first-leg/pricing/channels',
params: { originWarehouseId },
})
}
export function estimateMerchantFirstLegPrice(payload: MerchantPriceEstimateRequest) {
return apiRequest<MerchantPriceEstimate>({
method: 'POST',
url: '/merchant/v1/first-leg/pricing/estimate',
data: payload,
})
}
export function getMerchantFirstLegOrders(params: FirstLegOrderQuery = {}) {
return apiRequest<PageResult<MerchantFirstLegOrderRow>>({ method: 'GET', url: '/merchant/v1/orders/first-leg', params: { page: 1, size: 200, ...params } })
}
export function getMerchantFirstLegOrder(id: number | string) {
return apiRequest<MerchantFirstLegOrderDetail>({ method: 'GET', url: `/merchant/v1/orders/first-leg/${id}` })
}
export function getMerchantInboundCodePolicy() {
return apiRequest<InboundCodePolicy>({ method: 'GET', url: '/merchant/v1/orders/first-leg/inbound-code-policy' })
}
export function getMerchantOrderAddresses(addressType?: CustomerOrderAddressType) {
return apiRequest<CustomerOrderAddress[]>({ method: 'GET', url: '/merchant/v1/orders/first-leg/addresses', params: { addressType } })
}
export function createMerchantOrderAddress(payload: CustomerOrderAddressSaveRequest) {
return apiRequest<number | string>({ method: 'POST', url: '/merchant/v1/orders/first-leg/addresses', data: payload })
}
export function updateMerchantOrderAddress(id: number | string, payload: CustomerOrderAddressSaveRequest) {
return apiRequest<void>({ method: 'PUT', url: `/merchant/v1/orders/first-leg/addresses/${id}`, data: payload })
}
export function disableMerchantOrderAddress(id: number | string) {
return apiRequest<void>({ method: 'DELETE', url: `/merchant/v1/orders/first-leg/addresses/${id}` })
}
export function downloadMerchantFirstLegBatchImportTemplate() {
return apiBlobRequest({ method: 'GET', url: '/merchant/v1/orders/first-leg/batch-imports/template' })
}
export function importMerchantFirstLegOrders(file: File, mode: MerchantBatchImportMode, idempotencyKey: string) {
const data = new FormData()
data.append('file', file)
return apiRequest<MerchantBatchImportResponse>({
method: 'POST',
url: '/merchant/v1/orders/first-leg/batch-imports',
params: { mode },
headers: { 'Idempotency-Key': idempotencyKey },
data,
})
}
export function createMerchantFirstLegOrder(payload: FirstLegOrderSaveRequest) {
return apiRequest<number | string>({
method: 'POST',
url: '/merchant/v1/orders/first-leg',
data: payload,
headers: { 'Idempotency-Key': createIdempotencyKey('first-leg-create') },
})
}
export function createMerchantFirstLegOrderBatch(items: MerchantBatchDraftItem[], idempotencyKey: string) {
return apiRequest<MerchantBatchDraftResponse>({
method: 'POST',
url: '/merchant/v1/orders/first-leg/batch',
data: items,
headers: { 'Idempotency-Key': idempotencyKey },
})
}
export function updateMerchantFirstLegOrder(id: number | string, payload: { expectedVersion: number; draft: FirstLegOrderSaveRequest }, idempotencyKey: string) {
return apiRequest<void>({ method: 'PUT', url: `/merchant/v1/orders/first-leg/${id}`, data: payload, headers: { 'Idempotency-Key': idempotencyKey } })
}
export function cancelMerchantFirstLegOrder(id: number | string, payload: { expectedVersion: number; reason: string }, idempotencyKey: string) {
return apiRequest<void>({ method: 'POST', url: `/merchant/v1/orders/first-leg/${id}/cancel`, data: payload, headers: { 'Idempotency-Key': idempotencyKey } })
}
export function submitMerchantFirstLegOrder(id: number | string, payload: { expectedVersion: number }, idempotencyKey: string) {
return apiRequest<void>({
method: 'POST',
url: `/merchant/v1/orders/first-leg/${id}/submit`,
data: payload,
headers: { 'Idempotency-Key': idempotencyKey },
})
}
export function createIdempotencyKey(scope: string) {
const randomPart = globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`
return `${scope}-${randomPart}`
}