""" Coupon Management Endpoints for Mitai Jinkendo Handles coupon CRUD (admin) and redemption (users). """ from datetime import datetime, timedelta from typing import Optional from fastapi import APIRouter, HTTPException, Depends from db import get_db, get_cursor, r2d from auth import require_auth, require_admin router = APIRouter(prefix="/api/coupons", tags=["coupons"]) @router.get("") def list_coupons(session: dict = Depends(require_admin)): """Admin: List all coupons with redemption stats.""" with get_db() as conn: cur = get_cursor(conn) cur.execute(""" SELECT c.*, t.name as tier_name, (SELECT COUNT(*) FROM coupon_redemptions WHERE coupon_id = c.id) as redemptions FROM coupons c LEFT JOIN tiers t ON t.id = c.tier_id ORDER BY c.created DESC """) return [r2d(r) for r in cur.fetchall()] @router.post("") def create_coupon(data: dict, session: dict = Depends(require_admin)): """ Admin: Create new coupon. Required fields: - code: Unique coupon code - type: 'single_use', 'period', or 'wellpass' - tier_id: Target tier - duration_days: For period/wellpass coupons Optional fields: - max_redemptions: NULL = unlimited - valid_from, valid_until: Validity period - description: Internal note """ code = data.get('code', '').strip().upper() coupon_type = data.get('type') tier_id = data.get('tier_id') duration_days = data.get('duration_days') max_redemptions = data.get('max_redemptions') valid_from = data.get('valid_from') valid_until = data.get('valid_until') description = data.get('description', '') if not code: raise HTTPException(400, "Coupon-Code fehlt") if coupon_type not in ['single_use', 'period', 'wellpass']: raise HTTPException(400, "Ungültiger Coupon-Typ") if not tier_id: raise HTTPException(400, "Tier fehlt") if coupon_type in ['period', 'wellpass'] and not duration_days: raise HTTPException(400, "duration_days fehlt für period/wellpass Coupons") with get_db() as conn: cur = get_cursor(conn) # Check if code already exists cur.execute("SELECT id FROM coupons WHERE code = %s", (code,)) if cur.fetchone(): raise HTTPException(400, f"Coupon-Code '{code}' existiert bereits") # Create coupon cur.execute(""" INSERT INTO coupons ( code, type, tier_id, duration_days, max_redemptions, valid_from, valid_until, description, created_by ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING id """, ( code, coupon_type, tier_id, duration_days, max_redemptions, valid_from, valid_until, description, session['profile_id'] )) coupon_id = cur.fetchone()['id'] conn.commit() return {"ok": True, "id": coupon_id, "code": code} @router.put("/{coupon_id}") def update_coupon(coupon_id: str, data: dict, session: dict = Depends(require_admin)): """Admin: Update coupon.""" with get_db() as conn: cur = get_cursor(conn) updates = [] values = [] if 'active' in data: updates.append('active = %s') values.append(data['active']) if 'max_redemptions' in data: updates.append('max_redemptions = %s') values.append(data['max_redemptions']) if 'valid_until' in data: updates.append('valid_until = %s') values.append(data['valid_until']) if 'description' in data: updates.append('description = %s') values.append(data['description']) if not updates: return {"ok": True} updates.append('updated = CURRENT_TIMESTAMP') values.append(coupon_id) cur.execute( f"UPDATE coupons SET {', '.join(updates)} WHERE id = %s", values ) conn.commit() return {"ok": True} @router.delete("/{coupon_id}") def delete_coupon(coupon_id: str, session: dict = Depends(require_admin)): """Admin: Delete coupon (soft-delete: set active=false).""" with get_db() as conn: cur = get_cursor(conn) cur.execute("UPDATE coupons SET active = false WHERE id = %s", (coupon_id,)) conn.commit() return {"ok": True} @router.get("/{coupon_id}/redemptions") def get_coupon_redemptions(coupon_id: str, session: dict = Depends(require_admin)): """Admin: Get all redemptions for a coupon.""" with get_db() as conn: cur = get_cursor(conn) cur.execute(""" SELECT cr.id, cr.redeemed_at, p.name as profile_name, p.email as profile_email, ag.valid_from, ag.valid_until, ag.is_active FROM coupon_redemptions cr JOIN profiles p ON p.id = cr.profile_id LEFT JOIN access_grants ag ON ag.id = cr.access_grant_id WHERE cr.coupon_id = %s ORDER BY cr.redeemed_at DESC """, (coupon_id,)) return [r2d(r) for r in cur.fetchall()] @router.post("/redeem") def redeem_coupon(data: dict, session: dict = Depends(require_auth)): """ User: Redeem a coupon code. Creates an access_grant and handles Wellpass pause/resume logic. """ code = data.get('code', '').strip().upper() if not code: raise HTTPException(400, "Coupon-Code fehlt") profile_id = session['profile_id'] with get_db() as conn: cur = get_cursor(conn) # Get coupon cur.execute(""" SELECT * FROM coupons WHERE code = %s AND active = true """, (code,)) coupon = cur.fetchone() if not coupon: raise HTTPException(404, "Ungültiger Coupon-Code") # Check validity period now = datetime.now() if coupon['valid_from'] and now < coupon['valid_from']: raise HTTPException(400, "Coupon noch nicht gültig") if coupon['valid_until'] and now > coupon['valid_until']: raise HTTPException(400, "Coupon abgelaufen") # Check max redemptions if coupon['max_redemptions'] is not None: if coupon['redemption_count'] >= coupon['max_redemptions']: raise HTTPException(400, "Coupon bereits vollständig eingelöst") # Check if user already redeemed this coupon cur.execute(""" SELECT id FROM coupon_redemptions WHERE coupon_id = %s AND profile_id = %s """, (coupon['id'], profile_id)) if cur.fetchone(): raise HTTPException(400, "Du hast diesen Coupon bereits eingelöst") # Create access grant valid_from = now valid_until = now + timedelta(days=coupon['duration_days']) if coupon['duration_days'] else None # Wellpass logic: Pause existing personal grants if coupon['type'] == 'wellpass': cur.execute(""" SELECT id, valid_until FROM access_grants WHERE profile_id = %s AND is_active = true AND granted_by != 'wellpass' AND valid_until > CURRENT_TIMESTAMP """, (profile_id,)) active_grants = cur.fetchall() for grant in active_grants: # Calculate remaining days remaining = (grant['valid_until'] - now).days # Pause grant cur.execute(""" UPDATE access_grants SET is_active = false, paused_at = CURRENT_TIMESTAMP, remaining_days = %s WHERE id = %s """, (remaining, grant['id'])) # Insert access grant cur.execute(""" INSERT INTO access_grants ( profile_id, tier_id, granted_by, coupon_id, valid_from, valid_until, is_active ) VALUES (%s, %s, %s, %s, %s, %s, true) RETURNING id """, ( profile_id, coupon['tier_id'], coupon['type'], coupon['id'], valid_from, valid_until )) grant_id = cur.fetchone()['id'] # Record redemption cur.execute(""" INSERT INTO coupon_redemptions (coupon_id, profile_id, access_grant_id) VALUES (%s, %s, %s) """, (coupon['id'], profile_id, grant_id)) # Increment coupon redemption count cur.execute(""" UPDATE coupons SET redemption_count = redemption_count + 1 WHERE id = %s """, (coupon['id'],)) # Log activity cur.execute(""" INSERT INTO user_activity_log (profile_id, action, details) VALUES (%s, 'coupon_redeemed', %s) """, ( profile_id, f'{{"coupon_code": "{code}", "tier": "{coupon["tier_id"]}", "duration_days": {coupon["duration_days"]}}}' )) conn.commit() return { "ok": True, "message": f"Coupon erfolgreich eingelöst: {coupon['tier_id']} für {coupon['duration_days']} Tage", "grant_id": grant_id, "valid_until": valid_until.isoformat() if valid_until else None }