Revert "fix(billing): one reminder at 1 day, guarded by an advisory lock"

Revert "fix(billing): stop the reminder claim from rewriting stale metadata"

This reverts commits 037230496 and 14e059784.
This commit is contained in:
Mauricio Siu
2026-09-18 02:49:18 -06:00
parent 037230496c
commit 8236c31fcd
@@ -1,42 +1,21 @@
import { IS_CLOUD } from "@dokploy/server";
import { db, dbUrl } from "@dokploy/server/db";
import { db } from "@dokploy/server/db";
import TrialExpiringEmail from "@dokploy/server/emails/emails/trial-expiring";
import { sendEmail } from "@dokploy/server/verification/send-verification-email";
import { render } from "@react-email/components";
import { format } from "date-fns";
import { eq } from "drizzle-orm";
import { scheduleJob } from "node-schedule";
import postgres from "postgres";
import type Stripe from "stripe";
import { user } from "@/server/db/schema";
import { getStripeClient, planFromPriceIds } from "@/server/utils/billing";
import { WEBSITE_URL } from "@/server/utils/stripe";
const REMINDER_THRESHOLDS = [
{ days: 3, metadataKey: "trialReminder3Sent" },
{ days: 1, metadataKey: "trialReminder1Sent" },
] as const;
// Cloud runs several replicas and node-schedule fires on each one, so without
// this every trial user would get one email per replica. Advisory locks live
// in the connection rather than the schema, so no migration is needed.
const ADVISORY_LOCK_KEY = 4820260918;
const withAdvisoryLock = async <T>(run: () => Promise<T>) => {
const sql = postgres(dbUrl, { max: 1 });
try {
const [row] =
await sql`select pg_try_advisory_lock(${ADVISORY_LOCK_KEY}) as locked`;
if (!row?.locked) return null;
try {
return await run();
} finally {
await sql`select pg_advisory_unlock(${ADVISORY_LOCK_KEY})`;
}
} finally {
await sql.end({ timeout: 5 });
}
};
const PLAN_LABELS: Record<string, string> = {
hobby: "Hobby",
startup: "Startup",
@@ -115,11 +94,9 @@ const processSubscription = async (
);
const plan = planFromPriceIds(priceIds);
// Marked before sending so a crash mid-send cannot resend on the next run.
// Stripe merges metadata, so only the claimed key travels; spreading the
// snapshot read at list time would rewrite concurrent changes.
// Claim the reminder before sending so concurrent replicas don't double-send.
await stripe.subscriptions.update(subscription.id, {
metadata: { [threshold.metadataKey]: "sent" },
metadata: { ...subscription.metadata, [threshold.metadataKey]: "sent" },
});
try {
@@ -132,7 +109,7 @@ const processSubscription = async (
});
} catch (error) {
await stripe.subscriptions.update(subscription.id, {
metadata: { [threshold.metadataKey]: "" },
metadata: { ...subscription.metadata, [threshold.metadataKey]: "" },
});
throw error;
}
@@ -169,14 +146,7 @@ export const initTrialNotificationsCronJob = () => {
scheduleJob("trial-expiration-reminders", "0 14 * * *", async () => {
try {
const result = await withAdvisoryLock(processTrialExpirations);
if (!result) {
console.log("Trial reminders: another replica holds the lock");
return;
}
console.log(
`Trial reminders: scanned ${result.scanned}, sent ${result.sent}`,
);
await processTrialExpirations();
} catch (error) {
console.error(
"Trial reminder job failed:",