#![allow(dead_code)] use axum::{ Json, extract::{Path, Query, State}, http::StatusCode, }; use common::{auth::Claims, middleware::Org}; use serde::{Deserialize, Serialize}; use sqlx::PgPool; use chrono::{DateTime, Utc}; use uuid::Uuid; // ==================== Seguimiento del Uso de Tokens ==================== /// GET /api/admin/token-usage - Obtener estadísticas de uso de tokens para todos los usuarios pub async fn get_token_usage( _org_ctx: Org, _claims: Claims, State(pool): State, ) -> Result, (StatusCode, String)> { // Obtener el uso de tokens de usuario de la base de datos let usage: Vec = sqlx::query_as( r#" SELECT u.id as user_id, u.email, u.full_name, u.role, COALESCE(SUM(au.tokens_used), 0) as total_tokens, COALESCE(SUM(au.input_tokens), 0) as input_tokens, COALESCE(SUM(au.output_tokens), 0) as output_tokens, COUNT(au.id) as ai_requests, MAX(au.created_at) as last_used FROM users u LEFT JOIN ai_usage_logs au ON u.id = au.user_id GROUP BY u.id, u.email, u.full_name, u.role ORDER BY total_tokens DESC "# ) .fetch_all(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener el uso: {}", e)))?; // Calcular estadísticas let total_tokens: i64 = usage.iter().map(|u| u.total_tokens).sum(); let total_input: i64 = usage.iter().map(|u| u.input_tokens).sum(); let total_output: i64 = usage.iter().map(|u| u.output_tokens).sum(); let total_requests: i64 = usage.iter().map(|u| u.ai_requests).sum(); let top_user_tokens = usage.first().map(|u| u.total_tokens).unwrap_or(0); let avg_tokens = if !usage.is_empty() { total_tokens / usage.len() as i64 } else { 0 }; // Estimación de coste (usando precios aproximados de OpenAI: $0.001/1K entrada, $0.003/1K salida) let estimated_cost = (total_input as f64 * 0.000001) + (total_output as f64 * 0.000003); let stats = TokenUsageStats { total_tokens, total_input, total_output, total_requests, total_cost_usd: estimated_cost, top_user_tokens, avg_tokens_per_user: avg_tokens, }; // Convertir al formato de respuesta con estimación en USD por usuario let usage_with_cost: Vec = usage .into_iter() .map(|u| { let user_cost = (u.input_tokens as f64 * 0.000001) + (u.output_tokens as f64 * 0.000003); TokenUsage { user_id: u.user_id.to_string(), email: u.email, full_name: u.full_name, role: u.role, total_tokens: u.total_tokens, input_tokens: u.input_tokens, output_tokens: u.output_tokens, ai_requests: u.ai_requests, last_used: u .last_used .map(|ts| ts.to_rfc3339()) .unwrap_or_else(|| "Nunca".to_string()), estimated_cost_usd: user_cost, } }) .collect(); Ok(Json(TokenUsageResponse { usage: usage_with_cost, stats, })) } /// GET /api/admin/ai-usage-dashboard - Datos exhaustivos del panel de uso de IA pub async fn get_ai_usage_dashboard( Org(org_ctx): Org, _claims: Claims, State(pool): State, Query(filters): Query, ) -> Result, (StatusCode, String)> { // Obtener el uso diario para gráficos let daily_usage: Vec = sqlx::query_as( r#" SELECT DATE(au.created_at) as date, COALESCE(SUM(au.tokens_used), 0) as total_tokens, COALESCE(SUM(au.input_tokens), 0) as input_tokens, COALESCE(SUM(au.output_tokens), 0) as output_tokens, COALESCE(SUM(au.estimated_cost_usd), 0)::FLOAT8 as cost_usd, COUNT(au.id) as requests FROM ai_usage_logs au WHERE au.organization_id = $1 AND ($2::DATE IS NULL OR DATE(au.created_at) >= $2::DATE) AND ($3::DATE IS NULL OR DATE(au.created_at) <= $3::DATE) GROUP BY DATE(au.created_at) ORDER BY date DESC LIMIT 30 "# ) .bind(org_ctx.id) .bind(filters.start_date.clone()) .bind(filters.end_date.clone()) .fetch_all(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener el uso diario: {}", e)))?; // Obtener uso por punto de conexión/función let by_endpoint: Vec = sqlx::query_as( r#" SELECT au.endpoint, au.request_type, COALESCE(SUM(au.tokens_used), 0) as total_tokens, COALESCE(SUM(au.input_tokens), 0) as input_tokens, COALESCE(SUM(au.output_tokens), 0) as output_tokens, COALESCE(SUM(au.estimated_cost_usd), 0)::FLOAT8 as cost_usd, COUNT(au.id) as requests FROM ai_usage_logs au WHERE au.organization_id = $1 AND ($2::DATE IS NULL OR DATE(au.created_at) >= $2::DATE) AND ($3::DATE IS NULL OR DATE(au.created_at) <= $3::DATE) GROUP BY au.endpoint, au.request_type ORDER BY total_tokens DESC "# ) .bind(org_ctx.id) .bind(filters.start_date.clone()) .bind(filters.end_date.clone()) .fetch_all(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener el uso del punto de conexión: {}", e)))?; // Obtener usuarios principales let top_users: Vec = sqlx::query_as( r#" SELECT u.id as user_id, u.email, u.full_name, u.role, COALESCE(SUM(au.tokens_used), 0) as total_tokens, COALESCE(SUM(au.input_tokens), 0) as input_tokens, COALESCE(SUM(au.output_tokens), 0) as output_tokens, COALESCE(SUM(au.estimated_cost_usd), 0)::FLOAT8 as cost_usd, COUNT(au.id) as requests FROM ai_usage_logs au JOIN users u ON au.user_id = u.id WHERE au.organization_id = $1 AND ($2::DATE IS NULL OR DATE(au.created_at) >= $2::DATE) AND ($3::DATE IS NULL OR DATE(au.created_at) <= $3::DATE) GROUP BY u.id, u.email, u.full_name, u.role ORDER BY total_tokens DESC LIMIT 10 "# ) .bind(org_ctx.id) .bind(filters.start_date.clone()) .bind(filters.end_date.clone()) .fetch_all(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener los usuarios principales: {}", e)))?; // Calcular estadísticas de resumen let total_tokens: i64 = daily_usage.iter().map(|d| d.total_tokens).sum(); let total_input: i64 = daily_usage.iter().map(|d| d.input_tokens).sum(); let total_output: i64 = daily_usage.iter().map(|d| d.output_tokens).sum(); let total_cost: f64 = daily_usage.iter().map(|d| d.cost_usd).sum(); let total_requests: i64 = daily_usage.iter().map(|d| d.requests).sum(); // Calcular estimación de ahorros (frente a precios de OpenAI GPT-4) // GPT-4: ~$0.03/1K input, ~$0.06/1K output // Nuestra IA local: ~$0.001/1K entrada, ~$0.003/1K salida let openai_equivalent_cost = (total_input as f64 * 0.00003) + (total_output as f64 * 0.00006); let savings_vs_openai = openai_equivalent_cost - total_cost; Ok(Json(DashboardResponse { summary: DashboardSummary { total_tokens, total_input, total_output, total_requests, total_cost_usd: total_cost, savings_vs_openai_usd: savings_vs_openai, openai_equivalent_cost_usd: openai_equivalent_cost, }, daily_usage, by_endpoint, top_users, })) } /// GET /api/admin/ai-usage/logs - Obtener logs detallados de uso de IA con paginación pub async fn get_ai_usage_logs( Org(org_ctx): Org, _claims: Claims, State(pool): State, Query(filters): Query, ) -> Result, (StatusCode, String)> { let limit = filters.limit.unwrap_or(50).min(200); let offset = filters.offset.unwrap_or(0); let logs: Vec = sqlx::query_as( r#" SELECT au.id, au.user_id, u.email as user_email, u.full_name as user_name, au.endpoint, au.request_type, au.model, au.tokens_used, au.input_tokens, au.output_tokens, au.estimated_cost_usd, au.prompt, au.response, au.request_metadata, au.created_at FROM ai_usage_logs au JOIN users u ON au.user_id = u.id WHERE au.organization_id = $1 AND ($2::TEXT IS NULL OR au.endpoint = $2) AND ($3::TEXT IS NULL OR au.request_type = $3) AND ($4::UUID IS NULL OR au.user_id = $4) ORDER BY au.created_at DESC LIMIT $5 OFFSET $6 "# ) .bind(org_ctx.id) .bind(filters.endpoint.clone()) .bind(filters.request_type.clone()) .bind(filters.user_id.clone()) .bind(limit as i64) .bind(offset as i64) .fetch_all(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener los logs: {}", e)))?; // Obtener el conteo total para la paginación let count: (i64,) = sqlx::query_as( r#" SELECT COUNT(*) FROM ai_usage_logs WHERE organization_id = $1 AND ($2::TEXT IS NULL OR endpoint = $2) AND ($3::TEXT IS NULL OR request_type = $3) AND ($4::UUID IS NULL OR user_id = $4) "# ) .bind(org_ctx.id) .bind(filters.endpoint.clone()) .bind(filters.request_type.clone()) .bind(filters.user_id.clone()) .fetch_one(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al contar los logs: {}", e)))?; Ok(Json(UsageLogsResponse { logs, total: count.0, limit, offset, })) } #[derive(Debug, Deserialize)] pub struct DashboardFilters { pub start_date: Option, pub end_date: Option, } #[derive(Debug, Deserialize)] pub struct UsageLogFilters { pub endpoint: Option, pub request_type: Option, pub user_id: Option, pub limit: Option, pub offset: Option, } #[derive(Debug, Serialize)] pub struct DashboardSummary { pub total_tokens: i64, pub total_input: i64, pub total_output: i64, pub total_requests: i64, pub total_cost_usd: f64, pub savings_vs_openai_usd: f64, pub openai_equivalent_cost_usd: f64, } #[derive(Debug, Serialize, sqlx::FromRow)] pub struct DailyUsage { pub date: chrono::NaiveDate, pub total_tokens: i64, pub input_tokens: i64, pub output_tokens: i64, pub cost_usd: f64, pub requests: i64, } #[derive(Debug, Serialize, sqlx::FromRow)] pub struct UsageByEndpoint { pub endpoint: String, pub request_type: String, pub total_tokens: i64, pub input_tokens: i64, pub output_tokens: i64, pub cost_usd: f64, pub requests: i64, } #[derive(Debug, Serialize, sqlx::FromRow)] pub struct TopUserUsage { pub user_id: uuid::Uuid, pub email: String, pub full_name: String, pub role: String, pub total_tokens: i64, pub input_tokens: i64, pub output_tokens: i64, pub cost_usd: f64, pub requests: i64, } #[derive(Debug, Serialize)] pub struct DashboardResponse { pub summary: DashboardSummary, pub daily_usage: Vec, pub by_endpoint: Vec, pub top_users: Vec, } #[derive(Debug, Serialize, sqlx::FromRow)] pub struct UsageLogRecord { pub id: uuid::Uuid, pub user_id: uuid::Uuid, pub user_email: String, pub user_name: String, pub endpoint: String, pub request_type: String, pub model: String, pub tokens_used: i32, pub input_tokens: i32, pub output_tokens: i32, pub estimated_cost_usd: f64, pub prompt: Option, pub response: Option, pub request_metadata: serde_json::Value, pub created_at: chrono::DateTime, } #[derive(Debug, Serialize)] pub struct UsageLogsResponse { pub logs: Vec, pub total: i64, pub limit: u32, pub offset: u32, } #[derive(Debug, Deserialize)] pub struct TokenUsageFilters { pub role: Option, pub min_tokens: Option, } #[derive(Debug, Serialize)] pub struct TokenUsage { pub user_id: String, pub email: String, pub full_name: String, pub role: String, pub total_tokens: i64, pub input_tokens: i64, pub output_tokens: i64, pub ai_requests: i64, pub last_used: String, pub estimated_cost_usd: f64, } #[derive(Debug, sqlx::FromRow)] struct TokenUsageRecord { user_id: uuid::Uuid, email: String, full_name: String, role: String, total_tokens: i64, input_tokens: i64, output_tokens: i64, ai_requests: i64, last_used: Option>, } #[derive(Debug, Serialize)] pub struct TokenUsageStats { pub total_tokens: i64, pub total_input: i64, pub total_output: i64, pub total_requests: i64, pub total_cost_usd: f64, pub top_user_tokens: i64, pub avg_tokens_per_user: i64, } #[derive(Debug, Serialize)] pub struct TokenUsageResponse { pub usage: Vec, pub stats: TokenUsageStats, } // ==================== Panel de Uso Global de IA (Solo Root) ==================== /// GET /api/admin/ai-usage/global - Panel de uso global de IA para usuarios root pub async fn get_ai_usage_global( _claims: Claims, State(pool): State, Query(filters): Query, ) -> Result, (StatusCode, String)> { // Obtener el uso diario para gráficos (todas las organizaciones) let daily_usage: Vec = sqlx::query_as( r#" SELECT DATE(au.created_at) as date, COALESCE(SUM(au.tokens_used), 0) as total_tokens, COALESCE(SUM(au.input_tokens), 0) as input_tokens, COALESCE(SUM(au.output_tokens), 0) as output_tokens, COALESCE(SUM(au.estimated_cost_usd), 0)::FLOAT8 as cost_usd, COUNT(au.id) as requests FROM ai_usage_logs au WHERE ($1::DATE IS NULL OR au.created_at >= $1::DATE) AND ($2::DATE IS NULL OR au.created_at <= $2::DATE + INTERVAL '1 day') GROUP BY DATE(au.created_at) ORDER BY date ASC LIMIT 90 "# ) .bind(filters.start_date.clone()) .bind(filters.end_date.clone()) .fetch_all(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener el uso diario: {}", e)))?; // Obtener uso por punto de conexión/función let by_endpoint: Vec = sqlx::query_as( r#" SELECT au.endpoint, au.request_type, COALESCE(SUM(au.tokens_used), 0) as total_tokens, COALESCE(SUM(au.input_tokens), 0) as input_tokens, COALESCE(SUM(au.output_tokens), 0) as output_tokens, COALESCE(SUM(au.estimated_cost_usd), 0)::FLOAT8 as cost_usd, COUNT(au.id) as requests FROM ai_usage_logs au WHERE ($1::DATE IS NULL OR au.created_at >= $1::DATE) AND ($2::DATE IS NULL OR au.created_at <= $2::DATE + INTERVAL '1 day') GROUP BY au.endpoint, au.request_type ORDER BY total_tokens DESC "# ) .bind(filters.start_date.clone()) .bind(filters.end_date.clone()) .fetch_all(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener el uso del punto de conexión: {}", e)))?; // Obtener uso por organización let by_organization: Vec = sqlx::query_as( r#" SELECT o.id as org_id, o.name as org_name, COALESCE(SUM(au.tokens_used), 0) as total_tokens, COALESCE(SUM(au.input_tokens), 0) as input_tokens, COALESCE(SUM(au.output_tokens), 0) as output_tokens, COALESCE(SUM(au.estimated_cost_usd), 0)::FLOAT8 as cost_usd, COUNT(au.id) as requests, COUNT(DISTINCT au.user_id) as active_users FROM ai_usage_logs au JOIN organizations o ON au.organization_id = o.id WHERE ($1::DATE IS NULL OR DATE(au.created_at) >= $1::DATE) AND ($2::DATE IS NULL OR DATE(au.created_at) <= $2::DATE) GROUP BY o.id, o.name ORDER BY total_tokens DESC "# ) .bind(filters.start_date.clone()) .bind(filters.end_date.clone()) .fetch_all(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener el uso de la organización: {}", e)))?; // Obtener usuarios principales en todas las organizaciones let top_users: Vec = sqlx::query_as( r#" SELECT u.id as user_id, u.email, u.full_name, u.role, o.name as org_name, COALESCE(SUM(au.tokens_used), 0) as total_tokens, COALESCE(SUM(au.input_tokens), 0) as input_tokens, COALESCE(SUM(au.output_tokens), 0) as output_tokens, COALESCE(SUM(au.estimated_cost_usd), 0)::FLOAT8 as cost_usd, COUNT(au.id) as requests FROM ai_usage_logs au JOIN users u ON au.user_id = u.id JOIN organizations o ON au.organization_id = o.id WHERE ($1::DATE IS NULL OR DATE(au.created_at) >= $1::DATE) AND ($2::DATE IS NULL OR DATE(au.created_at) <= $2::DATE) GROUP BY u.id, u.email, u.full_name, u.role, o.name ORDER BY total_tokens DESC LIMIT 20 "# ) .bind(filters.start_date.clone()) .bind(filters.end_date.clone()) .fetch_all(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener los usuarios principales: {}", e)))?; // Obtener uso por tipo de solicitud (para gráfico circular) let by_request_type: Vec = sqlx::query_as( r#" SELECT au.request_type, COALESCE(SUM(au.tokens_used), 0) as total_tokens, COALESCE(SUM(au.estimated_cost_usd), 0)::FLOAT8 as cost_usd, COUNT(au.id) as requests FROM ai_usage_logs au WHERE ($1::DATE IS NULL OR DATE(au.created_at) >= $1::DATE) AND ($2::DATE IS NULL OR DATE(au.created_at) <= $2::DATE) GROUP BY au.request_type ORDER BY total_tokens DESC "# ) .bind(filters.start_date.clone()) .bind(filters.end_date.clone()) .fetch_all(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener el uso del tipo de solicitud: {}", e)))?; // Calcular estadísticas de resumen let total_tokens: i64 = daily_usage.iter().map(|d| d.total_tokens).sum(); let total_input: i64 = daily_usage.iter().map(|d| d.input_tokens).sum(); let total_output: i64 = daily_usage.iter().map(|d| d.output_tokens).sum(); let total_cost: f64 = daily_usage.iter().map(|d| d.cost_usd).sum(); let total_requests: i64 = daily_usage.iter().map(|d| d.requests).sum(); // Calcular estimación de ahorros (frente a precios de OpenAI GPT-4) // GPT-4: ~$0.03/1K input, ~$0.06/1K output // Nuestra IA local: ~$0.001/1K entrada, ~$0.003/1K salida let openai_equivalent_cost = (total_input as f64 * 0.00003) + (total_output as f64 * 0.00006); let savings_vs_openai = openai_equivalent_cost - total_cost; let savings_percentage = if openai_equivalent_cost > 0.0 { (savings_vs_openai / openai_equivalent_cost) * 100.0 } else { 0.0 }; // Obtener el conteo total de usuarios activos let total_active_users: i64 = sqlx::query_scalar( r#" SELECT COUNT(DISTINCT au.user_id) FROM ai_usage_logs au WHERE ($1::DATE IS NULL OR DATE(au.created_at) >= $1::DATE) AND ($2::DATE IS NULL OR DATE(au.created_at) <= $2::DATE) "# ) .bind(filters.start_date.clone()) .bind(filters.end_date.clone()) .fetch_one(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al contar los usuarios: {}", e)))?; // Calcular uso específico por estudiante (interacciones de chat) let student_chat_usage: Vec = sqlx::query_as( r#" SELECT u.id as user_id, u.email, u.full_name, o.name as org_name, COALESCE(SUM(au.tokens_used), 0) as total_tokens, COALESCE(SUM(au.estimated_cost_usd), 0)::FLOAT8 as cost_usd, COUNT(au.id) as chat_requests, MAX(au.created_at) as last_chat FROM ai_usage_logs au JOIN users u ON au.user_id = u.id JOIN organizations o ON au.organization_id = o.id WHERE au.request_type = 'chat' AND u.role = 'student' AND ($1::DATE IS NULL OR DATE(au.created_at) >= $1::DATE) AND ($2::DATE IS NULL OR DATE(au.created_at) <= $2::DATE) GROUP BY u.id, u.email, u.full_name, o.name ORDER BY total_tokens DESC LIMIT 50 "# ) .bind(filters.start_date.clone()) .bind(filters.end_date.clone()) .fetch_all(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener el uso de chat de estudiantes: {}", e)))?; // Calcular totales de chat de estudiantes let total_student_chat_tokens: i64 = student_chat_usage.iter().map(|s| s.total_tokens).sum(); let total_student_chat_cost: f64 = student_chat_usage.iter().map(|s| s.cost_usd).sum(); let total_student_chat_requests: i64 = student_chat_usage.iter().map(|s| s.chat_requests).sum(); Ok(Json(GlobalAiUsageResponse { summary: GlobalAiSummary { total_tokens, total_input, total_output, total_requests, total_cost_usd: total_cost, savings_vs_openai_usd: savings_vs_openai, savings_percentage, openai_equivalent_cost_usd: openai_equivalent_cost, total_organizations: by_organization.len() as i64, total_active_users, }, student_chat_summary: Some(StudentChatSummary { total_tokens: total_student_chat_tokens, total_requests: total_student_chat_requests, total_cost_usd: total_student_chat_cost, active_students: student_chat_usage.len() as i64, }), daily_usage, by_endpoint, by_organization, by_request_type, top_users, student_chat_usage, })) } #[derive(Debug, Serialize, sqlx::FromRow)] pub struct UsageByOrganization { pub org_id: uuid::Uuid, pub org_name: String, pub total_tokens: i64, pub input_tokens: i64, pub output_tokens: i64, pub cost_usd: f64, pub requests: i64, pub active_users: i64, } #[derive(Debug, Serialize, sqlx::FromRow)] pub struct UsageByRequestType { pub request_type: String, pub total_tokens: i64, pub cost_usd: f64, pub requests: i64, } #[derive(Debug, Serialize, sqlx::FromRow)] pub struct StudentChatUsage { pub user_id: uuid::Uuid, pub email: String, pub full_name: String, pub org_name: String, pub total_tokens: i64, pub cost_usd: f64, pub chat_requests: i64, pub last_chat: chrono::DateTime, } #[derive(Debug, Serialize)] pub struct StudentChatSummary { pub total_tokens: i64, pub total_requests: i64, pub total_cost_usd: f64, pub active_students: i64, } #[derive(Debug, Serialize)] pub struct GlobalAiSummary { pub total_tokens: i64, pub total_input: i64, pub total_output: i64, pub total_requests: i64, pub total_cost_usd: f64, pub savings_vs_openai_usd: f64, pub savings_percentage: f64, pub openai_equivalent_cost_usd: f64, pub total_organizations: i64, pub total_active_users: i64, } #[derive(Debug, Serialize)] pub struct GlobalAiUsageResponse { pub summary: GlobalAiSummary, pub student_chat_summary: Option, pub daily_usage: Vec, pub by_endpoint: Vec, pub by_organization: Vec, pub by_request_type: Vec, pub top_users: Vec, pub student_chat_usage: Vec, } // ==================== Límites de Tokens de Usuario ==================== #[derive(Debug, Deserialize)] pub struct SetTokenLimitPayload { pub monthly_token_limit: i32, pub token_limit_reset_day: Option, } /// PUT /admin/users/{user_id}/token-limit - Establecer límite mensual de tokens para el usuario pub async fn set_user_token_limit( _org: Org, _claims: Claims, State(pool): State, Path(user_id): Path, Json(payload): Json, ) -> Result { // Validar día de reinicio (1-28 para evitar problemas a fin de mes) let reset_day = payload.token_limit_reset_day.unwrap_or(1).clamp(1, 28); sqlx::query( r#" UPDATE users SET monthly_token_limit = $1, token_limit_reset_day = $2, updated_at = NOW() WHERE id = $3 "# ) .bind(payload.monthly_token_limit) .bind(reset_day) .bind(user_id) .execute(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al establecer el límite: {}", e)))?; Ok(StatusCode::OK) } /// GET /admin/users/{user_id}/token-usage - Obtener uso de tokens para un usuario específico pub async fn get_user_token_usage( _org: Org, _claims: Claims, State(pool): State, Path(user_id): Path, ) -> Result, (StatusCode, String)> { // Obtener uso del mes actual let usage: UserTokenUsageDetail = sqlx::query_as( r#" SELECT u.id as user_id, u.email, u.full_name, u.monthly_token_limit, u.token_limit_reset_day, COALESCE(SUM(au.tokens_used), 0) as used_tokens, COUNT(au.id) as total_requests, COALESCE(SUM(au.estimated_cost_usd), 0) as total_cost_usd, MAX(au.created_at) as last_used FROM users u LEFT JOIN ai_usage_logs au ON u.id = au.user_id AND au.created_at >= DATE_TRUNC('month', NOW()) WHERE u.id = $1 GROUP BY u.id, u.email, u.full_name, u.monthly_token_limit, u.token_limit_reset_day "# ) .bind(user_id) .fetch_one(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener el uso: {}", e)))?; Ok(Json(usage)) } /// GET /admin/users/{user_id}/token-limit/check - Comprobar si el usuario tiene tokens disponibles pub async fn check_user_token_limit( _org: Org, _claims: Claims, State(pool): State, Path(user_id): Path, ) -> Result, (StatusCode, String)> { let result: TokenLimitCheckResponse = sqlx::query_as( "SELECT * FROM check_token_limit($1, 0)" ) .bind(user_id) .fetch_one(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to check limit: {}", e)))?; Ok(Json(result)) } #[derive(Debug, Serialize, sqlx::FromRow)] pub struct UserTokenUsageDetail { pub user_id: Uuid, pub email: String, pub full_name: String, pub monthly_token_limit: i32, pub token_limit_reset_day: i32, pub used_tokens: i64, pub total_requests: i64, pub total_cost_usd: f64, pub last_used: Option>, } #[derive(Debug, Serialize, sqlx::FromRow)] pub struct TokenLimitCheckResponse { pub has_available_tokens: bool, pub monthly_limit: i32, pub used_tokens: i64, pub remaining_tokens: i64, pub reset_date: DateTime, }