← back to Govarbitrage
src/lib/tiers.ts
104 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: "Current listings and full money-math analysis.",
features: [
"Browse current auction listings",
"Full money-math: recommended max bid, ROI, net profit, scores",
"Up to 25 listings per page",
"Daily newsletter available after subscription confirmation",
],
limits: {
maxListings: 25,
freshnessDelayHours: 24,
showMoneyMath: true,
hotDealAlerts: false,
realtime: false,
allSources: false,
},
},
STANDARD: {
key: "STANDARD",
label: "Standard",
priceUsd: price("TIER_PRICE_STANDARD", 19),
blurb: "Preview plan with up to 500 listings per page.",
features: [
"Current listings and full money-math analysis",
"Up to 500 listings per page",
"Available sources are accessible on every plan",
"Daily newsletter available after subscription confirmation",
],
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: "Preview plan with a higher listing page-size allowance.",
features: [
"Current listings and full money-math analysis",
"Higher listing page-size allowance",
"Results remain subject to search and sorting limits",
"Daily newsletter available after subscription confirmation",
],
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";
}