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 { 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({ 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) { 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 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>({ 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>({ 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({ method: 'GET', url: `/merchant/v1/warehouses/${id}` }).catch(async () => { const fallback = await apiRequest({ method: 'GET', url: `/platform/v1/warehouse-admin/${encodeURIComponent(id)}`, params: { tenantId: 0 } }) return fallback }) } export function getMerchantPricingChannels(originWarehouseId: number | string) { return apiRequest({ method: 'GET', url: '/merchant/v1/first-leg/pricing/channels', params: { originWarehouseId }, }) } export function estimateMerchantFirstLegPrice(payload: MerchantPriceEstimateRequest) { return apiRequest({ method: 'POST', url: '/merchant/v1/first-leg/pricing/estimate', data: payload, }) } export function getMerchantFirstLegOrders(params: FirstLegOrderQuery = {}) { return apiRequest>({ method: 'GET', url: '/merchant/v1/orders/first-leg', params: { page: 1, size: 200, ...params } }) } export function getMerchantFirstLegOrder(id: number | string) { return apiRequest({ method: 'GET', url: `/merchant/v1/orders/first-leg/${id}` }) } export function getMerchantInboundCodePolicy() { return apiRequest({ method: 'GET', url: '/merchant/v1/orders/first-leg/inbound-code-policy' }) } export function getMerchantOrderAddresses(addressType?: CustomerOrderAddressType) { return apiRequest({ method: 'GET', url: '/merchant/v1/orders/first-leg/addresses', params: { addressType } }) } export function createMerchantOrderAddress(payload: CustomerOrderAddressSaveRequest) { return apiRequest({ method: 'POST', url: '/merchant/v1/orders/first-leg/addresses', data: payload }) } export function updateMerchantOrderAddress(id: number | string, payload: CustomerOrderAddressSaveRequest) { return apiRequest({ method: 'PUT', url: `/merchant/v1/orders/first-leg/addresses/${id}`, data: payload }) } export function disableMerchantOrderAddress(id: number | string) { return apiRequest({ 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({ method: 'POST', url: '/merchant/v1/orders/first-leg/batch-imports', params: { mode }, headers: { 'Idempotency-Key': idempotencyKey }, data, }) } export function createMerchantFirstLegOrder(payload: FirstLegOrderSaveRequest) { return apiRequest({ 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({ 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({ 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({ 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({ 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}` }