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
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
import type { AddonCode, ActivePlanCode } from './plans.js';
|
||||
import { getPlanByCode } from './plans.js';
|
||||
import type { UsageResource } from './entitlements.js';
|
||||
|
||||
export type AddonType = 'resource_pack' | 'feature_addon';
|
||||
|
||||
export type AddonPurchaseMode = 'one_time' | 'recurring';
|
||||
|
||||
export type AddonAvailability = 'active' | 'coming_soon' | 'internal_only';
|
||||
|
||||
export interface AddonDefinition {
|
||||
code: AddonCode;
|
||||
name: string;
|
||||
type: AddonType;
|
||||
resource: UsageResource | null;
|
||||
quantity: number | null;
|
||||
priceCents: number;
|
||||
currencyCode: 'USD';
|
||||
purchaseMode: AddonPurchaseMode;
|
||||
availability: AddonAvailability;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const addonCatalog: AddonDefinition[] = [
|
||||
{
|
||||
code: 'export_pack_10k',
|
||||
name: 'Export Pack 10k',
|
||||
type: 'resource_pack',
|
||||
resource: 'exports',
|
||||
quantity: 10000,
|
||||
priceCents: 2900,
|
||||
currencyCode: 'USD',
|
||||
purchaseMode: 'one_time',
|
||||
availability: 'active',
|
||||
description: 'Add 10,000 extra exports to a workspace. Base plan exports should be consumed before this pack is used.',
|
||||
},
|
||||
{
|
||||
code: 'export_pack_50k',
|
||||
name: 'Export Pack 50k',
|
||||
type: 'resource_pack',
|
||||
resource: 'exports',
|
||||
quantity: 50000,
|
||||
priceCents: 9900,
|
||||
currencyCode: 'USD',
|
||||
purchaseMode: 'one_time',
|
||||
availability: 'active',
|
||||
description: 'Add 50,000 extra exports to a workspace. Base plan exports should be consumed before this pack is used.',
|
||||
},
|
||||
{
|
||||
code: 'enrichment_pack_1k',
|
||||
name: 'Enrichment Pack 1k',
|
||||
type: 'resource_pack',
|
||||
resource: 'enrichments',
|
||||
quantity: 1000,
|
||||
priceCents: 4900,
|
||||
currencyCode: 'USD',
|
||||
purchaseMode: 'one_time',
|
||||
availability: 'coming_soon',
|
||||
description: 'Add 1,000 enrichment units once enrichment actions are live.',
|
||||
},
|
||||
{
|
||||
code: 'ai_assistant_monthly',
|
||||
name: 'AI Prospecting Assistant',
|
||||
type: 'feature_addon',
|
||||
resource: null,
|
||||
quantity: null,
|
||||
priceCents: 4900,
|
||||
currencyCode: 'USD',
|
||||
purchaseMode: 'recurring',
|
||||
availability: 'coming_soon',
|
||||
description: 'Recurring feature add-on for AI-assisted market and territory prompts.',
|
||||
},
|
||||
{
|
||||
code: 'white_label_monthly',
|
||||
name: 'White Label Toolkit',
|
||||
type: 'feature_addon',
|
||||
resource: null,
|
||||
quantity: null,
|
||||
priceCents: 19900,
|
||||
currencyCode: 'USD',
|
||||
purchaseMode: 'recurring',
|
||||
availability: 'coming_soon',
|
||||
description: 'Recurring agency add-on for branded outputs and white-label workflows.',
|
||||
},
|
||||
];
|
||||
|
||||
const addonCatalogByCode = new Map(addonCatalog.map((addon) => [addon.code, addon]));
|
||||
|
||||
export const ADDON_CATALOG = [...addonCatalog];
|
||||
|
||||
export function getAddonByCode(code: AddonCode) {
|
||||
return addonCatalogByCode.get(code) ?? null;
|
||||
}
|
||||
|
||||
export function getActiveAddons() {
|
||||
return ADDON_CATALOG.filter((addon) => addon.availability === 'active');
|
||||
}
|
||||
|
||||
export function getAddonsForResource(resource: UsageResource) {
|
||||
return ADDON_CATALOG.filter((addon) => addon.resource === resource);
|
||||
}
|
||||
|
||||
export function getEligibleAddonsForPlan(planCode: ActivePlanCode, options?: { includeComingSoon?: boolean }) {
|
||||
const plan = getPlanByCode(planCode);
|
||||
|
||||
if (!plan) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return plan.eligibleAddonCodes
|
||||
.map((addonCode) => getAddonByCode(addonCode))
|
||||
.filter((addon): addon is AddonDefinition => addon !== null)
|
||||
.filter((addon) => (options?.includeComingSoon ? addon.availability !== 'internal_only' : addon.availability === 'active'));
|
||||
}
|
||||
Reference in New Issue
Block a user