Create product and SKUs implementation
This commit is contained in:
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
api
|
api
|
||||||
node_modules/
|
node_modules/
|
||||||
.env.*
|
.env.*
|
||||||
./database/data.db
|
database/data.db
|
||||||
|
|||||||
Binary file not shown.
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user