Public Access
1
0
Files
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

321 lines
9.3 KiB
TypeScript

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<Record<ActivePlanCode, ActivePlanCode | 'enterprise_custom'>>;
}
const featureGatePolicies: Partial<Record<keyof PlanFeatures, FeatureGatePolicy>> = {
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<keyof PlanFeatures, string> = {
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.