fix(jobs): gate lifecycle-email cron behind prod-host check
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled

dev.sase.tr was running the daily lifecycle-email cron (trial-ending,
win-back) against sase_dev, which is a seeded copy of the prod DB —
real customers were getting duplicate mails (one from prod, one from
dev). Mirror the isCatalogBackfillEnabled() gate so the lifecycle cron
only registers on the canonical prod host (COOLIFY_FQDN=sase.tr or
BETTER_AUTH_URL=https://sase.tr). LIFECYCLE_EMAIL_ENABLED env can force-
enable for staging testing. The else branch removes any stale scheduler
from Redis so a previously-registered cron stops firing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
semih
2026-06-04 12:38:10 +03:00
parent 306e8eb691
commit 7e5e0ee446
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 { Inject, Module, type OnModuleDestroy, type OnModuleInit } from "@nestjs/common";
import { Queue } from "bullmq"; import { Queue } from "bullmq";
import { CategoriesModule } from "../categories/categories.module"; import { CategoriesModule } from "../categories/categories.module";
import { isCatalogBackfillEnabled } from "./prefetch-utils"; import { isCatalogBackfillEnabled, isLifecycleEmailEnabled } from "./prefetch-utils";
import { PrefetchWorkerService } from "./prefetch-worker.service"; import { PrefetchWorkerService } from "./prefetch-worker.service";
import { import {
CATALOG_PREFETCH_QUEUE, 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. // 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. // Daytime so the e-mails land at a reasonable hour for recipients.
await this.lifecycleEmailQueue.upsertJobScheduler( // PRODUCTION (sase.tr) ONLY: dev.sase.tr is a separate DB seeded from prod,
"lifecycle-email-daily", // so an ungated cron there would send duplicate real-customer mails. Gate
{ pattern: "0 9 * * *" }, // on the prod host (same pattern as catalog-backfill below).
{ if (isLifecycleEmailEnabled()) {
name: "lifecycle-email-run", await this.lifecycleEmailQueue.upsertJobScheduler(
data: {}, "lifecycle-email-daily",
opts: { { pattern: "0 9 * * *" },
removeOnComplete: { count: 30 }, {
removeOnFail: { count: 100 }, 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 // Catalog backfill: every hour at :00. Scans for decoded vehicles whose catalog
// isn't fully prefetched and queues them. Self-throttled (queue-depth guard), // 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. * Signals the worker that this job is rate-limited and should be deferred.
* *