Public Access
1
0
Files
leads4less/shared/billing/plans.ts
T
pguerrerox 94b8c357b4 feat: add billing foundation and entitlement enforcement
- add workspace-scoped billing storage, usage tracking, and add-on catalog
- enforce plan entitlements for search and deep research routes
- expand pricing and account UI around billing state, usage, and upgrades
2026-05-22 17:50:28 +00:00

620 lines
19 KiB
TypeScript

export type PlanTier = 'starter' | 'growth' | 'pro' | 'enterprise';
export type BillingInterval = 'monthly' | 'annual' | 'custom';
export type PlanListingCategory = 'pricing_page_primary' | 'pricing_page_hidden' | 'internal_only';
export type FeatureReadiness = 'launch_ready' | 'marketed_not_enforced' | 'future';
export type ActivePlanCode =
| 'starter_monthly'
| 'starter_annual'
| 'growth_monthly'
| 'growth_annual'
| 'pro_monthly'
| 'pro_annual'
| 'enterprise_custom';
export type ReservedPlanCode = 'founder_lifetime' | 'founder_pro_lifetime';
export type PlanCode = ActivePlanCode | ReservedPlanCode;
export type AddonCode = 'export_pack_10k' | 'export_pack_50k' | 'enrichment_pack_1k' | 'ai_assistant_monthly' | 'white_label_monthly';
export type ProcessingTier = 'standard' | 'priority' | 'dedicated';
export interface PlanLimits {
researchRunsPerMonth: number | null;
exportsPerMonth: number | null;
// These are commercial packaging allowances for now. Hard enforcement comes later
// after workspace-scoped ownership and collaboration rules are ready.
usersIncluded: number | null;
workspacesIncluded: number | null;
enrichmentCreditsIncluded: number | null;
pooledUsage: boolean;
}
export interface PlanFeatures {
csvExport: boolean;
mapSearch: boolean;
radiusSearch: boolean;
basicFilters: boolean;
advancedFilters: boolean;
savedSearches: boolean;
territoryMapping: boolean;
deduplication: boolean;
exportHistory: boolean;
taggingNotes: boolean;
sharedLists: boolean;
scheduledResearch: boolean;
bulkExports: boolean;
crmIntegrations: boolean;
apiAccess: boolean;
webhooks: boolean;
collaboration: boolean;
enrichments: boolean;
prioritySupport: boolean;
sso: boolean;
sla: boolean;
whiteLabel: boolean;
}
export interface PlanDefinition {
code: ActivePlanCode;
tier: PlanTier;
planFamily: PlanTier;
name: string;
billingInterval: BillingInterval;
listingCategory: PlanListingCategory;
isSelfServe: boolean;
contactSalesRequired: boolean;
priceCents: number | null;
currencyCode: 'USD';
annualDiscountPercent: number | null;
limits: PlanLimits;
features: PlanFeatures;
featureReadiness: Partial<Record<keyof PlanFeatures, FeatureReadiness>>;
processingTier: ProcessingTier;
eligibleAddonCodes: AddonCode[];
}
export interface PlanDisplayMeta {
audience: string;
summary: string;
ctaLabel: string;
ctaMode: 'sign_in' | 'sign_up';
badgeLabel?: string;
qualitativeBullets: string[];
}
function createFeatureFlags(overrides: Partial<PlanFeatures>): PlanFeatures {
return {
csvExport: false,
mapSearch: false,
radiusSearch: false,
basicFilters: false,
advancedFilters: false,
savedSearches: false,
territoryMapping: false,
deduplication: false,
exportHistory: false,
taggingNotes: false,
sharedLists: false,
scheduledResearch: false,
bulkExports: false,
crmIntegrations: false,
apiAccess: false,
webhooks: false,
collaboration: false,
enrichments: false,
prioritySupport: false,
sso: false,
sla: false,
whiteLabel: false,
...overrides,
};
}
function createPlan(plan: PlanDefinition): PlanDefinition {
return plan;
}
const activePlanCatalog = [
createPlan({
code: 'starter_monthly',
tier: 'starter',
planFamily: 'starter',
name: 'Starter',
billingInterval: 'monthly',
listingCategory: 'pricing_page_primary',
isSelfServe: true,
contactSalesRequired: false,
priceCents: 3900,
currencyCode: 'USD',
annualDiscountPercent: null,
limits: {
researchRunsPerMonth: 25,
exportsPerMonth: 2500,
usersIncluded: 1,
workspacesIncluded: 1,
enrichmentCreditsIncluded: 0,
pooledUsage: false,
},
features: createFeatureFlags({
csvExport: true,
mapSearch: true,
radiusSearch: true,
basicFilters: true,
}),
featureReadiness: {},
processingTier: 'standard',
eligibleAddonCodes: ['export_pack_10k', 'export_pack_50k'],
}),
createPlan({
code: 'starter_annual',
tier: 'starter',
planFamily: 'starter',
name: 'Starter',
billingInterval: 'annual',
listingCategory: 'pricing_page_hidden',
isSelfServe: true,
contactSalesRequired: false,
priceCents: 37440,
currencyCode: 'USD',
annualDiscountPercent: 20,
limits: {
researchRunsPerMonth: 25,
exportsPerMonth: 2500,
usersIncluded: 1,
workspacesIncluded: 1,
enrichmentCreditsIncluded: 0,
pooledUsage: false,
},
features: createFeatureFlags({
csvExport: true,
mapSearch: true,
radiusSearch: true,
basicFilters: true,
}),
featureReadiness: {},
processingTier: 'standard',
eligibleAddonCodes: ['export_pack_10k', 'export_pack_50k'],
}),
createPlan({
code: 'growth_monthly',
tier: 'growth',
planFamily: 'growth',
name: 'Growth',
billingInterval: 'monthly',
listingCategory: 'pricing_page_primary',
isSelfServe: true,
contactSalesRequired: false,
priceCents: 9900,
currencyCode: 'USD',
annualDiscountPercent: null,
limits: {
researchRunsPerMonth: 150,
exportsPerMonth: 15000,
usersIncluded: 3,
workspacesIncluded: 5,
enrichmentCreditsIncluded: 0,
pooledUsage: false,
},
features: createFeatureFlags({
csvExport: true,
mapSearch: true,
radiusSearch: true,
basicFilters: true,
advancedFilters: true,
savedSearches: true,
territoryMapping: true,
deduplication: true,
exportHistory: true,
taggingNotes: true,
prioritySupport: true,
}),
featureReadiness: {
savedSearches: 'marketed_not_enforced',
territoryMapping: 'launch_ready',
deduplication: 'marketed_not_enforced',
exportHistory: 'marketed_not_enforced',
taggingNotes: 'marketed_not_enforced',
},
processingTier: 'priority',
eligibleAddonCodes: ['export_pack_10k', 'export_pack_50k', 'enrichment_pack_1k'],
}),
createPlan({
code: 'growth_annual',
tier: 'growth',
planFamily: 'growth',
name: 'Growth',
billingInterval: 'annual',
listingCategory: 'pricing_page_hidden',
isSelfServe: true,
contactSalesRequired: false,
priceCents: 95040,
currencyCode: 'USD',
annualDiscountPercent: 20,
limits: {
researchRunsPerMonth: 150,
exportsPerMonth: 15000,
usersIncluded: 3,
workspacesIncluded: 5,
enrichmentCreditsIncluded: 0,
pooledUsage: false,
},
features: createFeatureFlags({
csvExport: true,
mapSearch: true,
radiusSearch: true,
basicFilters: true,
advancedFilters: true,
savedSearches: true,
territoryMapping: true,
deduplication: true,
exportHistory: true,
taggingNotes: true,
prioritySupport: true,
}),
featureReadiness: {
savedSearches: 'marketed_not_enforced',
territoryMapping: 'launch_ready',
deduplication: 'marketed_not_enforced',
exportHistory: 'marketed_not_enforced',
taggingNotes: 'marketed_not_enforced',
},
processingTier: 'priority',
eligibleAddonCodes: ['export_pack_10k', 'export_pack_50k', 'enrichment_pack_1k'],
}),
createPlan({
code: 'pro_monthly',
tier: 'pro',
planFamily: 'pro',
name: 'Pro',
billingInterval: 'monthly',
listingCategory: 'pricing_page_primary',
isSelfServe: true,
contactSalesRequired: false,
priceCents: 24900,
currencyCode: 'USD',
annualDiscountPercent: null,
limits: {
researchRunsPerMonth: 500,
exportsPerMonth: 75000,
usersIncluded: 15,
workspacesIncluded: null,
enrichmentCreditsIncluded: 0,
pooledUsage: false,
},
features: createFeatureFlags({
csvExport: true,
mapSearch: true,
radiusSearch: true,
basicFilters: true,
advancedFilters: true,
savedSearches: true,
territoryMapping: true,
deduplication: true,
exportHistory: true,
taggingNotes: true,
sharedLists: true,
scheduledResearch: true,
bulkExports: true,
crmIntegrations: true,
apiAccess: true,
webhooks: true,
collaboration: true,
enrichments: true,
prioritySupport: true,
}),
featureReadiness: {
savedSearches: 'marketed_not_enforced',
deduplication: 'marketed_not_enforced',
exportHistory: 'marketed_not_enforced',
taggingNotes: 'marketed_not_enforced',
sharedLists: 'marketed_not_enforced',
scheduledResearch: 'marketed_not_enforced',
bulkExports: 'marketed_not_enforced',
crmIntegrations: 'marketed_not_enforced',
apiAccess: 'marketed_not_enforced',
webhooks: 'marketed_not_enforced',
collaboration: 'marketed_not_enforced',
enrichments: 'marketed_not_enforced',
},
processingTier: 'priority',
eligibleAddonCodes: ['export_pack_10k', 'export_pack_50k', 'enrichment_pack_1k', 'ai_assistant_monthly', 'white_label_monthly'],
}),
createPlan({
code: 'pro_annual',
tier: 'pro',
planFamily: 'pro',
name: 'Pro',
billingInterval: 'annual',
listingCategory: 'pricing_page_hidden',
isSelfServe: true,
contactSalesRequired: false,
priceCents: 239040,
currencyCode: 'USD',
annualDiscountPercent: 20,
limits: {
researchRunsPerMonth: 500,
exportsPerMonth: 75000,
usersIncluded: 15,
workspacesIncluded: null,
enrichmentCreditsIncluded: 0,
pooledUsage: false,
},
features: createFeatureFlags({
csvExport: true,
mapSearch: true,
radiusSearch: true,
basicFilters: true,
advancedFilters: true,
savedSearches: true,
territoryMapping: true,
deduplication: true,
exportHistory: true,
taggingNotes: true,
sharedLists: true,
scheduledResearch: true,
bulkExports: true,
crmIntegrations: true,
apiAccess: true,
webhooks: true,
collaboration: true,
enrichments: true,
prioritySupport: true,
}),
featureReadiness: {
savedSearches: 'marketed_not_enforced',
deduplication: 'marketed_not_enforced',
exportHistory: 'marketed_not_enforced',
taggingNotes: 'marketed_not_enforced',
sharedLists: 'marketed_not_enforced',
scheduledResearch: 'marketed_not_enforced',
bulkExports: 'marketed_not_enforced',
crmIntegrations: 'marketed_not_enforced',
apiAccess: 'marketed_not_enforced',
webhooks: 'marketed_not_enforced',
collaboration: 'marketed_not_enforced',
enrichments: 'marketed_not_enforced',
},
processingTier: 'priority',
eligibleAddonCodes: ['export_pack_10k', 'export_pack_50k', 'enrichment_pack_1k', 'ai_assistant_monthly', 'white_label_monthly'],
}),
createPlan({
code: 'enterprise_custom',
tier: 'enterprise',
planFamily: 'enterprise',
name: 'Enterprise',
billingInterval: 'custom',
listingCategory: 'pricing_page_primary',
isSelfServe: false,
contactSalesRequired: true,
priceCents: null,
currencyCode: 'USD',
annualDiscountPercent: null,
limits: {
researchRunsPerMonth: null,
exportsPerMonth: null,
usersIncluded: null,
workspacesIncluded: null,
enrichmentCreditsIncluded: null,
pooledUsage: true,
},
features: createFeatureFlags({
csvExport: true,
mapSearch: true,
radiusSearch: true,
basicFilters: true,
advancedFilters: true,
savedSearches: true,
territoryMapping: true,
deduplication: true,
exportHistory: true,
taggingNotes: true,
sharedLists: true,
scheduledResearch: true,
bulkExports: true,
crmIntegrations: true,
apiAccess: true,
webhooks: true,
collaboration: true,
enrichments: true,
prioritySupport: true,
sso: true,
sla: true,
whiteLabel: true,
}),
featureReadiness: {
sharedLists: 'marketed_not_enforced',
scheduledResearch: 'marketed_not_enforced',
bulkExports: 'marketed_not_enforced',
crmIntegrations: 'marketed_not_enforced',
apiAccess: 'marketed_not_enforced',
webhooks: 'marketed_not_enforced',
collaboration: 'marketed_not_enforced',
enrichments: 'marketed_not_enforced',
sso: 'future',
sla: 'future',
whiteLabel: 'future',
},
processingTier: 'dedicated',
eligibleAddonCodes: ['export_pack_10k', 'export_pack_50k', 'enrichment_pack_1k', 'ai_assistant_monthly', 'white_label_monthly'],
}),
] as const satisfies readonly PlanDefinition[];
const activePlanCatalogByCode = new Map(activePlanCatalog.map((plan) => [plan.code, plan]));
const planDisplayMetaByCode: Record<ActivePlanCode, PlanDisplayMeta> = {
starter_monthly: {
audience: 'For freelancers and solo operators',
summary: 'A focused entry point for recurring local market research.',
ctaLabel: 'Choose Starter',
ctaMode: 'sign_up',
qualitativeBullets: ['CSV export, map search, radius search, and basic filters'],
},
starter_annual: {
audience: 'For freelancers and solo operators',
summary: 'A focused entry point for recurring local market research.',
ctaLabel: 'Choose Starter',
ctaMode: 'sign_up',
badgeLabel: 'Save 20%',
qualitativeBullets: ['CSV export, map search, radius search, and basic filters'],
},
growth_monthly: {
audience: 'For agencies and outbound teams',
summary: 'The best-value plan for repeatable territory workflows.',
ctaLabel: 'Choose Growth',
ctaMode: 'sign_up',
badgeLabel: 'Best Value',
qualitativeBullets: ['Saved searches, territory mapping, advanced filtering, deduplication, export history, and priority support'],
},
growth_annual: {
audience: 'For agencies and outbound teams',
summary: 'The best-value plan for repeatable territory workflows.',
ctaLabel: 'Choose Growth',
ctaMode: 'sign_up',
badgeLabel: 'Save 20%',
qualitativeBullets: ['Saved searches, territory mapping, advanced filtering, deduplication, export history, and priority support'],
},
pro_monthly: {
audience: 'For power users and scaling teams',
summary: 'Operational infrastructure for teams that need more scale and automation.',
ctaLabel: 'Choose Pro',
ctaMode: 'sign_up',
qualitativeBullets: ['Scheduled research, bulk exports, CRM integrations, API access, enrichment access, and collaboration features'],
},
pro_annual: {
audience: 'For power users and scaling teams',
summary: 'Operational infrastructure for teams that need more scale and automation.',
ctaLabel: 'Choose Pro',
ctaMode: 'sign_up',
badgeLabel: 'Save 20%',
qualitativeBullets: ['Scheduled research, bulk exports, CRM integrations, API access, enrichment access, and collaboration features'],
},
enterprise_custom: {
audience: 'For multi-location and enterprise rollouts',
summary: 'Custom market intelligence infrastructure for security, governance, and scale.',
ctaLabel: 'Talk to sales',
ctaMode: 'sign_in',
qualitativeBullets: ['SSO, SLA, onboarding, and account management', 'White-labeling, custom enrichments, and custom integrations', 'Dedicated infrastructure and advanced API scaling'],
},
};
export const ACTIVE_PLAN_CATALOG = [...activePlanCatalog];
export function getActivePlanCatalog() {
return ACTIVE_PLAN_CATALOG;
}
export function getPlanByCode(code: PlanCode) {
return activePlanCatalogByCode.get(code as ActivePlanCode) ?? null;
}
export function getSelfServePlans() {
return ACTIVE_PLAN_CATALOG.filter((plan) => plan.isSelfServe);
}
export function getAnnualPlans() {
return ACTIVE_PLAN_CATALOG.filter((plan) => plan.billingInterval === 'annual');
}
export function getPublicPricingPlans() {
return ACTIVE_PLAN_CATALOG.filter((plan) => plan.listingCategory === 'pricing_page_primary');
}
export function getPublicPricingPlansForInterval(billingInterval: BillingInterval) {
return ACTIVE_PLAN_CATALOG.filter((plan) => {
if (plan.code === 'enterprise_custom') {
return true;
}
return plan.listingCategory === 'pricing_page_hidden'
? plan.billingInterval === billingInterval
: plan.listingCategory === 'pricing_page_primary' && plan.billingInterval === billingInterval;
});
}
export function getPlanVariant(tier: PlanTier, billingInterval: BillingInterval) {
return ACTIVE_PLAN_CATALOG.find((plan) => plan.planFamily === tier && plan.billingInterval === billingInterval) ?? null;
}
export function getSiblingPlans(code: ActivePlanCode) {
const selectedPlan = getPlanByCode(code);
if (!selectedPlan) {
return [];
}
return ACTIVE_PLAN_CATALOG.filter((plan) => plan.planFamily === selectedPlan.planFamily);
}
export function isAnnualPlan(code: ActivePlanCode) {
return getPlanByCode(code)?.billingInterval === 'annual';
}
export function getPlanDisplayMeta(code: ActivePlanCode) {
return planDisplayMetaByCode[code];
}
export function getPlanCardBullets(code: ActivePlanCode) {
const plan = getPlanByCode(code);
const display = getPlanDisplayMeta(code);
if (!plan) {
return display.qualitativeBullets;
}
const quantitativeBullets = [
formatResearchRunsBullet(plan.limits.researchRunsPerMonth),
formatExportsBullet(plan.limits.exportsPerMonth),
formatSeatAllowanceBullet(plan.limits.usersIncluded, plan.limits.workspacesIncluded, plan.limits.pooledUsage),
].filter((bullet): bullet is string => bullet !== null);
return [...quantitativeBullets, ...display.qualitativeBullets];
}
function formatResearchRunsBullet(researchRunsPerMonth: number | null) {
if (researchRunsPerMonth === null) {
return 'Pooled or custom research capacity';
}
return `${formatCount(researchRunsPerMonth)} research runs / month`;
}
function formatExportsBullet(exportsPerMonth: number | null) {
if (exportsPerMonth === null) {
return null;
}
return `${formatCount(exportsPerMonth)} exports / month`;
}
function formatSeatAllowanceBullet(usersIncluded: number | null, workspacesIncluded: number | null, pooledUsage: boolean) {
if (pooledUsage) {
return 'Pooled or custom team usage';
}
if (usersIncluded === null && workspacesIncluded === null) {
return null;
}
if (usersIncluded !== null && workspacesIncluded === null) {
return usersIncluded === 1 ? '1 user and unlimited workspaces' : `${formatCount(usersIncluded)} users and unlimited workspaces`;
}
if (usersIncluded === null && workspacesIncluded !== null) {
return workspacesIncluded === 1 ? '1 workspace' : `${formatCount(workspacesIncluded)} workspaces`;
}
const userLabel = usersIncluded === 1 ? '1 user' : `${formatCount(usersIncluded as number)} users`;
const workspaceLabel = workspacesIncluded === 1 ? '1 workspace' : `${formatCount(workspacesIncluded as number)} workspaces`;
return `${userLabel} and ${workspaceLabel}`;
}
function formatCount(value: number) {
return new Intl.NumberFormat('en-US').format(value);
}