feat: disallow double lifetime buys

This commit is contained in:
nate 2026-03-24 22:01:05 +04:00
parent 1a00d49be2
commit bea1a91eb9
3 changed files with 21 additions and 7 deletions

View File

@ -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

View File

@ -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

View File

@ -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 @@
<div class="text-2xl font-bold text-gray-100">From $12<span class="text-sm font-normal text-gray-500">/mo</span></div>
<div class="text-xs text-gray-500 mt-2">200800 monitors, 5s intervals</div>
</label>
<% if (hasLifetime) { %>
<div class="text-left bg-surface border-2 border-border-subtle rounded-xl p-5 opacity-50 cursor-not-allowed relative">
<span class="absolute top-3 right-3 text-[10px] font-semibold px-2 py-0.5 rounded-full bg-gray-500/15 text-gray-500 border border-gray-500/20">Owned</span>
<div class="text-xs text-gray-500 uppercase tracking-wider font-mono mb-1">Lifetime</div>
<div class="text-2xl font-bold text-gray-400">$<%= lifetimeBase %></div>
<div class="text-xs text-gray-600 mt-2">You already have Lifetime</div>
</div>
<% } else { %>
<label for="plan-lifetime"
class="cursor-pointer text-left bg-surface border-2 border-border-subtle hover:border-yellow-500/40 rounded-xl p-5 transition-colors relative">
<span class="absolute top-3 right-3 text-[10px] font-semibold px-2 py-0.5 rounded-full bg-yellow-500/15 text-yellow-500 border border-yellow-500/20">Launch Deal</span>
@ -58,6 +67,7 @@
<% } %>
<div class="text-xs text-gray-500 mt-2">One-time, 200 monitors forever</div>
</label>
<% } %>
</div>
<!-- Pro tier + months (visible only when pro selected, via CSS) -->