Chuyển đến nội dung chính

第 15 課:運輸與物流 — 承運人整合、費率購物、追蹤與退貨

運輸架構、多承運商整合(UPS、FedEx、DHL、USPS)、費率購物、即時追蹤、國際運輸、海關/關稅、退貨管理、拆分訂單的合併運輸。

🏗️ 建築 — 第 15 課 第 15 課:運輸與物流 — 承運人 整合、價格購物、追蹤和 退貨

時裝設計與按需印刷系統架構-從領域分析到生產

第 4 部分:訂單處理與履行

亞洲開發網

1. 航運架構

Order Fulfillment → Shipping Pipeline

┌───────────┐   ┌──────────┐   ┌──────────┐   ┌──────────┐
│  Create   │──▶│  Rate    │──▶│  Create  │──▶│ Tracking │
│  Shipment │   │ Shopping │   │  Label   │   │  Events  │
│           │   │          │   │          │   │          │
│ - Address │   │ - UPS    │   │ - PDF    │   │ - Pickup │
│ - Weight  │   │ - FedEx  │   │ - ZPL    │   │ - Transit│
│ - Dims    │   │ - DHL    │   │ - PNG    │   │ - Deliver│
│ - Service │   │ - USPS   │   │          │   │ - Except │
└───────────┘   └──────────┘   └──────────┘   └──────────┘

2. 多運營商融合

// Abstract shipping interface
interface ShippingService {
  getRates(shipment: ShipmentRequest): Promise<ShippingRate[]>;
  createLabel(shipment: ShipmentRequest, carrier: string, service: string): Promise<ShippingLabel>;
  trackShipment(trackingNumber: string, carrier: string): Promise<TrackingInfo>;
  cancelShipment(shipmentId: string): Promise<void>;
}

interface ShipmentRequest {
  from: Address;                    // Supplier warehouse
  to: Address;                     // Customer address
  
  parcels: Parcel[];
  
  // Service preferences
  serviceLevel: 'economy' | 'standard' | 'express' | 'overnight';
  
  // Special requirements
  signatureRequired: boolean;
  insurance?: Money;
  saturdayDelivery: boolean;
  
  // International
  customs?: CustomsDeclaration;
}

interface Parcel {
  weight: { value: number; unit: 'oz' | 'lb' | 'g' | 'kg' };
  dimensions: { length: number; width: number; height: number; unit: 'in' | 'cm' };
  items: ParcelItem[];
}

interface ShippingRate {
  carrier: string;                  // 'ups', 'fedex', 'usps', 'dhl'
  service: string;                  // 'ground', 'express', '2day'
  serviceName: string;              // 'UPS Ground', 'FedEx 2Day'
  
  rate: Money;
  retailRate: Money;                // Carrier retail rate (before discount)
  discount: number;                 // % discount from volume agreement
  
  estimatedDelivery: {
    minDays: number;
    maxDays: number;
    guaranteedDate?: Date;
  };
  
  // Restrictions
  restrictions: string[];           // 'no_po_box', 'commercial_only'
}

// Rate shopping: Get best rates across all carriers
async function shopRates(
  shipment: ShipmentRequest,
): Promise<ShippingRate[]> {
  // Request rates from all carriers in parallel
  const [upsRates, fedexRates, uspsRates, dhlRates] = await Promise.all([
    upsClient.getRates(shipment).catch(() => []),
    fedexClient.getRates(shipment).catch(() => []),
    uspsClient.getRates(shipment).catch(() => []),
    dhlClient.getRates(shipment).catch(() => []),
  ]);

  const allRates = [...upsRates, ...fedexRates, ...uspsRates, ...dhlRates];

  // Sort by price (cheapest first) within service level
  return allRates
    .filter(r => meetsServiceLevel(r, shipment.serviceLevel))
    .sort((a, b) => a.rate.amount - b.rate.amount);
}

3. 標籤建立和生成

interface ShippingLabel {
  labelId: string;
  trackingNumber: string;
  carrier: string;
  service: string;
  
  // Label file
  labelFormat: 'pdf' | 'zpl' | 'png';
  labelUrl: string;
  
  // Rates
  chargedAmount: Money;
  
  // Dates
  shipDate: Date;
  estimatedDelivery: Date;
}

// Batch label creation cho supplier
async function createBatchLabels(
  subOrders: SubOrder[],
  supplierId: string,
): Promise<BatchLabelResult> {
  const supplier = await getSupplier(supplierId);
  
  const labels: ShippingLabel[] = [];
  const errors: LabelError[] = [];

  for (const subOrder of subOrders) {
    try {
      // 1. Calculate best rate
      const shipment = buildShipmentRequest(subOrder, supplier.warehouse);
      const rates = await shopRates(shipment);
      const bestRate = rates[0];

      // 2. Create label
      const label = await createLabel(shipment, bestRate.carrier, bestRate.service);
      
      labels.push(label);

      // 3. Update order with tracking
      await orderService.addTracking(subOrder.id, {
        carrier: label.carrier,
        trackingNumber: label.trackingNumber,
        labelUrl: label.labelUrl,
        estimatedDelivery: label.estimatedDelivery,
      });
    } catch (error) {
      errors.push({
        subOrderId: subOrder.id,
        error: error.message,
      });
    }
  }

  return { labels, errors, totalCost: sumCosts(labels) };
}

4. 即時追蹤

// Tracking event polling & webhook processing
interface TrackingService {
  pollTrackingUpdates(): Promise<void>;            // Scheduled cron job
  processTrackingWebhook(webhook: TrackingWebhook): Promise<void>;
}

