some organization and implement apparel_order_created event
This commit is contained in:
@@ -1,10 +1,14 @@
|
||||
import { PrintfulClient, ProductSyncer, WebflowClient } from "@blade-and-brawn/commerce";
|
||||
import { PrintfulClient, ProductSyncer, Webflow, WebflowClient } from "@blade-and-brawn/commerce";
|
||||
import { env, log } from "../util";
|
||||
import { db } from "../database/db";
|
||||
import { defineEventType, EventQueue } from "./event-queue";
|
||||
import { Type as t } from "@sinclair/typebox";
|
||||
import { Value } from "@sinclair/typebox/value";
|
||||
import { sql } from "kysely";
|
||||
import zipcodesUs from "zipcodes-us";
|
||||
|
||||
// TODO: read Retry-After header for how long to delay retry on 429 Too many Requests
|
||||
// https://developers.webflow.com/data/reference/rate-limits ...(do printful too)
|
||||
|
||||
export class CommerceService {
|
||||
readonly Printful = new PrintfulClient({
|
||||
@@ -17,11 +21,62 @@ export class CommerceService {
|
||||
token: env.WEBFLOW_AUTH,
|
||||
webhookSecret: env.WEBFLOW_WEBHOOK_SECRET,
|
||||
});
|
||||
readonly ProductSync = new ProductSyncService(this.Printful, this.Webflow);
|
||||
readonly Apparel = new ApparelService(this.Printful, this.Webflow);
|
||||
}
|
||||
|
||||
class ProductSyncService {
|
||||
static readonly EVENT_GROUP = "product_sync";
|
||||
class ApparelService {
|
||||
Syncs: ApparelSyncService;
|
||||
Orders: ApparelOrdersService;
|
||||
constructor(private readonly Printful: PrintfulClient, private readonly Webflow: WebflowClient) {
|
||||
this.Syncs = new ApparelSyncService(this.Printful, this.Webflow);
|
||||
this.Orders = new ApparelOrdersService(this.Printful, this.Webflow);
|
||||
}
|
||||
}
|
||||
|
||||
class ApparelOrdersService {
|
||||
readonly Queue;
|
||||
|
||||
constructor(private readonly Printful: PrintfulClient, private readonly Webflow: WebflowClient) {
|
||||
this.Queue = new EventQueue({
|
||||
group: "apparel_order",
|
||||
cfg: {
|
||||
retry: { limit: 3, delayMs: 10000 },
|
||||
concurrency: { limit: 1, timeoutMs: 600000 }
|
||||
},
|
||||
events: {
|
||||
"apparel_order_create": defineEventType({
|
||||
schemas: { payload: t.Object({ wOrder: t.Object({}) }), state: t.Null() },
|
||||
processor: async (id, payload, setState) => {
|
||||
const wOrder = payload.wOrder as Webflow.Orders.Order;
|
||||
|
||||
await this.Printful.Orders.create({
|
||||
external_id: wOrder.orderId,
|
||||
// TODO: derive from webflow
|
||||
shipping: "STANDARD",
|
||||
recipient: {
|
||||
name: wOrder.shippingAddress.addressee,
|
||||
address1: wOrder.shippingAddress.line1,
|
||||
address2: wOrder.shippingAddress.line2,
|
||||
city: wOrder.shippingAddress.city,
|
||||
state_code: zipcodesUs.find(
|
||||
wOrder.shippingAddress.postalCode.split("-")[0] ?? "",
|
||||
).stateCode,
|
||||
country_code: wOrder.shippingAddress.country,
|
||||
zip: wOrder.shippingAddress.postalCode,
|
||||
},
|
||||
items: wOrder.purchasedItems.map((wOrderSku) => ({
|
||||
external_variant_id: wOrderSku.variantId,
|
||||
quantity: wOrderSku.count,
|
||||
})),
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class ApparelSyncService {
|
||||
readonly Queue;
|
||||
private readonly ProductSyncer: ProductSyncer;
|
||||
|
||||
@@ -30,13 +85,13 @@ class ProductSyncService {
|
||||
constructor(private readonly Printful: PrintfulClient, private readonly Webflow: WebflowClient) {
|
||||
this.ProductSyncer = new ProductSyncer(this.Printful, this.Webflow, log);
|
||||
this.Queue = new EventQueue({
|
||||
group: ProductSyncService.EVENT_GROUP,
|
||||
group: "apparel_sync",
|
||||
cfg: {
|
||||
retry: { limit: 3, delayMs: 10000 },
|
||||
concurrency: { limit: 1, timeoutMs: 600000 }
|
||||
},
|
||||
events: {
|
||||
"product_sync_update": defineEventType({
|
||||
"apparel_sync_update": defineEventType({
|
||||
schemas: {
|
||||
payload: t.Object({
|
||||
session: t.Object({
|
||||
@@ -57,7 +112,7 @@ class ProductSyncService {
|
||||
])
|
||||
},
|
||||
processor: async (id, payload, setState) => {
|
||||
await this.ProductSyncer.sync({
|
||||
await this.ProductSyncer.syncApparel({
|
||||
filter: payload.filter,
|
||||
beforeStep: async (pProductIds: number[]) => {
|
||||
await setState({ pProductIds });
|
||||
@@ -65,7 +120,7 @@ class ProductSyncService {
|
||||
});
|
||||
}
|
||||
}),
|
||||
"product_sync_delete": defineEventType({
|
||||
"apparel_sync_delete": defineEventType({
|
||||
schemas: {
|
||||
payload: t.Object({ wProductId: t.String() }),
|
||||
state: t.Union([t.Object({}), t.Null()])
|
||||
@@ -81,8 +136,8 @@ class ProductSyncService {
|
||||
async getLatestSyncState(sessionId: string) {
|
||||
const latestSync = await db.selectFrom("events")
|
||||
.select(["started_at", "ended_at", "has_failed", "state"])
|
||||
.where("group", "=", ProductSyncService.EVENT_GROUP)
|
||||
.where("type", "=", "product_sync_update")
|
||||
.where("group", "=", this.Queue.group)
|
||||
.where("type", "=", "apparel_sync_update")
|
||||
.where(sql<string>`payload->'session'->>'id'`, "=", sessionId)
|
||||
.orderBy(sql`(started_at is not null and ended_at is null)`, "desc")
|
||||
.orderBy("created_at", "desc")
|
||||
@@ -90,7 +145,7 @@ class ProductSyncService {
|
||||
.executeTakeFirst();
|
||||
if (!latestSync) return;
|
||||
|
||||
Value.Assert(this.Queue.events["product_sync_update"].schemas.state, latestSync.state);
|
||||
Value.Assert(this.Queue.events["apparel_sync_update"].schemas.state, latestSync.state);
|
||||
|
||||
return {
|
||||
startDate: latestSync.started_at,
|
||||
|
||||
Reference in New Issue
Block a user