import type { ActivePlanCode, BillingInterval, FeatureReadiness, PlanCode, PlanFeatures } from './plans.js'; import { getPlanByCode } from './plans.js'; // Public pricing stays expressed as research runs per month, but internal accounting // uses research credits so future actions can consume variable amounts. export type UsageSubjectType = 'user'; export type UsageResource = 'research_credits' | 'exports' | 'enrichments' | 'api_requests'; export type UsageAllowanceAvailability = 'included' | 'not_available' | 'custom' | 'unlimited'; export type UsageAction = | 'basic_search_run' | 'deep_research_preview' | 'deep_research_batch_run' | 'csv_export' | 'enrichment_run' | 'api_request'; export type EntitlementDecisionStatus = | 'allowed' | 'blocked_upgrade_required' | 'blocked_addon_available' | 'contact_sales_required'; export type EntitlementDenialReason = | 'feature_not_available' | 'quota_exhausted' | 'custom_enterprise_only' | 'not_launch_ready' | 'billing_not_configured' | 'billing_past_due' | 'billing_canceled' | 'billing_inactive'; export interface UsageSubject { type: UsageSubjectType; id: string; } export interface UsageAmount { resource: UsageResource; amount: number; } export interface UsageAllowance { resource: UsageResource; included: number | null; billingInterval: BillingInterval | null; pooled: boolean; availability: UsageAllowanceAvailability; } export interface PlanUsageAllowance { planCode: ActivePlanCode; billingInterval: BillingInterval; researchCredits: UsageAllowance; exports: UsageAllowance; enrichments: UsageAllowance; apiRequests: UsageAllowance; } export interface UsageCostEstimate { action: UsageAction; amounts: UsageAmount[]; isChargeable: boolean; details?: Record; } export interface ActionPolicy { action: UsageAction; chargeable: boolean; consumedResources: UsageResource[]; requiredFeatures: Array; readinessFeatures: Array; } export interface EntitlementDecision { status: EntitlementDecisionStatus; denialReason: EntitlementDenialReason | null; action: UsageAction; resource: UsageResource; requiredAmount: number; remainingAmount: number | null; currentPlanCode: ActivePlanCode | null; suggestedUpgradePlanCode: ActivePlanCode | null; addonEligible: boolean; contactSalesRequired: boolean; } export interface DeepResearchBatchUsageInput { estimatedChildJobs?: number | null; totalAreas?: number | null; } export interface ActionEntitlementEvaluationInput { planCode: ActivePlanCode; action: UsageAction; resource: UsageResource; requiredAmount: number; remainingAmount: number | null; } const actionPolicies: Record = { basic_search_run: { action: 'basic_search_run', chargeable: true, consumedResources: ['research_credits'], requiredFeatures: [], readinessFeatures: [], }, deep_research_preview: { action: 'deep_research_preview', chargeable: false, consumedResources: [], requiredFeatures: ['territoryMapping'], readinessFeatures: ['territoryMapping'], }, deep_research_batch_run: { action: 'deep_research_batch_run', chargeable: true, consumedResources: ['research_credits'], requiredFeatures: ['territoryMapping'], readinessFeatures: ['territoryMapping'], }, csv_export: { action: 'csv_export', chargeable: true, consumedResources: ['exports'], requiredFeatures: ['csvExport'], readinessFeatures: ['csvExport'], }, enrichment_run: { action: 'enrichment_run', chargeable: true, consumedResources: ['enrichments'], requiredFeatures: ['enrichments'], readinessFeatures: ['enrichments'], }, api_request: { action: 'api_request', chargeable: true, consumedResources: ['api_requests'], requiredFeatures: ['apiAccess'], readinessFeatures: ['apiAccess'], }, }; export function isChargeableAction(action: UsageAction) { return actionPolicies[action].chargeable; } export function getActionPolicy(action: UsageAction) { return actionPolicies[action]; } export function getIncludedResearchCredits(planCode: ActivePlanCode) { const plan = getRequiredActivePlan(planCode); return plan.limits.researchRunsPerMonth; } export function getIncludedExports(planCode: ActivePlanCode) { const plan = getRequiredActivePlan(planCode); return plan.limits.exportsPerMonth; } export function getUsageAllowanceForPlan(planCode: ActivePlanCode): PlanUsageAllowance { const plan = getRequiredActivePlan(planCode); return { planCode, billingInterval: plan.billingInterval, researchCredits: { resource: 'research_credits', included: plan.limits.researchRunsPerMonth, billingInterval: plan.billingInterval, pooled: plan.limits.pooledUsage, availability: plan.limits.researchRunsPerMonth === null ? 'custom' : 'included', }, exports: { resource: 'exports', included: plan.limits.exportsPerMonth, billingInterval: plan.billingInterval, pooled: plan.limits.pooledUsage, availability: plan.limits.exportsPerMonth === null ? 'custom' : 'included', }, enrichments: { resource: 'enrichments', included: plan.limits.enrichmentCreditsIncluded, billingInterval: plan.billingInterval, pooled: plan.limits.pooledUsage, availability: plan.features.enrichments ? (plan.limits.enrichmentCreditsIncluded === null ? 'custom' : 'included') : 'not_available', }, apiRequests: { resource: 'api_requests', included: null, billingInterval: plan.billingInterval, pooled: plan.limits.pooledUsage, availability: plan.features.apiAccess ? 'custom' : 'not_available', }, }; } export function estimateBasicSearchCost(): UsageCostEstimate { return { action: 'basic_search_run', amounts: [{ resource: 'research_credits', amount: 1 }], isChargeable: true, }; } export function estimateDeepResearchPreviewCost(): UsageCostEstimate { return { action: 'deep_research_preview', amounts: [], isChargeable: false, details: { estimatedChargeableRuns: 0 }, }; } export function estimateDeepResearchBatchCost(input: DeepResearchBatchUsageInput): UsageCostEstimate { const estimatedChildJobs = normalizePositiveWholeNumber(input.estimatedChildJobs); const totalAreas = normalizePositiveWholeNumber(input.totalAreas); const amount = estimatedChildJobs ?? totalAreas ?? 0; const estimateConfidence = estimatedChildJobs !== null ? 'high' : totalAreas !== null ? 'fallback' : 'unknown'; return { action: 'deep_research_batch_run', amounts: [{ resource: 'research_credits', amount }], isChargeable: amount > 0, details: { estimatedChildJobs: estimatedChildJobs ?? null, totalAreas: totalAreas ?? null, billingBasis: estimatedChildJobs !== null ? 'estimated_child_jobs' : totalAreas !== null ? 'total_areas' : 'unknown', estimateConfidence, }, }; } export function estimateExportUsage(rowCount: number): UsageCostEstimate { const amount = Math.max(0, Math.floor(rowCount)); return { action: 'csv_export', amounts: [{ resource: 'exports', amount }], isChargeable: amount > 0, details: { exportedRows: amount }, }; } export function canAllowanceCoverCost(allowance: UsageAllowance, amount: number) { if (amount <= 0) { return true; } if (allowance.availability === 'not_available') { return false; } if (allowance.availability === 'custom' || allowance.availability === 'unlimited') { return true; } if (allowance.included === null) { return false; } return allowance.included >= amount; } // Quantity-only helper. This does not check whether the action itself is enabled for the plan. export function canPlanCoverEstimatedCost(planCode: ActivePlanCode, estimate: UsageCostEstimate) { if (!estimate.isChargeable) { return true; } const allowance = getUsageAllowanceForPlan(planCode); return estimate.amounts.every((usageAmount) => { const matchingAllowance = getAllowanceByResource(allowance, usageAmount.resource); return canAllowanceCoverCost(matchingAllowance, usageAmount.amount); }); } export function isFeatureAvailableForAction(planCode: ActivePlanCode, action: UsageAction) { const plan = getRequiredActivePlan(planCode); const policy = getActionPolicy(action); return policy.requiredFeatures.every((featureKey) => plan.features[featureKey]); } export function canPlanUseAction(planCode: ActivePlanCode, action: UsageAction) { const plan = getRequiredActivePlan(planCode); const policy = getActionPolicy(action); if (!isFeatureAvailableForAction(planCode, action)) { return false; } return policy.readinessFeatures.every((featureKey) => { const readiness = getFeatureReadiness(plan.featureReadiness[featureKey]); return readiness !== 'future'; }); } export function buildEntitlementDecision(input: { planCode: ActivePlanCode; action: UsageAction; resource: UsageResource; requiredAmount: number; remainingAmount: number | null; }): EntitlementDecision { const plan = getRequiredActivePlan(input.planCode); const addonEligible = hasEligibleAddonForResource(plan.code, input.resource); const actionFeatureAvailable = isFeatureAvailableForAction(plan.code, input.action); const actionReady = canPlanUseAction(plan.code, input.action); const canCover = input.remainingAmount === null || input.remainingAmount >= input.requiredAmount; if (!actionFeatureAvailable) { return { status: plan.contactSalesRequired ? 'contact_sales_required' : 'blocked_upgrade_required', denialReason: 'feature_not_available', action: input.action, resource: input.resource, requiredAmount: input.requiredAmount, remainingAmount: input.remainingAmount, currentPlanCode: plan.code, suggestedUpgradePlanCode: getSuggestedUpgradePlanCode(plan.code, input.resource), addonEligible: false, contactSalesRequired: plan.contactSalesRequired, }; } if (!actionReady) { return { status: plan.contactSalesRequired ? 'contact_sales_required' : 'blocked_upgrade_required', denialReason: 'not_launch_ready', action: input.action, resource: input.resource, requiredAmount: input.requiredAmount, remainingAmount: input.remainingAmount, currentPlanCode: plan.code, suggestedUpgradePlanCode: getSuggestedUpgradePlanCode(plan.code, input.resource), addonEligible: false, contactSalesRequired: plan.contactSalesRequired, }; } if (canCover) { return { status: 'allowed', denialReason: null, action: input.action, resource: input.resource, requiredAmount: input.requiredAmount, remainingAmount: input.remainingAmount, currentPlanCode: plan.code, suggestedUpgradePlanCode: null, addonEligible, contactSalesRequired: false, }; } if (!plan.isSelfServe || plan.contactSalesRequired) { return { status: 'contact_sales_required', denialReason: 'custom_enterprise_only', action: input.action, resource: input.resource, requiredAmount: input.requiredAmount, remainingAmount: input.remainingAmount, currentPlanCode: plan.code, suggestedUpgradePlanCode: null, addonEligible, contactSalesRequired: true, }; } if (addonEligible) { return { status: 'blocked_addon_available', denialReason: 'quota_exhausted', action: input.action, resource: input.resource, requiredAmount: input.requiredAmount, remainingAmount: input.remainingAmount, currentPlanCode: plan.code, suggestedUpgradePlanCode: getSuggestedUpgradePlanCode(plan.code, input.resource), addonEligible: true, contactSalesRequired: false, }; } return { status: 'blocked_upgrade_required', denialReason: 'quota_exhausted', action: input.action, resource: input.resource, requiredAmount: input.requiredAmount, remainingAmount: input.remainingAmount, currentPlanCode: plan.code, suggestedUpgradePlanCode: getSuggestedUpgradePlanCode(plan.code, input.resource), addonEligible: false, contactSalesRequired: false, }; } export function evaluateActionEntitlement(input: ActionEntitlementEvaluationInput) { return buildEntitlementDecision({ planCode: input.planCode, action: input.action, resource: input.resource, requiredAmount: input.requiredAmount, remainingAmount: input.remainingAmount, }); } function getRequiredActivePlan(planCode: ActivePlanCode) { const plan = getPlanByCode(planCode); if (!plan) { throw new Error(`Unknown active plan code: ${planCode}`); } return plan; } function getAllowanceByResource(allowance: PlanUsageAllowance, resource: UsageResource) { switch (resource) { case 'research_credits': return allowance.researchCredits; case 'exports': return allowance.exports; case 'enrichments': return allowance.enrichments; case 'api_requests': return allowance.apiRequests; } } function getSuggestedUpgradePlanCode(planCode: ActivePlanCode, resource: UsageResource): ActivePlanCode | null { switch (planCode) { case 'starter_monthly': return resource === 'research_credits' || resource === 'exports' ? 'growth_monthly' : 'growth_monthly'; case 'starter_annual': return resource === 'research_credits' || resource === 'exports' ? 'growth_annual' : 'growth_annual'; case 'growth_monthly': return 'pro_monthly'; case 'growth_annual': return 'pro_annual'; case 'pro_monthly': case 'pro_annual': return 'enterprise_custom'; case 'enterprise_custom': return null; } } function hasEligibleAddonForResource(planCode: ActivePlanCode, resource: UsageResource) { const plan = getRequiredActivePlan(planCode); switch (resource) { case 'exports': return plan.eligibleAddonCodes.includes('export_pack_10k') || plan.eligibleAddonCodes.includes('export_pack_50k'); case 'enrichments': return plan.eligibleAddonCodes.includes('enrichment_pack_1k'); case 'research_credits': case 'api_requests': return false; } } function getFeatureReadiness(readiness?: FeatureReadiness) { return readiness ?? 'launch_ready'; } function normalizePositiveWholeNumber(value: number | null | undefined) { if (typeof value !== 'number' || !Number.isFinite(value)) { return null; } const rounded = Math.max(0, Math.ceil(value)); return rounded > 0 ? rounded : 0; } // Reserved plan codes are intentionally excluded from the active policy helpers until // founder/LTD packaging is activated in a later step. export function isActivePlanCodeForEntitlements(planCode: PlanCode): planCode is ActivePlanCode { return getPlanByCode(planCode) !== null; } // Export policy is defined here for later backend enforcement, but current CSV export // remains client-side until a server-backed export endpoint exists.