# backend/app/api/v1/endpoints/billing.py from fastapi import APIRouter, Depends, HTTPException, status, Request, Header from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from typing import Optional, Dict, Any import logging from app.api.deps import get_db, get_current_user from app.models.identity import User, Wallet, UserRole from app.models.audit import FinancialLedger, WalletType from app.models.payment import PaymentIntent, PaymentIntentStatus from app.services.config_service import config from app.services.payment_router import PaymentRouter from app.services.stripe_adapter import stripe_adapter router = APIRouter() logger = logging.getLogger(__name__) @router.post("/upgrade") async def upgrade_account(target_package: str, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)): """ Univerzális csomagváltó. Kezeli az 5+ csomagot, a Rank-ugrást és a különleges 'Service Coin' bónuszokat. """ # 1. Lekérjük a teljes csomagmátrixot az adminból # Példa JSON: {"premium": {"price": 2000, "rank": 5, "type": "credit"}, "service_pro": {"price": 10000, "rank": 30, "type": "coin"}} package_matrix = await config.get_setting(db, "subscription_packages_matrix") if target_package not in package_matrix: raise HTTPException(status_code=400, detail="Érvénytelen csomagválasztás.") pkg_info = package_matrix[target_package] price = pkg_info["price"] # 2. Pénztárca ellenőrzése stmt = select(Wallet).where(Wallet.user_id == current_user.id) wallet = (await db.execute(stmt)).scalar_one_or_none() total_balance = wallet.purchased_credits + wallet.earned_credits if total_balance < price: raise HTTPException(status_code=402, detail="Nincs elég kredited a csomagváltáshoz.") # 3. Levonási logika (Purchased -> Earned sorrend) if wallet.purchased_credits >= price: wallet.purchased_credits -= price else: remaining = price - wallet.purchased_credits wallet.purchased_credits = 0 wallet.earned_credits -= remaining # 4. Speciális Szerviz Logika (Service Coins) # Ha a csomag típusa 'coin', akkor a szerviz kap egy kezdő Coin csomagot is if pkg_info.get("type") == "coin": initial_coins = pkg_info.get("initial_coin_bonus", 100) wallet.service_coins += initial_coins logger.info(f"User {current_user.id} upgraded to Service Pro, awarded {initial_coins} coins.") # 5. Rang frissítése és naplózás current_user.role = target_package # Pl. 'service_pro' vagy 'vip' db.add(FinancialLedger( user_id=current_user.id, amount=-price, transaction_type=f"UPGRADE_{target_package.upper()}", details=pkg_info )) await db.commit() return {"status": "success", "package": target_package, "rank_granted": pkg_info["rank"]} @router.post("/payment-intent/create") async def create_payment_intent( request: Dict[str, Any], db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """ PaymentIntent létrehozása (Prior Intent - Kettős Lakat 1. lépés). Body: - net_amount: float (kötelező) - handling_fee: float (alapértelmezett: 0) - target_wallet_type: string (EARNED, PURCHASED, SERVICE_COINS, VOUCHER) - beneficiary_id: int (opcionális) - currency: string (alapértelmezett: "EUR") - metadata: dict (opcionális) """ try: # Adatok kinyerése net_amount = request.get("net_amount") handling_fee = request.get("handling_fee", 0.0) target_wallet_type_str = request.get("target_wallet_type") beneficiary_id = request.get("beneficiary_id") currency = request.get("currency", "EUR") metadata = request.get("metadata", {}) # Validáció if net_amount is None or net_amount <= 0: raise HTTPException(status_code=400, detail="net_amount pozitív szám kell legyen") if handling_fee < 0: raise HTTPException(status_code=400, detail="handling_fee nem lehet negatív") try: target_wallet_type = WalletType(target_wallet_type_str) except ValueError: raise HTTPException( status_code=400, detail=f"Érvénytelen target_wallet_type: {target_wallet_type_str}. Használd: {[wt.value for wt in WalletType]}" ) # PaymentIntent létrehozása payment_intent = await PaymentRouter.create_payment_intent( db=db, payer_id=current_user.id, net_amount=net_amount, handling_fee=handling_fee, target_wallet_type=target_wallet_type, beneficiary_id=beneficiary_id, currency=currency, metadata=metadata ) return { "success": True, "payment_intent_id": payment_intent.id, "intent_token": str(payment_intent.intent_token), "net_amount": float(payment_intent.net_amount), "handling_fee": float(payment_intent.handling_fee), "gross_amount": float(payment_intent.gross_amount), "currency": payment_intent.currency, "status": payment_intent.status.value, "expires_at": payment_intent.expires_at.isoformat() if payment_intent.expires_at else None, } except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) except Exception as e: logger.error(f"PaymentIntent létrehozási hiba: {e}") raise HTTPException(status_code=500, detail=f"Belső hiba: {str(e)}") @router.post("/payment-intent/{payment_intent_id}/stripe-checkout") async def initiate_stripe_checkout( payment_intent_id: int, request: Dict[str, Any], db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """ Stripe Checkout Session indítása PaymentIntent alapján. Body: - success_url: string (kötelező) - cancel_url: string (kötelező) """ try: success_url = request.get("success_url") cancel_url = request.get("cancel_url") if not success_url or not cancel_url: raise HTTPException(status_code=400, detail="success_url és cancel_url kötelező") # Ellenőrizzük, hogy a PaymentIntent a felhasználóhoz tartozik-e stmt = select(PaymentIntent).where( PaymentIntent.id == payment_intent_id, PaymentIntent.payer_id == current_user.id ) result = await db.execute(stmt) payment_intent = result.scalar_one_or_none() if not payment_intent: raise HTTPException(status_code=404, detail="PaymentIntent nem található vagy nincs hozzáférésed") # Stripe Checkout indítása session_data = await PaymentRouter.initiate_stripe_payment( db=db, payment_intent_id=payment_intent_id, success_url=success_url, cancel_url=cancel_url ) return { "success": True, "checkout_url": session_data["checkout_url"], "stripe_session_id": session_data["stripe_session_id"], "expires_at": session_data["expires_at"], } except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) except Exception as e: logger.error(f"Stripe Checkout indítási hiba: {e}") raise HTTPException(status_code=500, detail=f"Belső hiba: {str(e)}") @router.post("/payment-intent/{payment_intent_id}/process-internal") async def process_internal_payment( payment_intent_id: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """ Belső ajándékozás feldolgozása (SmartDeduction használatával). Csak akkor engedélyezett, ha a PaymentIntent PENDING státuszú és a felhasználó a payer. """ try: # Ellenőrizzük, hogy a PaymentIntent a felhasználóhoz tartozik-e stmt = select(PaymentIntent).where( PaymentIntent.id == payment_intent_id, PaymentIntent.payer_id == current_user.id, PaymentIntent.status == PaymentIntentStatus.PENDING ) result = await db.execute(stmt) payment_intent = result.scalar_one_or_none() if not payment_intent: raise HTTPException( status_code=404, detail="PaymentIntent nem található, nincs hozzáférésed, vagy nem PENDING státuszú" ) # Belső fizetés feldolgozása result = await PaymentRouter.process_internal_payment(db, payment_intent_id) if not result["success"]: raise HTTPException(status_code=400, detail=result.get("error", "Ismeretlen hiba")) return { "success": True, "transaction_id": result.get("transaction_id"), "used_amounts": result.get("used_amounts"), "beneficiary_credited": result.get("beneficiary_credited", False), } except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) except Exception as e: logger.error(f"Belső fizetés feldolgozási hiba: {e}") raise HTTPException(status_code=500, detail=f"Belső hiba: {str(e)}") @router.post("/stripe-webhook") async def stripe_webhook( request: Request, stripe_signature: Optional[str] = Header(None), db: AsyncSession = Depends(get_db) ): """ Stripe webhook végpont a Kettős Lakat validációval. Stripe a következő header-t küldi: Stripe-Signature """ if not stripe_signature: raise HTTPException(status_code=400, detail="Missing Stripe-Signature header") try: # Request body kiolvasása payload = await request.body() # Webhook feldolgozása result = await PaymentRouter.process_stripe_webhook( db=db, payload=payload, signature=stripe_signature ) if not result.get("success", False): error_msg = result.get("error", "Unknown error") logger.error(f"Stripe webhook feldolgozás sikertelen: {error_msg}") raise HTTPException(status_code=400, detail=error_msg) return result except HTTPException: raise except Exception as e: logger.error(f"Stripe webhook végpont hiba: {e}") raise HTTPException(status_code=500, detail=f"Belső hiba: {str(e)}") @router.get("/payment-intent/{payment_intent_id}/status") async def get_payment_intent_status( payment_intent_id: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """ PaymentIntent státusz lekérdezése. """ try: # Ellenőrizzük, hogy a PaymentIntent a felhasználóhoz tartozik-e stmt = select(PaymentIntent).where( PaymentIntent.id == payment_intent_id, PaymentIntent.payer_id == current_user.id ) result = await db.execute(stmt) payment_intent = result.scalar_one_or_none() if not payment_intent: raise HTTPException(status_code=404, detail="PaymentIntent nem található vagy nincs hozzáférésed") return { "id": payment_intent.id, "intent_token": str(payment_intent.intent_token), "net_amount": float(payment_intent.net_amount), "handling_fee": float(payment_intent.handling_fee), "gross_amount": float(payment_intent.gross_amount), "currency": payment_intent.currency, "status": payment_intent.status.value, "target_wallet_type": payment_intent.target_wallet_type.value, "beneficiary_id": payment_intent.beneficiary_id, "stripe_session_id": payment_intent.stripe_session_id, "transaction_id": str(payment_intent.transaction_id) if payment_intent.transaction_id else None, "created_at": payment_intent.created_at.isoformat(), "updated_at": payment_intent.updated_at.isoformat(), "completed_at": payment_intent.completed_at.isoformat() if payment_intent.completed_at else None, "expires_at": payment_intent.expires_at.isoformat() if payment_intent.expires_at else None, } except Exception as e: logger.error(f"PaymentIntent státusz lekérdezési hiba: {e}") raise HTTPException(status_code=500, detail=f"Belső hiba: {str(e)}") @router.get("/wallet/balance") async def get_wallet_balance( db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """ Felhasználó pénztárca egyenlegének lekérdezése. """ try: stmt = select(Wallet).where(Wallet.user_id == current_user.id) result = await db.execute(stmt) wallet = result.scalar_one_or_none() if not wallet: raise HTTPException(status_code=404, detail="Pénztárca nem található") return { "earned": float(wallet.earned_credits), "purchased": float(wallet.purchased_credits), "service_coins": float(wallet.service_coins), "total": float( wallet.earned_credits + wallet.purchased_credits + wallet.service_coins ), } except Exception as e: logger.error(f"Pénztárca egyenleg lekérdezési hiba: {e}") raise HTTPException(status_code=500, detail=f"Belső hiba: {str(e)}")