bmc_hub/app/prepaid/backend/router.py

264 lines
9.0 KiB
Python
Raw Normal View History

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))