591 lines
18 KiB
TypeScript
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}`
|
|
}
|