From bea1a91eb9309fd71297ae4914ea992c309031d6 Mon Sep 17 00:00:00 2001 From: nate Date: Tue, 24 Mar 2026 22:01:05 +0400 Subject: [PATCH] feat: disallow double lifetime buys --- apps/pay/src/routes.ts | 8 +++++--- apps/web/src/routes/dashboard.ts | 8 +++++--- apps/web/src/views/checkout.ejs | 12 +++++++++++- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/apps/pay/src/routes.ts b/apps/pay/src/routes.ts index 60be715..5c8db62 100644 --- a/apps/pay/src/routes.ts +++ b/apps/pay/src/routes.ts @@ -60,10 +60,12 @@ export const routes = new Elysia() const { plan, months, coin } = body; - // Validate plan + // Validate plan — block duplicate lifetime if (plan === "lifetime") { - const [acc] = await sql`SELECT plan FROM accounts WHERE id = ${accountId}`; - if (acc.plan === "lifetime") { set.status = 400; return { error: "Already on lifetime plan" }; } + const [acc] = await sql`SELECT plan, plan_stack FROM accounts WHERE id = ${accountId}`; + const stack = typeof acc.plan_stack === "string" ? JSON.parse(acc.plan_stack) : (acc.plan_stack || []); + const hasLifetime = acc.plan === "lifetime" || stack.some((s: any) => s.plan === "lifetime"); + if (hasLifetime) { set.status = 400; return { error: "You already have a lifetime plan" }; } } // Validate coin diff --git a/apps/web/src/routes/dashboard.ts b/apps/web/src/routes/dashboard.ts index b3ccb1a..8ffc0ce 100644 --- a/apps/web/src/routes/dashboard.ts +++ b/apps/web/src/routes/dashboard.ts @@ -270,8 +270,10 @@ export const dashboard = new Elysia() .get("/dashboard/checkout", async ({ cookie, headers }) => { const resolved = await getAccountId(cookie, headers); if (!resolved?.accountId) return redirect("/dashboard"); - const [acc] = await sql`SELECT plan, plan_expires_at FROM accounts WHERE id = ${resolved.accountId}`; - if (acc.plan === "lifetime") return redirect("/dashboard/settings"); + const [acc] = await sql`SELECT plan, plan_expires_at, plan_stack FROM accounts WHERE id = ${resolved.accountId}`; + const stack = typeof acc.plan_stack === "string" ? JSON.parse(acc.plan_stack) : (acc.plan_stack || []); + const hasLifetime = acc.plan === "lifetime" || stack.some((s: any) => s.plan === "lifetime"); + if (acc.plan === "lifetime" && stack.length === 0) return redirect("/dashboard/settings"); // Total spent on paid invoices (for lifetime discount) const [{ total_spent }] = await sql`SELECT COALESCE(SUM(amount_usd), 0)::numeric as total_spent FROM payments WHERE account_id = ${resolved.accountId} AND status = 'paid'`; @@ -285,7 +287,7 @@ export const dashboard = new Elysia() coins = data.coins || []; } catch {} - return html("checkout", { nav: "settings", account: acc, payApi, invoiceId: null, coins, invoice: null, totalSpent: Number(total_spent) }); + return html("checkout", { nav: "settings", account: acc, payApi, invoiceId: null, coins, invoice: null, totalSpent: Number(total_spent), hasLifetime }); }) // Existing invoice by ID — SSR the payment status diff --git a/apps/web/src/views/checkout.ejs b/apps/web/src/views/checkout.ejs index eacf1df..73c3887 100644 --- a/apps/web/src/views/checkout.ejs +++ b/apps/web/src/views/checkout.ejs @@ -7,8 +7,9 @@ const invoice = it.invoice; const payApi = it.payApi || ''; const totalSpent = it.totalSpent || 0; + const hasLifetime = it.hasLifetime || false; const lifetimeBase = 140; - const lifetimeDiscount = Math.min(totalSpent, lifetimeBase * 0.75); + const lifetimeDiscount = hasLifetime ? 0 : Math.min(totalSpent, lifetimeBase * 0.75); const lifetimePrice = lifetimeBase - lifetimeDiscount; %> @@ -46,6 +47,14 @@
From $12/mo
200–800 monitors, 5s intervals
+ <% if (hasLifetime) { %> +
+ Owned +
Lifetime
+
$<%= lifetimeBase %>
+
You already have Lifetime
+
+ <% } else { %> + <% } %>