More service naming consistency cleanup

This commit is contained in:
Dominic Ferrando
2026-07-04 15:10:06 -04:00
parent f4aad56ad3
commit afdc8de77c
5 changed files with 113 additions and 102 deletions
+23 -22
View File
@@ -22,9 +22,10 @@ import { StandardsParametersSchema } from "@blade-and-brawn/calculator";
// SERVICES // SERVICES
// ----------- // -----------
const s = {
const calculatorService = new CalculatorService(); Calculator: new CalculatorService(),
const commerceService = new CommerceService(); Commerce: new CommerceService()
};
// ELYSIA // ELYSIA
// ----------- // -----------
@@ -103,7 +104,7 @@ export const app = new Elysia()
(app) => app (app) => app
// Non-authenticated // Non-authenticated
.post("/calculate", async ({ body }) => { .post("/calculate", async ({ body }) => {
return { levels: calculatorService.calculate(body.player, body.activityPerformances) }; return { levels: s.Calculator.calculate(body.player, body.activityPerformances) };
}, { }, {
body: t.Object({ body: t.Object({
player: PlayerSchema, player: PlayerSchema,
@@ -117,23 +118,23 @@ export const app = new Elysia()
return { sessionId: token.sessionId.toString() }; return { sessionId: token.sessionId.toString() };
}) })
.put("/config/:id", async ({ params: { id }, body: { datasetId, parameters } }) => { .put("/config/:id", async ({ params: { id }, body: { datasetId, parameters } }) => {
await calculatorService.standards.setConfig(id, datasetId, parameters); await s.Calculator.Standards.Config.update(id, datasetId, parameters);
}, { }, {
params: t.Object({ id: t.String() }), params: t.Object({ id: t.String() }),
body: t.Object({ datasetId: t.String(), parameters: StandardsParametersSchema }) body: t.Object({ datasetId: t.String(), parameters: StandardsParametersSchema })
}) })
.post("/config", async ({ body: { datasetId, parameters } }) => { .post("/config", async ({ body: { datasetId, parameters } }) => {
await calculatorService.standards.createConfig(datasetId, parameters); await s.Calculator.Standards.Config.create(datasetId, parameters);
}, { }, {
body: t.Object({ datasetId: t.String(), parameters: StandardsParametersSchema }) body: t.Object({ datasetId: t.String(), parameters: StandardsParametersSchema })
}) })
.post("/config/:id/select", async ({ params: { id } }) => { .post("/config/:id/switch", async ({ params: { id } }) => {
await calculatorService.standards.selectConfig(id); await s.Calculator.Standards.Config.switch(id);
}, { }, {
params: t.Object({ id: t.String() }) params: t.Object({ id: t.String() })
}) })
.get("/config", async () => { .get("/config", async () => {
return await calculatorService.standards.getSelectedConfig(); return await s.Calculator.Standards.Config.get();
}) })
) )
@@ -149,17 +150,17 @@ export const app = new Elysia()
app app
// Sync status // Sync status
.get("/", async ({ sessionId }) => { .get("/", async ({ sessionId }) => {
const latestSync = await commerceService.getLatestSync(sessionId); const latestSync = await s.Commerce.Sync.getLatest(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: commerceService.deriveSyncStatus(latestSync), status: s.Commerce.Sync.deriveStatus(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 commerceService.sync({ await s.Commerce.Sync.start({
session: { id: sessionId, name: "Portal" }, session: { id: sessionId, name: "Portal" },
filter: { filter: {
printfulProductIds: printfulProductId ? [printfulProductId] : null printfulProductIds: printfulProductId ? [printfulProductId] : null
@@ -168,13 +169,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: { printfulProductId } }) => { .get("/:printfulProductId", async ({ params: { printfulProductId } }) => {
const printfulProduct = await commerceService.printful.Products.get(printfulProductId); const printfulProduct = await s.Commerce.Printful.Products.get(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 commerceService.webflow.Products.get(webflowProductId); const webflowProduct = await s.Commerce.Webflow.Products.get(webflowProductId);
return { printfulProduct, webflowProduct }; return { printfulProduct, webflowProduct };
}, { }, {
@@ -192,7 +193,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 commerceService.sync({ await s.Commerce.Sync.start({
session: { id: randomUUIDv7(), name: "Printful" }, session: { id: randomUUIDv7(), name: "Printful" },
filter: { filter: {
printfulProductIds: [printfulProduct.id] printfulProductIds: [printfulProduct.id]
@@ -206,7 +207,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 commerceService.webflow.Products.remove(webflowProductId); await s.Commerce.Webflow.Products.remove(webflowProductId);
break; break;
} }
case Printful.Webhook.Event.PackageShipped: { case Printful.Webhook.Event.PackageShipped: {
@@ -214,12 +215,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 commerceService.webflow.Orders.update(webflowOrderId, { await s.Commerce.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 commerceService.webflow.Orders.fulfill(webflowOrderId, { await s.Commerce.Webflow.Orders.fulfill(webflowOrderId, {
sendOrderFulfilledEmail: true, sendOrderFulfilledEmail: true,
}); });
break; break;
@@ -229,7 +230,7 @@ export const app = new Elysia()
} }
}) })
.post("/webhook/webflow", async ({ request, body }) => { .post("/webhook/webflow", async ({ request, body }) => {
if (!commerceService.webflow.Util.verifyWebflowSignature(request, body)) if (!s.Commerce.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;
@@ -239,7 +240,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 commerceService.printful.Orders.create({ await s.Commerce.Printful.Orders.create({
external_id: webflowOrder.orderId, external_id: webflowOrder.orderId,
// TODO: derive from webflow // TODO: derive from webflow
shipping: "STANDARD", shipping: "STANDARD",
@@ -272,8 +273,8 @@ app.listen(3000, async () => {
log.info({ port: 3000 }, "server started") log.info({ port: 3000 }, "server started")
try { try {
const frontQueuedSync = await commerceService.queue.front(); const frontQueuedSync = await s.Commerce.Sync.Queue.front();
if (frontQueuedSync) await commerceService.sync(frontQueuedSync); if (frontQueuedSync) await s.Commerce.Sync.start(frontQueuedSync);
} }
catch (err) { catch (err) {
log.error({ err }, "failed to resume product sync queue") log.error({ err }, "failed to resume product sync queue")
+42 -33
View File
@@ -5,45 +5,46 @@ import { Value } from "@sinclair/typebox/value";
import { log } from "../util"; import { log } from "../util";
export class CalculatorService { export class CalculatorService {
private calculator: LevelCalculator = new LevelCalculator(new Standards(DEFAULT_STANDARDS_DATASET)); private Calculator = new LevelCalculator(new Standards(DEFAULT_STANDARDS_DATASET));
standards: StandardsService = new StandardsService(this.calculator); Standards: StandardsService = new StandardsService(this.Calculator);
calculate(player: Player, activityPerformances: ActivityPerformance[]) { calculate(player: Player, activityPerformances: ActivityPerformance[]) {
this.standards.refreshConfig({ onlyIfStale: true }).catch((err) => log.error({ err }, "failed to refresh standards config")); this.Standards.Config.refresh({ onlyIfStale: true }).catch((err) => log.error({ err }, "failed to refresh standards config"));
return this.calculator.calculate(player, activityPerformances); return this.Calculator.calculate(player, activityPerformances);
} }
} }
class StandardsService { class StandardsService {
private static readonly CONFIG_REFRESH_INTERVAL_MS = 30_000; Config: StandardsConfigService;
constructor(private Calculator: LevelCalculator) {
this.Config = new StandardsConfigService(this.Calculator);
}
}
class StandardsConfigService {
private static readonly REFRESH_INTERVAL_MS = 30_000;
private cachedParametersStringified: string | null = null; private cachedParametersStringified: string | null = null;
private cachedDatasetId: string | null = null; private cachedDatasetId: string | null = null;
private lastRefreshedAt = 0; private lastRefreshedAt = 0;
constructor(private calculator: LevelCalculator) { } constructor(private Calculator: LevelCalculator) { }
async getSelectedConfig(opt: { skipCache?: boolean } = {}): Promise<StandardsParameters> { async create(datasetId: string, parameters: StandardsParameters) {
if (!opt.skipCache) return this.calculator.standards.params; await db.insertInto("standards_configs")
.values({ parameters, dataset_id: datasetId })
await this.refreshConfig(); .execute();
return this.calculator.standards.params;
} }
async selectConfig(id: string) { /** Retrieves the currently selected configuration */
await db.transaction().execute(async (trx) => { async get(opt: { skipCache?: boolean } = {}): Promise<StandardsParameters> {
await trx.updateTable("standards_configs") if (!opt.skipCache) return this.Calculator.Standards.params;
.set({ is_selected: false })
.where("is_selected", "=", true) await this.refresh();
.execute(); return this.Calculator.Standards.params;
const result = await trx.updateTable("standards_configs")
.set({ is_selected: true })
.where("id", "=", id)
.execute();
if (result[0]?.numUpdatedRows === 0n) throw new Error(`No config with id "${id}"`);
});
} }
async setConfig(id: string, datasetId: string, parameters: StandardsParameters) { async update(id: string, datasetId: string, parameters: StandardsParameters) {
const result = await db.updateTable("standards_configs") const result = await db.updateTable("standards_configs")
.set({ parameters, dataset_id: datasetId }) .set({ parameters, dataset_id: datasetId })
.where("id", "=", id) .where("id", "=", id)
@@ -51,15 +52,9 @@ class StandardsService {
if (result[0]?.numUpdatedRows === 0n) throw new Error(`No config with id "${id}"`); if (result[0]?.numUpdatedRows === 0n) throw new Error(`No config with id "${id}"`);
} }
async createConfig(datasetId: string, parameters: StandardsParameters) { async refresh(opt: { onlyIfStale?: boolean } = {}) {
await db.insertInto("standards_configs")
.values({ parameters, dataset_id: datasetId })
.execute();
}
async refreshConfig(opt: { onlyIfStale?: boolean } = {}) {
if (opt.onlyIfStale) if (opt.onlyIfStale)
if (Date.now() - this.lastRefreshedAt < StandardsService.CONFIG_REFRESH_INTERVAL_MS) return; if (Date.now() - this.lastRefreshedAt < StandardsConfigService.REFRESH_INTERVAL_MS) return;
this.lastRefreshedAt = Date.now(); this.lastRefreshedAt = Date.now();
@@ -79,8 +74,22 @@ class StandardsService {
const parametersStringified = JSON.stringify(config.parameters); const parametersStringified = JSON.stringify(config.parameters);
if (parametersStringified === this.cachedParametersStringified && config.datasetId === this.cachedDatasetId) return; if (parametersStringified === this.cachedParametersStringified && config.datasetId === this.cachedDatasetId) return;
this.calculator.standards = new Standards(config.dataset, config.parameters); this.Calculator.Standards = new Standards(config.dataset, config.parameters);
this.cachedParametersStringified = parametersStringified; this.cachedParametersStringified = parametersStringified;
this.cachedDatasetId = config.datasetId; this.cachedDatasetId = config.datasetId;
} }
async switch(id: string) {
await db.transaction().execute(async (trx) => {
await trx.updateTable("standards_configs")
.set({ is_selected: false })
.where("is_selected", "=", true)
.execute();
const result = await trx.updateTable("standards_configs")
.set({ is_selected: true })
.where("id", "=", id)
.execute();
if (result[0]?.numUpdatedRows === 0n) throw new Error(`No config with id "${id}"`);
});
}
} }
+22 -21
View File
@@ -19,32 +19,33 @@ type QueuedSync = {
class AlreadyClaimedError extends Error { } class AlreadyClaimedError extends Error { }
export class CommerceService { export class CommerceService {
public readonly printful: PrintfulClient; public readonly Printful = new PrintfulClient({
public readonly webflow: WebflowClient;
private readonly productSyncer: ProductSyncer;
readonly queue: SyncQueue;
constructor() {
this.printful = new PrintfulClient({
token: env.PRINTFUL_AUTH, token: env.PRINTFUL_AUTH,
storeId: env.PRINTFUL_STORE_ID, storeId: env.PRINTFUL_STORE_ID,
}); });
this.webflow = new WebflowClient({ public readonly Webflow = new WebflowClient({
siteId: env.WEBFLOW_SITE_ID, siteId: env.WEBFLOW_SITE_ID,
collectionsId: env.WEBFLOW_COLLECTION_ID, collectionsId: env.WEBFLOW_COLLECTION_ID,
token: env.WEBFLOW_AUTH, token: env.WEBFLOW_AUTH,
webhookSecret: env.WEBFLOW_WEBHOOK_SECRET, webhookSecret: env.WEBFLOW_WEBHOOK_SECRET,
}); });
this.productSyncer = new ProductSyncer(this.printful, this.webflow, log); readonly Sync = new SyncService(this.Printful, this.Webflow);
this.queue = new SyncQueue();
} }
async sync(queuedSync: QueuedSync) { class SyncService {
readonly Queue = new SyncQueueService();
private readonly ProductSyncer: ProductSyncer;
constructor(private readonly Printful: PrintfulClient, private readonly Webflow: WebflowClient) {
this.ProductSyncer = new ProductSyncer(this.Printful, this.Webflow, log);
}
async start(queuedSync: QueuedSync) {
let id = queuedSync.id ?? null; let id = queuedSync.id ?? null;
let freedSlot = false; let freedSlot = false;
try { try {
log.info({ session: queuedSync.session }, "starting sync run"); log.info({ session: queuedSync.session }, "starting sync run");
await this.productSyncer.sync({ await this.ProductSyncer.run({
filter: queuedSync.filter, filter: queuedSync.filter,
onStart: async (startDate) => { onStart: async (startDate) => {
const values: Insertable<ProductSyncs> = { const values: Insertable<ProductSyncs> = {
@@ -95,7 +96,7 @@ export class CommerceService {
return; return;
} }
const activeSync = await this.getActiveSync(); const activeSync = await this.getActive();
const activeSyncConflict = activeSync && err instanceof DatabaseError && err.code === "23505"; const activeSyncConflict = activeSync && err instanceof DatabaseError && err.code === "23505";
if (activeSyncConflict) { if (activeSyncConflict) {
@@ -106,11 +107,11 @@ export class CommerceService {
.set({ syncing_printful_product_ids: [], ended_at: new Date(), has_failed: true }) .set({ syncing_printful_product_ids: [], ended_at: new Date(), has_failed: true })
.where("id", "=", activeSync.id) .where("id", "=", activeSync.id)
.execute(); .execute();
await this.sync(queuedSync); await this.start(queuedSync);
} }
else { else {
log.info("sync already active, enqueuing next sync"); log.info("sync already active, enqueuing next sync");
await this.queue.enqueue(queuedSync); await this.Queue.enqueue(queuedSync);
} }
} }
else { else {
@@ -127,8 +128,8 @@ export class CommerceService {
finally { finally {
if (freedSlot) { if (freedSlot) {
try { try {
const frontQueuedSync = await this.queue.front(); const frontQueuedSync = await this.Queue.front();
if (frontQueuedSync) await this.sync(frontQueuedSync); if (frontQueuedSync) await this.start(frontQueuedSync);
} }
catch (err) { catch (err) {
log.error({ err }, "failed to process next queued sync") log.error({ err }, "failed to process next queued sync")
@@ -137,7 +138,7 @@ export class CommerceService {
} }
} }
async getActiveSync(sessionId?: string) { async getActive(sessionId?: string) {
return await db.selectFrom("product_syncs") return await db.selectFrom("product_syncs")
.selectAll() .selectAll()
.$if(sessionId !== undefined, (qb) => .$if(sessionId !== undefined, (qb) =>
@@ -149,7 +150,7 @@ export class CommerceService {
.executeTakeFirst(); .executeTakeFirst();
} }
async getLatestSync(sessionId?: string) { async getLatest(sessionId?: string) {
return await db.selectFrom("product_syncs") return await db.selectFrom("product_syncs")
.selectAll() .selectAll()
.$if(sessionId !== undefined, (qb) => .$if(sessionId !== undefined, (qb) =>
@@ -160,14 +161,14 @@ export class CommerceService {
.executeTakeFirst(); .executeTakeFirst();
} }
deriveSyncStatus(sync: Selectable<ProductSyncs>): SyncStatus { deriveStatus(sync: Selectable<ProductSyncs>): SyncStatus {
if (!sync.started_at) return "queued"; if (!sync.started_at) return "queued";
if (!sync.ended_at) return "active"; if (!sync.ended_at) return "active";
return sync.has_failed ? "failed" : "succeeded"; return sync.has_failed ? "failed" : "succeeded";
} }
} }
class SyncQueue { class SyncQueueService {
async enqueue(queuedSync: QueuedSync) { async enqueue(queuedSync: QueuedSync) {
const values: Insertable<ProductSyncs> = { const values: Insertable<ProductSyncs> = {
session_id: queuedSync.session.id, session_id: queuedSync.session.id,
+6 -6
View File
@@ -73,10 +73,10 @@ const metricPriority = (m: NumberMetric) => {
export const DEFAULT_STANDARDS_DATASET = defaultStandardsDataset as StandardsDataset; export const DEFAULT_STANDARDS_DATASET = defaultStandardsDataset as StandardsDataset;
export class LevelCalculator { export class LevelCalculator {
standards: Standards; Standards: Standards;
public constructor(standards: Standards) { public constructor(standards: Standards) {
this.standards = standards; this.Standards = standards;
} }
// ------------------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------------------
@@ -138,7 +138,7 @@ export class LevelCalculator {
(Object.values(Attribute) as Attribute[]).forEach((attribute) => { (Object.values(Attribute) as Attribute[]).forEach((attribute) => {
const attrActivityPerformances = activityPerformances.filter( const attrActivityPerformances = activityPerformances.filter(
(p) => (p) =>
this.standards.byActivity(p.activity).getMetadata() this.Standards.byActivity(p.activity).getMetadata()
.attribute === attribute, .attribute === attribute,
); );
attrLevels[attribute] = this.calculateAttributeLevel( attrLevels[attribute] = this.calculateAttributeLevel(
@@ -160,7 +160,7 @@ export class LevelCalculator {
if ( if (
activityPerformances.some( activityPerformances.some(
(p) => (p) =>
this.standards.byActivity(p.activity).getMetadata() this.Standards.byActivity(p.activity).getMetadata()
.attribute !== attribute, .attribute !== attribute,
) )
) { ) {
@@ -168,7 +168,7 @@ export class LevelCalculator {
} }
// must perform all of an attributes activities // must perform all of an attributes activities
for (const activity of this.standards.getAttributeActivities( for (const activity of this.Standards.getAttributeActivities(
attribute, attribute,
)) { )) {
const filteredActivites = activityPerformances.map( const filteredActivites = activityPerformances.map(
@@ -183,7 +183,7 @@ export class LevelCalculator {
} }
const activityLevels = activityPerformances.map((p) => { const activityLevels = activityPerformances.map((p) => {
const interpolatedStandard = this.standards const interpolatedStandard = this.Standards
.byActivity(p.activity) .byActivity(p.activity)
.byMetrics(player.metrics) .byMetrics(player.metrics)
.getOneInterpolated(); .getOneInterpolated();
+13 -13
View File
@@ -30,14 +30,14 @@ export class ProductSyncer {
private log: pino.Logger; private log: pino.Logger;
constructor( constructor(
private printful: PrintfulClient, private Printful: PrintfulClient,
private webflow: WebflowClient, private Webflow: WebflowClient,
log?: pino.Logger, log?: pino.Logger,
) { ) {
this.log = (log ?? pino({ level: "silent" })).child({ component: "ProductSyncer" }); this.log = (log ?? pino({ level: "silent" })).child({ component: "ProductSyncer" });
} }
async sync(opt: ProductSyncerOptions = {}) { async run(opt: ProductSyncerOptions = {}) {
const startDate = new Date(); const startDate = new Date();
await opt.onStart?.(startDate); await opt.onStart?.(startDate);
@@ -47,10 +47,10 @@ export class ProductSyncer {
this.log.info("sync started (all)"); this.log.info("sync started (all)");
this.log.debug("retrieving webflow products"); this.log.debug("retrieving webflow products");
const allWebflowProducts = await this.webflow.Products.list({ forceAll: true }); const allWebflowProducts = await this.Webflow.Products.list({ forceAll: true });
this.log.debug("retrieving printful products"); this.log.debug("retrieving printful products");
const allPrintfulProducts = await this.printful.Products.list({ forceAll: true }); const allPrintfulProducts = await this.Printful.Products.list({ forceAll: true });
this.log.info({ printfulProductCount: allPrintfulProducts.length, filter: opt.filter ?? "N/A" }, "generating meta printful products"); this.log.info({ printfulProductCount: allPrintfulProducts.length, filter: opt.filter ?? "N/A" }, "generating meta printful products");
const metaPrintfulProducts = await this.generateMetaPrintfulProducts(allWebflowProducts, allPrintfulProducts, opt); const metaPrintfulProducts = await this.generateMetaPrintfulProducts(allWebflowProducts, allPrintfulProducts, opt);
@@ -120,7 +120,7 @@ export class ProductSyncer {
this.log.warn({ webflowSku: newWebflowSkus[0] }, "could not find existing webflow SKU that matches"); this.log.warn({ webflowSku: newWebflowSkus[0] }, "could not find existing webflow SKU that matches");
} }
await this.webflow.Products.update(webflowProduct.product.id, { await this.Webflow.Products.update(webflowProduct.product.id, {
product: { fieldData: webflowProductFieldData }, product: { fieldData: webflowProductFieldData },
sku: newWebflowSkus[0], sku: newWebflowSkus[0],
}); });
@@ -133,13 +133,13 @@ export class ProductSyncer {
newWebflowSku.id, newWebflowSku.id,
); );
if (existingWebflowSku) { if (existingWebflowSku) {
await this.webflow.Products.Skus.update( await this.Webflow.Products.Skus.update(
metaPrintfulProduct.webflowProductId, metaPrintfulProduct.webflowProductId,
existingWebflowSku.id, existingWebflowSku.id,
newWebflowSku, newWebflowSku,
); );
} else { } else {
await this.webflow.Products.Skus.create( await this.Webflow.Products.Skus.create(
metaPrintfulProduct.webflowProductId, metaPrintfulProduct.webflowProductId,
[newWebflowSku], [newWebflowSku],
); );
@@ -148,18 +148,18 @@ export class ProductSyncer {
} else { } else {
this.log.info("existing webflow product not found, creating it"); this.log.info("existing webflow product not found, creating it");
const webflowProductId = await this.webflow.Products.create({ const webflowProductId = await this.Webflow.Products.create({
product: { fieldData: webflowProductFieldData }, product: { fieldData: webflowProductFieldData },
sku: newWebflowSkus[0], sku: newWebflowSkus[0],
}); });
metaPrintfulProduct.webflowProductId = webflowProductId; metaPrintfulProduct.webflowProductId = webflowProductId;
if (newWebflowSkus.length > 1) if (newWebflowSkus.length > 1)
await this.webflow.Products.Skus.create(webflowProductId, newWebflowSkus.slice(1)); await this.Webflow.Products.Skus.create(webflowProductId, newWebflowSkus.slice(1));
} }
// Re-fetch after SKU mutations so newly created SKUs have their real IDs. // Re-fetch after SKU mutations so newly created SKUs have their real IDs.
const freshWebflowProduct = await this.webflow.Products.get(metaPrintfulProduct.webflowProductId); const freshWebflowProduct = await this.Webflow.Products.get(metaPrintfulProduct.webflowProductId);
if (!freshWebflowProduct) throw new Error("webflow product missing"); if (!freshWebflowProduct) throw new Error("webflow product missing");
// Update printful external IDs // Update printful external IDs
@@ -186,7 +186,7 @@ export class ProductSyncer {
await sleep(10000); await sleep(10000);
this.log.info({ printfulProduct: printfulProduct.sync_product.id, externalId: expectedExternalId }, "updating printful product's external ID"); this.log.info({ printfulProduct: printfulProduct.sync_product.id, externalId: expectedExternalId }, "updating printful product's external ID");
await this.printful.Products.update(printfulProduct.sync_product.id, { await this.Printful.Products.update(printfulProduct.sync_product.id, {
sync_product: { sync_product: {
id: printfulProduct.sync_product.id, id: printfulProduct.sync_product.id,
external_id: expectedExternalId, external_id: expectedExternalId,
@@ -286,7 +286,7 @@ export class ProductSyncer {
metaPrintfulProduct.webflowProductId = webflowProductId; metaPrintfulProduct.webflowProductId = webflowProductId;
} }
const fullPrintfulProduct = await this.printful.Products.get(printfulProduct.id); const fullPrintfulProduct = await this.Printful.Products.get(printfulProduct.id);
if (!fullPrintfulProduct) throw new Error(`Printful product ${printfulProduct.id} not found`); if (!fullPrintfulProduct) throw new Error(`Printful product ${printfulProduct.id} not found`);
const isDuplicate = allPrintfulProducts.some((p) => p.id !== printfulProduct.id && p.name === printfulProduct.name); const isDuplicate = allPrintfulProducts.some((p) => p.id !== printfulProduct.id && p.name === printfulProduct.name);