配送アーキテクチャ、複数配送業者の統合 (UPS、FedEx、DHL、USPS)、料金ショッピング、リアルタイム追跡、国際配送、関税/関税、返品管理、分割注文の混載配送。
ファッション デザインとプリント オン デマンド システム アーキテクチャ — ドメイン分析から生産まで
パート 4: 注文の処理と履行
xdev.asia
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 };
}
}
7. まとめ
| コンポーネント | 主な特長 | POD 固有 |
| ショッピングを評価する | マルチキャリアのパラレルクォート | サプライヤー倉庫 → 顧客直接 |
| ラベルの作成 | サプライヤー向けバッチラベル | サプライヤーが直接印刷して発送します |
| 追跡 | リアルタイム Webhook + ポーリング | 販売チャネルへのステータスの同期 |
| インターナショナル | HSコード、関税計算ツール | POD製品HSコードマッピング |
| 返品 | 印刷欠陥による返品返金不可 | カスタムアイテムは再入荷不可 |