diff --git a/apps/api/src/server.ts b/apps/api/src/server.ts index ba3a04a..b2b9afd 100644 --- a/apps/api/src/server.ts +++ b/apps/api/src/server.ts @@ -1,9 +1,3 @@ -import { - DEFAULT_STANDARDS_DATA, - LevelCalculator, - Standards, -} from "@blade-and-brawn/calculator"; - import { ActivityPerformanceSchema, PlayerSchema, @@ -11,9 +5,7 @@ import { import { cors } from "@elysiajs/cors"; import { Elysia, NotFoundError, status, t } from "elysia"; import { - PrintfulClient, PrintfulError, - WebflowClient, WebflowError, Printful, Webflow, @@ -22,29 +14,16 @@ import zipcodesUs from "zipcodes-us"; import { env, log } from "./util"; import serverTiming from "@elysia/server-timing"; import jwt from "@elysia/jwt"; -import { SyncService } from "./services/sync"; +import { CommerceService } from "./services/commerce"; import cluster from "node:cluster"; import { randomUUIDv7 } from "bun"; +import { CalculatorService } from "./services/calculator"; // SERVICES // ----------- -const levelCalculator = new LevelCalculator( - new Standards(DEFAULT_STANDARDS_DATA), -); -const printful = new PrintfulClient({ - token: env.PRINTFUL_AUTH, - storeId: env.PRINTFUL_STORE_ID, -}); - -const webflow = new WebflowClient({ - siteId: env.WEBFLOW_SITE_ID, - collectionsId: env.WEBFLOW_COLLECTION_ID, - token: env.WEBFLOW_AUTH, - webhookSecret: env.WEBFLOW_WEBHOOK_SECRET, -}); - -const syncService = new SyncService(printful, webflow); +const calculatorService = new CalculatorService(); +const commerceService = new CommerceService(); // ELYSIA // ----------- @@ -122,7 +101,7 @@ export const app = new Elysia() .group("/calculator", (app) => app .post("/calculate", async ({ body }) => { - return { levels: levelCalculator.calculate(body.player, body.activityPerformances) }; + return { levels: calculatorService.calculate(body.player, body.activityPerformances) }; }, { body: t.Object({ player: PlayerSchema, @@ -150,17 +129,17 @@ export const app = new Elysia() app // Sync status .get("/", async ({ sessionId }) => { - const latestSync = await syncService.getLatestSync(sessionId); + const latestSync = await commerceService.getLatestSync(sessionId); if (!latestSync) throw new NotFoundError("No sync found"); return { startDate: latestSync.started_at, - status: syncService.deriveSyncStatus(latestSync), + status: commerceService.deriveSyncStatus(latestSync), syncingPrintfulProductIds: latestSync.syncing_printful_product_ids } }) // Run sync .post("/:printfulProductId?", async ({ params: { printfulProductId }, sessionId }) => { - await syncService.sync({ + await commerceService.sync({ session: { id: sessionId, name: "Portal" }, filter: { printfulProductIds: printfulProductId ? [printfulProductId] : null @@ -169,13 +148,13 @@ export const app = new Elysia() }, { params: t.Object({ printfulProductId: t.Optional(t.Numeric()) }) }), ) .get("/:printfulProductId", async ({ params }) => { - const printfulProduct = await printful.Products.get(params.printfulProductId); + const printfulProduct = await commerceService.printful.Products.get(params.printfulProductId); if (!printfulProduct) throw new NotFoundError("Missing printful product"); const webflowProductId = printfulProduct.sync_product.external_id.split("-")[0]; if (!webflowProductId) throw new NotFoundError("Missing webflow product ID"); - const webflowProduct = await webflow.Products.get(webflowProductId); + const webflowProduct = await commerceService.webflow.Products.get(webflowProductId); return { printfulProduct, webflowProduct }; }, { @@ -193,7 +172,7 @@ export const app = new Elysia() const printfulProduct = payload.data.sync_product; log.info({ productId: printfulProduct.id }, "printful webhook: product updated"); - await syncService.sync({ + await commerceService.sync({ session: { id: randomUUIDv7(), name: "Printful" }, filter: { printfulProductIds: [printfulProduct.id] @@ -207,7 +186,7 @@ export const app = new Elysia() log.info({ externalId: payload.data.sync_product.external_id, webflowProductId }, "printful webhook: product deleted"); if (!webflowProductId) throw new NotFoundError("Missing webflow product ID"); - await webflow.Products.remove(webflowProductId); + await commerceService.webflow.Products.remove(webflowProductId); break; } case Printful.Webhook.Event.PackageShipped: { @@ -215,12 +194,12 @@ export const app = new Elysia() const shipInfo = payload.data.shipment; log.info({ webflowOrderId, carrier: shipInfo.carrier, tracking: shipInfo.tracking_number }, "printful webhook: package shipped"); - await webflow.Orders.update(webflowOrderId, { + await commerceService.webflow.Orders.update(webflowOrderId, { shippingTrackingURL: shipInfo.tracking_url, shippingTracking: shipInfo.tracking_number, shippingProvider: shipInfo.carrier, }); - await webflow.Orders.fulfill(webflowOrderId, { + await commerceService.webflow.Orders.fulfill(webflowOrderId, { sendOrderFulfilledEmail: true, }); break; @@ -230,7 +209,7 @@ export const app = new Elysia() } }) .post("/webhook/webflow", async ({ request, body }) => { - if (!webflow.Util.verifyWebflowSignature(request, body)) + if (!commerceService.webflow.Util.verifyWebflowSignature(request, body)) throw status(400, "Invalid signature"); const payload = body as Webflow.Webhook.EventPayload; @@ -240,7 +219,7 @@ export const app = new Elysia() const webflowOrder = payload.payload; log.info({ orderId: webflowOrder.orderId }, "webflow webhook: order created"); - await printful.Orders.create({ + await commerceService.printful.Orders.create({ external_id: webflowOrder.orderId, // TODO: derive from webflow shipping: "STANDARD", @@ -273,8 +252,8 @@ app.listen(3000, async () => { log.info({ port: 3000 }, "server started") try { - const frontQueuedSync = await syncService.queue.front(); - if (frontQueuedSync) await syncService.sync(frontQueuedSync); + const frontQueuedSync = await commerceService.queue.front(); + if (frontQueuedSync) await commerceService.sync(frontQueuedSync); } catch (err) { log.error({ err }, "failed to resume product sync queue") diff --git a/apps/api/src/services/calculator.ts b/apps/api/src/services/calculator.ts index 398290e..1eb8cfe 100644 --- a/apps/api/src/services/calculator.ts +++ b/apps/api/src/services/calculator.ts @@ -1,3 +1,16 @@ -export class CalculatorService { +import { LevelCalculator, Standards, DEFAULT_STANDARDS_DATA } from "@blade-and-brawn/calculator"; +import type { ActivityPerformance, Player } from "@blade-and-brawn/domain"; +export class CalculatorService { + private readonly levelCalculator; + + constructor() { + this.levelCalculator = new LevelCalculator( + new Standards(DEFAULT_STANDARDS_DATA), + ); + } + + calculate(player: Player, activityPerformances: ActivityPerformance[]) { + return this.levelCalculator.calculate(player, activityPerformances) + } } diff --git a/apps/api/src/services/sync.ts b/apps/api/src/services/commerce.ts similarity index 91% rename from apps/api/src/services/sync.ts rename to apps/api/src/services/commerce.ts index ad8d5c5..76c811b 100644 --- a/apps/api/src/services/sync.ts +++ b/apps/api/src/services/commerce.ts @@ -1,5 +1,5 @@ import { PrintfulClient, ProductSyncer, WebflowClient, type ProductSyncerOptions } from "@blade-and-brawn/commerce"; -import { log } from "../util"; +import { env, log } from "../util"; import { db } from "../database/db"; import { DatabaseError } from "pg"; import type { Insertable, Selectable } from "kysely"; @@ -18,12 +18,24 @@ type QueuedSync = { class AlreadyClaimedError extends Error { } -export class SyncService { +export class CommerceService { + public readonly printful: PrintfulClient; + public readonly webflow: WebflowClient; private readonly productSyncer: ProductSyncer; readonly queue: SyncQueue; - constructor(printful: PrintfulClient, webflow: WebflowClient) { - this.productSyncer = new ProductSyncer(printful, webflow, log); + constructor() { + this.printful = new PrintfulClient({ + token: env.PRINTFUL_AUTH, + storeId: env.PRINTFUL_STORE_ID, + }); + this.webflow = new WebflowClient({ + siteId: env.WEBFLOW_SITE_ID, + collectionsId: env.WEBFLOW_COLLECTION_ID, + token: env.WEBFLOW_AUTH, + webhookSecret: env.WEBFLOW_WEBHOOK_SECRET, + }); + this.productSyncer = new ProductSyncer(this.printful, this.webflow, log); this.queue = new SyncQueue(); } diff --git a/packages/calculator/src/index.ts b/packages/calculator/src/index.ts index 5272d9c..bb54f57 100644 --- a/packages/calculator/src/index.ts +++ b/packages/calculator/src/index.ts @@ -11,8 +11,8 @@ import { type StandardUnit, } from "@blade-and-brawn/domain"; import { levenbergMarquardt as LM } from "ml-levenberg-marquardt"; -import defaultConfig from "./config.json" assert { type: "json" }; -import defaultStandardsJson from "./data/default-standards.json" assert { type: "json" }; +import defaultConfig from "./config.json" with { type: "json" }; +import defaultStandardsJson from "./data/default-standards.json" with { type: "json" }; import { getAvgWeight } from "./avg-weights"; // SOURCES