158 lines
3.9 KiB
TypeScript
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,
|
|
};
|
|
}
|