getOrCreateBudget($user); // If budget is already exceeded, deny immediately if ($budget->is_budget_exceeded) { throw new InsufficientBudgetException( "Budget limit exceeded. Current spending: $" . number_format($budget->current_month_spending, 2) . " / Monthly limit: $" . number_format($budget->monthly_limit, 2) ); } // Check daily limit if set if ($budget->daily_limit > 0) { $projectedDailySpending = $budget->current_day_spending + $estimatedCost; if ($projectedDailySpending > $budget->daily_limit) { throw new InsufficientBudgetException( "Daily budget limit would be exceeded. Current: $" . number_format($budget->current_day_spending, 2) . " / Daily limit: $" . number_format($budget->daily_limit, 2) ); } } // Check monthly limit if ($budget->monthly_limit > 0) { $projectedMonthlySpending = $budget->current_month_spending + $estimatedCost; if ($projectedMonthlySpending > $budget->monthly_limit) { throw new InsufficientBudgetException( "Monthly budget limit would be exceeded. Current: $" . number_format($budget->current_month_spending, 2) . " / Monthly limit: $" . number_format($budget->monthly_limit, 2) ); } // Check alert threshold $usagePercentage = ($projectedMonthlySpending / $budget->monthly_limit) * 100; if ($usagePercentage >= $budget->alert_threshold_percentage) { $this->sendBudgetAlert($user, $budget, $usagePercentage); } } return true; } /** * Update user budget after a request * * @param User $user * @param float $actualCost * @return void */ public function updateBudget(User $user, float $actualCost): void { $budget = $this->getOrCreateBudget($user); // Reset periods if needed $this->checkAndResetPeriods($budget); // Update spending $budget->current_month_spending += $actualCost; $budget->current_day_spending += $actualCost; // Check if budget is now exceeded if ($budget->monthly_limit > 0 && $budget->current_month_spending >= $budget->monthly_limit) { $budget->is_budget_exceeded = true; } $budget->save(); // Invalidate cache Cache::forget("user_budget:{$user->id}"); Log::info('Budget updated', [ 'user_id' => $user->id, 'cost' => $actualCost, 'monthly_spending' => $budget->current_month_spending, 'daily_spending' => $budget->current_day_spending ]); } /** * Get or create user budget * * @param User $user * @return UserBudget */ private function getOrCreateBudget(User $user): UserBudget { $budget = $user->budget; if (!$budget) { $budget = UserBudget::create([ 'user_id' => $user->id, 'monthly_limit' => config('llm.default_monthly_budget', 100.00), 'daily_limit' => config('llm.default_daily_budget', 10.00), 'month_started_at' => now()->startOfMonth(), 'day_started_at' => now()->startOfDay(), 'alert_threshold_percentage' => 80, ]); Log::info('Budget created for user', ['user_id' => $user->id]); } return $budget; } /** * Check and reset budget periods if needed * * @param UserBudget $budget * @return void */ private function checkAndResetPeriods(UserBudget $budget): void { $now = now(); // Reset monthly budget if new month if ($now->startOfMonth()->greaterThan($budget->month_started_at)) { $budget->current_month_spending = 0.0; $budget->month_started_at = $now->startOfMonth(); $budget->is_budget_exceeded = false; $budget->last_alert_sent_at = null; Log::info('Monthly budget reset', ['user_id' => $budget->user_id]); } // Reset daily budget if new day if ($now->startOfDay()->greaterThan($budget->day_started_at)) { $budget->current_day_spending = 0.0; $budget->day_started_at = $now->startOfDay(); Log::info('Daily budget reset', ['user_id' => $budget->user_id]); } } /** * Send budget alert to user * * @param User $user * @param UserBudget $budget * @param float $usagePercentage * @return void */ private function sendBudgetAlert(User $user, UserBudget $budget, float $usagePercentage): void { // Only send alert once per day if ($budget->last_alert_sent_at && $budget->last_alert_sent_at->isToday()) { return; } Log::warning('Budget threshold reached', [ 'user_id' => $user->id, 'user_email' => $user->email, 'usage_percentage' => round($usagePercentage, 2), 'current_spending' => $budget->current_month_spending, 'monthly_limit' => $budget->monthly_limit ]); // TODO: Send email notification // Mail::to($user->email)->send(new BudgetAlertMail($budget, $usagePercentage)); $budget->last_alert_sent_at = now(); $budget->save(); } /** * Get budget status for user * * @param User $user * @return array */ public function getBudgetStatus(User $user): array { $budget = $this->getOrCreateBudget($user); return [ 'monthly_limit' => $budget->monthly_limit, 'daily_limit' => $budget->daily_limit, 'current_month_spending' => $budget->current_month_spending, 'current_day_spending' => $budget->current_day_spending, 'monthly_remaining' => max(0, $budget->monthly_limit - $budget->current_month_spending), 'daily_remaining' => max(0, $budget->daily_limit - $budget->current_day_spending), 'monthly_usage_percentage' => $budget->monthly_limit > 0 ? ($budget->current_month_spending / $budget->monthly_limit) * 100 : 0, 'is_exceeded' => $budget->is_budget_exceeded, 'month_started_at' => $budget->month_started_at, 'day_started_at' => $budget->day_started_at, ]; } }