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

Bài 15: Shipping & Logistics — Carrier Integration, Rate Shopping, Tracking & Returns

Shipping architecture, multi-carrier integration (UPS, FedEx, DHL, USPS), rate shopping, real-time tracking, international shipping, customs/duties, returns management, consolidated shipping for split orders.

🏗️ Kiến trúc — Bài 15 Bài 15: Shipping & Logistics — Carrier Integration, Rate Shopping, Tracking & Returns

Kiến trúc Hệ thống Fashion Design & Print-on-Demand — Từ Domain Analysis đến Production

Phần 4: Order Processing & Fulfillment

xdev.asia

1. Shipping Architecture

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. Multi-carrier Integration

// 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. Label Creation & Generation

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. Real-time Tracking

// 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. International Shipping & Customs

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. Returns Management

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 };
  }
}

7. Tổng kết

ComponentKey FeaturePOD Specific
Rate ShoppingMulti-carrier parallel quotesSupplier warehouse → customer direct
Label CreationBatch labels for supplierSupplier prints & ships directly
TrackingReal-time webhook + pollingStatus sync back to sales channels
InternationalHS codes, duties calculatorPOD product HS code mapping
ReturnsNo-return refund for print defectsCustom items can't be restocked