docs: Create vTiger & Simply-CRM integration setup guide with credential requirements feat: Implement ticket system enhancements including relations, calendar events, templates, and AI suggestions refactor: Update ticket system migration to include audit logging and enhanced email metadata
274 lines
9.3 KiB
Python
274 lines
9.3 KiB
Python
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
|
|
"""
|
|
try:
|
|
# Calculate total amount
|
|
total_amount = card.purchased_hours * card.price_per_hour
|
|
|
|
# Check if customer already has active card
|
|
existing = execute_query("""
|
|
SELECT id FROM tticket_prepaid_cards
|
|
WHERE customer_id = %s AND status = 'active'
|
|
""", (card.customer_id,))
|
|
|
|
if existing and len(existing) > 0:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail="Customer already has an active prepaid card"
|
|
)
|
|
|
|
# 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))
|