Skip to content

Commit 22320ae

Browse files
committed
fix(billing): inline consumeFromOrderedGrants in integration test to avoid db side effect
The test was failing in CI because importing from balance-calculator.ts triggered a transitive import of @codebuff/internal/db which eagerly creates a database connection using env.DATABASE_URL (manicode_user_local) instead of our test database URL. Inlined the consumeFromOrderedGrants and updateGrantBalance functions directly in the test file to avoid this side effect.
1 parent afdc2e3 commit 22320ae

File tree

1 file changed

+125
-6
lines changed

1 file changed

+125
-6
lines changed

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

Lines changed: 125 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,129 @@ import postgres from 'postgres'
2424
import { eq, and, asc, desc, ne, or, gt, isNull, sql } from 'drizzle-orm'
2525
import { union } from 'drizzle-orm/pg-core'
2626
import * as schema from '@codebuff/internal/db/schema'
27-
import { consumeFromOrderedGrants } from '../balance-calculator'
2827

2928
import type { Logger } from '@codebuff/common/types/contracts/logger'
3029

30+
// Inlined from balance-calculator.ts to avoid importing db (which has side effects)
31+
// that would try to connect with env.DATABASE_URL before our test URL is set
32+
interface CreditConsumptionResult {
33+
consumed: number
34+
fromPurchased: number
35+
}
36+
37+
// Minimal type for database connection that works with both db and tx
38+
type TestDbConn = ReturnType<typeof drizzle<typeof schema>>
39+
40+
async function updateGrantBalance(params: {
41+
userId: string
42+
grant: typeof schema.creditLedger.$inferSelect
43+
consumed: number
44+
newBalance: number
45+
tx: TestDbConn
46+
logger: Logger
47+
}) {
48+
const { grant, newBalance, tx } = params
49+
await tx
50+
.update(schema.creditLedger)
51+
.set({ balance: newBalance })
52+
.where(eq(schema.creditLedger.operation_id, grant.operation_id))
53+
}
54+
55+
async function consumeFromOrderedGrants(params: {
56+
userId: string
57+
creditsToConsume: number
58+
grants: (typeof schema.creditLedger.$inferSelect)[]
59+
tx: TestDbConn
60+
logger: Logger
61+
}): Promise<CreditConsumptionResult> {
62+
const { userId, creditsToConsume, grants, tx, logger } = params
63+
64+
let remainingToConsume = creditsToConsume
65+
let consumed = 0
66+
let fromPurchased = 0
67+
68+
// First pass: try to repay any debt
69+
for (const grant of grants) {
70+
if (grant.balance < 0 && remainingToConsume > 0) {
71+
const debtAmount = Math.abs(grant.balance)
72+
const repayAmount = Math.min(debtAmount, remainingToConsume)
73+
const newBalance = grant.balance + repayAmount
74+
remainingToConsume -= repayAmount
75+
consumed += repayAmount
76+
77+
await updateGrantBalance({
78+
userId,
79+
grant,
80+
consumed: -repayAmount,
81+
newBalance,
82+
tx,
83+
logger,
84+
})
85+
86+
logger.debug(
87+
{ userId, grantId: grant.operation_id, repayAmount, newBalance },
88+
'Repaid debt in grant',
89+
)
90+
}
91+
}
92+
93+
// Second pass: consume from positive balances
94+
for (const grant of grants) {
95+
if (remainingToConsume <= 0) break
96+
if (grant.balance <= 0) continue
97+
98+
const consumeFromThisGrant = Math.min(remainingToConsume, grant.balance)
99+
const newBalance = grant.balance - consumeFromThisGrant
100+
remainingToConsume -= consumeFromThisGrant
101+
consumed += consumeFromThisGrant
102+
103+
// Track consumption from purchased credits
104+
if (grant.type === 'purchase') {
105+
fromPurchased += consumeFromThisGrant
106+
}
107+
108+
await updateGrantBalance({
109+
userId,
110+
grant,
111+
consumed: consumeFromThisGrant,
112+
newBalance,
113+
tx,
114+
logger,
115+
})
116+
}
117+
118+
// If we still have remaining to consume and no grants left, create debt in the last grant
119+
if (remainingToConsume > 0 && grants.length > 0) {
120+
const lastGrant = grants[grants.length - 1]
121+
122+
if (lastGrant.balance <= 0) {
123+
const newBalance = lastGrant.balance - remainingToConsume
124+
await updateGrantBalance({
125+
userId,
126+
grant: lastGrant,
127+
consumed: remainingToConsume,
128+
newBalance,
129+
tx,
130+
logger,
131+
})
132+
consumed += remainingToConsume
133+
134+
logger.warn(
135+
{
136+
userId,
137+
grantId: lastGrant.operation_id,
138+
requested: remainingToConsume,
139+
consumed: remainingToConsume,
140+
newDebt: Math.abs(newBalance),
141+
},
142+
'Created new debt in grant',
143+
)
144+
}
145+
}
146+
147+
return { consumed, fromPurchased }
148+
}
149+
31150
// Test logger that silently discards all logs
32151
const testLogger: Logger = {
33152
debug: () => {},
@@ -546,7 +665,7 @@ describe('Balance Calculator - Integration Tests (Real DB)', () => {
546665
userId: TEST_USER_ID,
547666
creditsToConsume: 70,
548667
grants,
549-
tx: db as any,
668+
tx: db,
550669
logger: testLogger,
551670
})
552671

@@ -596,7 +715,7 @@ describe('Balance Calculator - Integration Tests (Real DB)', () => {
596715
userId: TEST_USER_ID,
597716
creditsToConsume: 100,
598717
grants,
599-
tx: db as any,
718+
tx: db,
600719
logger: testLogger,
601720
})
602721

@@ -654,7 +773,7 @@ describe('Balance Calculator - Integration Tests (Real DB)', () => {
654773
userId: TEST_USER_ID,
655774
creditsToConsume: 60,
656775
grants,
657-
tx: db as any,
776+
tx: db,
658777
logger: testLogger,
659778
})
660779

@@ -713,7 +832,7 @@ describe('Balance Calculator - Integration Tests (Real DB)', () => {
713832
userId: TEST_USER_ID,
714833
creditsToConsume: 80,
715834
grants,
716-
tx: db as any,
835+
tx: db,
717836
logger: testLogger,
718837
})
719838

@@ -771,7 +890,7 @@ describe('Balance Calculator - Integration Tests (Real DB)', () => {
771890
userId: TEST_USER_ID,
772891
creditsToConsume: 50,
773892
grants,
774-
tx: db as any,
893+
tx: db,
775894
logger: testLogger,
776895
})
777896

0 commit comments

Comments
 (0)