Merge pull request 'main' (#103) from main into dev

Reviewed-on: #103
This commit was merged in pull request #103.
This commit is contained in:
2026-06-04 15:19:23 +00:00
4 changed files with 43 additions and 16 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.
*

View File

@@ -64,7 +64,7 @@ async function sendTrialEnding(db: Database, now: Date): Promise<number> {
ctaUrl: buildTrackedUrl(
"trial-ending",
r.email,
webUrl("/dashboard/settings?tab=subscription"),
webUrl("/dashboard/subscription"),
),
},
);

View File

@@ -123,7 +123,7 @@ export class NovuService {
ctaUrl: buildTrackedUrl(
"payment-success",
user.email,
webUrl("/dashboard/settings?tab=subscription"),
webUrl("/dashboard/subscription"),
),
});
}
@@ -139,7 +139,7 @@ export class NovuService {
ctaUrl: buildTrackedUrl(
"payment-failed",
user.email,
webUrl("/dashboard/settings?tab=subscription"),
webUrl("/dashboard/subscription"),
),
});
}