Merge pull request 'fix(jobs): gate lifecycle-email cron to prod host only' (#97) from fix/lifecycle-cron-prod-only into main

Reviewed-on: #97
This commit was merged in pull request #97.
This commit is contained in:
2026-06-04 09:44:02 +00:00
2 changed files with 40 additions and 13 deletions

View File

@@ -1,7 +1,7 @@
import { Inject, Module, type OnModuleDestroy, type OnModuleInit } from "@nestjs/common";
import { Queue } from "bullmq";
import { CategoriesModule } from "../categories/categories.module";
import { isCatalogBackfillEnabled } from "./prefetch-utils";
import { isCatalogBackfillEnabled, isLifecycleEmailEnabled } from "./prefetch-utils";
import { PrefetchWorkerService } from "./prefetch-worker.service";
import {
CATALOG_PREFETCH_QUEUE,
@@ -76,19 +76,29 @@ export class JobsModule implements OnModuleInit, OnModuleDestroy {
// Lifecycle e-mails (trial-ending + win-back): every day at 9:00 AM.
// Daytime so the e-mails land at a reasonable hour for recipients.
await this.lifecycleEmailQueue.upsertJobScheduler(
"lifecycle-email-daily",
{ pattern: "0 9 * * *" },
{
name: "lifecycle-email-run",
data: {},
opts: {
removeOnComplete: { count: 30 },
removeOnFail: { count: 100 },
// PRODUCTION (sase.tr) ONLY: dev.sase.tr is a separate DB seeded from prod,
// so an ungated cron there would send duplicate real-customer mails. Gate
// on the prod host (same pattern as catalog-backfill below).
if (isLifecycleEmailEnabled()) {
await this.lifecycleEmailQueue.upsertJobScheduler(
"lifecycle-email-daily",
{ pattern: "0 9 * * *" },
{
name: "lifecycle-email-run",
data: {},
opts: {
removeOnComplete: { count: 30 },
removeOnFail: { count: 100 },
},
},
},
);
console.log("[jobs] Registered lifecycle-email cron: 0 9 * * *");
);
console.log("[jobs] Registered lifecycle-email cron: 0 9 * * *");
} else {
// Clean up any stale scheduler (e.g. previously registered on dev) so it
// doesn't keep firing from Redis after this gate is added.
await this.lifecycleEmailQueue.removeJobScheduler("lifecycle-email-daily").catch(() => {});
console.log("[jobs] Skipped lifecycle-email cron (not prod host)");
}
// Catalog backfill: every hour at :00. Scans for decoded vehicles whose catalog
// isn't fully prefetched and queues them. Self-throttled (queue-depth guard),

View File

@@ -18,6 +18,23 @@ export function isCatalogBackfillEnabled(): boolean {
);
}
/**
* True ONLY on the real production deployment (sase.tr).
*
* Same rationale as {@link isCatalogBackfillEnabled} — both prod and dev run
* with `NODE_ENV=production`, so we gate on the prod host. Without this gate
* the dev deploy would send duplicate trial-ending / win-back e-mails to real
* users (sase_dev DB is a copy of prod data). An explicit
* `LIFECYCLE_EMAIL_ENABLED` env wins (e.g. to force-test on staging).
*/
export function isLifecycleEmailEnabled(): boolean {
const flag = process.env.LIFECYCLE_EMAIL_ENABLED;
if (flag != null && flag !== "") return flag === "true";
return (
process.env.COOLIFY_FQDN === "sase.tr" || process.env.BETTER_AUTH_URL === "https://sase.tr"
);
}
/**
* Signals the worker that this job is rate-limited and should be deferred.
*