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
node_modules/
.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";
interface ProductsRecord {
interface ProductRecord {
webflowProductId: number;
printfulProductId: number;
}
@@ -12,31 +12,31 @@ export class ProductRecords {
this.db = new Database("./database/data.db");
}
findFromWebflow(webflowProductId: number): ProductsRecord | undefined {
findFromWebflow(webflowProductId: number): ProductRecord | undefined {
const row = this.db.query(`
SELECT * FROM product_records WHERE 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(`
SELECT * FROM product_records WHERE printfulProductId = ?
`).get(printfulProductId);
return row as ProductsRecord | undefined;
return row as ProductRecord | undefined;
}
add(record: ProductsRecord): void {
add(record: ProductRecord): void {
this.db.run(`
INSERT OR IGNORE INTO product_records (webflowProductId, printfulProductId)
VALUES (?, ?)
`, [record.webflowProductId, record.printfulProductId]);
}
all(): ProductsRecord[] {
return this.db.query(`SELECT * FROM product_records`).all() as ProductsRecord[];
all(): ProductRecord[] {
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,
printfulProductId INTEGER NOT NULL,
UNIQUE(webflowProductId, printfulProductId)
+5 -3
View File
@@ -1,5 +1,6 @@
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();
@@ -8,12 +9,13 @@ const server = Bun.serve({
"/webhook/printful": {
async POST(req) {
const payload: Printful.Webhook.EventPayload = await req.json()
switch (payload.type) {
case Printful.Webhook.Event.ProductUpdated:
const printfulProduct = await Printful.getSyncProduct(payload.data.sync_product.id)
const productRecord = productRecords.findFromPrintful(payload.data.sync_product.id)
if (productRecord) {
if (productRecord) {
await Webflow.createProduct(printfulProduct, productRecords);
}
else {
+38 -26
View File
@@ -1,5 +1,5 @@
export namespace Printful {
const API_ROUTE = "https://api.printful.com/store";
export const API_URL = "https://api.printful.com/store";
export namespace Webhook {
// meta
@@ -78,37 +78,49 @@ export namespace Printful {
}
interface SyncProductData {
id: number;
external_id: 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;
sync_product: {
id: number;
external_id: 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;
variants: number;
synced: number;
thumbnail_url: string;
is_ignored: boolean;
},
sync_variants: {
id: number;
external_id: 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 async function getSyncProduct(syncProductId: String): Promise<Products.SyncProduct> {
export async function getSyncProduct(syncProductId: number): Promise<Products.SyncProduct> {
const payload: Products.SyncProduct = await (
await fetch(`${API_ROUTE}/products/${syncProductId}`, {
await fetch(`${API_URL}/products/${syncProductId}`, {
method: "GET",
headers: {
"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 });
}
}