import type { UsageAction } from './entitlements.js'; import type { ActivePlanCode, FeatureReadiness, PlanFeatures } from './plans.js'; import { getPlanByCode } from './plans.js'; export type FeatureGateState = 'available' | 'upgrade_required' | 'coming_soon' | 'contact_sales' | 'hidden'; export interface FeatureGateResult { feature: keyof PlanFeatures; state: FeatureGateState; currentPlanCode: ActivePlanCode; upgradePlanCode: ActivePlanCode | null; message: string | null; } export interface FeatureGatePolicy { enterpriseOnly?: boolean; hiddenWhenUnavailable?: boolean; upgradeTargetByPlan?: Partial>; } const featureGatePolicies: Partial> = { advancedFilters: { upgradeTargetByPlan: { starter_monthly: 'growth_monthly', starter_annual: 'growth_annual', }, }, savedSearches: { upgradeTargetByPlan: { starter_monthly: 'growth_monthly', starter_annual: 'growth_annual', }, }, territoryMapping: { upgradeTargetByPlan: { starter_monthly: 'growth_monthly', starter_annual: 'growth_annual', }, }, deduplication: { upgradeTargetByPlan: { starter_monthly: 'growth_monthly', starter_annual: 'growth_annual', }, }, exportHistory: { upgradeTargetByPlan: { starter_monthly: 'growth_monthly', starter_annual: 'growth_annual', }, }, taggingNotes: { upgradeTargetByPlan: { starter_monthly: 'growth_monthly', starter_annual: 'growth_annual', }, }, prioritySupport: { upgradeTargetByPlan: { starter_monthly: 'growth_monthly', starter_annual: 'growth_annual', }, }, sharedLists: { hiddenWhenUnavailable: true, upgradeTargetByPlan: { starter_monthly: 'pro_monthly', starter_annual: 'pro_annual', growth_monthly: 'pro_monthly', growth_annual: 'pro_annual', }, }, scheduledResearch: { hiddenWhenUnavailable: true, upgradeTargetByPlan: { starter_monthly: 'pro_monthly', starter_annual: 'pro_annual', growth_monthly: 'pro_monthly', growth_annual: 'pro_annual', }, }, bulkExports: { hiddenWhenUnavailable: true, upgradeTargetByPlan: { starter_monthly: 'pro_monthly', starter_annual: 'pro_annual', growth_monthly: 'pro_monthly', growth_annual: 'pro_annual', }, }, crmIntegrations: { hiddenWhenUnavailable: true, upgradeTargetByPlan: { starter_monthly: 'pro_monthly', starter_annual: 'pro_annual', growth_monthly: 'pro_monthly', growth_annual: 'pro_annual', }, }, apiAccess: { hiddenWhenUnavailable: true, upgradeTargetByPlan: { starter_monthly: 'pro_monthly', starter_annual: 'pro_annual', growth_monthly: 'pro_monthly', growth_annual: 'pro_annual', }, }, webhooks: { hiddenWhenUnavailable: true, upgradeTargetByPlan: { starter_monthly: 'pro_monthly', starter_annual: 'pro_annual', growth_monthly: 'pro_monthly', growth_annual: 'pro_annual', }, }, collaboration: { hiddenWhenUnavailable: true, upgradeTargetByPlan: { starter_monthly: 'pro_monthly', starter_annual: 'pro_annual', growth_monthly: 'pro_monthly', growth_annual: 'pro_annual', }, }, enrichments: { hiddenWhenUnavailable: true, upgradeTargetByPlan: { starter_monthly: 'pro_monthly', starter_annual: 'pro_annual', growth_monthly: 'pro_monthly', growth_annual: 'pro_annual', }, }, sso: { enterpriseOnly: true }, sla: { enterpriseOnly: true }, whiteLabel: { enterpriseOnly: true }, }; export function getFeatureGate(planCode: ActivePlanCode, feature: keyof PlanFeatures): FeatureGateResult { const plan = getRequiredActivePlan(planCode); const included = plan.features[feature]; const readiness = resolveFeatureReadiness(planCode, feature); const upgradePlanCode = getUpgradePlanForFeature(planCode, feature); if (included && readiness === 'launch_ready') { return { feature, state: 'available', currentPlanCode: planCode, upgradePlanCode: null, message: getFeatureGateMessageForState('available', feature, null), }; } if (included && readiness !== 'launch_ready') { const comingSoonState = shouldUseContactSalesForReadyLaterFeature(feature) ? 'contact_sales' : 'coming_soon'; return { feature, state: comingSoonState, currentPlanCode: planCode, upgradePlanCode: comingSoonState === 'contact_sales' ? 'enterprise_custom' : null, message: getFeatureGateMessageForState(comingSoonState, feature, upgradePlanCode), }; } if (upgradePlanCode) { const upgradeState = upgradePlanCode === 'enterprise_custom' ? 'contact_sales' : 'upgrade_required'; return { feature, state: upgradeState, currentPlanCode: planCode, upgradePlanCode, message: getFeatureGateMessageForState(upgradeState, feature, upgradePlanCode), }; } if (isEnterpriseOnlyFeature(feature)) { return { feature, state: 'contact_sales', currentPlanCode: planCode, upgradePlanCode: 'enterprise_custom', message: getFeatureGateMessageForState('contact_sales', feature, 'enterprise_custom'), }; } const hidden = shouldHideWhenUnavailable(feature); return { feature, state: hidden ? 'hidden' : 'coming_soon', currentPlanCode: planCode, upgradePlanCode: null, message: getFeatureGateMessageForState(hidden ? 'hidden' : 'coming_soon', feature, null), }; } export function isFeatureAvailable(planCode: ActivePlanCode, feature: keyof PlanFeatures) { return getFeatureGate(planCode, feature).state === 'available'; } export function getUpgradePlanForFeature(planCode: ActivePlanCode, feature: keyof PlanFeatures): ActivePlanCode | null { const policy = featureGatePolicies[feature]; const directTarget = policy?.upgradeTargetByPlan?.[planCode] ?? null; if (directTarget) { return directTarget; } if (policy?.enterpriseOnly) { return 'enterprise_custom'; } return null; } export function getFeatureGateMessage(planCode: ActivePlanCode, feature: keyof PlanFeatures) { const result = getFeatureGate(planCode, feature); return result.message; } // territoryMapping currently proxies deep research capability until a dedicated // deep-research feature key exists in the plan catalog. export function getFeatureForAction(action: UsageAction): keyof PlanFeatures | null { switch (action) { case 'basic_search_run': return null; case 'deep_research_preview': case 'deep_research_batch_run': return 'territoryMapping'; case 'csv_export': return 'csvExport'; case 'enrichment_run': return 'enrichments'; case 'api_request': return 'apiAccess'; } } function resolveFeatureReadiness(planCode: ActivePlanCode, feature: keyof PlanFeatures): FeatureReadiness { const plan = getRequiredActivePlan(planCode); return plan.featureReadiness[feature] ?? 'launch_ready'; } function isEnterpriseOnlyFeature(feature: keyof PlanFeatures) { return featureGatePolicies[feature]?.enterpriseOnly === true; } function shouldHideWhenUnavailable(feature: keyof PlanFeatures) { return featureGatePolicies[feature]?.hiddenWhenUnavailable === true; } function shouldUseContactSalesForReadyLaterFeature(feature: keyof PlanFeatures) { return isEnterpriseOnlyFeature(feature); } function getFeatureGateMessageForState( state: FeatureGateState, feature: keyof PlanFeatures, upgradePlanCode: ActivePlanCode | null, ) { switch (state) { case 'available': return null; case 'upgrade_required': return upgradePlanCode ? `Upgrade to ${getRequiredActivePlan(upgradePlanCode).name} to access ${formatFeatureName(feature)}.` : `Upgrade to access ${formatFeatureName(feature)}.`; case 'coming_soon': return `${formatFeatureName(feature)} is included in this tier and coming soon.`; case 'contact_sales': return `Contact sales to access ${formatFeatureName(feature)}.`; case 'hidden': return null; } } function getRequiredActivePlan(planCode: ActivePlanCode) { const plan = getPlanByCode(planCode); if (!plan) { throw new Error(`Unknown active plan code: ${planCode}`); } return plan; } function formatFeatureName(feature: keyof PlanFeatures) { const names: Record = { csvExport: 'CSV export', mapSearch: 'map search', radiusSearch: 'radius search', basicFilters: 'basic filters', advancedFilters: 'advanced filters', savedSearches: 'saved searches', territoryMapping: 'territory mapping', deduplication: 'deduplication', exportHistory: 'export history', taggingNotes: 'tagging and notes', sharedLists: 'shared lists', scheduledResearch: 'scheduled research', bulkExports: 'bulk exports', crmIntegrations: 'CRM integrations', apiAccess: 'API access', webhooks: 'webhooks', collaboration: 'collaboration features', enrichments: 'enrichments', prioritySupport: 'priority support', sso: 'SSO', sla: 'SLA support', whiteLabel: 'white-labeling', }; return names[feature]; } // This module interprets plan feature availability and rollout state. It does not // perform route enforcement or usage quota checks.