Restructure services

This commit is contained in:
Dominic Ferrando
2026-07-04 00:51:42 -04:00
parent 2eea7216d6
commit d042340c8b
4 changed files with 50 additions and 46 deletions
+18 -39
View File
@@ -1,9 +1,3 @@
import {
DEFAULT_STANDARDS_DATA,
LevelCalculator,
Standards,
} from "@blade-and-brawn/calculator";
import { import {
ActivityPerformanceSchema, ActivityPerformanceSchema,
PlayerSchema, PlayerSchema,
@@ -11,9 +5,7 @@ import {
import { cors } from "@elysiajs/cors"; import { cors } from "@elysiajs/cors";
import { Elysia, NotFoundError, status, t } from "elysia"; import { Elysia, NotFoundError, status, t } from "elysia";
import { import {
PrintfulClient,
PrintfulError, PrintfulError,
WebflowClient,
WebflowError, WebflowError,
Printful, Printful,
Webflow, Webflow,
@@ -22,29 +14,16 @@ import zipcodesUs from "zipcodes-us";
import { env, log } from "./util"; import { env, log } from "./util";
import serverTiming from "@elysia/server-timing"; import serverTiming from "@elysia/server-timing";
import jwt from "@elysia/jwt"; import jwt from "@elysia/jwt";
import { SyncService } from "./services/sync"; import { CommerceService } from "./services/commerce";
import cluster from "node:cluster"; import cluster from "node:cluster";
import { randomUUIDv7 } from "bun"; import { randomUUIDv7 } from "bun";
import { CalculatorService } from "./services/calculator";
// SERVICES // SERVICES
// ----------- // -----------
const levelCalculator = new LevelCalculator(
new Standards(DEFAULT_STANDARDS_DATA),
);
const printful = new PrintfulClient({ const calculatorService = new CalculatorService();
token: env.PRINTFUL_AUTH, const commerceService = new CommerceService();
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);
// ELYSIA // ELYSIA
// ----------- // -----------
@@ -122,7 +101,7 @@ export const app = new Elysia()
.group("/calculator", .group("/calculator",
(app) => app (app) => app
.post("/calculate", async ({ body }) => { .post("/calculate", async ({ body }) => {
return { levels: levelCalculator.calculate(body.player, body.activityPerformances) }; return { levels: calculatorService.calculate(body.player, body.activityPerformances) };
}, { }, {
body: t.Object({ body: t.Object({
player: PlayerSchema, player: PlayerSchema,
@@ -150,17 +129,17 @@ export const app = new Elysia()
app app
// Sync status // Sync status
.get("/", async ({ sessionId }) => { .get("/", async ({ sessionId }) => {
const latestSync = await syncService.getLatestSync(sessionId); const latestSync = await commerceService.getLatestSync(sessionId);
if (!latestSync) throw new NotFoundError("No sync found"); if (!latestSync) throw new NotFoundError("No sync found");
return { return {
startDate: latestSync.started_at, startDate: latestSync.started_at,
status: syncService.deriveSyncStatus(latestSync), status: commerceService.deriveSyncStatus(latestSync),
syncingPrintfulProductIds: latestSync.syncing_printful_product_ids syncingPrintfulProductIds: latestSync.syncing_printful_product_ids
} }
}) })
// Run sync // Run sync
.post("/:printfulProductId?", async ({ params: { printfulProductId }, sessionId }) => { .post("/:printfulProductId?", async ({ params: { printfulProductId }, sessionId }) => {
await syncService.sync({ await commerceService.sync({
session: { id: sessionId, name: "Portal" }, session: { id: sessionId, name: "Portal" },
filter: { filter: {
printfulProductIds: printfulProductId ? [printfulProductId] : null printfulProductIds: printfulProductId ? [printfulProductId] : null
@@ -169,13 +148,13 @@ export const app = new Elysia()
}, { params: t.Object({ printfulProductId: t.Optional(t.Numeric()) }) }), }, { params: t.Object({ printfulProductId: t.Optional(t.Numeric()) }) }),
) )
.get("/:printfulProductId", async ({ params }) => { .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"); if (!printfulProduct) throw new NotFoundError("Missing printful product");
const webflowProductId = printfulProduct.sync_product.external_id.split("-")[0]; const webflowProductId = printfulProduct.sync_product.external_id.split("-")[0];
if (!webflowProductId) throw new NotFoundError("Missing webflow product ID"); 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 }; return { printfulProduct, webflowProduct };
}, { }, {
@@ -193,7 +172,7 @@ export const app = new Elysia()
const printfulProduct = payload.data.sync_product; const printfulProduct = payload.data.sync_product;
log.info({ productId: printfulProduct.id }, "printful webhook: product updated"); log.info({ productId: printfulProduct.id }, "printful webhook: product updated");
await syncService.sync({ await commerceService.sync({
session: { id: randomUUIDv7(), name: "Printful" }, session: { id: randomUUIDv7(), name: "Printful" },
filter: { filter: {
printfulProductIds: [printfulProduct.id] 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"); log.info({ externalId: payload.data.sync_product.external_id, webflowProductId }, "printful webhook: product deleted");
if (!webflowProductId) throw new NotFoundError("Missing webflow product ID"); if (!webflowProductId) throw new NotFoundError("Missing webflow product ID");
await webflow.Products.remove(webflowProductId); await commerceService.webflow.Products.remove(webflowProductId);
break; break;
} }
case Printful.Webhook.Event.PackageShipped: { case Printful.Webhook.Event.PackageShipped: {
@@ -215,12 +194,12 @@ export const app = new Elysia()
const shipInfo = payload.data.shipment; const shipInfo = payload.data.shipment;
log.info({ webflowOrderId, carrier: shipInfo.carrier, tracking: shipInfo.tracking_number }, "printful webhook: package shipped"); 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, shippingTrackingURL: shipInfo.tracking_url,
shippingTracking: shipInfo.tracking_number, shippingTracking: shipInfo.tracking_number,
shippingProvider: shipInfo.carrier, shippingProvider: shipInfo.carrier,
}); });
await webflow.Orders.fulfill(webflowOrderId, { await commerceService.webflow.Orders.fulfill(webflowOrderId, {
sendOrderFulfilledEmail: true, sendOrderFulfilledEmail: true,
}); });
break; break;
@@ -230,7 +209,7 @@ export const app = new Elysia()
} }
}) })
.post("/webhook/webflow", async ({ request, body }) => { .post("/webhook/webflow", async ({ request, body }) => {
if (!webflow.Util.verifyWebflowSignature(request, body)) if (!commerceService.webflow.Util.verifyWebflowSignature(request, body))
throw status(400, "Invalid signature"); throw status(400, "Invalid signature");
const payload = body as Webflow.Webhook.EventPayload; const payload = body as Webflow.Webhook.EventPayload;
@@ -240,7 +219,7 @@ export const app = new Elysia()
const webflowOrder = payload.payload; const webflowOrder = payload.payload;
log.info({ orderId: webflowOrder.orderId }, "webflow webhook: order created"); log.info({ orderId: webflowOrder.orderId }, "webflow webhook: order created");
await printful.Orders.create({ await commerceService.printful.Orders.create({
external_id: webflowOrder.orderId, external_id: webflowOrder.orderId,
// TODO: derive from webflow // TODO: derive from webflow
shipping: "STANDARD", shipping: "STANDARD",
@@ -273,8 +252,8 @@ app.listen(3000, async () => {
log.info({ port: 3000 }, "server started") log.info({ port: 3000 }, "server started")
try { try {
const frontQueuedSync = await syncService.queue.front(); const frontQueuedSync = await commerceService.queue.front();
if (frontQueuedSync) await syncService.sync(frontQueuedSync); if (frontQueuedSync) await commerceService.sync(frontQueuedSync);
} }
catch (err) { catch (err) {
log.error({ err }, "failed to resume product sync queue") log.error({ err }, "failed to resume product sync queue")
+14 -1
View File
@@ -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)
}
} }
@@ -1,5 +1,5 @@
import { PrintfulClient, ProductSyncer, WebflowClient, type ProductSyncerOptions } from "@blade-and-brawn/commerce"; 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 { db } from "../database/db";
import { DatabaseError } from "pg"; import { DatabaseError } from "pg";
import type { Insertable, Selectable } from "kysely"; import type { Insertable, Selectable } from "kysely";
@@ -18,12 +18,24 @@ type QueuedSync = {
class AlreadyClaimedError extends Error { } class AlreadyClaimedError extends Error { }
export class SyncService { export class CommerceService {
public readonly printful: PrintfulClient;
public readonly webflow: WebflowClient;
private readonly productSyncer: ProductSyncer; private readonly productSyncer: ProductSyncer;
readonly queue: SyncQueue; readonly queue: SyncQueue;
constructor(printful: PrintfulClient, webflow: WebflowClient) { constructor() {
this.productSyncer = new ProductSyncer(printful, webflow, log); 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(); this.queue = new SyncQueue();
} }
+2 -2
View File
@@ -11,8 +11,8 @@ import {
type StandardUnit, type StandardUnit,
} from "@blade-and-brawn/domain"; } from "@blade-and-brawn/domain";
import { levenbergMarquardt as LM } from "ml-levenberg-marquardt"; import { levenbergMarquardt as LM } from "ml-levenberg-marquardt";
import defaultConfig from "./config.json" assert { type: "json" }; import defaultConfig from "./config.json" with { type: "json" };
import defaultStandardsJson from "./data/default-standards.json" assert { type: "json" }; import defaultStandardsJson from "./data/default-standards.json" with { type: "json" };
import { getAvgWeight } from "./avg-weights"; import { getAvgWeight } from "./avg-weights";
// SOURCES // SOURCES