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;
|
const { plan, months, coin } = body;
|
||||||
|
|
||||||
// Validate plan
|
// Validate plan — block duplicate lifetime
|
||||||
if (plan === "lifetime") {
|
if (plan === "lifetime") {
|
||||||
const [acc] = await sql`SELECT plan FROM accounts WHERE id = ${accountId}`;
|
const [acc] = await sql`SELECT plan, plan_stack FROM accounts WHERE id = ${accountId}`;
|
||||||
if (acc.plan === "lifetime") { set.status = 400; return { error: "Already on lifetime plan" }; }
|
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
|
// Validate coin
|
||||||
|
|
|
||||||
|
|
@ -270,8 +270,10 @@ export const dashboard = new Elysia()
|
||||||
.get("/dashboard/checkout", async ({ cookie, headers }) => {
|
.get("/dashboard/checkout", async ({ cookie, headers }) => {
|
||||||
const resolved = await getAccountId(cookie, headers);
|
const resolved = await getAccountId(cookie, headers);
|
||||||
if (!resolved?.accountId) return redirect("/dashboard");
|
if (!resolved?.accountId) return redirect("/dashboard");
|
||||||
const [acc] = await sql`SELECT plan, plan_expires_at FROM accounts WHERE id = ${resolved.accountId}`;
|
const [acc] = await sql`SELECT plan, plan_expires_at, plan_stack FROM accounts WHERE id = ${resolved.accountId}`;
|
||||||
if (acc.plan === "lifetime") return redirect("/dashboard/settings");
|
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)
|
// 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'`;
|
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 || [];
|
coins = data.coins || [];
|
||||||
} catch {}
|
} 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
|
// Existing invoice by ID — SSR the payment status
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,9 @@
|
||||||
const invoice = it.invoice;
|
const invoice = it.invoice;
|
||||||
const payApi = it.payApi || '';
|
const payApi = it.payApi || '';
|
||||||
const totalSpent = it.totalSpent || 0;
|
const totalSpent = it.totalSpent || 0;
|
||||||
|
const hasLifetime = it.hasLifetime || false;
|
||||||
const lifetimeBase = 140;
|
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;
|
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-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>
|
<div class="text-xs text-gray-500 mt-2">200–800 monitors, 5s intervals</div>
|
||||||
</label>
|
</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"
|
<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">
|
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>
|
<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>
|
<div class="text-xs text-gray-500 mt-2">One-time, 200 monitors forever</div>
|
||||||
</label>
|
</label>
|
||||||
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pro tier + months (visible only when pro selected, via CSS) -->
|
<!-- Pro tier + months (visible only when pro selected, via CSS) -->
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue