/** * Prodigi v4 API client. * * Handles order submission, product specs, and quotes. * Sandbox: https://api.sandbox.prodigi.com/v4.0/ * Production: https://api.prodigi.com/v4.0/ */ import type { PodOrder, PodRecipient, PodQuote } from "./types"; const SANDBOX_URL = "https://api.sandbox.prodigi.com/v4.0"; const PRODUCTION_URL = "https://api.prodigi.com/v4.0"; export class ProdigiClient { private apiKey: string; private baseUrl: string; readonly enabled: boolean; constructor() { this.apiKey = process.env.PRODIGI_API_KEY || ""; const sandbox = process.env.POD_SANDBOX_MODE !== "false"; this.baseUrl = sandbox ? SANDBOX_URL : PRODUCTION_URL; this.enabled = !!this.apiKey; } private get headers(): Record { return { "X-API-Key": this.apiKey, "Content-Type": "application/json", }; } /** * Create a Prodigi print order. * * Items should include: sku, copies, sizing, assets. * Recipient uses Prodigi address format. */ async createOrder( items: { sku: string; copies: number; sizing?: "fillPrintArea" | "fitPrintArea" | "stretchToPrintArea"; assets: { printArea: string; url: string }[]; }[], recipient: PodRecipient, shippingMethod: "Budget" | "Standard" | "Express" = "Budget", metadata?: Record, ): Promise { if (!this.enabled) throw new Error("Prodigi API key not configured"); const payload: any = { shippingMethod, recipient: { name: recipient.name, ...(recipient.email ? { email: recipient.email } : {}), address: { line1: recipient.address1, ...(recipient.address2 ? { line2: recipient.address2 } : {}), townOrCity: recipient.city, ...(recipient.stateCode ? { stateOrCounty: recipient.stateCode } : {}), postalOrZipCode: recipient.zip, countryCode: recipient.countryCode, }, }, items: items.map((item) => ({ sku: item.sku, copies: item.copies, sizing: item.sizing || "fillPrintArea", assets: item.assets, })), }; if (metadata) payload.metadata = metadata; const resp = await fetch(`${this.baseUrl}/Orders`, { method: "POST", headers: this.headers, body: JSON.stringify(payload), }); if (!resp.ok) { const text = await resp.text(); throw new Error(`Prodigi order error ${resp.status}: ${text.slice(0, 500)}`); } const result = await resp.json(); return { id: String(result.id || result.order?.id || ""), provider: "prodigi", status: result.status?.stage || "InProgress", items: items.map((i) => ({ sku: i.sku, quantity: i.copies, imageUrl: i.assets[0]?.url || "", })), recipient, createdAt: new Date().toISOString(), raw: result, }; } async getOrder(orderId: string): Promise { const resp = await fetch(`${this.baseUrl}/Orders/${orderId}`, { headers: this.headers, }); if (!resp.ok) throw new Error(`Prodigi get order error: ${resp.status}`); const result = await resp.json(); const order = result.order || result; return { id: String(order.id || orderId), provider: "prodigi", status: order.status?.stage || "unknown", items: [], recipient: { name: "", address1: "", city: "", countryCode: "", zip: "" }, createdAt: order.created || new Date().toISOString(), raw: result, }; } async getProduct(sku: string): Promise { const resp = await fetch(`${this.baseUrl}/products/${sku}`, { headers: this.headers, }); if (!resp.ok) throw new Error(`Prodigi product error: ${resp.status}`); return resp.json(); } async getQuote( items: { sku: string; copies: number }[], shippingMethod: "Budget" | "Standard" | "Express" = "Budget", destinationCountry = "US", ): Promise { const payload = { shippingMethod, destinationCountryCode: destinationCountry, items: items.map((i) => ({ sku: i.sku, copies: i.copies })), }; const resp = await fetch(`${this.baseUrl}/quotes`, { method: "POST", headers: this.headers, body: JSON.stringify(payload), }); if (!resp.ok) throw new Error(`Prodigi quote error: ${resp.status}`); const result = await resp.json(); const quotes = result.quotes || [result]; const q = quotes[0] || {}; return { items: (q.items || []).map((i: any) => ({ sku: i.sku, unitCost: parseFloat(i.unitCost?.amount || "0"), quantity: i.copies || 1, })), shippingCost: parseFloat(q.shipmentSummary?.items?.[0]?.cost?.amount || "0"), totalCost: parseFloat(q.totalCost?.amount || "0"), currency: q.totalCost?.currency || "USD", }; } }