Epic 3: Economy & Billing Engine (Pénzügyi Motor)

This commit is contained in:
Roo
2026-03-08 23:15:52 +00:00
parent 8d25f44ec6
commit 4e40af8a08
69 changed files with 3758 additions and 72 deletions

View File

@@ -1,13 +1,20 @@
# backend/app/api/v1/endpoints/billing.py
from fastapi import APIRouter, Depends, HTTPException, status
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
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)):
@@ -60,4 +67,291 @@ async def upgrade_account(target_package: str, db: AsyncSession = Depends(get_db
))
await db.commit()
return {"status": "success", "package": target_package, "rank_granted": pkg_info["rank"]}
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)}")