@@ -2,7 +2,7 @@ import "server-only";
22import { eq , sql , desc , and , lt , isNull , gt , or , asc } from "drizzle-orm" ;
33import { getDB } from "@/db" ;
44import { userTable , creditTransactionTable , CREDIT_TRANSACTION_TYPE , purchasedItemsTable } from "@/db/schema" ;
5- import { updateAllSessionsOfUser , KVSession } from "./kv-session" ;
5+ import { updateAllSessionsOfUser , updateKVSession , KVSession } from "./kv-session" ;
66import { CREDIT_PACKAGES , FREE_MONTHLY_CREDITS , DISABLE_CREDIT_BILLING_SYSTEM } from "@/constants" ;
77
88export type CreditPackage = typeof CREDIT_PACKAGES [ number ] ;
@@ -18,8 +18,22 @@ function shouldRefreshCredits(session: KVSession, currentTime: Date): boolean {
1818 }
1919
2020 // Calculate the date exactly one month after the last refresh
21- const oneMonthAfterLastRefresh = new Date ( session . user . lastCreditRefreshAt ) ;
22- oneMonthAfterLastRefresh . setMonth ( oneMonthAfterLastRefresh . getMonth ( ) + 1 ) ;
21+ // Using a more reliable approach to avoid edge cases with setMonth()
22+ const lastRefresh = new Date ( session . user . lastCreditRefreshAt ) ;
23+ const year = lastRefresh . getFullYear ( ) ;
24+ const month = lastRefresh . getMonth ( ) ;
25+ const day = lastRefresh . getDate ( ) ;
26+
27+ // Calculate one month later, handling edge cases (e.g., Jan 31 + 1 month = Feb 28/29)
28+ let oneMonthAfterLastRefresh = new Date ( year , month + 1 , day ) ;
29+
30+ // If the day changed (e.g., Jan 31 -> Mar 3 instead of Feb 28), use last day of target month
31+ if ( oneMonthAfterLastRefresh . getDate ( ) !== day ) {
32+ oneMonthAfterLastRefresh = new Date ( year , month + 2 , 0 ) ; // Last day of target month
33+ }
34+
35+ // Preserve the original time of day
36+ oneMonthAfterLastRefresh . setHours ( lastRefresh . getHours ( ) , lastRefresh . getMinutes ( ) , lastRefresh . getSeconds ( ) , lastRefresh . getMilliseconds ( ) ) ;
2337
2438 // Only refresh if we've passed the one month mark
2539 return currentTime >= oneMonthAfterLastRefresh ;
@@ -149,14 +163,33 @@ export async function addFreeMonthlyCreditsIfNeeded(session: KVSession): Promise
149163 } ,
150164 } ) ;
151165
166+ // Convert DB date string to Date object to ensure consistent type handling
167+ const dbLastRefreshAt = user ?. lastCreditRefreshAt
168+ ? new Date ( user . lastCreditRefreshAt )
169+ : null ;
170+
152171 // This should prevent race conditions between multiple sessions
153- if ( ! shouldRefreshCredits ( { ...session , user : { ...session . user , lastCreditRefreshAt : user ?. lastCreditRefreshAt ?? null } } , currentTime ) ) {
172+ if ( ! shouldRefreshCredits ( { ...session , user : { ...session . user , lastCreditRefreshAt : dbLastRefreshAt } } , currentTime ) ) {
173+ // KV session is out of sync with DB - update it to prevent this check on next request
174+ await updateKVSession ( session . id , session . userId , new Date ( session . expiresAt ) ) ;
154175 return user ?. currentCredits ?? 0 ;
155176 }
156177
157- // Calculate one month ago from current time (using calendar month logic)
158- const oneMonthAgo = new Date ( currentTime ) ;
159- oneMonthAgo . setMonth ( oneMonthAgo . getMonth ( ) - 1 ) ;
178+ // Calculate one month ago from current time
179+ // Using a more reliable approach to avoid edge cases with setMonth()
180+ const year = currentTime . getFullYear ( ) ;
181+ const month = currentTime . getMonth ( ) ;
182+ const day = currentTime . getDate ( ) ;
183+
184+ let oneMonthAgo = new Date ( year , month - 1 , day ) ;
185+
186+ // Handle edge cases (e.g., Mar 31 - 1 month should be Feb 28/29, not Mar 3)
187+ if ( oneMonthAgo . getDate ( ) !== day ) {
188+ oneMonthAgo = new Date ( year , month , 0 ) ; // Last day of previous month
189+ }
190+
191+ // Preserve the original time of day
192+ oneMonthAgo . setHours ( currentTime . getHours ( ) , currentTime . getMinutes ( ) , currentTime . getSeconds ( ) , currentTime . getMilliseconds ( ) ) ;
160193
161194 // Update last refresh date FIRST to act as a distributed lock
162195 // This prevents race conditions where multiple requests try to add credits simultaneously
0 commit comments