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