/** * Order fulfillment service — routes rCart orders to POD providers. * * Flow: * 1. rCart order → paid status * 2. This service picks up the order and routes to correct POD provider * 3. POD webhooks update tracking → pushed back to rCart order doc */ import { PrintfulClient } from "./pod/printful"; import { ProdigiClient } from "./pod/prodigi"; import type { PodOrder, PodRecipient, PodTracking } from "./pod/types"; // ── Types ── export interface FulfillmentRequest { /** rCart order ID */ orderId: string; /** Items to fulfill */ items: FulfillmentItem[]; /** Shipping address */ recipient: PodRecipient; /** Optional metadata */ metadata?: Record; } export interface FulfillmentItem { /** Design image public URL */ imageUrl: string; /** POD provider to use */ provider: "printful" | "prodigi"; /** Provider-specific SKU */ sku: string; /** Quantity */ quantity: number; /** Size variant (for apparel) */ size?: string; /** Color variant (for apparel) */ color?: string; /** Placement on product */ placement?: string; } export interface FulfillmentResult { orderId: string; provider: "printful" | "prodigi"; providerOrderId: string; status: string; items: FulfillmentItem[]; submittedAt: string; } // ── In-memory fulfillment tracking ── // Maps rCart orderId → fulfillment results const fulfillmentLog = new Map(); // ── Service ── const printful = new PrintfulClient(); const prodigi = new ProdigiClient(); /** * Submit an order to the appropriate POD provider(s). * * If items span multiple providers, creates separate orders per provider. */ export async function submitFulfillment( request: FulfillmentRequest, ): Promise { const results: FulfillmentResult[] = []; // Group items by provider const printfulItems = request.items.filter((i) => i.provider === "printful"); const prodigiItems = request.items.filter((i) => i.provider === "prodigi"); // Submit Printful items if (printfulItems.length > 0) { const result = await submitToPrintful( request.orderId, printfulItems, request.recipient, ); results.push(result); } // Submit Prodigi items if (prodigiItems.length > 0) { const result = await submitToProdigi( request.orderId, prodigiItems, request.recipient, request.metadata, ); results.push(result); } // Store in tracking log fulfillmentLog.set(request.orderId, results); return results; } async function submitToPrintful( orderId: string, items: FulfillmentItem[], recipient: PodRecipient, ): Promise { if (!printful.enabled) { throw new Error("Printful API not configured"); } // Resolve catalog variant IDs for each item const orderItems = await Promise.all( items.map(async (item) => { const productId = parseInt(item.sku, 10); let catalogVariantId: number | null = null; if (item.size) { catalogVariantId = await printful.resolveVariantId( productId, item.size, item.color || "Black", ); } if (!catalogVariantId) { // Fallback: get first available variant const variants = await printful.getCatalogVariants(productId); catalogVariantId = variants[0]?.id || 0; } return { catalogVariantId, quantity: item.quantity, imageUrl: item.imageUrl, placement: item.placement || "front", }; }), ); const order = await printful.createOrder(orderItems, recipient); return { orderId, provider: "printful", providerOrderId: order.id, status: order.status, items, submittedAt: new Date().toISOString(), }; } async function submitToProdigi( orderId: string, items: FulfillmentItem[], recipient: PodRecipient, metadata?: Record, ): Promise { if (!prodigi.enabled) { throw new Error("Prodigi API not configured"); } const prodigiItems = items.map((item) => ({ sku: item.sku, copies: item.quantity, sizing: "fillPrintArea" as const, assets: [{ printArea: "default", url: item.imageUrl }], })); const order = await prodigi.createOrder( prodigiItems, recipient, "Budget", metadata ? { ...metadata, rCartOrderId: orderId } : { rCartOrderId: orderId }, ); return { orderId, provider: "prodigi", providerOrderId: order.id, status: order.status, items, submittedAt: new Date().toISOString(), }; } // ── Webhook handlers ── export interface WebhookEvent { provider: "printful" | "prodigi"; type: string; orderId: string; tracking?: PodTracking; status?: string; raw: unknown; } /** * Process a Printful webhook event. * Returns parsed event data for the caller to update rCart. */ export function parsePrintfulWebhook(body: any): WebhookEvent | null { if (!body?.type || !body?.data) return null; const event: WebhookEvent = { provider: "printful", type: body.type, orderId: String(body.data?.order?.id || body.data?.id || ""), raw: body, }; // Extract tracking info from shipment events if (body.type === "package_shipped" || body.type === "order_updated") { const shipment = body.data?.shipment || body.data; if (shipment?.tracking_number) { event.tracking = { carrier: shipment.carrier || "", trackingNumber: shipment.tracking_number, trackingUrl: shipment.tracking_url || "", }; } event.status = body.data?.order?.status || body.data?.status; } return event; } /** * Process a Prodigi webhook event. */ export function parseProdigiWebhook(body: any): WebhookEvent | null { if (!body?.topic) return null; const order = body.order || body; const event: WebhookEvent = { provider: "prodigi", type: body.topic, orderId: String(order?.id || ""), raw: body, }; // Extract tracking from shipment events if (body.topic === "order.shipped") { const shipment = order?.shipments?.[0]; if (shipment) { event.tracking = { carrier: shipment.carrier || "", trackingNumber: shipment.trackingNumber || "", trackingUrl: shipment.trackingUrl || "", }; } } event.status = order?.status?.stage; return event; } // ── Tracking lookup ── /** * Get fulfillment status for an rCart order. */ export function getFulfillmentStatus(orderId: string): FulfillmentResult[] | null { return fulfillmentLog.get(orderId) || null; } /** * Get tracking info from the POD provider. */ export async function getTrackingInfo( provider: "printful" | "prodigi", providerOrderId: string, ): Promise { try { if (provider === "printful") { const order = await printful.getOrder(providerOrderId); const raw = order.raw as any; const shipment = raw?.shipments?.[0]; if (shipment?.tracking_number) { return { carrier: shipment.carrier || "", trackingNumber: shipment.tracking_number, trackingUrl: shipment.tracking_url || "", }; } } else { const order = await prodigi.getOrder(providerOrderId); const raw = order.raw as any; const shipment = raw?.order?.shipments?.[0]; if (shipment?.trackingNumber) { return { carrier: shipment.carrier || "", trackingNumber: shipment.trackingNumber, trackingUrl: shipment.trackingUrl || "", }; } } } catch (err) { console.warn(`[rSwag] Failed to get tracking for ${provider} order ${providerOrderId}:`, err); } return null; }