@trendyvideo/billing 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/dist/__tests__/billing-coverage.test.d.ts +2 -0
  2. package/dist/__tests__/billing-coverage.test.d.ts.map +1 -0
  3. package/dist/__tests__/billing-coverage.test.js +170 -0
  4. package/dist/__tests__/billing-coverage.test.js.map +1 -0
  5. package/dist/__tests__/plan-limits.test.d.ts +2 -0
  6. package/dist/__tests__/plan-limits.test.d.ts.map +1 -0
  7. package/dist/__tests__/plan-limits.test.js +139 -0
  8. package/dist/__tests__/plan-limits.test.js.map +1 -0
  9. package/dist/__tests__/pricing.test.d.ts +2 -0
  10. package/dist/__tests__/pricing.test.d.ts.map +1 -0
  11. package/dist/__tests__/pricing.test.js +146 -0
  12. package/dist/__tests__/pricing.test.js.map +1 -0
  13. package/dist/__tests__/usage-tracker.test.d.ts +2 -0
  14. package/dist/__tests__/usage-tracker.test.d.ts.map +1 -0
  15. package/dist/__tests__/usage-tracker.test.js +210 -0
  16. package/dist/__tests__/usage-tracker.test.js.map +1 -0
  17. package/dist/alerts.d.ts +19 -0
  18. package/dist/alerts.d.ts.map +1 -0
  19. package/dist/alerts.js +90 -0
  20. package/dist/alerts.js.map +1 -0
  21. package/dist/guards/project-guard.d.ts +6 -0
  22. package/dist/guards/project-guard.d.ts.map +1 -0
  23. package/dist/guards/project-guard.js +56 -0
  24. package/dist/guards/project-guard.js.map +1 -0
  25. package/dist/guards/resolution-guard.d.ts +6 -0
  26. package/dist/guards/resolution-guard.d.ts.map +1 -0
  27. package/dist/guards/resolution-guard.js +40 -0
  28. package/dist/guards/resolution-guard.js.map +1 -0
  29. package/dist/guards/seat-guard.d.ts +8 -0
  30. package/dist/guards/seat-guard.d.ts.map +1 -0
  31. package/dist/guards/seat-guard.js +55 -0
  32. package/dist/guards/seat-guard.js.map +1 -0
  33. package/dist/guards/social-guard.d.ts +6 -0
  34. package/dist/guards/social-guard.d.ts.map +1 -0
  35. package/dist/guards/social-guard.js +52 -0
  36. package/dist/guards/social-guard.js.map +1 -0
  37. package/dist/guards/storage-guard.d.ts +14 -0
  38. package/dist/guards/storage-guard.d.ts.map +1 -0
  39. package/dist/guards/storage-guard.js +85 -0
  40. package/dist/guards/storage-guard.js.map +1 -0
  41. package/dist/guards/stream-guard.d.ts +7 -0
  42. package/dist/guards/stream-guard.d.ts.map +1 -0
  43. package/dist/guards/stream-guard.js +73 -0
  44. package/dist/guards/stream-guard.js.map +1 -0
  45. package/dist/guards/watch-guard.d.ts +13 -0
  46. package/dist/guards/watch-guard.d.ts.map +1 -0
  47. package/dist/guards/watch-guard.js +71 -0
  48. package/dist/guards/watch-guard.js.map +1 -0
  49. package/dist/index.d.ts +16 -0
  50. package/dist/index.d.ts.map +1 -0
  51. package/dist/index.js +49 -0
  52. package/dist/index.js.map +1 -0
  53. package/dist/plan-limits.d.ts +25 -0
  54. package/dist/plan-limits.d.ts.map +1 -0
  55. package/dist/plan-limits.js +138 -0
  56. package/dist/plan-limits.js.map +1 -0
  57. package/dist/pricing.d.ts +32 -0
  58. package/dist/pricing.d.ts.map +1 -0
  59. package/dist/pricing.js +126 -0
  60. package/dist/pricing.js.map +1 -0
  61. package/dist/types.d.ts +188 -0
  62. package/dist/types.d.ts.map +1 -0
  63. package/dist/types.js +3 -0
  64. package/dist/types.js.map +1 -0
  65. package/dist/usage-checker.d.ts +26 -0
  66. package/dist/usage-checker.d.ts.map +1 -0
  67. package/dist/usage-checker.js +295 -0
  68. package/dist/usage-checker.js.map +1 -0
  69. package/dist/usage-tracker.d.ts +25 -0
  70. package/dist/usage-tracker.d.ts.map +1 -0
  71. package/dist/usage-tracker.js +181 -0
  72. package/dist/usage-tracker.js.map +1 -0
  73. package/package.json +41 -0
