from fastapi import APIRouter, HTTPException from app.core.database import execute_query from typing import List, Optional, Dict, Any from pydantic import BaseModel from datetime import datetime, date import logging logger = logging.getLogger(__name__) router = APIRouter() # Pydantic Models class PrepaidCard(BaseModel): id: Optional[int] = None card_number: str customer_id: int purchased_hours: float used_hours: float remaining_hours: float price_per_hour: float total_amount: float status: str purchased_at: Optional[datetime] = None expires_at: Optional[datetime] = None economic_invoice_number: Optional[str] = None economic_product_number: Optional[str] = None notes: Optional[str] = None created_at: Optional[datetime] = None updated_at: Optional[datetime] = None class PrepaidCardCreate(BaseModel): customer_id: int purchased_hours: float price_per_hour: float expires_at: Optional[date] = None notes: Optional[str] = None @router.get("/prepaid-cards", response_model=List[Dict[str, Any]]) async def get_prepaid_cards(status: Optional[str] = None, customer_id: Optional[int] = None): """ Get all prepaid cards with customer information """ try: query = """ SELECT pc.*, c.name as customer_name, c.email as customer_email, (SELECT COUNT(*) FROM tticket_prepaid_transactions WHERE card_id = pc.id) as transaction_count FROM tticket_prepaid_cards pc LEFT JOIN customers c ON pc.customer_id = c.id WHERE 1=1 """ params = [] if status: query += " AND pc.status = %s" params.append(status) if customer_id: query += " AND pc.customer_id = %s" params.append(customer_id) query += " ORDER BY pc.created_at DESC" cards = execute_query(query, tuple(params) if params else None) logger.info(f"✅ Retrieved {len(cards) if cards else 0} prepaid cards") return cards or [] except Exception as e: logger.error(f"❌ Error fetching prepaid cards: {e}", exc_info=True) raise HTTPException(status_code=500, detail=str(e)) @router.get("/prepaid-cards/{card_id}", response_model=Dict[str, Any]) async def get_prepaid_card(card_id: int): """ Get a specific prepaid card with transactions """ try: result = execute_query(""" SELECT pc.*, c.name as customer_name, c.email as customer_email FROM tticket_prepaid_cards pc LEFT JOIN customers c ON pc.customer_id = c.id WHERE pc.id = %s """, (card_id,)) if not result or len(result) == 0: raise HTTPException(status_code=404, detail="Prepaid card not found") card = result[0] # Get transactions transactions = execute_query(""" SELECT pt.*, w.ticket_id, t.subject as ticket_title FROM tticket_prepaid_transactions pt LEFT JOIN tticket_worklog w ON pt.worklog_id = w.id LEFT JOIN tticket_tickets t ON w.ticket_id = t.id WHERE pt.card_id = %s ORDER BY pt.created_at DESC """, (card_id,)) card['transactions'] = transactions or [] return card except HTTPException: raise except Exception as e: logger.error(f"❌ Error fetching prepaid card {card_id}: {e}", exc_info=True) raise HTTPException(status_code=500, detail=str(e)) @router.post("/prepaid-cards", response_model=Dict[str, Any]) async def create_prepaid_card(card: PrepaidCardCreate): """ Create a new prepaid card Note: As of migration 065, customers can have multiple active cards simultaneously. """ try: # Calculate total amount total_amount = card.purchased_hours * card.price_per_hour # Create card (need to use fetch=False for INSERT RETURNING) conn = None try: from app.core.database import get_db_connection, release_db_connection from psycopg2.extras import RealDictCursor conn = get_db_connection() with conn.cursor(cursor_factory=RealDictCursor) as cursor: cursor.execute(""" INSERT INTO tticket_prepaid_cards (customer_id, purchased_hours, price_per_hour, total_amount, expires_at, notes) VALUES (%s, %s, %s, %s, %s, %s) RETURNING * """, ( card.customer_id, card.purchased_hours, card.price_per_hour, total_amount, card.expires_at, card.notes )) conn.commit() result = cursor.fetchone() logger.info(f"✅ Created prepaid card: {result['card_number']}") return result finally: if conn: release_db_connection(conn) except HTTPException: raise except Exception as e: logger.error(f"❌ Error creating prepaid card: {e}", exc_info=True) raise HTTPException(status_code=500, detail=str(e)) @router.put("/prepaid-cards/{card_id}/status") async def update_card_status(card_id: int, status: str): """ Update prepaid card status (cancel, reactivate) """ try: if status not in ['active', 'cancelled']: raise HTTPException(status_code=400, detail="Invalid status") conn = None try: from app.core.database import get_db_connection, release_db_connection from psycopg2.extras import RealDictCursor conn = get_db_connection() with conn.cursor(cursor_factory=RealDictCursor) as cursor: cursor.execute(""" UPDATE tticket_prepaid_cards SET status = %s WHERE id = %s RETURNING * """, (status, card_id)) conn.commit() result = cursor.fetchone() if not result: raise HTTPException(status_code=404, detail="Card not found") logger.info(f"✅ Updated card {card_id} status to {status}") return result finally: if conn: release_db_connection(conn) except HTTPException: raise except Exception as e: logger.error(f"❌ Error updating card status: {e}", exc_info=True) raise HTTPException(status_code=500, detail=str(e)) @router.delete("/prepaid-cards/{card_id}") async def delete_prepaid_card(card_id: int): """ Delete a prepaid card (only if no transactions) """ try: # Check for transactions transactions = execute_query(""" SELECT COUNT(*) as count FROM tticket_prepaid_transactions WHERE card_id = %s """, (card_id,)) if transactions and len(transactions) > 0 and transactions[0]['count'] > 0: raise HTTPException( status_code=400, detail="Cannot delete card with transactions" ) execute_query("DELETE FROM tticket_prepaid_cards WHERE id = %s", (card_id,), fetch=False) logger.info(f"✅ Deleted prepaid card {card_id}") return {"message": "Card deleted successfully"} except HTTPException: raise except Exception as e: logger.error(f"❌ Error deleting card: {e}", exc_info=True) raise HTTPException(status_code=500, detail=str(e)) @router.get("/prepaid-cards/stats/summary", response_model=Dict[str, Any]) async def get_prepaid_stats(): """ Get prepaid cards statistics """ try: result = execute_query(""" SELECT COUNT(*) FILTER (WHERE status = 'active') as active_count, COUNT(*) FILTER (WHERE status = 'depleted') as depleted_count, COUNT(*) FILTER (WHERE status = 'expired') as expired_count, COUNT(*) FILTER (WHERE status = 'cancelled') as cancelled_count, COALESCE(SUM(remaining_hours) FILTER (WHERE status = 'active'), 0) as total_remaining_hours, COALESCE(SUM(used_hours), 0) as total_used_hours, COALESCE(SUM(purchased_hours), 0) as total_purchased_hours, COALESCE(SUM(total_amount), 0) as total_revenue FROM tticket_prepaid_cards """) return result[0] if result and len(result) > 0 else {} except Exception as e: logger.error(f"❌ Error fetching prepaid stats: {e}", exc_info=True) raise HTTPException(status_code=500, detail=str(e))