← back to Govarbitrage
src/lib/tiers.ts
107 lines
// 3-tier paid-alerts SaaS config — the single source of truth for pricing,
// features, and enforceable data limits. Prices are DRAFT (Steve sets final);
// live charging stays gated (billing runs TEST/mock until a live key is set).
import type { Tier } from "@prisma/client";
export interface TierDef {
key: Tier;
label: string;
priceUsd: number; // per month; 0 = free
blurb: string;
features: string[];
limits: {
maxListings: number; // rows the dashboard/API returns (Infinity via a big number)
freshnessDelayHours: number; // Free sees deals aged by this many hours (not real-time)
showMoneyMath: boolean; // recommended max bid / ROI / net profit visible?
hotDealAlerts: boolean; // receive the 🔥 email alerts?
realtime: boolean; // real-time vs delayed
allSources: boolean; // all 8 feeds vs a limited subset
};
}
// env override for prices without a code change: TIER_PRICE_STANDARD, TIER_PRICE_PREMIUM
function price(env: string, dflt: number): number {
const v = process.env[env];
const n = v == null ? NaN : Number(v);
return Number.isFinite(n) && n >= 0 ? n : dflt;
}
export const TIERS: Record<Tier, TierDef> = {
FREE: {
key: "FREE",
label: "Free",
priceUsd: 0,
blurb: "See what the platform finds — on a delay.",
features: [
"Browse the auction catalog",
"Deals delayed 24 hours",
"Up to 25 listings",
"No money-math (max bid / ROI hidden)",
"No email alerts",
],
limits: {
maxListings: 25,
freshnessDelayHours: 24,
showMoneyMath: false,
hotDealAlerts: false,
realtime: false,
allSources: false,
},
},
STANDARD: {
key: "STANDARD",
label: "Standard",
priceUsd: price("TIER_PRICE_STANDARD", 19),
blurb: "Real-time deals + the daily hot-deal alert.",
features: [
"Real-time listings (no delay)",
"🔥 Daily hot-deal email alerts",
"Up to 500 listings",
"Full money-math: recommended max bid, ROI, net profit",
"All 8 auction sources",
],
limits: {
maxListings: 500,
freshnessDelayHours: 0,
showMoneyMath: true,
hotDealAlerts: true,
realtime: true,
allSources: true,
},
},
PREMIUM: {
key: "PREMIUM",
label: "Premium",
priceUsd: price("TIER_PRICE_PREMIUM", 49),
blurb: "Everything, unlimited, with confidence bands + priority.",
features: [
"Everything in Standard",
"Unlimited listings",
"Honesty-Gate confidence bands on every deal",
"Full dashboard + saved views + exports",
"Priority hot-deal alerts (sent first)",
],
limits: {
maxListings: 1_000_000,
freshnessDelayHours: 0,
showMoneyMath: true,
hotDealAlerts: true,
realtime: true,
allSources: true,
},
},
};
export const TIER_ORDER: Tier[] = ["FREE", "STANDARD", "PREMIUM"];
export function tierDef(t: Tier | string | null | undefined): TierDef {
return TIERS[(t as Tier) in TIERS ? (t as Tier) : "FREE"];
}
/** Map a Stripe price/lookup to a tier (used by the webhook). */
export function tierFromPriceUsd(usd: number): Tier {
if (usd >= TIERS.PREMIUM.priceUsd && TIERS.PREMIUM.priceUsd > 0) return "PREMIUM";
if (usd >= TIERS.STANDARD.priceUsd && TIERS.STANDARD.priceUsd > 0) return "STANDARD";
return "FREE";
}