feat: disallow double lifetime buys
This commit is contained in:
parent
1a00d49be2
commit
bea1a91eb9
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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">200–800 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) -->
|
||||
|
|
|
|||
Loading…
Reference in New Issue