11import { NextResponse } from "next/server" ;
22import AIUsage , { AIUsageCategory } from "@/lib/models/AIUsage" ;
33import { dbConnect } from "@/lib/db" ;
4-
5- type UserRole = "student" | "teacher" | "admin" ;
4+ import type { SubscriptionTier } from "@/lib/auth/jwt" ;
65
76/**
8- * Daily request limits by role and category.
7+ * Daily limits by category and subscription tier.
8+ * Infinity means unlimited (skip DB entirely).
99 */
10- const DAILY_LIMITS : Record < AIUsageCategory , Record < UserRole , number > > = {
11- chat : { student : 50 , teacher : 200 , admin : 10_000 } ,
12- generate : { student : 10 , teacher : 50 , admin : 10_000 } ,
13- course_generation : { student : 5 , teacher : 20 , admin : 10_000 } ,
10+ const DAILY_LIMITS : Record < AIUsageCategory , Record < SubscriptionTier , number > > = {
11+ questions : { free : 50 , plus : Infinity , admin : Infinity } ,
12+ credits : { free : 10 , plus : 100 , admin : Infinity } ,
1413} ;
1514
1615/**
@@ -25,22 +24,36 @@ export interface RateLimitResult {
2524 limit : number ;
2625 used : number ;
2726 remaining : number ;
27+ cost : number ;
2828 resetAt : string ; // ISO timestamp of next UTC midnight
2929}
3030
3131/**
3232 * Checks and atomically increments the AI rate limit counter.
33- * Race-condition-free: uses conditional $inc so two concurrent requests
34- * cannot both sneak past the limit .
33+ * Supports variable cost (e.g. 1 credit per lesson in a module).
34+ * For unlimited tiers, skips the DB entirely .
3535 */
3636export async function checkAIRateLimit (
3737 userId : string ,
38- role : UserRole ,
39- category : AIUsageCategory
38+ tier : SubscriptionTier ,
39+ category : AIUsageCategory ,
40+ cost : number = 1
4041) : Promise < RateLimitResult > {
42+ const limit = DAILY_LIMITS [ category ] [ tier ] ;
43+
44+ // Compute next UTC midnight for reset header
45+ const tomorrow = new Date ( ) ;
46+ tomorrow . setUTCDate ( tomorrow . getUTCDate ( ) + 1 ) ;
47+ tomorrow . setUTCHours ( 0 , 0 , 0 , 0 ) ;
48+ const resetAt = tomorrow . toISOString ( ) ;
49+
50+ // Unlimited tier — skip DB entirely
51+ if ( ! isFinite ( limit ) ) {
52+ return { allowed : true , limit, used : 0 , remaining : Infinity , cost, resetAt } ;
53+ }
54+
4155 await dbConnect ( ) ;
4256
43- const limit = DAILY_LIMITS [ category ] [ role ] ;
4457 const dateKey = getDateKey ( ) ;
4558
4659 // Ensure document exists
@@ -50,31 +63,26 @@ export async function checkAIRateLimit(
5063 { upsert : true }
5164 ) ;
5265
53- // Conditionally increment — only if under the limit
66+ // Conditionally increment — only if there's enough headroom for the full cost
5467 const result = await AIUsage . findOneAndUpdate (
55- { user : userId , category, dateKey, count : { $lt : limit } } ,
56- { $inc : { count : 1 } } ,
68+ { user : userId , category, dateKey, count : { $lte : limit - cost } } ,
69+ { $inc : { count : cost } } ,
5770 { new : true }
5871 ) ;
5972
60- // Compute next UTC midnight for reset header
61- const tomorrow = new Date ( ) ;
62- tomorrow . setUTCDate ( tomorrow . getUTCDate ( ) + 1 ) ;
63- tomorrow . setUTCHours ( 0 , 0 , 0 , 0 ) ;
64- const resetAt = tomorrow . toISOString ( ) ;
65-
6673 if ( ! result ) {
6774 // Limit reached — fetch current count for headers
6875 const doc = await AIUsage . findOne ( { user : userId , category, dateKey } ) ;
6976 const used = doc ?. count ?? limit ;
70- return { allowed : false , limit, used, remaining : 0 , resetAt } ;
77+ return { allowed : false , limit, used, remaining : Math . max ( 0 , limit - used ) , cost , resetAt } ;
7178 }
7279
7380 return {
7481 allowed : true ,
7582 limit,
7683 used : result . count ,
7784 remaining : limit - result . count ,
85+ cost,
7886 resetAt,
7987 } ;
8088}
@@ -84,20 +92,23 @@ export async function checkAIRateLimit(
8492 */
8593export async function enforceAIRateLimit (
8694 userId : string ,
87- role : UserRole ,
88- category : AIUsageCategory
95+ tier : SubscriptionTier ,
96+ category : AIUsageCategory ,
97+ cost : number = 1
8998) : Promise <
9099 | { blocked : true ; response : NextResponse }
91100 | { blocked : false ; result : RateLimitResult }
92101> {
93- const result = await checkAIRateLimit ( userId , role , category ) ;
102+ const result = await checkAIRateLimit ( userId , tier , category , cost ) ;
94103
95104 if ( ! result . allowed ) {
96105 const response = NextResponse . json (
97106 {
98107 error : "Daily AI rate limit exceeded. Please try again tomorrow." ,
99108 limit : result . limit ,
100109 used : result . used ,
110+ remaining : result . remaining ,
111+ cost : result . cost ,
101112 resetAt : result . resetAt ,
102113 } ,
103114 { status : 429 }
0 commit comments