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

Bài 9: Multi-channel Sales — Shopify, Etsy, Amazon, TikTok Shop & API Integration

Kiến trúc multi-channel sales cho POD, integration với Shopify, Etsy, Amazon, TikTok Shop, WooCommerce, product listing sync, order routing, inventory sync, webhook handling, channel-specific requirements.

🏗️ Kiến trúc — Bài 9 Bài 9: Multi-channel Sales — Shopify, Etsy, Amazon, TikTok Shop & API Integration

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

Phần 3: Product & E-Commerce Platform

xdev.asia

1. Multi-channel Architecture

                    ┌──────────────────────┐
                    │   POD Platform Core  │
                    │   (Product Catalog)  │
                    └──────────┬───────────┘
                               │
                    ┌──────────▼───────────┐
                    │  Channel Manager     │
                    │  (Sync orchestrator) │
                    └──────────┬───────────┘
                               │
         ┌─────────┬───────────┼───────────┬─────────┐
         ▼         ▼           ▼           ▼         ▼
    ┌─────────┐┌─────────┐┌─────────┐┌─────────┐┌─────────┐
    │ Shopify ││  Etsy   ││ Amazon  ││ TikTok  ││  Woo    │
    │Connector││Connector││Connector││  Shop   ││Commerce │
    │         ││         ││         ││Connector││Connector│
    └────┬────┘└────┬────┘└────┬────┘└────┬────┘└────┬────┘
         │          │          │          │          │
    ┌────▼────┐┌────▼────┐┌────▼────┐┌────▼────┐┌────▼────┐
    │ Shopify ││  Etsy   ││ Amazon  ││ TikTok  ││  Woo    │
    │   API   ││  API    ││SP-API   ││  API    ││REST API │
    └─────────┘└─────────┘└─────────┘└─────────┘└─────────┘

2. Channel Connector Interface

// Abstract connector — mỗi channel implement interface này
interface ChannelConnector {
  channelId: string;
  channelType: ChannelType;
  
  // Product sync
  publishProduct(product: SellableProduct): Promise<ChannelProductResult>;
  updateProduct(product: SellableProduct): Promise<void>;
  unpublishProduct(productId: string): Promise<void>;
  
  // Order handling
  fetchNewOrders(since: Date): Promise<ChannelOrder[]>;
  updateOrderStatus(orderId: string, status: OrderStatus, tracking?: TrackingInfo): Promise<void>;
  
  // Inventory sync (availability changes)
  updateAvailability(variantId: string, available: boolean): Promise<void>;
  
  // Webhooks
  handleWebhook(payload: unknown): Promise<WebhookResult>;
}

type ChannelType = 'shopify' | 'etsy' | 'amazon' | 'tiktok_shop' | 'woocommerce' | 'custom_api';

// Shopify connector implementation
class ShopifyConnector implements ChannelConnector {
  private client: ShopifyAdminAPI;
  
  constructor(
    private config: {
      shopDomain: string;
      accessToken: string;
      apiVersion: string;     // '2024-01'
    }
  ) {
    this.client = new ShopifyAdminAPI(config);
  }

  async publishProduct(product: SellableProduct): Promise<ChannelProductResult> {
    // Transform internal product → Shopify product format
    const shopifyProduct = this.transformToShopify(product);
    
    // Create product via GraphQL Admin API
    const result = await this.client.graphql(`
      mutation productCreate($input: ProductInput!) {
        productCreate(input: $input) {
          product { id legacyResourceId handle }
          userErrors { field message }
        }
      }
    `, {
      input: shopifyProduct,
    });

    if (result.productCreate.userErrors.length > 0) {
      throw new ChannelSyncError('shopify', result.productCreate.userErrors);
    }

    return {
      channelProductId: result.productCreate.product.id,
      externalUrl: `https://${this.config.shopDomain}/products/${result.productCreate.product.handle}`,
    };
  }

  private transformToShopify(product: SellableProduct): ShopifyProductInput {
    return {
      title: product.title,
      descriptionHtml: product.description,
      productType: product.category,
      tags: product.tags,
      variants: product.variants.map(v => ({
        sku: v.sku,
        price: v.price.amount.toString(),
        compareAtPrice: product.compareAtPrice?.amount.toString(),
        options: [v.size.name, v.color.name],
        inventoryPolicy: 'CONTINUE',      // Always available (POD)
        requiresShipping: true,
        weight: v.weight.value,
        weightUnit: v.weight.unit.toUpperCase(),
      })),
      images: product.images.map(img => ({
        src: img.url,
        altText: img.alt,
      })),
      seo: {
        title: product.seoTitle,
        description: product.seoDescription,
      },
    };
  }
}

3. Product Listing Sync

// Central sync orchestrator
class ChannelSyncService {
  constructor(
    private connectors: Map<string, ChannelConnector>,
    private eventBus: EventBus,
    private syncRepo: SyncStateRepository,
  ) {}

