""" Subscriptions API Sag-based subscriptions listing and stats """ from fastapi import APIRouter, HTTPException, Query from typing import List, Dict, Any from app.core.database import execute_query, execute_query_single, get_db_connection, release_db_connection from psycopg2.extras import RealDictCursor import logging logger = logging.getLogger(__name__) router = APIRouter() ALLOWED_STATUSES = {"draft", "active", "paused", "cancelled"} @router.get("/subscriptions/by-sag/{sag_id}", response_model=Dict[str, Any]) async def get_subscription_by_sag(sag_id: int): """Get latest subscription for a case.""" try: query = """ SELECT s.id, s.subscription_number, s.sag_id, sg.titel AS sag_title, s.customer_id, c.name AS customer_name, s.product_name, s.billing_interval, s.billing_day, s.price, s.start_date, s.end_date, s.status, s.notes FROM sag_subscriptions s LEFT JOIN sag_sager sg ON sg.id = s.sag_id LEFT JOIN customers c ON c.id = s.customer_id WHERE s.sag_id = %s ORDER BY s.id DESC LIMIT 1 """ subscription = execute_query_single(query, (sag_id,)) if not subscription: raise HTTPException(status_code=404, detail="Subscription not found") items = execute_query( """ SELECT i.id, i.line_no, i.product_id, p.name AS product_name, i.description, i.quantity, i.unit_price, i.line_total FROM sag_subscription_items i LEFT JOIN products p ON p.id = i.product_id WHERE i.subscription_id = %s ORDER BY i.line_no ASC, i.id ASC """, (subscription["id"],) ) subscription["line_items"] = items or [] return subscription except HTTPException: raise except Exception as e: logger.error(f"❌ Error loading subscription by case: {e}", exc_info=True) raise HTTPException(status_code=500, detail=str(e)) @router.post("/subscriptions", response_model=Dict[str, Any]) async def create_subscription(payload: Dict[str, Any]): """Create a new subscription tied to a case (status = draft).""" try: sag_id = payload.get("sag_id") billing_interval = payload.get("billing_interval") billing_day = payload.get("billing_day") start_date = payload.get("start_date") notes = payload.get("notes") line_items = payload.get("line_items") or [] if not sag_id: raise HTTPException(status_code=400, detail="sag_id is required") if not billing_interval: raise HTTPException(status_code=400, detail="billing_interval is required") if billing_day is None: raise HTTPException(status_code=400, detail="billing_day is required") if not start_date: raise HTTPException(status_code=400, detail="start_date is required") if not line_items: raise HTTPException(status_code=400, detail="line_items is required") sag = execute_query_single( "SELECT id, customer_id FROM sag_sager WHERE id = %s", (sag_id,) ) if not sag or not sag.get("customer_id"): raise HTTPException(status_code=400, detail="Case must have a customer") existing = execute_query_single( """ SELECT id FROM sag_subscriptions WHERE sag_id = %s AND status != 'cancelled' ORDER BY id DESC LIMIT 1 """, (sag_id,) ) if existing: raise HTTPException(status_code=400, detail="Subscription already exists for this case") product_ids = [item.get("product_id") for item in line_items if item.get("product_id")] product_map = {} if product_ids: rows = execute_query( "SELECT id, name, sales_price FROM products WHERE id = ANY(%s)", (product_ids,) ) product_map = {row["id"]: row for row in (rows or [])} cleaned_items = [] total_price = 0 for idx, item in enumerate(line_items, start=1): product_id = item.get("product_id") description = (item.get("description") or "").strip() quantity = item.get("quantity") unit_price = item.get("unit_price") product = product_map.get(product_id) if not description and product: description = product.get("name") or "" if unit_price is None and product and product.get("sales_price") is not None: unit_price = product.get("sales_price") if not description: raise HTTPException(status_code=400, detail="line_items description is required") if quantity is None or float(quantity) <= 0: raise HTTPException(status_code=400, detail="line_items quantity must be > 0") if unit_price is None or float(unit_price) < 0: raise HTTPException(status_code=400, detail="line_items unit_price must be >= 0") line_total = float(quantity) * float(unit_price) total_price += line_total cleaned_items.append({ "line_no": idx, "product_id": product_id, "description": description, "quantity": quantity, "unit_price": unit_price, "line_total": line_total, }) product_name = cleaned_items[0]["description"] if len(cleaned_items) > 1: product_name = f"{product_name} (+{len(cleaned_items) - 1})" conn = get_db_connection() try: with conn.cursor(cursor_factory=RealDictCursor) as cursor: cursor.execute( """ INSERT INTO sag_subscriptions ( sag_id, customer_id, product_name, billing_interval, billing_day, price, start_date, status, notes ) VALUES (%s, %s, %s, %s, %s, %s, %s, 'draft', %s) RETURNING * """, ( sag_id, sag["customer_id"], product_name, billing_interval, billing_day, total_price, start_date, notes, ) ) subscription = cursor.fetchone() for item in cleaned_items: cursor.execute( """ INSERT INTO sag_subscription_items ( subscription_id, line_no, product_id, description, quantity, unit_price, line_total ) VALUES (%s, %s, %s, %s, %s, %s, %s) """, ( subscription["id"], item["line_no"], item["product_id"], item["description"], item["quantity"], item["unit_price"], item["line_total"], ) ) conn.commit() subscription["line_items"] = cleaned_items return subscription finally: release_db_connection(conn) except HTTPException: raise except Exception as e: logger.error(f"❌ Error creating subscription: {e}", exc_info=True) raise HTTPException(status_code=500, detail=str(e)) @router.patch("/subscriptions/{subscription_id}/status", response_model=Dict[str, Any]) async def update_subscription_status(subscription_id: int, payload: Dict[str, Any]): """Update subscription status.""" try: status = payload.get("status") if status not in ALLOWED_STATUSES: raise HTTPException(status_code=400, detail="Invalid status") query = """ UPDATE sag_subscriptions SET status = %s, updated_at = CURRENT_TIMESTAMP WHERE id = %s RETURNING * """ result = execute_query(query, (status, subscription_id)) if not result: raise HTTPException(status_code=404, detail="Subscription not found") return result[0] except HTTPException: raise except Exception as e: logger.error(f"❌ Error updating subscription status: {e}", exc_info=True) raise HTTPException(status_code=500, detail=str(e)) @router.get("/subscriptions", response_model=List[Dict[str, Any]]) async def list_subscriptions(status: str = Query("all")): """List subscriptions by status (default: all).""" try: where_clause = "" params: List[Any] = [] if status and status != "all": where_clause = "WHERE s.status = %s" params.append(status) query = f""" SELECT s.id, s.subscription_number, s.sag_id, sg.titel AS sag_title, s.customer_id, c.name AS customer_name, s.product_name, s.billing_interval, s.billing_day, s.price, s.start_date, s.end_date, s.status FROM sag_subscriptions s LEFT JOIN sag_sager sg ON sg.id = s.sag_id LEFT JOIN customers c ON c.id = s.customer_id {where_clause} ORDER BY s.start_date DESC, s.id DESC """ return execute_query(query, tuple(params)) or [] except Exception as e: logger.error(f"❌ Error listing subscriptions: {e}", exc_info=True) raise HTTPException(status_code=500, detail=str(e)) @router.get("/subscriptions/stats/summary", response_model=Dict[str, Any]) async def subscription_stats(status: str = Query("all")): """Summary stats for subscriptions by status (default: all).""" try: where_clause = "" params: List[Any] = [] if status and status != "all": where_clause = "WHERE status = %s" params.append(status) query = f""" SELECT COUNT(*) AS subscription_count, COALESCE(SUM(price), 0) AS total_amount, COALESCE(AVG(price), 0) AS avg_amount FROM sag_subscriptions {where_clause} """ result = execute_query(query, tuple(params)) return result[0] if result else { "subscription_count": 0, "total_amount": 0, "avg_amount": 0 } except Exception as e: logger.error(f"❌ Error loading subscription stats: {e}", exc_info=True) raise HTTPException(status_code=500, detail=str(e))