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
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:
@@ -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),
|
||||||
|
|||||||
@@ -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.
|
||||||
*
|
*
|
||||||
|
|||||||
Reference in New Issue
Block a user