type PrintfulResponse = { code: number; result: T; }; type PrintfulSyncProduct = { id: number; external_id?: string | null; name: string; thumbnail_url?: string | null; is_ignored?: boolean; }; type PrintfulSyncVariant = { id: number; sync_product_id?: number; variant_id?: number; external_id?: string | null; name?: string | null; retail_price?: string | null; currency?: string | null; }; type PrintfulStoreProduct = { sync_product: PrintfulSyncProduct; sync_variants: PrintfulSyncVariant[]; }; type PrintfulRecipient = { name: string; email?: string; phone?: string; address1: string; address2?: string; city: string; state_code?: string; country_code: string; zip: string; }; type PrintfulOrderItem = { sync_variant_id: number; quantity: number; retail_price?: string; }; const PRINTFUL_BASE_URL = "https://api.printful.com"; function getPrintfulHeaders() { const token = process.env.PRINTFUL_ACCESS_TOKEN; if (!token) { throw new Error("Missing PRINTFUL_ACCESS_TOKEN."); } const headers: Record = { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }; const storeId = process.env.PRINTFUL_STORE_ID; if (storeId) { headers["X-PF-Store-Id"] = storeId; } return headers; } async function printfulRequest(path: string, init?: RequestInit) { const res = await fetch(`${PRINTFUL_BASE_URL}${path}`, { ...init, headers: { ...getPrintfulHeaders(), ...(init?.headers || {}), }, }); const data = (await res.json()) as PrintfulResponse | { error?: any }; if (!res.ok) { const message = (data as any)?.error?.message || (data as any)?.error || (data as any)?.result?.error || `Printful request failed (${res.status})`; throw new Error(message); } return (data as PrintfulResponse).result; } export async function listPrintfulStoreProducts(limit = 20, offset = 0) { const qs = new URLSearchParams({ limit: String(limit), offset: String(offset), }); const result = await printfulRequest( `/store/products?${qs.toString()}` ); if (Array.isArray(result)) { return result as PrintfulStoreProduct[]; } if (result?.items && Array.isArray(result.items)) { return result.items as PrintfulStoreProduct[]; } return []; } export async function listPrintfulStores() { return printfulRequest("/stores"); } export async function getPrintfulTokenScopes() { return printfulRequest("/oauth/scopes"); } export async function getPrintfulStoreProduct(id: number) { return printfulRequest(`/store/products/${id}`); } export async function createPrintfulOrder(options: { externalId?: string; recipient: PrintfulRecipient; items: PrintfulOrderItem[]; confirm?: boolean; }) { const qs = options.confirm ? "?confirm=1" : ""; const payload = { external_id: options.externalId ?? null, recipient: options.recipient, items: options.items, }; return printfulRequest(`/orders${qs}`, { method: "POST", body: JSON.stringify(payload), }); } export async function createPrintfulSyncProduct(payload: any) { return printfulRequest("/store/products", { method: "POST", body: JSON.stringify(payload), }); } export function normalizePrintfulProduct(detail: PrintfulStoreProduct) { if (!detail || !detail.sync_product) { return null as any; } const variant = detail.sync_variants?.[0]; const retail = variant?.retail_price ? Number(variant.retail_price) : NaN; const unitAmount = Number.isFinite(retail) ? Math.round(retail * 100) : null; return { id: String(detail.sync_product.id), stripeProductId: String(detail.sync_product.id), stripePriceId: variant?.id ? String(variant.id) : null, name: detail.sync_product.name, description: null as string | null, unitAmount, currency: variant?.currency ?? null, thumbnailUrl: detail.sync_product.thumbnail_url ?? null, }; }