Skip to content

Commit 5e0a028

Browse files
committed
pass in logger to grant-credits
1 parent ced2b16 commit 5e0a028

File tree

8 files changed

+144
-118
lines changed

8 files changed

+144
-118
lines changed

backend/src/websockets/middleware.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ protec.use(async (action, clientSessionId, ws, userInfo) => {
321321
})
322322

323323
// Check and trigger monthly reset if needed
324-
await triggerMonthlyResetAndGrant(userId)
324+
await triggerMonthlyResetAndGrant({ userId, logger })
325325

326326
// Check if we need to trigger auto top-up and get the amount added (if any)
327327
let autoTopupAdded: number | undefined = undefined

packages/billing/src/auto-topup.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -183,14 +183,14 @@ async function processAutoTopupPayment(params: {
183183
throw new AutoTopupPaymentError('Payment failed or requires action')
184184
}
185185

186-
await processAndGrantCredit(
187-
userId,
188-
amountToTopUp,
189-
'purchase',
190-
`Auto top-up of ${amountToTopUp.toLocaleString()} credits`,
191-
null,
186+
await processAndGrantCredit({
187+
...params,
188+
amount: amountToTopUp,
189+
type: 'purchase',
190+
description: `Auto top-up of ${amountToTopUp.toLocaleString()} credits`,
191+
expiresAt: null,
192192
operationId,
193-
)
193+
})
194194

195195
logger.info(
196196
{

packages/billing/src/grant-credits.ts

Lines changed: 78 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { and, desc, eq, gt, isNull, lte, or, sql } from 'drizzle-orm'
1313
import { generateOperationIdTimestamp } from './utils'
1414

1515
import type { GrantType } from '@codebuff/common/db/schema'
16+
import type { Logger } from '@codebuff/types/logger'
1617

1718
type CreditGrantSelect = typeof schema.creditLedger.$inferSelect
1819
type DbTransaction = Parameters<typeof db.transaction>[0] extends (
@@ -30,9 +31,12 @@ type DbTransaction = Parameters<typeof db.transaction>[0] extends (
3031
* @param userId The ID of the user.
3132
* @returns The amount of the last expired free grant (capped at 2000) or the default.
3233
*/
33-
export async function getPreviousFreeGrantAmount(
34-
userId: string,
35-
): Promise<number> {
34+
export async function getPreviousFreeGrantAmount(params: {
35+
userId: string
36+
logger: Logger
37+
}): Promise<number> {
38+
const { userId, logger } = params
39+
3640
const now = new Date()
3741
const lastExpiredFreeGrant = await db
3842
.select({
@@ -72,9 +76,12 @@ export async function getPreviousFreeGrantAmount(
7276
* @param userId The ID of the user.
7377
* @returns The total referral bonus credits earned.
7478
*/
75-
export async function calculateTotalReferralBonus(
76-
userId: string,
77-
): Promise<number> {
79+
export async function calculateTotalReferralBonus(params: {
80+
userId: string
81+
logger: Logger
82+
}): Promise<number> {
83+
const { userId, logger } = params
84+
7885
try {
7986
const result = await db
8087
.select({
@@ -103,15 +110,27 @@ export async function calculateTotalReferralBonus(
103110
/**
104111
* Core grant operation that can be part of a larger transaction.
105112
*/
106-
export async function grantCreditOperation(
107-
userId: string,
108-
amount: number,
109-
type: GrantType,
110-
description: string,
111-
expiresAt: Date | null,
112-
operationId: string,
113-
tx?: DbTransaction,
114-
) {
113+
export async function grantCreditOperation(params: {
114+
userId: string
115+
amount: number
116+
type: GrantType
117+
description: string
118+
expiresAt: Date | null
119+
operationId: string
120+
tx?: DbTransaction
121+
logger: Logger
122+
}) {
123+
const {
124+
userId,
125+
amount,
126+
type,
127+
description,
128+
expiresAt,
129+
operationId,
130+
tx,
131+
logger,
132+
} = params
133+
115134
const dbClient = tx || db
116135

117136
const now = new Date()
@@ -228,36 +247,28 @@ export async function grantCreditOperation(
228247
* Processes a credit grant request with retries and failure logging.
229248
* Used for standalone credit grants that need retry logic and failure tracking.
230249
*/
231-
export async function processAndGrantCredit(
232-
userId: string,
233-
amount: number,
234-
type: GrantType,
235-
description: string,
236-
expiresAt: Date | null,
237-
operationId: string,
238-
): Promise<void> {
250+
export async function processAndGrantCredit(params: {
251+
userId: string
252+
amount: number
253+
type: GrantType
254+
description: string
255+
expiresAt: Date | null
256+
operationId: string
257+
logger: Logger
258+
}): Promise<void> {
259+
const { operationId, logger } = params
260+
239261
try {
240-
await withRetry(
241-
() =>
242-
grantCreditOperation(
243-
userId,
244-
amount,
245-
type,
246-
description,
247-
expiresAt,
248-
operationId,
249-
),
250-
{
251-
maxRetries: 3,
252-
retryIf: () => true,
253-
onRetry: (error, attempt) => {
254-
logger.warn(
255-
{ operationId, attempt, error },
256-
`processAndGrantCredit retry ${attempt}`,
257-
)
258-
},
262+
await withRetry(() => grantCreditOperation(params), {
263+
maxRetries: 3,
264+
retryIf: () => true,
265+
onRetry: (error, attempt) => {
266+
logger.warn(
267+
{ operationId, attempt, error },
268+
`processAndGrantCredit retry ${attempt}`,
269+
)
259270
},
260-
)
271+
})
261272
} catch (error: any) {
262273
await logSyncFailure({
263274
id: operationId,
@@ -336,9 +347,12 @@ export async function revokeGrantByOperationId(
336347
* @param userId The ID of the user
337348
* @returns The effective quota reset date (either existing or new)
338349
*/
339-
export async function triggerMonthlyResetAndGrant(
340-
userId: string,
341-
): Promise<Date> {
350+
export async function triggerMonthlyResetAndGrant(params: {
351+
userId: string
352+
logger: Logger
353+
}): Promise<Date> {
354+
const { userId, logger } = params
355+
342356
return await db.transaction(async (tx) => {
343357
const now = new Date()
344358

@@ -366,8 +380,8 @@ export async function triggerMonthlyResetAndGrant(
366380

367381
// Calculate grant amounts separately
368382
const [freeGrantAmount, referralBonus] = await Promise.all([
369-
getPreviousFreeGrantAmount(userId),
370-
calculateTotalReferralBonus(userId),
383+
getPreviousFreeGrantAmount(params),
384+
calculateTotalReferralBonus(params),
371385
])
372386

373387
// Generate a deterministic operation ID based on userId and reset date to minute precision
@@ -382,25 +396,25 @@ export async function triggerMonthlyResetAndGrant(
382396
.where(eq(schema.user.id, userId))
383397

384398
// Always grant free credits
385-
await processAndGrantCredit(
386-
userId,
387-
freeGrantAmount,
388-
'free',
389-
'Monthly free credits',
390-
newResetDate, // Free credits expire at next reset
391-
freeOperationId,
392-
)
399+
await processAndGrantCredit({
400+
...params,
401+
amount: freeGrantAmount,
402+
type: 'free',
403+
description: 'Monthly free credits',
404+
expiresAt: newResetDate, // Free credits expire at next reset
405+
operationId: freeOperationId,
406+
})
393407

394408
// Only grant referral credits if there are any
395409
if (referralBonus > 0) {
396-
await processAndGrantCredit(
397-
userId,
398-
referralBonus,
399-
'referral',
400-
'Monthly referral bonus',
401-
newResetDate, // Referral credits expire at next reset
402-
referralOperationId,
403-
)
410+
await processAndGrantCredit({
411+
...params,
412+
amount: referralBonus,
413+
type: 'referral',
414+
description: 'Monthly referral bonus',
415+
expiresAt: newResetDate, // Referral credits expire at next reset
416+
operationId: referralOperationId,
417+
})
404418
}
405419

406420
logger.info(

packages/billing/src/usage-service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export async function getUserUsageData(params: {
5353
const now = new Date()
5454

5555
// Check if we need to reset quota and grant new credits
56-
const effectiveQuotaResetDate = await triggerMonthlyResetAndGrant(userId)
56+
const effectiveQuotaResetDate = await triggerMonthlyResetAndGrant(params)
5757

5858
// Check if we need to trigger auto top-up
5959
let autoTopupTriggered = false

web/src/app/api/auth/[...nextauth]/auth-options.ts

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { loops, env } from '@codebuff/internal'
1313
import { eq } from 'drizzle-orm'
1414
import GitHubProvider from 'next-auth/providers/github'
1515

16+
import type { Logger } from '@codebuff/types/logger'
1617
import type { NextAuthOptions } from 'next-auth'
1718
import type { Adapter } from 'next-auth/adapters'
1819

@@ -77,22 +78,25 @@ async function createAndLinkStripeCustomer(
7778
}
7879
}
7980

80-
async function createInitialCreditGrant(
81-
userId: string,
81+
async function createInitialCreditGrant(params: {
82+
userId: string
8283
expiresAt: Date | null
83-
): Promise<void> {
84+
logger: Logger
85+
}): Promise<void> {
86+
const { userId, expiresAt, logger } = params
87+
8488
try {
8589
const operationId = `free-${userId}-${generateCompactId()}`
8690
const nextQuotaReset = getNextQuotaReset(expiresAt)
8791

88-
await processAndGrantCredit(
89-
userId,
90-
DEFAULT_FREE_CREDITS_GRANT,
91-
'free',
92-
'Initial free credits',
93-
nextQuotaReset,
94-
operationId
95-
)
92+
await processAndGrantCredit({
93+
...params,
94+
amount: DEFAULT_FREE_CREDITS_GRANT,
95+
type: 'free',
96+
description: 'Initial free credits',
97+
expiresAt: nextQuotaReset,
98+
operationId,
99+
})
96100

97101
logger.info(
98102
{
@@ -231,7 +235,11 @@ export const authOptions: NextAuthOptions = {
231235
)
232236

233237
if (customerId) {
234-
await createInitialCreditGrant(userData.id, userData.next_quota_reset)
238+
await createInitialCreditGrant({
239+
userId: userData.id,
240+
expiresAt: userData.next_quota_reset,
241+
logger,
242+
})
235243
}
236244

237245
// Call the imported function

web/src/app/api/referrals/helpers.ts

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { grantCreditOperation } from '@codebuff/billing'
2-
import { CREDITS_REFERRAL_BONUS } from '@codebuff/common/old-constants'
32
import db from '@codebuff/common/db'
43
import * as schema from '@codebuff/common/db/schema'
4+
import { CREDITS_REFERRAL_BONUS } from '@codebuff/common/old-constants'
55
import { and, eq, sql } from 'drizzle-orm'
66
import { NextResponse } from 'next/server'
77

@@ -154,15 +154,16 @@ export async function redeemReferralCode(referralCode: string, userId: string) {
154154

155155
// Process Referrer
156156
grantPromises.push(
157-
grantCreditOperation(
158-
referrer.id,
159-
CREDITS_REFERRAL_BONUS,
160-
'referral',
161-
'Referral bonus (referrer)',
162-
user.next_quota_reset,
163-
`${operationId}-referrer`,
164-
tx
165-
)
157+
grantCreditOperation({
158+
userId: referrer.id,
159+
amount: CREDITS_REFERRAL_BONUS,
160+
type: 'referral',
161+
description: 'Referral bonus (referrer)',
162+
expiresAt: user.next_quota_reset,
163+
operationId: `${operationId}-referrer`,
164+
tx,
165+
logger,
166+
})
166167
.then(() => true)
167168
.catch((error: Error) => {
168169
logger.error(
@@ -180,15 +181,16 @@ export async function redeemReferralCode(referralCode: string, userId: string) {
180181

181182
// Process Referred User
182183
grantPromises.push(
183-
grantCreditOperation(
184-
referred.id,
185-
CREDITS_REFERRAL_BONUS,
186-
'referral',
187-
'Referral bonus (referred)',
188-
user.next_quota_reset,
189-
`${operationId}-referred`,
190-
tx
191-
)
184+
grantCreditOperation({
185+
userId: referred.id,
186+
amount: CREDITS_REFERRAL_BONUS,
187+
type: 'referral',
188+
description: 'Referral bonus (referred)',
189+
expiresAt: user.next_quota_reset,
190+
operationId: `${operationId}-referred`,
191+
tx,
192+
logger,
193+
})
192194
.then(() => true)
193195
.catch((error: Error) => {
194196
logger.error(

web/src/app/api/stripe/buy-credits/route.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -127,14 +127,15 @@ export async function POST(req: NextRequest) {
127127

128128
if (paymentIntent.status === 'succeeded') {
129129
// Grant credits immediately
130-
await processAndGrantCredit(
130+
await processAndGrantCredit({
131131
userId,
132-
credits,
133-
'purchase',
134-
`Direct purchase of ${credits.toLocaleString()} credits`,
135-
null,
136-
operationId
137-
)
132+
amount: credits,
133+
type: 'purchase',
134+
description: `Direct purchase of ${credits.toLocaleString()} credits`,
135+
expiresAt: null,
136+
operationId,
137+
logger,
138+
})
138139

139140
logger.info(
140141
{

0 commit comments

Comments
 (0)