Multi-channel sales architecture for POD, integration with Shopify, Etsy, Amazon, TikTok Shop, WooCommerce, product listing sync, order routing, inventory sync, webhook handling, channel-specific requirements.
Fashion Design & Print-on-Demand System Architecture — From Domain Analysis to Production
Part 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 from 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
| Features | Shopify | Etsy | Amazon | TikTok Shop | WooCommerce |
| API Type | GraphQL + REST | REST v3 | SP-API | REST | REST v3 |
| Auth | OAuth 2.0 | OAuth 2.0 | IAM + OAuth | OAuth 2.0 | API Key |
| Max Images | 250 | 10 | 9 | 9 | Unlimited |
| Max Variants | 100 | Varies | Per category | Varies | Unlimited |
| Webhooks | Yes (HMAC) | Limited | SQS/SNS | Yes | Yes (JWT) |
| Commission | 0% | 6.5% | 8-15% | 5% | 0% |
| Order Flow | Push (webhook) | Pull (poll) | Pull (SP-API) | Push | Push (webhook) |