  // Publish product to all enabled channels
  async publishToChannels(
    product: SellableProduct,
    channelIds: string[],
  ): Promise<PublishResult[]> {
    const results: PublishResult[] = [];

    for (const channelId of channelIds) {
      const connector = this.connectors.get(channelId);
      if (!connector) continue;

      try {
        // Transform product for channel-specific requirements
        const adapted = await this.adaptForChannel(product, connector.channelType);
        
        const result = await connector.publishProduct(adapted);
        
        // Save sync state
        await this.syncRepo.saveSyncState({
          productId: product.id,
          channelId,
          channelProductId: result.channelProductId,
          externalUrl: result.externalUrl,
          lastSyncedAt: new Date(),
          status: 'synced',
        });

        results.push({ channelId, success: true, result });
      } catch (error) {
        results.push({ channelId, success: false, error: error.message });
        
        // Emit failure event for monitoring
        this.eventBus.emit('channel.sync.failed', {
          productId: product.id,
          channelId,
          error: error.message,
        });
      }
    }

    return results;
  }

  // Channel-specific adaptations
  private async adaptForChannel(
    product: SellableProduct,
    channelType: ChannelType,
  ): Promise<SellableProduct> {
    const adapted = { ...product };

    switch (channelType) {
      case 'etsy':
        // Etsy: max 10 images, requires shipping profile, taxonomy ID
        adapted.images = adapted.images.slice(0, 10);
        adapted.tags = adapted.tags.slice(0, 13);             // Etsy max 13 tags
        break;

      case 'amazon':
        // Amazon: bullet points, search terms, browse node
        adapted.description = formatAmazonBulletPoints(product);
        break;

      case 'tiktok_shop':
        // TikTok: video preferred, category ID, brand name
        adapted.images = await addVideoMockup(product);
        break;
    }

    return adapted;
  }
}

4. Order Routing từ Channels

// Order flow: Channel → Webhook → POD Platform → Supplier
interface ChannelOrderRouter {
  // Process incoming order from any channel
  processChannelOrder(channelOrder: ChannelOrder): Promise<InternalOrder>;
}

async function processChannelOrder(channelOrder: ChannelOrder): Promise<InternalOrder> {
  // 1. Map channel product IDs to internal product IDs
  const lineItems = await Promise.all(
    channelOrder.items.map(async (item) => {
      const syncState = await syncRepo.findByChannelProductId(item.channelProductId);
      return {
        productId: syncState.productId,
        variantSku: syncState.sku,
        quantity: item.quantity,
        price: item.price,
      };
    })
  );

  // 2. Create internal order
  const internalOrder = await orderService.createOrder({
    source: channelOrder.channelType,
    sourceOrderId: channelOrder.orderId,
    customer: channelOrder.customer,
    shippingAddress: channelOrder.shippingAddress,
    lineItems,
    payment: {
      status: 'paid',                    // Channel already collected payment
      amount: channelOrder.totalAmount,
      channelTransactionId: channelOrder.transactionId,
    },
  });

  // 3. Emit event for downstream processing
  eventBus.emit('order.created', {
    orderId: internalOrder.id,
    source: channelOrder.channelType,
  });

  return internalOrder;
}

5. Webhook Handling

// Centralized webhook handler
class WebhookRouter {
  private handlers: Map<string, WebhookHandler>;

  // POST /webhooks/:channel
  async handleWebhook(channel: string, req: Request): Promise<Response> {
    // 1. Verify webhook signature
    const isValid = await this.verifySignature(channel, req);
    if (!isValid) {
      return new Response('Unauthorized', { status: 401 });
    }

    // 2. Parse & deduplicate
    const webhookId = req.headers.get('x-webhook-id');
    if (await this.isDuplicate(webhookId)) {
      return new Response('Already processed', { status: 200 });
    }

    // 3. Route to channel handler
    const handler = this.handlers.get(channel);
    const event = await handler.parseWebhook(req);

    // 4. Process based on event type
    switch (event.type) {
      case 'order.created':
        await this.processChannelOrder(event.data);
        break;
      case 'order.cancelled':
        await this.cancelOrder(event.data);
        break;
      case 'order.refunded':
        await this.processRefund(event.data);
        break;
    }

    // 5. Mark as processed
    await this.markProcessed(webhookId);

    return new Response('OK', { status: 200 });
  }
}

6. Channel Comparison

FeatureShopifyEtsyAmazonTikTok ShopWooCommerce
API TypeGraphQL + RESTREST v3SP-APIRESTREST v3
AuthOAuth 2.0OAuth 2.0IAM + OAuthOAuth 2.0API Key
Max Images2501099Unlimited
Max Variants100VariesPer categoryVariesUnlimited
WebhookYes (HMAC)LimitedSQS/SNSYesYes (JWT)
Commission0%6.5%8-15%5%0%
Order FlowPush (webhook)Pull (poll)Pull (SP-API)PushPush (webhook)