Skip to content

Commit 0af83cf

Browse files
committed
refactor: clean up grant-credits.ts type patterns and fix transactional consistency
- Add centralized getBillingDbClient() helper to avoid repeated casting - Use generic type parameters on .select<T>() for cleaner type inference - Fix transactional consistency bug: pass deps: { db: tx } to getPreviousFreeGrantAmount and calculateTotalReferralBonus in triggerMonthlyResetAndGrant so they use the transaction instead of global db Addresses all issues identified by Codex, Claude Code, and Gemini CLI reviews.
1 parent 28923bc commit 0af83cf

File tree

1 file changed

+13
-11
lines changed

1 file changed

+13
-11
lines changed

packages/billing/src/grant-credits.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ import type { GrantType } from '@codebuff/internal/db/schema'
2121

2222
import type { CodebuffTransaction } from '@codebuff/internal/db'
2323

24+
const getBillingDbClient = (
25+
dbOverride?: BillingDbConnection,
26+
): BillingDbConnection => (dbOverride ?? db) as BillingDbConnection
27+
2428
/**
2529
* Dependencies for getPreviousFreeGrantAmount (for testing)
2630
*/
@@ -38,12 +42,11 @@ export async function getPreviousFreeGrantAmount(params: {
3842
deps?: GetPreviousFreeGrantAmountDeps
3943
}): Promise<number> {
4044
const { userId, logger, deps = {} } = params
41-
// Cast to BillingDbConnection to allow either real db or mock to be used
42-
const dbClient = (deps.db ?? db) as BillingDbConnection
45+
const dbClient = getBillingDbClient(deps.db)
4346

4447
const now = new Date()
4548
const lastExpiredFreeGrant = await dbClient
46-
.select({
49+
.select<{ principal: number }>({
4750
principal: schema.creditLedger.principal,
4851
})
4952
.from(schema.creditLedger)
@@ -55,7 +58,7 @@ export async function getPreviousFreeGrantAmount(params: {
5558
),
5659
)
5760
.orderBy(desc(schema.creditLedger.expires_at)) // Most recent expiry first
58-
.limit(1) as { principal: number }[]
61+
.limit(1)
5962

6063
if (lastExpiredFreeGrant.length > 0) {
6164
// TODO: remove this once it's past May 22nd, after all users have been migrated over
@@ -93,12 +96,11 @@ export async function calculateTotalReferralBonus(params: {
9396
deps?: CalculateTotalReferralBonusDeps
9497
}): Promise<number> {
9598
const { userId, logger, deps = {} } = params
96-
// Cast to BillingDbConnection to allow either real db or mock to be used
97-
const dbClient = (deps.db ?? db) as BillingDbConnection
99+
const dbClient = getBillingDbClient(deps.db)
98100

99101
try {
100102
const result = await dbClient
101-
.select({
103+
.select<{ totalCredits: string }>({
102104
totalCredits: sql<string>`COALESCE(SUM(${schema.referral.credits}), 0)`,
103105
})
104106
.from(schema.referral)
@@ -107,7 +109,7 @@ export async function calculateTotalReferralBonus(params: {
107109
eq(schema.referral.referrer_id, userId),
108110
eq(schema.referral.referred_id, userId),
109111
),
110-
) as { totalCredits: string }[]
112+
)
111113

112114
const totalBonus = parseInt(result[0]?.totalCredits ?? '0')
113115
logger.debug({ userId, totalBonus }, 'Calculated total referral bonus.')
@@ -428,10 +430,10 @@ export async function triggerMonthlyResetAndGrant(params: {
428430
// Calculate new reset date
429431
const newResetDate = getNextQuotaReset(currentResetDate)
430432

431-
// Calculate grant amounts separately
433+
// Calculate grant amounts separately - pass tx to maintain transaction consistency
432434
const [freeGrantAmount, referralBonus] = await Promise.all([
433-
getPreviousFreeGrantAmount({ userId, logger }),
434-
calculateTotalReferralBonus({ userId, logger }),
435+
getPreviousFreeGrantAmount({ userId, logger, deps: { db: tx as BillingDbConnection } }),
436+
calculateTotalReferralBonus({ userId, logger, deps: { db: tx as BillingDbConnection } }),
435437
])
436438

437439
// Generate a deterministic operation ID based on userId and reset date to minute precision

0 commit comments

Comments
 (0)