Spaces:
Sleeping
Sleeping
| """ | |
| Credits Router - API endpoints for credit management. | |
| Provides endpoints for checking credit balance and viewing credit history. | |
| """ | |
| from fastapi import APIRouter, Depends, Query, Request | |
| from pydantic import BaseModel | |
| from typing import List, Optional | |
| from datetime import datetime | |
| from sqlalchemy.ext.asyncio import AsyncSession | |
| from sqlalchemy import select, desc | |
| from core.database import get_db | |
| from core.models import User, GeminiJob | |
| router = APIRouter(prefix="/credits", tags=["credits"]) | |
| # Response Models | |
| class CreditBalanceResponse(BaseModel): | |
| """Response for credit balance endpoint.""" | |
| user_id: str | |
| credits: int | |
| last_used_at: Optional[str] = None | |
| class CreditHistoryItem(BaseModel): | |
| """Single item in credit history.""" | |
| job_id: str | |
| job_type: str | |
| status: str | |
| credits_reserved: int | |
| credits_refunded: bool | |
| error_message: Optional[str] = None | |
| created_at: str | |
| completed_at: Optional[str] = None | |
| class CreditHistoryResponse(BaseModel): | |
| """Response for credit history endpoint.""" | |
| user_id: str | |
| current_balance: int | |
| history: List[CreditHistoryItem] | |
| total_count: int | |
| page: int | |
| limit: int | |
| async def get_credits( | |
| request: Request | |
| ): | |
| """ | |
| Get current credit balance. | |
| Returns the user's current credit balance and last usage time. | |
| Auth is handled by AuthMiddleware - user is in request.state.user | |
| """ | |
| user = request.state.user | |
| return CreditBalanceResponse( | |
| user_id=user.user_id, | |
| credits=user.credits, | |
| last_used_at=user.last_used_at.isoformat() if user.last_used_at else None | |
| ) | |
| async def get_credit_history( | |
| request: Request, | |
| db: AsyncSession = Depends(get_db), | |
| page: int = Query(1, ge=1, description="Page number"), | |
| limit: int = Query(20, ge=1, le=100, description="Items per page") | |
| ): | |
| """ | |
| Get credit usage history. | |
| Returns a paginated list of jobs with credit transactions, | |
| showing which jobs used credits and which were refunded. | |
| Only includes jobs where credits were reserved (credits_reserved > 0). | |
| Auth is handled by AuthMiddleware - user is in request.state.user | |
| """ | |
| user = request.state.user | |
| offset = (page - 1) * limit | |
| # Query jobs with credit transactions | |
| query = select(GeminiJob).where( | |
| GeminiJob.user_id == user.user_id, | |
| GeminiJob.credits_reserved > 0 # Only jobs that had credits reserved | |
| ).order_by(desc(GeminiJob.created_at)).offset(offset).limit(limit) | |
| result = await db.execute(query) | |
| jobs = result.scalars().all() | |
| # Get total count | |
| count_query = select(GeminiJob).where( | |
| GeminiJob.user_id == user.user_id, | |
| GeminiJob.credits_reserved > 0 | |
| ) | |
| count_result = await db.execute(count_query) | |
| total_count = len(count_result.scalars().all()) | |
| # Build history items | |
| history = [] | |
| for job in jobs: | |
| history.append(CreditHistoryItem( | |
| job_id=job.job_id, | |
| job_type=job.job_type, | |
| status=job.status, | |
| credits_reserved=job.credits_reserved, | |
| credits_refunded=job.credits_refunded or False, | |
| error_message=job.error_message, | |
| created_at=job.created_at.isoformat() if job.created_at else None, | |
| completed_at=job.completed_at.isoformat() if job.completed_at else None | |
| )) | |
| return CreditHistoryResponse( | |
| user_id=user.user_id, | |
| current_balance=user.credits, | |
| history=history, | |
| total_count=total_count, | |
| page=page, | |
| limit=limit | |
| ) | |
| # ============================================================================= | |
| # New Transaction History Endpoints | |
| # ============================================================================= | |
| class CreditTransactionItem(BaseModel): | |
| """Single credit transaction record.""" | |
| transaction_id: str | |
| transaction_type: str # reserve, refund, confirm, purchase | |
| amount: int | |
| balance_before: int | |
| balance_after: int | |
| source: str | |
| reference_type: Optional[str] = None | |
| reference_id: Optional[str] = None | |
| request_path: Optional[str] = None | |
| request_method: Optional[str] = None | |
| response_status: Optional[int] = None | |
| reason: Optional[str] = None | |
| created_at: str | |
| class TransactionHistoryResponse(BaseModel): | |
| """Response for transaction history endpoint.""" | |
| user_id: str | |
| current_balance: int | |
| transactions: List[CreditTransactionItem] | |
| total_count: int | |
| page: int | |
| limit: int | |
| async def get_transaction_history( | |
| request: Request, | |
| db: AsyncSession = Depends(get_db), | |
| transaction_type: Optional[str] = Query(None, description="Filter by type (reserve, refund, confirm, purchase)"), | |
| page: int = Query(1, ge=1, description="Page number"), | |
| limit: int = Query(50, ge=1, le=100, description="Items per page") | |
| ): | |
| """ | |
| Get detailed credit transaction history. | |
| Returns complete audit trail of all credit operations including: | |
| - Reservations (when credits were deducted for API calls) | |
| - Refunds (when credits were returned due to failures) | |
| - Confirmations (when credits were confirmed as used) | |
| - Purchases (when credits were added via payments) | |
| Auth is handled by AuthMiddleware - user is in request.state.user | |
| """ | |
| from core.models import CreditTransaction | |
| from sqlalchemy import func | |
| user = request.state.user | |
| offset = (page - 1) * limit | |
| # Build query | |
| query = select(CreditTransaction).where( | |
| CreditTransaction.user_id == user.id | |
| ) | |
| if transaction_type: | |
| query = query.where(CreditTransaction.transaction_type == transaction_type) | |
| query = query.order_by(desc(CreditTransaction.created_at)).offset(offset).limit(limit) | |
| # Execute query | |
| result = await db.execute(query) | |
| transactions = result.scalars().all() | |
| # Get total count | |
| count_query = select(func.count(CreditTransaction.id)).where( | |
| CreditTransaction.user_id == user.id | |
| ) | |
| if transaction_type: | |
| count_query = count_query.where(CreditTransaction.transaction_type == transaction_type) | |
| count_result = await db.execute(count_query) | |
| total_count = count_result.scalar() or 0 | |
| # Build response | |
| transaction_items = [] | |
| for tx in transactions: | |
| transaction_items.append(CreditTransactionItem( | |
| transaction_id=tx.transaction_id, | |
| transaction_type=tx.transaction_type, | |
| amount=tx.amount, | |
| balance_before=tx.balance_before, | |
| balance_after=tx.balance_after, | |
| source=tx.source, | |
| reference_type=tx.reference_type, | |
| reference_id=tx.reference_id, | |
| request_path=tx.request_path, | |
| request_method=tx.request_method, | |
| response_status=tx.response_status, | |
| reason=tx.reason, | |
| created_at=tx.created_at.isoformat() if tx.created_at else None | |
| )) | |
| return TransactionHistoryResponse( | |
| user_id=user.user_id, | |
| current_balance=user.credits, | |
| transactions=transaction_items, | |
| total_count=total_count, | |
| page=page, | |
| limit=limit | |
| ) | |
| class BalanceVerificationResponse(BaseModel): | |
| """Response for balance verification endpoint.""" | |
| user_id: str | |
| current_balance: int | |
| calculated_balance: int | |
| is_valid: bool | |
| discrepancy: int | |
| last_transaction_at: Optional[str] = None | |
| async def verify_balance( | |
| request: Request, | |
| db: AsyncSession = Depends(get_db) | |
| ): | |
| """ | |
| Verify user balance against transaction history. | |
| Calculates balance from all transactions and compares with stored balance. | |
| Useful for debugging credit discrepancies. | |
| Auth is handled by AuthMiddleware - user is in request.state.user | |
| """ | |
| from core.models import CreditTransaction | |
| from sqlalchemy import func | |
| user = request.state.user | |
| # Calculate balance from transactions | |
| result = await db.execute( | |
| select(func.sum(CreditTransaction.amount)).where( | |
| CreditTransaction.user_id == user.id | |
| ) | |
| ) | |
| calculated_balance = result.scalar() or 0 | |
| # Get last transaction time | |
| last_tx_result = await db.execute( | |
| select(CreditTransaction).where( | |
| CreditTransaction.user_id == user.id | |
| ).order_by(desc(CreditTransaction.created_at)).limit(1) | |
| ) | |
| last_tx = last_tx_result.scalar_one_or_none() | |
| is_valid = (calculated_balance == user.credits) | |
| discrepancy = user.credits - calculated_balance | |
| return BalanceVerificationResponse( | |
| user_id=user.user_id, | |
| current_balance=user.credits, | |
| calculated_balance=calculated_balance, | |
| is_valid=is_valid, | |
| discrepancy=discrepancy, | |
| last_transaction_at=last_tx.created_at.isoformat() if last_tx and last_tx.created_at else None | |
| ) | |