@@ -0,0 +1,126 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MICRODOLLARS_PER_CREDIT = exports.COST_MARKUP = exports.DEFAULT_PRICING = exports.MODEL_PRICING = void 0;
4
+ exports.getPricing = getPricing;
5
+ exports.calculateCostMicrodollars = calculateCostMicrodollars;
6
+ exports.microdollarsToCredits = microdollarsToCredits;
7
+ exports.creditsToMicrodollars = creditsToMicrodollars;
8
+ // ============================================================================
9
+ // Model Pricing — Single Source of Truth
10
+ // All prices in USD per 1 million tokens
11
+ // ============================================================================
12
+ exports.MODEL_PRICING = {
13
+ // ---------------------------------------------------------------------------
14
+ // Claude models
15
+ // ---------------------------------------------------------------------------
16
+ 'claude-opus-4-6': { input: 5, output: 25, cacheRead: 0.50, cacheWrite: 6.25 },
17
+ 'claude-opus-4-20250514': { input: 15, output: 75, cacheRead: 1.50, cacheWrite: 18.75 },
18
+ 'claude-sonnet-4-6': { input: 3, output: 15, cacheRead: 0.30, cacheWrite: 3.75 },
19
+ 'claude-sonnet-4-5-20250929': { input: 3, output: 15, cacheRead: 0.30, cacheWrite: 3.75 },
20
+ 'claude-sonnet-4-20250514': { input: 3, output: 15, cacheRead: 0.30, cacheWrite: 3.75 },
21
+ 'claude-haiku-4-5-20251001': { input: 1, output: 5, cacheRead: 0.10, cacheWrite: 1.25 },
22
+ 'claude-haiku-3-5-20241022': { input: 0.80, output: 4, cacheRead: 0.08, cacheWrite: 1 },
23
+ // ---------------------------------------------------------------------------
24
+ // Gemini models
25
+ // ---------------------------------------------------------------------------
26
+ 'gemini-3.1-pro-preview': { input: 2.50, output: 15, cacheRead: 0, cacheWrite: 0 },
27
+ // Tool-call-optimized Pro variant — same rate card as vanilla 3.1 Pro preview.
28
+ 'gemini-3.1-pro-preview-customtools': { input: 2.50, output: 15, cacheRead: 0, cacheWrite: 0 },
29
+ // gemini-3.1-flash-preview was a phantom SKU — never published on Vertex AI
30
+ // (returns 404). The actual Flash 3.x SKUs are:
31
+ // - `gemini-3-flash-preview` (regular Flash, global region)
32
+ // - `gemini-3.1-flash-lite-preview` (cheaper Lite variant, global region)
33
+ 'gemini-3-flash-preview': { input: 0.30, output: 2.50, cacheRead: 0, cacheWrite: 0 },
34
+ 'gemini-3.1-flash-lite-preview': { input: 0.15, output: 0.60, cacheRead: 0, cacheWrite: 0 },
35
+ 'gemini-2.5-pro': { input: 1.25, output: 10, cacheRead: 0, cacheWrite: 0 },
36
+ 'gemini-2.5-flash': { input: 0.15, output: 0.60, cacheRead: 0, cacheWrite: 0 },
37
+ 'gemini-2.0-flash': { input: 0.10, output: 0.40, cacheRead: 0, cacheWrite: 0 },
38
+ 'gemini-1.5-pro': { input: 1.25, output: 5, cacheRead: 0, cacheWrite: 0 },
39
+ 'gemini-1.5-flash': { input: 0.075, output: 0.30, cacheRead: 0, cacheWrite: 0 },
40
+ // ---------------------------------------------------------------------------
41
+ // Gemini image generation models (flat-rate — use costMicrodollarsOverride)
42
+ // Pricing: ~$0.04/image for Pro, ~$0.02/image for Flash
43
+ // ---------------------------------------------------------------------------
44
+ 'gemini-3-pro-image-preview': { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
45
+ 'gemini-3.1-flash-image-preview': { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
46
+ // ---------------------------------------------------------------------------
47
+ // Non-token services (flat-rate — use costMicrodollarsOverride when recording)
48
+ // Pricing listed here for reference; input = cost per 1M "units"
49
+ // ---------------------------------------------------------------------------
50
+ 'render-engine': { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
51
+ 'chirp-tts': { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
52
+ 'veo-video-gen': { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
53
+ 'text-embedding-005': { input: 0.025, output: 0, cacheRead: 0, cacheWrite: 0 },
54
+ // ---------------------------------------------------------------------------
55
+ // Storage (flat-rate — $0.05/GB/month, use costMicrodollarsOverride)
56
+ // ---------------------------------------------------------------------------
57
+ 'storage': { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
58
+ };
59
+ /**
60
+ * Default pricing used when the model is not found in MODEL_PRICING.
61
+ * Falls back to Claude Sonnet pricing as a safe default.
62
+ */
63
+ exports.DEFAULT_PRICING = {
64
+ input: 3,
65
+ output: 15,
66
+ cacheRead: 0.30,
67
+ cacheWrite: 3.75,
68
+ };
69
+ /**
70
+ * Look up pricing for a model. Tries exact match first, then prefix match
71
+ * (stripping date suffixes like `-20250514`), then falls back to DEFAULT_PRICING.
72
+ */
73
+ function getPricing(model) {
74
+ // Exact match
75
+ if (exports.MODEL_PRICING[model])
76
+ return exports.MODEL_PRICING[model];
77
+ // Prefix match (e.g. "gemini-2.5-flash-preview-05-20" → "gemini-2.5-flash")
78
+ for (const [key, pricing] of Object.entries(exports.MODEL_PRICING)) {
79
+ if (model.startsWith(key))
80
+ return pricing;
81
+ }
82
+ // Strip trailing date suffix and retry (e.g. "claude-sonnet-4-6-20260101" → "claude-sonnet-4-6")
83
+ const stripped = model.replace(/-\d{8}$/, '');
84
+ if (stripped !== model && exports.MODEL_PRICING[stripped]) {
85
+ return exports.MODEL_PRICING[stripped];
86
+ }
87
+ return exports.DEFAULT_PRICING;
88
+ }
89
+ /**
90
+ * Markup applied to all API costs to ensure profit margin.
91
+ * 1.5x means we charge 50% more than raw cost → ~33% margin.
92
+ * Applied to both token-based costs AND flat-rate overrides.
93
+ */
94
+ exports.COST_MARKUP = 1.5;
95
+ /**
96
+ * Calculate cost in microdollars (1 microdollar = $0.000001).
97
+ *
98
+ * Since pricing is per-million tokens:
99
+ * cost_usd = (tokens / 1_000_000) * rate_per_million
100
+ * cost_microdollars = cost_usd * 1_000_000 = tokens * rate_per_million
101
+ *
102
+ * All costs include COST_MARKUP (1.5x) for profit margin.
103
+ */
104
+ function calculateCostMicrodollars(model, inputTokens, outputTokens, cacheReadTokens = 0, cacheCreationTokens = 0, costMicrodollarsOverride) {
105
+ if (costMicrodollarsOverride !== undefined) {
106
+ return Math.round(costMicrodollarsOverride * exports.COST_MARKUP);
107
+ }
108
+ const p = getPricing(model);
109
+ const inputCost = inputTokens * p.input;
110
+ const outputCost = outputTokens * p.output;
111
+ const cacheReadCost = cacheReadTokens * p.cacheRead;
112
+ const cacheWriteCost = cacheCreationTokens * p.cacheWrite;
113
+ const rawCost = inputCost + outputCost + cacheReadCost + cacheWriteCost;
114
+ return Math.round(rawCost * exports.COST_MARKUP);
115
+ }
116
+ // ============================================================================
117
+ // Credit System — 1 credit = $0.01 = 10,000 microdollars
118
+ // ============================================================================
119
+ exports.MICRODOLLARS_PER_CREDIT = 10_000;
120
+ function microdollarsToCredits(microdollars) {
121
+ return Math.ceil(microdollars / exports.MICRODOLLARS_PER_CREDIT);
122
+ }
123
+ function creditsToMicrodollars(credits) {
124
+ return credits * exports.MICRODOLLARS_PER_CREDIT;
125
+ }
126
+ //# sourceMappingURL=pricing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pricing.js","sourceRoot":"","sources":["../src/pricing.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Minimal Supabase client interface — accepts any version of @supabase/supabase-js
3
+ * without requiring exact type identity (avoids duplicate package type mismatches)
4
+ */
5
+ export interface SupabaseClientLike {
6
+ rpc(fn: string, params?: Record<string, unknown>): PromiseLike<{
7
+ data: unknown;
8
+ error: {
9
+ message: string;
10
+ } | null;
11
+ }>;
12
+ from(table: string): any;
13
+ }
14
+ export interface ModelPricing {
15
+ /** Cost per 1M input tokens (USD) */
16
+ input: number;
17
+ /** Cost per 1M output tokens (USD) */
18
+ output: number;
19
+ /** Cost per 1M cached read tokens (USD). 0 if not applicable. */
20
+ cacheRead: number;
21
+ /** Cost per 1M cache creation tokens (USD). 0 if not applicable. */
22
+ cacheWrite: number;
23
+ }
24
+ export type PlanName = 'free' | 'creator' | 'pro' | 'agency';
25
+ export interface PlanLimits {
26
+ /** Storage quota in bytes */
27
+ storageBytes: number;
28
+ /** Max renders per billing period */
29
+ rendersPerMonth: number;
30
+ /** Max concurrent streams */
31
+ streams: number;
32
+ /** Max projects */
33
+ projects: number;
34
+ /** Max connected social accounts */
35
+ socialAccounts: number;
36
+ /** Max export resolution */
37
+ maxResolution: '720p' | '1080p' | '4k';
38
+ /** Number of base team seats included in the plan */
39
+ seats: number;
40
+ /** Price per additional seat in cents (0 = no seat add-ons allowed) */
41
+ seatPrice: number;
42
+ /** Credits granted per additional seat */
43
+ creditsPerSeat: number;
44
+ /** Whether exports include a watermark */
45
+ watermark: boolean;
46
+ /** Max streamer watches */
47
+ maxWatches: number;
48
+ /** Included credits per billing period (1 credit = $0.01) */
49
+ includedCredits: number;
50
+ /** Max overage credits before hard block */
51
+ overageCapCredits: number;
52
+ }
53
+ export interface UsageEvent {
54
+ organizationId: string;
55
+ /** Service name (e.g. 'agent', 'chunk-analyzer', 'reviral') */
56
+ service: string;
57
+ /** Model identifier (e.g. 'claude-sonnet-4-6', 'gemini-2.5-pro') */
58
+ model: string;
59
+ inputTokens: number;
60
+ outputTokens: number;
61
+ cacheReadTokens?: number;
62
+ cacheCreationTokens?: number;
63
+ apiCalls?: number;
64
+ /** Override auto-calculated cost for flat-rate services (Chirp TTS, Veo, etc.) */
65
+ costMicrodollarsOverride?: number;
66
+ /** Credits consumed by this event (auto-computed from costMicrodollars) */
67
+ creditsConsumed?: number;
68
+ /** Optional conversation ID for agent usage */
69
+ conversationId?: string;
70
+ /** Optional context (stream_id, chunk_number, project_id, etc.) */
71
+ metadata?: Record<string, unknown>;
72
+ /**
73
+ * True when the platform eats this cost (conversation naming, compaction,
74
+ * summaries, project auto-naming, memories, studio suggestions, etc.).
75
+ * Recorded to usage_events for analytics, but skipped from the usage_records
76
+ * upsert so it never hits the user's credit/subscription balance.
77
+ * Metadata is auto-augmented with { platform_paid: true } in recordUsage.
78
+ */
79
+ platformPaid?: boolean;
80
+ }
81
+ export interface UsageRecord {
82
+ organizationId: string;
83
+ periodStart: string;
84
+ periodEnd: string;
85
+ inputTokens: number;
86
+ outputTokens: number;
87
+ cacheReadTokens: number;
88
+ cacheCreationTokens: number;
89
+ apiCallCount: number;
90
+ renderCount: number;
91
+ /** Total credits used in this billing period */
92
+ creditsUsed?: number;
93
+ }
94
+ export interface UsageCheckResult {
95
+ allowed: boolean;
96
+ reason?: 'no_subscription' | 'subscription_inactive' | 'free_limit_exceeded' | 'overage_cap_reached';
97
+ warning?: 'approaching_limit' | 'in_overage';
98
+ usage: {
99
+ /** Credits used in the current billing period */
100
+ creditsUsed: number;
101
+ /** Credits included in the plan */
102
+ creditsIncluded: number;
103
+ /** Remaining credits (can be negative if in overage) */
104
+ creditsRemaining: number;
105
+ /** Percentage of included credits consumed */
106
+ creditPercentUsed: number;
107
+ /** Whether the organization has exceeded included credits */
108
+ isOverCredits: boolean;
109
+ /** Number of overage credits consumed beyond the included amount */
110
+ overageCredits: number;
111
+ totalTokens?: number;
112
+ includedLimit?: number;
113
+ overageTokens?: number;
114
+ percentUsed?: number;
115
+ inputTokens?: number;
116
+ outputTokens?: number;
117
+ includedInputLimit?: number;
118
+ includedOutputLimit?: number;
119
+ inputOverageTokens?: number;
120
+ outputOverageTokens?: number;
121
+ inputOverageCost?: number;
122
+ outputOverageCost?: number;
123
+ totalOverageCost?: number;
124
+ overageCap?: number;
125
+ };
126
+ plan: {
127
+ id: string;
128
+ name: string;
129
+ inputOverageRatePer1k?: number;
130
+ outputOverageRatePer1k?: number;
131
+ };
132
+ }
133
+ export interface RenderLimitResult {
134
+ allowed: boolean;
135
+ rendersUsed: number;
136
+ maxRenders: number;
137
+ message?: string;
138
+ }
139
+ export interface StorageQuotaResult {
140
+ allowed: boolean;
141
+ currentUsageBytes: number;
142
+ quotaBytes: number;
143
+ planName: string;
144
+ reason?: string;
145
+ }
146
+ export interface StreamLimitResult {
147
+ allowed: boolean;
148
+ activeStreams: number;
149
+ maxStreams: number;
150
+ reason?: string;
151
+ }
152
+ export interface ProjectLimitResult {
153
+ allowed: boolean;
154
+ currentProjects: number;
155
+ maxProjects: number;
156
+ reason?: string;
157
+ }
158
+ export interface SocialAccountLimitResult {
159
+ allowed: boolean;
160
+ currentAccounts: number;
161
+ maxAccounts: number;
162
+ reason?: string;
163
+ }
164
+ export interface ResolutionLimitResult {
165
+ allowed: boolean;
166
+ requestedResolution: string;
167
+ maxResolution: string;
168
+ reason?: string;
169
+ }
170
+ export interface SeatLimitResult {
171
+ allowed: boolean;
172
+ currentSeats: number;
173
+ maxSeats: number;
174
+ reason?: string;
175
+ }
176
+ export interface UsageAlertResult {
177
+ shouldSend: boolean;
178
+ percentUsed: number;
179
+ alreadySent: boolean;
180
+ }
181
+ export interface BillingLogger {
182
+ debug(message: string, meta?: Record<string, unknown>): void;
183
+ info(message: string, meta?: Record<string, unknown>): void;
184
+ warn(message: string, meta?: Record<string, unknown>): void;
185
+ error(message: string, meta?: Record<string, unknown>): void;
186
+ }
187
+ export type SupabaseClient = SupabaseClientLike;
188
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,26 @@
1
+ import { SupabaseClient, UsageCheckResult, RenderLimitResult, BillingLogger } from './types';
2
+ /**
3
+ * Check whether an organization is allowed to continue using AI services.
4
+ * Credits are the SOLE enforcement mechanism.
5
+ *
6
+ * Logic:
7
+ * - Free plan: hard block when credits exhausted (no overages).
8
+ * - Paid plans: allow overages up to the plan's overage credit cap.
9
+ *
10
+ * Token counts are still returned for analytics/logging but are NOT
11
+ * used for any enforcement decisions.
12
+ */
13
+ export declare function checkUsage(supabase: SupabaseClient, orgId: string, logger?: BillingLogger): Promise<UsageCheckResult>;
14
+ /**
15
+ * Check if an organization has remaining renders in the current billing period.
16
+ * Fails CLOSED on errors — if we cannot verify the render quota, the render
17
+ * is denied. Includes 1 retry with a short delay.
18
+ */
19
+ export declare function checkRenderLimit(supabase: SupabaseClient, orgId: string, logger?: BillingLogger): Promise<RenderLimitResult>;
20
+ /**
21
+ * Increment the render count for an organization in the current billing period.
22
+ * Should be called after a render job completes successfully.
23
+ * Never throws.
24
+ */
25
+ export declare function incrementRenders(supabase: SupabaseClient, orgId: string, count?: number, logger?: BillingLogger): Promise<void>;
26
+ //# sourceMappingURL=usage-checker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usage-checker.d.ts","sourceRoot":"","sources":["../src/usage-checker.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAiF7F;;;;;;;;;;GAUG;AACH,wBAAsB,UAAU,CAC9B,QAAQ,EAAE,cAAc,EACxB,KAAK,EAAE,MAAM,EACb,MAAM,GAAE,aAA6B,GACpC,OAAO,CAAC,gBAAgB,CAAC,CA8G3B;AASD;;;;GAIG;AACH,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,cAAc,EACxB,KAAK,EAAE,MAAM,EACb,MAAM,GAAE,aAA6B,GACpC,OAAO,CAAC,iBAAiB,CAAC,CAsE5B;AAMD;;;;GAIG;AACH,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,cAAc,EACxB,KAAK,EAAE,MAAM,EACb,KAAK,SAAI,EACT,MAAM,GAAE,aAA6B,GACpC,OAAO,CAAC,IAAI,CAAC,CAyCf"}
@@ -0,0 +1,295 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.checkUsage = checkUsage;
4
+ exports.checkRenderLimit = checkRenderLimit;
5
+ exports.incrementRenders = incrementRenders;
6
+ const pricing_1 = require("./pricing");
7
+ const plan_limits_1 = require("./plan-limits");
8
+ const defaultLogger = {
9
+ debug: () => { },
10
+ info: (msg, meta) => console.log(`[billing] ${msg}`, meta ?? ''),
11
+ warn: (msg, meta) => console.warn(`[billing] ${msg}`, meta ?? ''),
12
+ error: (msg, meta) => console.error(`[billing] ${msg}`, meta ?? ''),
13
+ };
14
+ /**
15
+ * Log an enforcement/audit event to the billing_events table.
16
+ * Fire-and-forget — never throws or blocks the caller.
17
+ */
18
+ async function logBillingEvent(supabase, orgId, eventType, description, details, logger) {
19
+ try {
20
+ const { error } = await supabase.from('billing_events').insert({
21
+ organization_id: orgId,
22
+ event_type: eventType,
23
+ description,
24
+ amount_cents: 0,
25
+ related_data: details,
26
+ });
27
+ if (error) {
28
+ logger.warn('Failed to log billing event', {
29
+ orgId,
30
+ eventType,
31
+ error: error.message,
32
+ });
33
+ }
34
+ }
35
+ catch (err) {
36
+ // Never block the caller
37
+ logger.warn('Unexpected error logging billing event', {
38
+ orgId,
39
+ eventType,
40
+ error: err instanceof Error ? err.message : String(err),
41
+ });
42
+ }
43
+ }
44
+ // ============================================================================
45
+ // Empty / default values
46
+ // ============================================================================
47
+ const EMPTY_USAGE = {
48
+ creditsUsed: 0,
49
+ creditsIncluded: 0,
50
+ creditsRemaining: 0,
51
+ creditPercentUsed: 0,
52
+ isOverCredits: false,
53
+ overageCredits: 0,
54
+ };
55
+ const UNKNOWN_PLAN = {
56
+ id: 'unknown',
57
+ name: 'Unknown',
58
+ };
59
+ // ============================================================================
60
+ // checkUsage
61
+ // ============================================================================
62
+ /**
63
+ * Check whether an organization is allowed to continue using AI services.
64
+ * Credits are the SOLE enforcement mechanism.
65
+ *
66
+ * Logic:
67
+ * - Free plan: hard block when credits exhausted (no overages).
68
+ * - Paid plans: allow overages up to the plan's overage credit cap.
69
+ *
70
+ * Token counts are still returned for analytics/logging but are NOT
71
+ * used for any enforcement decisions.
72
+ */
73
+ async function checkUsage(supabase, orgId, logger = defaultLogger) {
74
+ const { data, error } = await supabase.rpc('get_org_current_usage', { org_id: orgId });
75
+ if (error) {
76
+ logger.error('Failed to check org usage', { orgId, error: error.message });
77
+ return { allowed: false, reason: 'no_subscription', usage: EMPTY_USAGE, plan: UNKNOWN_PLAN };
78
+ }
79
+ const row = Array.isArray(data) ? data[0] : data;
80
+ if (!row) {
81
+ return {
82
+ allowed: false,
83
+ reason: 'no_subscription',
84
+ usage: EMPTY_USAGE,
85
+ plan: { ...UNKNOWN_PLAN, id: 'none', name: 'None' },
86
+ };
87
+ }
88
+ const planId = row.plan_id ?? 'unknown';
89
+ const planName = row.plan_name ?? 'Unknown';
90
+ // Credit calculations — sole enforcement mechanism
91
+ const costMicrodollars = row.cost_microdollars ?? 0;
92
+ const creditsUsed = (0, pricing_1.microdollarsToCredits)(costMicrodollars);
93
+ const planLimits = (0, plan_limits_1.getPlanLimits)((0, plan_limits_1.normalizePlanName)(planName));
94
+ // Include bonus credits from additional purchased seats
95
+ const additionalSeats = row.additional_seats ?? 0;
96
+ const seatBonusCredits = additionalSeats * planLimits.creditsPerSeat;
97
+ const creditsIncluded = planLimits.includedCredits + seatBonusCredits;
98
+ const creditsRemaining = creditsIncluded - creditsUsed;
99
+ const creditPercentUsed = creditsIncluded > 0 ? (creditsUsed / creditsIncluded) * 100 : 0;
100
+ const isOverCredits = creditsUsed > creditsIncluded;
101
+ const overageCredits = Math.max(0, creditsUsed - creditsIncluded);
102
+ // Raw token counts (analytics only — NOT used for enforcement)
103
+ const inputTokens = row.used_input_tokens ?? 0;
104
+ const outputTokens = row.used_output_tokens ?? 0;
105
+ const totalTokens = inputTokens + outputTokens;
106
+ const usage = {
107
+ creditsUsed,
108
+ creditsIncluded,
109
+ creditsRemaining,
110
+ creditPercentUsed,
111
+ isOverCredits,
112
+ overageCredits,
113
+ // Analytics-only token data
114
+ totalTokens,
115
+ inputTokens,
116
+ outputTokens,
117
+ };
118
+ const plan = {
119
+ id: planId,
120
+ name: planName,
121
+ };
122
+ // Free plan hard limit — block when credits exhausted
123
+ if (planId === 'free' && isOverCredits) {
124
+ logBillingEvent(supabase, orgId, 'limit_exceeded', 'Free plan credit limit exceeded', {
125
+ planId,
126
+ creditsUsed,
127
+ creditsIncluded,
128
+ creditPercentUsed,
129
+ }, logger);
130
+ return { allowed: false, reason: 'free_limit_exceeded', usage, plan };
131
+ }
132
+ // Paid plans — block when overage credits reach the cap
133
+ const overageCapCredits = planLimits.overageCapCredits;
134
+ if (planId !== 'free' && overageCredits >= overageCapCredits && overageCapCredits > 0) {
135
+ logBillingEvent(supabase, orgId, 'overage_cap_reached', 'Monthly overage credit cap reached', {
136
+ planId,
137
+ creditsUsed,
138
+ creditsIncluded,
139
+ overageCredits,
140
+ overageCapCredits,
141
+ }, logger);
142
+ return { allowed: false, reason: 'overage_cap_reached', usage, plan };
143
+ }
144
+ // Warnings (credit-based only)
145
+ let warning;
146
+ if (isOverCredits) {
147
+ warning = 'in_overage';
148
+ // Log overage start only when first entering overage (within 5% band to reduce noise)
149
+ if (creditPercentUsed <= 105) {
150
+ logBillingEvent(supabase, orgId, 'usage_warning', 'Organization entered credit overage', {
151
+ planId,
152
+ creditsUsed,
153
+ creditsIncluded,
154
+ overageCredits,
155
+ warning: 'in_overage',
156
+ }, logger);
157
+ }
158
+ }
159
+ else if (creditPercentUsed > 80) {
160
+ warning = 'approaching_limit';
161
+ // Log warning when crossing 80% threshold (within 2% band to reduce noise)
162
+ if (creditPercentUsed <= 82) {
163
+ logBillingEvent(supabase, orgId, 'usage_warning', 'Credit usage approaching plan limit (80%)', {
164
+ planId,
165
+ creditsUsed,
166
+ creditsIncluded,
167
+ creditPercentUsed,
168
+ warning: 'approaching_limit',
169
+ }, logger);
170
+ }
171
+ }
172
+ return { allowed: true, warning, usage, plan };
173
+ }
174
+ // ============================================================================
175
+ // checkRenderLimit
176
+ // ============================================================================
177
+ const RENDER_RETRY_DELAY_MS = 500;
178
+ const RENDER_MAX_ATTEMPTS = 2;
179
+ /**
180
+ * Check if an organization has remaining renders in the current billing period.
181
+ * Fails CLOSED on errors — if we cannot verify the render quota, the render
182
+ * is denied. Includes 1 retry with a short delay.
183
+ */
184
+ async function checkRenderLimit(supabase, orgId, logger = defaultLogger) {
185
+ const failClosed = {
186
+ allowed: false,
187
+ rendersUsed: 0,
188
+ maxRenders: 0,
189
+ message: 'Unable to verify render quota. Please try again.',
190
+ };
191
+ for (let attempt = 1; attempt <= RENDER_MAX_ATTEMPTS; attempt++) {
192
+ try {
193
+ const { data, error } = await supabase.rpc('check_render_limit', {
194
+ p_organization_id: orgId,
195
+ });
196
+ if (error) {
197
+ logger.error('Failed to check render limit', {
198
+ orgId,
199
+ error: error.message,
200
+ attempt,
201
+ });
202
+ if (attempt < RENDER_MAX_ATTEMPTS) {
203
+ await new Promise((r) => setTimeout(r, RENDER_RETRY_DELAY_MS));
204
+ continue;
205
+ }
206
+ return failClosed;
207
+ }
208
+ const row = Array.isArray(data) ? data[0] : data;
209
+ if (!row) {
210
+ return {
211
+ allowed: false,
212
+ rendersUsed: 0,
213
+ maxRenders: 0,
214
+ message: 'No active subscription found. Please check your billing settings.',
215
+ };
216
+ }
217
+ const rendersUsed = row.renders_used ?? 0;
218
+ const maxRenders = row.max_renders ?? 2147483647;
219
+ if (rendersUsed >= maxRenders) {
220
+ logBillingEvent(supabase, orgId, 'render_limit_reached', 'Monthly render limit reached', {
221
+ rendersUsed,
222
+ maxRenders,
223
+ }, logger);
224
+ return {
225
+ allowed: false,
226
+ rendersUsed,
227
+ maxRenders,
228
+ message: `Render limit reached: ${rendersUsed}/${maxRenders} used. Upgrade your plan for more renders.`,
229
+ };
230
+ }
231
+ return { allowed: true, rendersUsed, maxRenders };
232
+ }
233
+ catch (err) {
234
+ logger.error('Unexpected error checking render limit', {
235
+ orgId,
236
+ error: err instanceof Error ? err.message : String(err),
237
+ attempt,
238
+ });
239
+ if (attempt < RENDER_MAX_ATTEMPTS) {
240
+ await new Promise((r) => setTimeout(r, RENDER_RETRY_DELAY_MS));
241
+ continue;
242
+ }
243
+ }
244
+ }
245
+ return failClosed;
246
+ }
247
+ // ============================================================================
248
+ // incrementRenders
249
+ // ============================================================================
250
+ /**
251
+ * Increment the render count for an organization in the current billing period.
252
+ * Should be called after a render job completes successfully.
253
+ * Never throws.
254
+ */
255
+ async function incrementRenders(supabase, orgId, count = 1, logger = defaultLogger) {
256
+ try {
257
+ // Get the current billing period for the RPC
258
+ const { data: sub, error: subError } = await supabase
259
+ .from('organization_subscriptions')
260
+ .select('current_period_start, current_period_end')
261
+ .eq('organization_id', orgId)
262
+ .order('created_at', { ascending: false })
263
+ .limit(1)
264
+ .single();
265
+ if (subError || !sub) {
266
+ logger.warn('No subscription found for render count increment', {
267
+ orgId,
268
+ error: subError?.message,
269
+ });
270
+ return;
271
+ }
272
+ // Call the correct RPC: increment_renders_used (matches DB migration)
273
+ for (let i = 0; i < count; i++) {
274
+ const { error } = await supabase.rpc('increment_renders_used', {
275
+ p_organization_id: orgId,
276
+ p_period_start: sub.current_period_start,
277
+ p_period_end: sub.current_period_end,
278
+ });
279
+ if (error) {
280
+ logger.error('RPC increment_renders_used failed', {
281
+ orgId,
282
+ error: error.message,
283
+ iteration: i + 1,
284
+ });
285
+ }
286
+ }
287
+ }
288
+ catch (err) {
289
+ logger.error('Failed to increment render count', {
290
+ orgId,
291
+ error: err instanceof Error ? err.message : String(err),
292
+ });
293
+ }
294
+ }
295
+ //# sourceMappingURL=usage-checker.js.map