rspace-online/modules/rswag/pod/prodigi.ts

165 lines
4.5 KiB
TypeScript

/**
* 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<string, string> {
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<string, string>,
): Promise<PodOrder> {
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<PodOrder> {
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<any> {
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<PodQuote> {
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",
};
}
}