billing-logic.mjs
230 lines 7.8 KB
Raw
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor ⚠ breaking 1 day ago
1 /**
2 * Deduction order: monthly included first, then add-on rollover (Netlify-style).
3 */
4 import {
5 MONTHLY_INCLUDED_CENTS_BY_TIER,
6 MONTHLY_INDEXING_TOKENS_INCLUDED_BY_TIER,
7 MONTHLY_SEARCHES_INCLUDED_BY_TIER,
8 MONTHLY_INDEX_JOBS_INCLUDED_BY_TIER,
9 CONSOLIDATION_PASSES_BY_TIER,
10 PACK_TOKENS,
11 PACK_CONSOLIDATIONS,
12 } from './billing-constants.mjs';
13 import {
14 clampConsolidationInt,
15 HOSTED_CONSOL_DEFAULT_LOOKBACK_HOURS,
16 HOSTED_CONSOL_DEFAULT_MAX_EVENTS,
17 HOSTED_CONSOL_DEFAULT_MAX_TOPICS,
18 HOSTED_CONSOL_DEFAULT_LLM_MAX_TOKENS,
19 HOSTED_CONSOL_LOOKBACK_MIN,
20 HOSTED_CONSOL_LOOKBACK_MAX,
21 HOSTED_CONSOL_MAX_EVENTS_MIN,
22 HOSTED_CONSOL_MAX_EVENTS_MAX,
23 HOSTED_CONSOL_MAX_TOPICS_MIN,
24 HOSTED_CONSOL_MAX_TOPICS_MAX,
25 HOSTED_CONSOL_LLM_TOKENS_MIN,
26 HOSTED_CONSOL_LLM_TOKENS_MAX,
27 } from '../../lib/hosted-consolidation-advanced.mjs';
28
29 /**
30 * @param {object} u - Billing user record
31 * @returns {number|null} null = unlimited (beta or pro)
32 */
33 export function effectiveMonthlyIndexingTokensIncluded(u) {
34 const tier = String(u?.tier || 'beta');
35 if (tier === 'beta') return null;
36 const val = MONTHLY_INDEXING_TOKENS_INCLUDED_BY_TIER[tier];
37 if (val === null || val === undefined) return null;
38 if (tier === 'free') return val;
39 return val;
40 }
41
42 /** Ensure all billing fields exist on loaded JSON records. */
43 export function normalizeBillingUser(u) {
44 if (!u || typeof u !== 'object') return u;
45 if (typeof u.monthly_indexing_tokens_used !== 'number' || !Number.isFinite(u.monthly_indexing_tokens_used)) {
46 u.monthly_indexing_tokens_used = 0;
47 }
48 if (typeof u.pack_indexing_tokens_balance !== 'number' || !Number.isFinite(u.pack_indexing_tokens_balance)) {
49 u.pack_indexing_tokens_balance = 0;
50 }
51 if (typeof u.pack_consolidation_passes_balance !== 'number' || !Number.isFinite(u.pack_consolidation_passes_balance)) {
52 u.pack_consolidation_passes_balance = 0;
53 }
54 if (u.pack_consolidation_legacy_inferred !== true) {
55 u.pack_consolidation_legacy_inferred = false;
56 }
57 if (typeof u.monthly_searches_used !== 'number' || !Number.isFinite(u.monthly_searches_used)) {
58 u.monthly_searches_used = 0;
59 }
60 if (typeof u.monthly_index_jobs_used !== 'number' || !Number.isFinite(u.monthly_index_jobs_used)) {
61 u.monthly_index_jobs_used = 0;
62 }
63 if (typeof u.monthly_consolidation_jobs_used !== 'number' || !Number.isFinite(u.monthly_consolidation_jobs_used)) {
64 u.monthly_consolidation_jobs_used = 0;
65 }
66 if (u.consolidation_enabled === undefined) {
67 u.consolidation_enabled = false;
68 }
69 if (u.consolidation_last_pass_at === undefined) {
70 u.consolidation_last_pass_at = null;
71 }
72 if (u.consolidation_interval_minutes === undefined) {
73 u.consolidation_interval_minutes = null;
74 }
75 if (!u.consolidation_passes || typeof u.consolidation_passes !== 'object') {
76 u.consolidation_passes = { consolidate: true, verify: true, discover: false };
77 }
78 u.consolidation_lookback_hours = clampConsolidationInt(
79 u.consolidation_lookback_hours,
80 HOSTED_CONSOL_LOOKBACK_MIN,
81 HOSTED_CONSOL_LOOKBACK_MAX,
82 HOSTED_CONSOL_DEFAULT_LOOKBACK_HOURS,
83 );
84 u.consolidation_max_events_per_pass = clampConsolidationInt(
85 u.consolidation_max_events_per_pass,
86 HOSTED_CONSOL_MAX_EVENTS_MIN,
87 HOSTED_CONSOL_MAX_EVENTS_MAX,
88 HOSTED_CONSOL_DEFAULT_MAX_EVENTS,
89 );
90 u.consolidation_max_topics_per_pass = clampConsolidationInt(
91 u.consolidation_max_topics_per_pass,
92 HOSTED_CONSOL_MAX_TOPICS_MIN,
93 HOSTED_CONSOL_MAX_TOPICS_MAX,
94 HOSTED_CONSOL_DEFAULT_MAX_TOPICS,
95 );
96 u.consolidation_llm_max_tokens = clampConsolidationInt(
97 u.consolidation_llm_max_tokens,
98 HOSTED_CONSOL_LLM_TOKENS_MIN,
99 HOSTED_CONSOL_LLM_TOKENS_MAX,
100 HOSTED_CONSOL_DEFAULT_LLM_MAX_TOKENS,
101 );
102 return u;
103 }
104
105 /**
106 * @param {object} u - Billing user record
107 * @returns {number|null} null = unlimited
108 */
109 export function effectiveMonthlySearchesIncluded(u) {
110 const tier = String(u?.tier || 'beta');
111 const val = MONTHLY_SEARCHES_INCLUDED_BY_TIER[tier];
112 return val === undefined ? null : val;
113 }
114
115 /**
116 * @param {object} u - Billing user record
117 * @returns {number|null} null = unlimited
118 */
119 export function effectiveMonthlyIndexJobsIncluded(u) {
120 const tier = String(u?.tier || 'beta');
121 const val = MONTHLY_INDEX_JOBS_INCLUDED_BY_TIER[tier];
122 return val === undefined ? null : val;
123 }
124
125 /**
126 * @param {object} u - Billing user record
127 * @returns {number|null} null = unlimited; 0 = no hosted consolidation on this tier
128 */
129 export function effectiveMonthlyConsolidationPassesIncluded(u) {
130 const tier = String(u?.tier || 'beta');
131 const val = CONSOLIDATION_PASSES_BY_TIER[tier];
132 return val === undefined ? null : val;
133 }
134
135 /**
136 * Infer how many pack consolidation passes correspond to a remaining indexing-token pack balance.
137 * Uses greedy decomposition into known pack sizes (large → medium → small), then the small-pack
138 * ratio (20M tokens / 50 passes) for any remainder. Used once per account to backfill purchases
139 * made before `pack_consolidation_passes_balance` was credited in Stripe webhooks.
140 *
141 * @param {number} tokenBalance
142 * @returns {number}
143 */
144 export function inferPackConsolidationPassesFromIndexingTokenBalance(tokenBalance) {
145 const n = Math.max(0, Math.floor(Number(tokenBalance) || 0));
146 if (n <= 0) return 0;
147 let t = n;
148 let passes = 0;
149 const packs = [
150 { tok: PACK_TOKENS.large, pass: PACK_CONSOLIDATIONS.large },
151 { tok: PACK_TOKENS.medium, pass: PACK_CONSOLIDATIONS.medium },
152 { tok: PACK_TOKENS.small, pass: PACK_CONSOLIDATIONS.small },
153 ];
154 for (const p of packs) {
155 while (t >= p.tok) {
156 passes += p.pass;
157 t -= p.tok;
158 }
159 }
160 if (t > 0) {
161 const tokensPerPass = PACK_TOKENS.small / PACK_CONSOLIDATIONS.small;
162 passes += Math.floor(t / tokensPerPass);
163 }
164 return passes;
165 }
166
167 /**
168 * @param {object} user - Billing user record from store
169 * @param {number} costCents
170 * @returns {{ ok: boolean, code?: string }}
171 */
172 export function tryDeduct(user, costCents) {
173 const cost = Math.max(0, Math.floor(Number(costCents) || 0));
174 if (cost === 0) return { ok: true };
175
176 if (user.tier === 'beta') return { ok: true };
177
178 if (user.tier === 'free') {
179 user.monthly_included_cents = MONTHLY_INCLUDED_CENTS_BY_TIER.free ?? 0;
180 }
181
182 const included = Math.max(0, Math.floor(Number(user.monthly_included_cents) || 0));
183 const used = Math.max(0, Math.floor(Number(user.monthly_used_cents) || 0));
184 const addon = Math.max(0, Math.floor(Number(user.addon_cents) || 0));
185
186 const remainingMonthly = Math.max(0, included - used);
187
188 if (cost <= remainingMonthly) {
189 user.monthly_used_cents = used + cost;
190 return { ok: true };
191 }
192
193 const needFromAddon = cost - remainingMonthly;
194 if (needFromAddon <= addon) {
195 user.monthly_used_cents = included;
196 user.addon_cents = addon - needFromAddon;
197 return { ok: true };
198 }
199
200 return { ok: false, code: 'QUOTA_EXHAUSTED' };
201 }
202
203 export function defaultUserRecord(userId) {
204 return {
205 user_id: userId,
206 tier: 'beta',
207 stripe_customer_id: null,
208 stripe_subscription_id: null,
209 period_start: null,
210 period_end: null,
211 monthly_included_cents: 0,
212 monthly_used_cents: 0,
213 addon_cents: 0,
214 monthly_indexing_tokens_used: 0,
215 pack_indexing_tokens_balance: 0,
216 pack_consolidation_passes_balance: 0,
217 pack_consolidation_legacy_inferred: false,
218 monthly_searches_used: 0,
219 monthly_index_jobs_used: 0,
220 monthly_consolidation_jobs_used: 0,
221 consolidation_enabled: false,
222 consolidation_last_pass_at: null,
223 consolidation_interval_minutes: null,
224 consolidation_passes: { consolidate: true, verify: true, discover: false },
225 consolidation_lookback_hours: HOSTED_CONSOL_DEFAULT_LOOKBACK_HOURS,
226 consolidation_max_events_per_pass: HOSTED_CONSOL_DEFAULT_MAX_EVENTS,
227 consolidation_max_topics_per_pass: HOSTED_CONSOL_DEFAULT_MAX_TOPICS,
228 consolidation_llm_max_tokens: HOSTED_CONSOL_DEFAULT_LLM_MAX_TOKENS,
229 };
230 }
File History 2 commits
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor 1 day ago
sha256:9103f98c89257ed2b01c237cea895dabb3e85ea337dccb1161c175e4422355b6 docs: accept Calendar Events v0 spec with Phase 0 security … Human 1 day ago