Create product and SKUs implementation

This commit is contained in:
Dominic Ferrando
2025-05-03 16:33:11 -04:00
parent bb7c041c48
commit d5da472661
7 changed files with 206 additions and 39 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
api api
node_modules/ node_modules/
.env.* .env.*
./database/data.db database/data.db
BIN
View File
Binary file not shown.
+8 -8
View File
@@ -1,6 +1,6 @@
import { Database } from "bun:sqlite"; import { Database } from "bun:sqlite";
interface ProductsRecord { interface ProductRecord {
webflowProductId: number; webflowProductId: number;
printfulProductId: number; printfulProductId: number;
} }
@@ -12,31 +12,31 @@ export class ProductRecords {
this.db = new Database("./database/data.db"); this.db = new Database("./database/data.db");
} }
findFromWebflow(webflowProductId: number): ProductsRecord | undefined { findFromWebflow(webflowProductId: number): ProductRecord | undefined {
const row = this.db.query(` const row = this.db.query(`
SELECT * FROM product_records WHERE webflowProductId = ? SELECT * FROM product_records WHERE webflowProductId = ?
`).get(webflowProductId); `).get(webflowProductId);
return row as ProductsRecord | undefined; return row as ProductRecord | undefined;
} }
findFromPrintful(printfulProductId: number): ProductsRecord | undefined { findFromPrintful(printfulProductId: number): ProductRecord | undefined {
const row = this.db.query(` const row = this.db.query(`
SELECT * FROM product_records WHERE printfulProductId = ? SELECT * FROM product_records WHERE printfulProductId = ?
`).get(printfulProductId); `).get(printfulProductId);
return row as ProductsRecord | undefined; return row as ProductRecord | undefined;
} }
add(record: ProductsRecord): void { add(record: ProductRecord): void {
this.db.run(` this.db.run(`
INSERT OR IGNORE INTO product_records (webflowProductId, printfulProductId) INSERT OR IGNORE INTO product_records (webflowProductId, printfulProductId)
VALUES (?, ?) VALUES (?, ?)
`, [record.webflowProductId, record.printfulProductId]); `, [record.webflowProductId, record.printfulProductId]);
} }
all(): ProductsRecord[] { all(): ProductRecord[] {
return this.db.query(`SELECT * FROM product_records`).all() as ProductsRecord[]; return this.db.query(`SELECT * FROM product_records`).all() as ProductRecord[];
} }
} }
+1 -1
View File
@@ -1,4 +1,4 @@
CREATE TABLE IF NOT EXISTS products ( CREATE TABLE IF NOT EXISTS product_records (
webflowProductId INTEGER NOT NULL, webflowProductId INTEGER NOT NULL,
printfulProductId INTEGER NOT NULL, printfulProductId INTEGER NOT NULL,
UNIQUE(webflowProductId, printfulProductId) UNIQUE(webflowProductId, printfulProductId)
+5 -3
View File
@@ -1,5 +1,6 @@
import { Printful } from "./printful.ts" import { Printful } from "./printful.ts"
import { ProductRecords } from "./storage.ts" import { ProductRecords } from "./database/product-records.ts"
import { Webflow } from "./webflow.ts";
const productRecords = new ProductRecords(); const productRecords = new ProductRecords();
@@ -8,12 +9,13 @@ const server = Bun.serve({
"/webhook/printful": { "/webhook/printful": {
async POST(req) { async POST(req) {
const payload: Printful.Webhook.EventPayload = await req.json() const payload: Printful.Webhook.EventPayload = await req.json()
switch (payload.type) { switch (payload.type) {
case Printful.Webhook.Event.ProductUpdated: case Printful.Webhook.Event.ProductUpdated:
const printfulProduct = await Printful.getSyncProduct(payload.data.sync_product.id)
const productRecord = productRecords.findFromPrintful(payload.data.sync_product.id) const productRecord = productRecords.findFromPrintful(payload.data.sync_product.id)
if (productRecord) {
if (productRecord) {
await Webflow.createProduct(printfulProduct, productRecords);
} }
else { else {
+38 -26
View File
@@ -1,5 +1,5 @@
export namespace Printful { export namespace Printful {
const API_ROUTE = "https://api.printful.com/store"; export const API_URL = "https://api.printful.com/store";
export namespace Webhook { export namespace Webhook {
// meta // meta
@@ -78,37 +78,49 @@ export namespace Printful {
} }
interface SyncProductData { interface SyncProductData {
id: number; sync_product: {
external_id: string; id: number;
sync_product_id: number; external_id: string;
name: string;
synced: boolean;
variant_id: number;
retail_price: string;
currency: string;
is_ignored: boolean;
sku: string;
product: {
variant_id: number;
product_id: number;
image: string;
name: string; name: string;
}; variants: number;
files: File[]; synced: number;
options: Option[]; thumbnail_url: string;
main_category_id: number; is_ignored: boolean;
warehouse_product_id: number; },
warehouse_product_variant_id: number; sync_variants: {
size: string; id: number;
color: string; external_id: string;
availability_status: string; sync_product_id: number;
name: string;
synced: boolean;
variant_id: number;
retail_price: string;
currency: string;
is_ignored: boolean;
sku: string;
product: {
variant_id: number;
product_id: number;
image: string;
name: string;
};
files: File[];
options: Option[];
main_category_id: number;
warehouse_product_id: number;
warehouse_product_variant_id: number;
size: string;
color: string;
availability_status: string;
}[]
} }
export type SyncProduct = MetaData<SyncProductData>; export type SyncProduct = MetaData<SyncProductData>;
} }
export async function getSyncProduct(syncProductId: String): Promise<Products.SyncProduct> { export async function getSyncProduct(syncProductId: number): Promise<Products.SyncProduct> {
const payload: Products.SyncProduct = await ( const payload: Products.SyncProduct = await (
await fetch(`${API_ROUTE}/products/${syncProductId}`, { await fetch(`${API_URL}/products/${syncProductId}`, {
method: "GET", method: "GET",
headers: { headers: {
"Authorization": `Bearer ${Bun.env.PRINTFUL_AUTH}`, "Authorization": `Bearer ${Bun.env.PRINTFUL_AUTH}`,
+153
View File
@@ -0,0 +1,153 @@
import { ProductRecords } from "./database/product-records";
import { Printful } from "./printful"
export namespace Webflow {
export const API_URL = `https://api.webflow.com/v2/sites/${Bun.env.WEBFLOW_SITE_ID}`;
const formatSlug = name => name.replaceAll(" ", "-").replaceAll("/", "").toLowerCase();
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 CreateProduct {
product: {
fieldData: {
name: string;
slug: string;
description: string;
"sku-properties": {
id: string;
name: string;
enum: {
id: string;
name: string;
slug: string;
}[];
}[];
}
};
sku: Sku;
publishStatus: string;
}
}
export async function createProduct(printfulProduct: Printful.Products.SyncProduct, productRecords: ProductRecords) {
const printfulVariants = printfulProduct.result.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
},
// TODO
"main-image": "https://www.example.com/image.jpg"
}
});
}
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 fetch(`${API_URL}/products`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `bearer ${Bun.env.WEBFLOW_AUTH}`,
},
body: JSON.stringify({
"product": {
"fieldData": {
"name": printfulProduct.result.sync_product.name,
"slug": formatSlug(printfulProduct.result.sync_product.name),
"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],
})
});
addProductResponse = await addProductResponse.json();
const webflowProductId = addProductResponse["product"]["id"];
const printfulProductId = printfulProduct.result.sync_product.id;
// CREATE WEBFLOW PRODUCT SKUs
let createSkuResponse = await fetch(`${API_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();
// CACHE WEBFLOW/PRINTFUL PRODUCT ASSOCIATION
productRecords.add({ printfulProductId, webflowProductId });
}
}