357 lines
14 KiB
Python
Executable File
357 lines
14 KiB
Python
Executable File
# 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)}") |