Files
platform/commerce/webflow.ts
T
2025-09-13 18:53:02 -04:00

279 lines
12 KiB
TypeScript

import { Printful } from "./printful"
export namespace Webflow {
export const API_SITES_URL = `https://api.webflow.com/v2/sites/${Bun.env.WEBFLOW_SITE_ID}`;
export const API_COLLECTIONS_URL = `https://api.webflow.com/v2/collections/${Bun.env.WEBFLOW_COLLECTION_ID}`;
const formatSlug = (name: string): string => {
return name
.toLowerCase()
.replace(/[^a-z0-9-_]/g, '-') // replace illegal chars with dashes
.replace(/^-+/, '') // remove leading dashes
.replace(/-+$/, '') // remove trailing dashes
.replace(/--+/g, '-') // collapse multiple dashes
.replace(/^([^a-z0-9_])/, '_$1'); // if it starts with an invalid char, prefix underscore
};
export namespace Products {
export interface Sku {
fieldData: {
name: string;
slug: string;
"sku-values"?: Record<string, string>,
price: {
value: number;
unit: string;
currency: string;
};
"main-image"?: string;
}
}
export interface UpsertProduct {
product: {
fieldData: {
name: string;
slug: string;
description: string;
shippable?: boolean;
"tax-category"?: string;
"sku-properties": {
id: string;
name: string;
enum: {
id: string;
name: string;
slug: string;
}[];
}[];
}
};
sku: Sku;
publishStatus?: "staging" | "live";
}
export async function remove(webflowProductId: string) {
const res = await fetch(`${Webflow.API_COLLECTIONS_URL}/items/${webflowProductId}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
"Authorization": `bearer ${Bun.env.WEBFLOW_AUTH}`
},
body: JSON.stringify({})
});
}
export async function exists(webflowProductId: string): Promise<boolean> {
let findProductResponse = await fetch(`${Webflow.API_SITES_URL}/products/${webflowProductId}`, {
method: "GET",
headers: {
"Authorization": `bearer ${Bun.env.WEBFLOW_AUTH}`,
}
});
return findProductResponse.status === 200;
}
export async function updateUsingPrintful(printfulProduct: Printful.Products.SyncProduct) {
const webflowProductId = printfulProduct.sync_product.external_id;
const printfulVariants = printfulProduct.sync_variants;
const webflowVariants: Products.Sku[] = [];
for (const printfulVariant of printfulVariants) {
webflowVariants.push({
"fieldData": {
"name": printfulVariant.name,
"slug": formatSlug(printfulVariant.name),
"sku-values": {
"color": printfulVariant.color,
"size": printfulVariant.size,
},
"price": {
"value": +printfulVariant.retail_price * 100,
"unit": printfulVariant.currency,
"currency": printfulVariant.currency
},
"main-image": Printful.Products.getVariantPreviewUrl(printfulVariant)
}
});
}
const foundColors = Array.from(new Set(printfulVariants.map(v => v.color)));
const foundSizes = Array.from(new Set(printfulVariants.map(v => v.size)));
let updateProductResponse = await fetch(`${Webflow.API_SITES_URL}/products/${webflowProductId}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
"Authorization": `bearer ${Bun.env.WEBFLOW_AUTH}`,
},
body: JSON.stringify({
"product": {
"fieldData": {
"name": printfulProduct.sync_product.name,
"slug": formatSlug(printfulProduct.sync_product.name),
"shippable": true,
"tax-category": "standard-taxable",
"sku-properties": [
{
"id": "color",
"name": "Color",
"enum": foundColors.map(color => ({
"id": color,
"slug": formatSlug(color),
"name": color
}))
},
{
"id": "size",
"name": "Size",
"enum": foundSizes.map(size => ({
"id": size,
"slug": formatSlug(size),
"name": size
}))
},
]
}
},
"sku": webflowVariants[0],
} as Products.UpsertProduct)
});
updateProductResponse = await updateProductResponse.json();
const getProductResponse = await fetch(`${Webflow.API_SITES_URL}/products/${webflowProductId}`, {
method: "GET",
headers: {
"Authorization": `bearer ${Bun.env.WEBFLOW_AUTH}`
}
});
const webflowProduct = await getProductResponse.json();
for (let i = 0; i < webflowProduct["skus"].length; ++i) {
const webflowSkuId = printfulVariants[i].external_id;
const res = await fetch(`${Webflow.API_SITES_URL}/products/${webflowProductId}/skus/${webflowSkuId}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
"Authorization": `bearer ${Bun.env.WEBFLOW_AUTH}`
},
body: JSON.stringify({
"sku": webflowVariants[i]
})
});
}
}
export async function createUsingPrintful(printfulProduct: Printful.Products.SyncProduct) {
const printfulVariants = printfulProduct.sync_variants;
const webflowVariants: Products.Sku[] = [];
for (const printfulVariant of printfulVariants) {
webflowVariants.push({
"fieldData": {
"name": printfulVariant.name,
"slug": formatSlug(printfulVariant.name),
"sku-values": {
"color": printfulVariant.color,
"size": printfulVariant.size,
},
"price": {
"value": +printfulVariant.retail_price * 100,
"unit": printfulVariant.currency,
"currency": printfulVariant.currency
},
"main-image": Printful.Products.getVariantPreviewUrl(printfulVariant)
}
});
}
const foundColors = Array.from(new Set(printfulVariants.map(v => v.color)));
const foundSizes = Array.from(new Set(printfulVariants.map(v => v.size)));
// CREATE WEBFLOW PRODUCT
let addProductResponse = await (await fetch(`${Webflow.API_SITES_URL}/products`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `bearer ${Bun.env.WEBFLOW_AUTH}`,
},
body: JSON.stringify({
"product": {
"fieldData": {
"name": printfulProduct.sync_product.name,
"slug": formatSlug(printfulProduct.sync_product.name),
"shippable": true,
"tax-category": "standard-taxable",
"sku-properties": [
{
"id": "color",
"name": "Color",
"enum": foundColors.map(color => ({
"id": color,
"slug": formatSlug(color),
"name": color
}))
},
{
"id": "size",
"name": "Size",
"enum": foundSizes.map(size => ({
"id": size,
"slug": formatSlug(size),
"name": size
}))
},
]
}
},
"sku": webflowVariants[0],
} as Products.UpsertProduct)
}))?.json();
if (!addProductResponse?.product?.id) {
console.error("Webflow product creation failed:", addProductResponse);
throw new Error("Failed to create Webflow product");
}
const webflowProductId = addProductResponse["product"]["id"];
const printfulProductId = printfulProduct.sync_product.id;
// CREATE WEBFLOW PRODUCT SKUs
let createSkuResponse = await fetch(`${Webflow.API_SITES_URL}/products/${webflowProductId}/skus`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `bearer ${Bun.env.WEBFLOW_AUTH}`
},
body: JSON.stringify({
"skus": webflowVariants.slice(1)
})
});
createSkuResponse = await createSkuResponse.json();
const responseSkus = [addProductResponse["sku"], ...createSkuResponse["skus"]];
// SYNC WEBFLOW/PRINTFUL PRODUCT AND VARIANT IDs
let modifyPrintfulProductResponse = await fetch(`${Printful.API_URL}/store/products/${printfulProductId}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${Bun.env.PRINTFUL_AUTH}`,
"X-PF-Store-Id": Bun.env.PRINTFUL_STORE_ID ?? ""
},
body: JSON.stringify({
"sync_product": {
"external_id": String(webflowProductId)
},
"sync_variants": responseSkus.map((sku, i) => ({
"id": Number(printfulVariants[i].id), // printful variant id
"external_id": String(sku.id), // webflow variant id
}))
})
});
modifyPrintfulProductResponse = await modifyPrintfulProductResponse.json();
}
}
}