Public Access
1
0
Files
leads4less/shared/billing/plans.ts
T
pguerrerox f1a46c79f2 feat: add billing plan foundations and refresh LocaleScope pricing
Introduce a shared pricing and entitlement model so plan metadata can drive public pricing and future subscription enforcement. Rebrand the product to LocaleScope and align the UI copy around market intelligence and business research workflows.
2026-05-13 03:50:29 +00:00

608 lines
18 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 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);
}