Skip to content

Commit 8c65530

Browse files
committed
Don't include subscription credits in /usage stats
1 parent d407cdf commit 8c65530

File tree

2 files changed

+196
-2
lines changed

2 files changed

+196
-2
lines changed

packages/billing/src/__tests__/balance-calculator.test.ts

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
142335
describe('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`.

packages/billing/src/balance-calculator.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -326,8 +326,9 @@ export async function calculateUsageAndBalance(
326326
for (const grant of grants) {
327327
const grantType = grant.type as GrantType
328328

329-
// Skip organization credits for personal context
330-
if (isPersonalContext && grantType === 'organization') {
329+
// Skip organization and subscription credits for personal context
330+
// Subscription credits are shown separately in the CLI with progress bars
331+
if (isPersonalContext && (grantType === 'organization' || grantType === 'subscription')) {
331332
continue
332333
}
333334

0 commit comments

Comments
 (0)