← 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";
}