From bdacfb535e621f45b1f2df58bfc5e715fde89ce4 Mon Sep 17 00:00:00 2001 From: Dominic Ferrando Date: Sat, 13 Sep 2025 13:52:53 -0400 Subject: [PATCH] Product sync --- .env | 6 ----- .gitignore | 2 +- commerce/printful.ts | 52 ++++++++++++++++++++++++++----------- commerce/product-records.ts | 8 +++--- commerce/webflow.ts | 31 +++++++++++----------- fly.toml | 2 +- main.ts | 42 +++++++++++++++++++++++++----- 7 files changed, 93 insertions(+), 50 deletions(-) delete mode 100644 .env diff --git a/.env b/.env deleted file mode 100644 index e6a1ba9..0000000 --- a/.env +++ /dev/null @@ -1,6 +0,0 @@ -PRINTFUL_AUTH=RGI4WiWeQkrEYek3obwxZL6c424LAJsUimDX1bHF -PRINTFUL_STORE_ID=15619272 - -WEBFLOW_AUTH=115ded1e97162f5d4835bcb87e236fe4bce1ad602f519bca8179394092a8c6f6 -WEBFLOW_SITE_ID=6737d2c9511d414861f1726e -WEBFLOW_COLLECTION_ID=675a3fbe966e58ea9aa7110f diff --git a/.gitignore b/.gitignore index 3f45fa8..16a0751 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ node_modules/ -.env.* +.env database/data.db diff --git a/commerce/printful.ts b/commerce/printful.ts index 9744046..2112e1b 100644 --- a/commerce/printful.ts +++ b/commerce/printful.ts @@ -46,11 +46,16 @@ export namespace Printful { } export namespace Products { - interface MetaData { + export interface MetaDataSingle { code: number; result: T; } + export interface MetaDataMulti { + code: number; + result: T[]; + } + interface Option { id: string; value: string; @@ -77,7 +82,7 @@ export namespace Printful { stitch_count_tier: string; } - interface SyncProductData { + export interface SyncProduct { sync_product: { id: number; external_id: string; @@ -115,20 +120,37 @@ export namespace Printful { }[] } - export type SyncProduct = MetaData; - } - export async function getSyncProduct(syncProductId: number): Promise { - const payload: Products.SyncProduct = await ( - await fetch(`${Printful.API_URL}/store/products/${syncProductId}`, { - method: "GET", - headers: { - "Authorization": `Bearer ${Bun.env.PRINTFUL_AUTH}`, - "X-PF-Store-Id": `${Bun.env.PRINTFUL_STORE_ID}` - } - }) - ).json(); + export async function getSyncProduct(syncProductId: number): Promise { + const payload: MetaDataSingle = await ( + await fetch(`${Printful.API_URL}/store/products/${syncProductId}`, { + method: "GET", + headers: { + "Authorization": `Bearer ${Bun.env.PRINTFUL_AUTH}`, + "X-PF-Store-Id": `${Bun.env.PRINTFUL_STORE_ID}` + } + }) + ).json(); - return payload; + return payload.result; + } + + export async function getSyncProducts(): Promise { + const payload: MetaDataMulti = await ( + await fetch(`${Printful.API_URL}/store/products`, { + method: "GET", + headers: { + "Authorization": `Bearer ${Bun.env.PRINTFUL_AUTH}`, + "X-PF-Store-Id": `${Bun.env.PRINTFUL_STORE_ID}` + } + }) + ).json(); + + // need to fetch variants + const syncProducts = await Promise.all( + payload.result.map(p => getSyncProduct(p.id)) + ); + return syncProducts; + } } } diff --git a/commerce/product-records.ts b/commerce/product-records.ts index 46d826b..eba7101 100644 --- a/commerce/product-records.ts +++ b/commerce/product-records.ts @@ -16,7 +16,7 @@ class ProductRecords { this.db = new Database(dbPath); } - findFromWebflow(webflowProductId: number): ProductRecord | undefined { + findUsingWebflow(webflowProductId: number): ProductRecord | undefined { const row = this.db.query(` SELECT * FROM product_records WHERE webflowProductId = ? `).get(webflowProductId); @@ -24,7 +24,7 @@ class ProductRecords { return row as ProductRecord | undefined; } - findFromPrintful(printfulProductId: number): ProductRecord | undefined { + findUsingPrintful(printfulProductId: number): ProductRecord | undefined { const row = this.db.query(` SELECT * FROM product_records WHERE printfulProductId = ? `).get(printfulProductId); @@ -32,13 +32,13 @@ class ProductRecords { return row as ProductRecord | undefined; } - deleteFromWebflow(webflowProductId: number) { + deleteUsingWebflow(webflowProductId: number) { this.db.run(` DELETE FROM product_records WHERE webflowProductId = ? `, [webflowProductId]); } - deleteFromPrintful(printfulProductId: number) { + deleteUsingPrintful(printfulProductId: number) { this.db.run(` DELETE FROM product_records WHERE printfulProductId = ? `, [printfulProductId]); diff --git a/commerce/webflow.ts b/commerce/webflow.ts index c891132..8515c2b 100644 --- a/commerce/webflow.ts +++ b/commerce/webflow.ts @@ -26,7 +26,7 @@ export namespace Webflow { unit: string; currency: string; }; - "main-image": string; + "main-image"?: string; } } @@ -52,8 +52,8 @@ export namespace Webflow { } } - export async function deleteProductFromPrintful(printfulProductId: number) { - const webflowProductId = productRecords.findFromPrintful(printfulProductId)?.webflowProductId; + export async function deleteProductUsingPrintful(printfulProductId: number) { + const webflowProductId = productRecords.findUsingPrintful(printfulProductId)?.webflowProductId; if (webflowProductId) { const res = await fetch(`${Webflow.API_COLLECTIONS_URL}/items/${webflowProductId}`, { method: "DELETE", @@ -63,17 +63,17 @@ export namespace Webflow { }, body: JSON.stringify({}) }); - productRecords.deleteFromWebflow(webflowProductId); + productRecords.deleteUsingWebflow(webflowProductId); } } - export async function updateProductFromPrintful(printfulProduct: Printful.Products.SyncProduct) { - const webflowProductId = productRecords.findFromPrintful(printfulProduct.result.sync_product.id)?.webflowProductId; + export async function updateProductUsingPrintful(printfulProduct: Printful.Products.SyncProduct) { + const webflowProductId = productRecords.findUsingPrintful(printfulProduct.sync_product.id)?.webflowProductId; if (!webflowProductId) { throw new Error("Product is not recognized"); } - const printfulVariants = printfulProduct.result.sync_variants; + const printfulVariants = printfulProduct.sync_variants; const webflowVariants: Products.Sku[] = []; for (const printfulVariant of printfulVariants) { webflowVariants.push({ @@ -107,8 +107,8 @@ export namespace Webflow { body: JSON.stringify({ "product": { "fieldData": { - "name": printfulProduct.result.sync_product.name, - "slug": formatSlug(printfulProduct.result.sync_product.name), + "name": printfulProduct.sync_product.name, + "slug": formatSlug(printfulProduct.sync_product.name), "sku-properties": [ { "id": "color", @@ -157,12 +157,11 @@ export namespace Webflow { "sku": webflowVariants[i] }) }); - console.log(JSON.stringify((await res.json()))); } } - export async function createProductFromPrintful(printfulProduct: Printful.Products.SyncProduct) { - const printfulVariants = printfulProduct.result.sync_variants; + export async function createProductUsingPrintful(printfulProduct: Printful.Products.SyncProduct) { + const printfulVariants = printfulProduct.sync_variants; const webflowVariants: Products.Sku[] = []; for (const printfulVariant of printfulVariants) { @@ -180,7 +179,7 @@ export namespace Webflow { "currency": printfulVariant.currency }, // TODO: sync image - "main-image": "https://www.example.com/image.jpg" + // "main-image": "https://www.example.com/image.jpg" } }); } @@ -198,8 +197,8 @@ export namespace Webflow { body: JSON.stringify({ "product": { "fieldData": { - "name": printfulProduct.result.sync_product.name, - "slug": formatSlug(printfulProduct.result.sync_product.name), + "name": printfulProduct.sync_product.name, + "slug": formatSlug(printfulProduct.sync_product.name), "sku-properties": [ { "id": "color", @@ -232,7 +231,7 @@ export namespace Webflow { } const webflowProductId = addProductResponse["product"]["id"]; - const printfulProductId = printfulProduct.result.sync_product.id; + const printfulProductId = printfulProduct.sync_product.id; // CREATE WEBFLOW PRODUCT SKUs let createSkuResponse = await fetch(`${Webflow.API_SITES_URL}/products/${webflowProductId}/skus`, { diff --git a/fly.toml b/fly.toml index 695367f..83b46eb 100644 --- a/fly.toml +++ b/fly.toml @@ -26,4 +26,4 @@ primary_region = 'ord' [[mounts]] source = "data" - destination = "/data" \ No newline at end of file + destination = "/data" diff --git a/main.ts b/main.ts index e897274..3243c77 100644 --- a/main.ts +++ b/main.ts @@ -61,32 +61,60 @@ const server = Bun.serve({ }) } }, + "/products/sync": { + OPTIONS() { + return new ClientResponse(undefined, { status: 204 }); + }, + async POST(req) { + const printfulProducts = await Printful.Products.getSyncProducts(); + + // handle update/create + for (const printfulProduct of printfulProducts) { + const productRecord = productRecords.findUsingPrintful(printfulProduct.sync_product.id); + if (productRecord) { + await Webflow.updateProductUsingPrintful(printfulProduct); + } + else { + await Webflow.createProductUsingPrintful(printfulProduct); + } + } + + // handle delete + // for (const productRecord of productRecords.all()) { + // const existsInPrintful = printfulProducts + // .some(printfulProduct => printfulProduct.sync_product.id === productRecord.printfulProductId); + // if (!existsInPrintful) { + // await Webflow.deleteProductUsingPrintful(productRecord.printfulProductId); + // } + // } + + return ClientResponse.json({}); + } + }, "/webhook/printful": { OPTIONS() { return new ClientResponse(undefined, { status: 204 }); }, async POST(req) { const payload: Printful.Webhook.EventPayload = await req.json() - console.log(JSON.stringify(payload)); - console.log("PRINTFUL WEBHOOK: " + payload.type); try { switch (payload.type) { // ADD/UPDATE 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 printfulProduct = await Printful.Products.getSyncProduct(payload.data.sync_product.id); + const productRecord = productRecords.findUsingPrintful(payload.data.sync_product.id); if (productRecord) { - await Webflow.updateProductFromPrintful(printfulProduct); + await Webflow.updateProductUsingPrintful(printfulProduct); } else { - await Webflow.createProductFromPrintful(printfulProduct); + await Webflow.createProductUsingPrintful(printfulProduct); } break; } // DELETE case Printful.Webhook.Event.ProductDeleted: { - await Webflow.deleteProductFromPrintful(payload.data.sync_product.id); + await Webflow.deleteProductUsingPrintful(payload.data.sync_product.id); break; } }