Restructure services
This commit is contained in:
+18
-39
@@ -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")
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user