user(); // Get budget configuration $budget = Budget::where('gateway_user_id', $user->user_id)->first(); if (!$budget) { return response()->json([ 'error' => [ 'code' => 'not_found', 'message' => 'No budget configured for this user', 'status' => 404, ], ], 404); } // Calculate period dates $now = now(); $periodStart = $now->copy()->startOfMonth(); $periodEnd = $now->copy()->endOfMonth(); $daysRemaining = $now->diffInDays($periodEnd, false); $daysInMonth = $periodStart->daysInMonth; $daysElapsed = $now->diffInDays($periodStart); // Get current month's spending $monthlySpending = LlmRequest::where('gateway_user_id', $user->user_id) ->whereYear('created_at', $now->year) ->whereMonth('created_at', $now->month) ->where('status', 'success') ->sum('total_cost') ?? 0; // Get today's spending $dailySpending = LlmRequest::where('gateway_user_id', $user->user_id) ->whereDate('created_at', $now->toDateString()) ->where('status', 'success') ->sum('total_cost') ?? 0; // Calculate projections $dailyAverage = $daysElapsed > 0 ? ($monthlySpending / $daysElapsed) : 0; $projectedSpend = $dailyAverage * $daysInMonth; // Calculate remaining budget $remainingBudget = $budget->monthly_limit - $monthlySpending; $budgetPercentage = $budget->monthly_limit > 0 ? ($monthlySpending / $budget->monthly_limit) * 100 : 0; // Get breakdown by provider $providerBreakdown = LlmRequest::where('gateway_user_id', $user->user_id) ->whereYear('created_at', $now->year) ->whereMonth('created_at', $now->month) ->where('status', 'success') ->select('provider', DB::raw('SUM(total_cost) as spent'), DB::raw('COUNT(*) as requests')) ->groupBy('provider') ->orderByDesc('spent') ->get() ->map(function ($item) use ($monthlySpending) { $percentage = $monthlySpending > 0 ? ($item->spent / $monthlySpending) * 100 : 0; return [ 'provider' => $item->provider, 'provider_name' => $this->getProviderName($item->provider), 'spent' => round($item->spent, 4), 'percentage' => round($percentage, 1), 'requests' => $item->requests, ]; }); // Calculate daily limit (monthly limit / days in month) $dailyLimit = $budget->monthly_limit / $daysInMonth; return response()->json([ 'data' => [ 'total_budget' => round($budget->monthly_limit, 2), 'used_budget' => round($monthlySpending, 4), 'remaining_budget' => round($remainingBudget, 4), 'budget_percentage' => round($budgetPercentage, 2), 'currency' => 'USD', 'period' => 'monthly', 'period_start' => $periodStart->toIso8601String(), 'period_end' => $periodEnd->toIso8601String(), 'days_remaining' => max(0, $daysRemaining), 'projected_spend' => round($projectedSpend, 2), 'projected_overspend' => $projectedSpend > $budget->monthly_limit, 'limits' => [ 'daily_limit' => round($dailyLimit, 2), 'daily_used' => round($dailySpending, 4), 'daily_remaining' => round($dailyLimit - $dailySpending, 4), ], 'alerts' => [ 'threshold_50_percent' => $budgetPercentage >= 50, 'threshold_75_percent' => $budgetPercentage >= 75, 'threshold_90_percent' => $budgetPercentage >= 90, 'approaching_daily_limit' => ($dailySpending / $dailyLimit) >= 0.8, ], 'breakdown_by_provider' => $providerBreakdown, 'updated_at' => now()->toIso8601String(), ], ]); } /** * Get budget history over time * * Returns historical budget data with daily, weekly, or monthly aggregation. * * ## Query Parameters * * - `period` (optional) - Aggregation period: daily, weekly, monthly (default: daily) * - `days` (optional) - Number of days to look back (default: 30) * * ## Example Response * * ```json * { * "data": [ * { * "date": "2025-11-19", * "total_cost": 2.45, * "total_requests": 45, * "total_tokens": 45000, * "breakdown": [ * { * "provider": "openai", * "cost": 1.35, * "requests": 25 * } * ] * } * ], * "meta": { * "period": "daily", * "days": 30, * "total_cost": 45.67, * "total_requests": 1250, * "avg_daily_cost": 1.52, * "avg_daily_requests": 41.7 * } * } * ``` * * @tags Budget * * @param Request $request * @return JsonResponse */ public function history(Request $request): JsonResponse { $validator = Validator::make($request->all(), [ 'period' => 'sometimes|string|in:daily,weekly,monthly', 'days' => 'sometimes|integer|min:1|max:365', ]); if ($validator->fails()) { return response()->json([ 'error' => [ 'code' => 'validation_error', 'message' => 'Invalid query parameters', 'status' => 422, 'details' => $validator->errors(), ], ], 422); } $user = $request->user(); $period = $request->input('period', 'daily'); $days = $request->input('days', 30); $startDate = now()->subDays($days)->startOfDay(); // Get daily aggregated data $dailyData = LlmRequest::where('gateway_user_id', $user->user_id) ->where('created_at', '>=', $startDate) ->where('status', 'success') ->select( DB::raw('DATE(created_at) as date'), 'provider', DB::raw('SUM(total_cost) as cost'), DB::raw('COUNT(*) as requests'), DB::raw('SUM(total_tokens) as tokens') ) ->groupBy('date', 'provider') ->orderBy('date') ->get(); // Group by date $groupedByDate = $dailyData->groupBy('date')->map(function ($items, $date) { return [ 'date' => $date, 'total_cost' => round($items->sum('cost'), 4), 'total_requests' => $items->sum('requests'), 'total_tokens' => $items->sum('tokens'), 'breakdown' => $items->map(function ($item) { return [ 'provider' => $item->provider, 'cost' => round($item->cost, 4), 'requests' => $item->requests, ]; })->values(), ]; })->values(); // Calculate meta statistics $totalCost = $groupedByDate->sum('total_cost'); $totalRequests = $groupedByDate->sum('total_requests'); $dataPoints = $groupedByDate->count(); return response()->json([ 'data' => $groupedByDate, 'meta' => [ 'period' => $period, 'days' => $days, 'total_cost' => round($totalCost, 4), 'total_requests' => $totalRequests, 'avg_daily_cost' => $dataPoints > 0 ? round($totalCost / $dataPoints, 4) : 0, 'avg_daily_requests' => $dataPoints > 0 ? round($totalRequests / $dataPoints, 1) : 0, ], ]); } /** * Get human-readable provider name */ private function getProviderName(string $provider): string { return match ($provider) { 'openai' => 'OpenAI', 'anthropic' => 'Anthropic', 'gemini' => 'Google Gemini', 'deepseek' => 'DeepSeek', 'mistral' => 'Mistral AI', default => ucfirst($provider), }; } }