Files
Shifted/lib/printful.ts
2026-02-10 01:14:19 +00:00

158 lines
3.9 KiB
TypeScript

type PrintfulResponse<T> = {
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<string, string> = {
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<T>(path: string, init?: RequestInit) {
const res = await fetch(`${PRINTFUL_BASE_URL}${path}`, {
...init,
headers: {
...getPrintfulHeaders(),
...(init?.headers || {}),
},
});
const data = (await res.json()) as PrintfulResponse<T> | { 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<T>).result;
}
export async function listPrintfulStoreProducts(limit = 20, offset = 0) {
const qs = new URLSearchParams({
limit: String(limit),
offset: String(offset),
});
const result = await printfulRequest<any>(
`/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<any[]>("/stores");
}
export async function getPrintfulTokenScopes() {
return printfulRequest<any>("/oauth/scopes");
}
export async function getPrintfulStoreProduct(id: number) {
return printfulRequest<PrintfulStoreProduct>(`/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<any>(`/orders${qs}`, {
method: "POST",
body: JSON.stringify(payload),
});
}
export async function createPrintfulSyncProduct(payload: any) {
return printfulRequest<any>("/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,
};
}