@@ -139,6 +139,199 @@ function createDbMockForUnion(options: {
139139 }
140140}
141141
142+ describe ( 'Balance Calculator - calculateUsageAndBalance' , ( ) => {
143+ afterEach ( ( ) => {
144+ clearMockedModules ( )
145+ } )
146+
147+ describe ( 'isPersonalContext behavior' , ( ) => {
148+ it ( 'should exclude subscription credits when isPersonalContext is true' , async ( ) => {
149+ const now = new Date ( )
150+ const quotaResetDate = new Date ( now . getTime ( ) - 7 * 24 * 60 * 60 * 1000 ) // 7 days ago
151+
152+ const grants = [
153+ createMockGrant ( {
154+ operation_id : 'free-grant' ,
155+ balance : 500 ,
156+ principal : 1000 ,
157+ priority : 20 ,
158+ type : 'purchase' ,
159+ expires_at : new Date ( now . getTime ( ) + 30 * 24 * 60 * 60 * 1000 ) ,
160+ created_at : new Date ( now . getTime ( ) - 10 * 24 * 60 * 60 * 1000 ) ,
161+ } ) ,
162+ createMockGrant ( {
163+ operation_id : 'subscription-grant' ,
164+ balance : 2000 ,
165+ principal : 5000 ,
166+ priority : 10 ,
167+ type : 'subscription' ,
168+ expires_at : new Date ( now . getTime ( ) + 30 * 24 * 60 * 60 * 1000 ) ,
169+ created_at : new Date ( now . getTime ( ) - 5 * 24 * 60 * 60 * 1000 ) ,
170+ } ) ,
171+ ]
172+
173+ // Mock the database to return our test grants
174+ await mockModule ( '@codebuff/internal/db' , ( ) => ( {
175+ default : {
176+ select : ( ) => ( {
177+ from : ( ) => ( {
178+ where : ( ) => ( {
179+ orderBy : ( ) => grants ,
180+ } ) ,
181+ } ) ,
182+ } ) ,
183+ } ,
184+ } ) )
185+
186+ // Mock analytics to prevent actual tracking
187+ await mockModule ( '@codebuff/common/analytics' , ( ) => ( {
188+ trackEvent : ( ) => { } ,
189+ } ) )
190+
191+ const { calculateUsageAndBalance } = await import (
192+ '@codebuff/billing/balance-calculator'
193+ )
194+
195+ const result = await calculateUsageAndBalance ( {
196+ userId : 'user-123' ,
197+ quotaResetDate,
198+ now,
199+ isPersonalContext : true ,
200+ logger,
201+ } )
202+
203+ // Should only include purchase credits (500), not subscription (2000)
204+ expect ( result . balance . totalRemaining ) . toBe ( 500 )
205+ expect ( result . balance . breakdown . purchase ) . toBe ( 500 )
206+ expect ( result . balance . breakdown . subscription ) . toBe ( 0 )
207+
208+ // Usage should only include purchase usage (1000 - 500 = 500), not subscription (5000 - 2000 = 3000)
209+ expect ( result . usageThisCycle ) . toBe ( 500 )
210+ } )
211+
212+ it ( 'should include subscription credits when isPersonalContext is false' , async ( ) => {
213+ const now = new Date ( )
214+ const quotaResetDate = new Date ( now . getTime ( ) - 7 * 24 * 60 * 60 * 1000 ) // 7 days ago
215+
216+ const grants = [
217+ createMockGrant ( {
218+ operation_id : 'free-grant' ,
219+ balance : 500 ,
220+ principal : 1000 ,
221+ priority : 20 ,
222+ type : 'purchase' ,
223+ expires_at : new Date ( now . getTime ( ) + 30 * 24 * 60 * 60 * 1000 ) ,
224+ created_at : new Date ( now . getTime ( ) - 10 * 24 * 60 * 60 * 1000 ) ,
225+ } ) ,
226+ createMockGrant ( {
227+ operation_id : 'subscription-grant' ,
228+ balance : 2000 ,
229+ principal : 5000 ,
230+ priority : 10 ,
231+ type : 'subscription' ,
232+ expires_at : new Date ( now . getTime ( ) + 30 * 24 * 60 * 60 * 1000 ) ,
233+ created_at : new Date ( now . getTime ( ) - 5 * 24 * 60 * 60 * 1000 ) ,
234+ } ) ,
235+ ]
236+
237+ await mockModule ( '@codebuff/internal/db' , ( ) => ( {
238+ default : {
239+ select : ( ) => ( {
240+ from : ( ) => ( {
241+ where : ( ) => ( {
242+ orderBy : ( ) => grants ,
243+ } ) ,
244+ } ) ,
245+ } ) ,
246+ } ,
247+ } ) )
248+
249+ await mockModule ( '@codebuff/common/analytics' , ( ) => ( {
250+ trackEvent : ( ) => { } ,
251+ } ) )
252+
253+ const { calculateUsageAndBalance } = await import (
254+ '@codebuff/billing/balance-calculator'
255+ )
256+
257+ const result = await calculateUsageAndBalance ( {
258+ userId : 'user-123' ,
259+ quotaResetDate,
260+ now,
261+ isPersonalContext : false ,
262+ logger,
263+ } )
264+
265+ // Should include both purchase (500) and subscription (2000) credits
266+ expect ( result . balance . totalRemaining ) . toBe ( 2500 )
267+ expect ( result . balance . breakdown . purchase ) . toBe ( 500 )
268+ expect ( result . balance . breakdown . subscription ) . toBe ( 2000 )
269+
270+ // Usage should include both: (1000 - 500) + (5000 - 2000) = 3500
271+ expect ( result . usageThisCycle ) . toBe ( 3500 )
272+ } )
273+
274+ it ( 'should exclude organization credits when isPersonalContext is true' , async ( ) => {
275+ const now = new Date ( )
276+ const quotaResetDate = new Date ( now . getTime ( ) - 7 * 24 * 60 * 60 * 1000 )
277+
278+ const grants = [
279+ createMockGrant ( {
280+ operation_id : 'free-grant' ,
281+ balance : 500 ,
282+ principal : 1000 ,
283+ priority : 20 ,
284+ type : 'purchase' ,
285+ expires_at : new Date ( now . getTime ( ) + 30 * 24 * 60 * 60 * 1000 ) ,
286+ created_at : new Date ( now . getTime ( ) - 10 * 24 * 60 * 60 * 1000 ) ,
287+ } ) ,
288+ createMockGrant ( {
289+ operation_id : 'org-grant' ,
290+ balance : 3000 ,
291+ principal : 5000 ,
292+ priority : 5 ,
293+ type : 'organization' ,
294+ expires_at : new Date ( now . getTime ( ) + 30 * 24 * 60 * 60 * 1000 ) ,
295+ created_at : new Date ( now . getTime ( ) - 5 * 24 * 60 * 60 * 1000 ) ,
296+ } ) ,
297+ ]
298+
299+ await mockModule ( '@codebuff/internal/db' , ( ) => ( {
300+ default : {
301+ select : ( ) => ( {
302+ from : ( ) => ( {
303+ where : ( ) => ( {
304+ orderBy : ( ) => grants ,
305+ } ) ,
306+ } ) ,
307+ } ) ,
308+ } ,
309+ } ) )
310+
311+ await mockModule ( '@codebuff/common/analytics' , ( ) => ( {
312+ trackEvent : ( ) => { } ,
313+ } ) )
314+
315+ const { calculateUsageAndBalance } = await import (
316+ '@codebuff/billing/balance-calculator'
317+ )
318+
319+ const result = await calculateUsageAndBalance ( {
320+ userId : 'user-123' ,
321+ quotaResetDate,
322+ now,
323+ isPersonalContext : true ,
324+ logger,
325+ } )
326+
327+ // Should only include purchase credits (500), not organization (3000)
328+ expect ( result . balance . totalRemaining ) . toBe ( 500 )
329+ expect ( result . balance . breakdown . purchase ) . toBe ( 500 )
330+ expect ( result . balance . breakdown . organization ) . toBe ( 0 )
331+ } )
332+ } )
333+ } )
334+
142335describe ( 'Balance Calculator - Grant Ordering for Consumption' , ( ) => {
143336 // NOTE: This test suite uses a complex mock (createDbMockForUnion) to simulate the
144337 // behavior of the UNION query in `getOrderedActiveGrantsForConsumption`.
0 commit comments