interface TrackingEvent {
  timestamp: Date;
  status: TrackingStatus;
  location: {
    city: string;
    state: string;
    country: string;
    postalCode: string;
  };
  description: string;
  carrier: string;
}

type TrackingStatus =
  | 'label_created'
  | 'picked_up'
  | 'in_transit'
  | 'out_for_delivery'
  | 'delivered'
  | 'delivery_attempted'
  | 'exception'
  | 'returned_to_sender';

// Customer notification based on tracking events
async function processTrackingEvent(event: TrackingEvent, orderId: string): Promise<void> {
  // Update order status
  const statusMap: Partial<Record<TrackingStatus, OrderStatus>> = {
    picked_up: OrderStatus.SHIPPED,
    in_transit: OrderStatus.IN_TRANSIT,
    out_for_delivery: OrderStatus.OUT_FOR_DELIVERY,
    delivered: OrderStatus.DELIVERED,
  };

  const newStatus = statusMap[event.status];
  if (newStatus) {
    await orderService.updateStatus(orderId, newStatus);
  }

  // Send customer notification for key events
  const notifyEvents: TrackingStatus[] = [
    'picked_up', 'out_for_delivery', 'delivered', 'exception',
  ];

  if (notifyEvents.includes(event.status)) {
    await notificationService.sendTrackingUpdate(orderId, event);
  }

  // Handle exceptions
  if (event.status === 'exception') {
    await handleShippingException(orderId, event);
  }
}

5. 國際運輸與海關

interface CustomsDeclaration {
  // Sender & recipient
  senderTaxId?: string;
  recipientTaxId?: string;
  
  // Contents
  contentType: 'merchandise' | 'gift' | 'sample' | 'return';
  
  items: CustomsItem[];
  
  // Duties payment
  dutiesPayment: 'sender' | 'recipient' | 'third_party';  // DDP / DAP
  
  // Incoterms
  incoterms: 'DDP' | 'DAP' | 'DDU';
}

interface CustomsItem {
  description: string;              // "Cotton T-shirt with printed design"
  hsCode: string;                   // HS tariff code: 6109.10 (T-shirts, knitted)
  quantity: number;
  unitValue: Money;
  weight: Weight;
  countryOfOrigin: string;          // Country where product was manufactured
  countryOfManufacture: string;
}

// HS Code mapping for common POD products
const HS_CODES: Record<string, string> = {
  tshirt_cotton: '6109.10.0012',      // T-shirts, singlets, cotton, knitted
  tshirt_synthetic: '6109.90.1007',    // T-shirts, man-made fibers
  hoodie: '6110.20.2079',             // Pullovers, cotton, knitted
  mug_ceramic: '6912.00.4400',        // Ceramic tableware
  phone_case: '3926.90.9996',         // Articles of plastics
  tote_bag_cotton: '4202.92.1500',    // Travel bags, cotton
  poster: '4911.91.2040',             // Printed pictures, designs
};

// Calculate import duties & taxes
async function calculateDuties(
  shipment: ShipmentRequest,
  destination: string,
): Promise<DutiesEstimate> {
  // Use duty calculator API (e.g., Zonos, Avalara Cross-Border)
  const estimate = await dutiesCalculator.calculate({
    items: shipment.customs!.items,
    destination,
    shippingCost: shipment.shippingCost,
  });

  return {
    dutyAmount: estimate.duty,
    taxAmount: estimate.tax,           // VAT/GST
    totalLandedCost: estimate.landedCost,
    deMinimisMet: estimate.duty.amount === 0,  // Under duty threshold
  };
}

6. 退貨管理

interface ReturnService {
  createReturn(request: ReturnRequest): Promise<ReturnAuthorization>;
  processReturn(returnId: string): Promise<void>;
}

interface ReturnRequest {
  orderId: string;
  reason: ReturnReason;
  items: Array<{ sku: string; quantity: number }>;
  customerNote: string;
  photos?: string[];                   // Photos of defect/issue
}

type ReturnReason =
  | 'wrong_size'                       // → Offer exchange
  | 'print_quality'                    // → Full refund + no return needed (POD = no restock)
  | 'wrong_item'                       // → Reship correct item
  | 'damaged_in_transit'               // → Full refund or reship
  | 'not_as_described'                 // → Review + partial/full refund
  | 'changed_mind';                    // → Policy dependent

// POD-specific: No physical return needed for print quality issues
// (Item can't be restocked — it's custom printed)
async function handleReturn(request: ReturnRequest): Promise<ReturnResult> {
  const policy = getReturnPolicy(request.reason);

  switch (policy.action) {
    case 'refund_no_return':
      // Print defect → refund immediately, no need to ship back
      await paymentService.refund(request.orderId, policy.refundAmount);
      return { action: 'refunded', requiresReturn: false };

    case 'reship':
      // Wrong item → reship correct one
      await orderService.createReship(request.orderId, request.items);
      return { action: 'reshipped', requiresReturn: false };

    case 'exchange':
      // Wrong size → create exchange order
      return { action: 'exchange_offered', requiresReturn: true };

    default:
      return { action: 'review_needed', requiresReturn: false };
  }
}

七、總結

組件主要特點POD 特定
評價購物多承運人平行報價供應商倉庫→客戶直接
標籤創建Batch labels for suppliers供應商直接列印並出貨
追蹤即時 webhook + 輪詢狀態同步回銷售管道
國際HS 代碼、關稅計算器POD產品HS編碼映射
退貨列印缺陷不予退貨退款訂製商品無法補貨