Files
service-finder/backend/app/api/v1/endpoints/billing.py

410 lines
16 KiB
Python
Executable File

from fastapi import APIRouter, Depends, HTTPException, status, Request, Header, Query
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, and_
from typing import Optional, Dict, Any, List
import logging
import uuid
from app.api.deps import get_db, get_current_user
from app.models.identity import User, Wallet, UserRole
from app.models import FinancialLedger, WalletType, LedgerEntryType, LedgerStatus
from app.models.marketplace.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
from app.services.billing_engine import upgrade_subscription, get_user_balance
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ó a Billing Engine segítségével.
Kezeli az 5+ csomagot, a Rank-ugrást és a különleges 'Service Coin' bónuszokat.
"""
try:
result = await upgrade_subscription(db, current_user.id, target_package)
return {
"status": "success",
"package": target_package,
"price_paid": result.get("price_paid", 0.0),
"new_plan": result.get("new_plan"),
"expires_at": result.get("expires_at"),
"transaction": result.get("transaction")
}
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"Upgrade error: {e}")
raise HTTPException(status_code=500, detail=f"Belső hiba: {str(e)}")
@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 a Billing Engine segítségével.
"""
try:
balances = await get_user_balance(db, current_user.id)
return balances
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
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)}")
@router.get("/wallet/transactions")
async def get_wallet_transactions(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
page: int = Query(1, ge=1, description="Oldalszám"),
page_size: int = Query(20, ge=1, le=100, description="Elemek száma oldalanként"),
wallet_type: Optional[str] = Query(None, description="Szűrés pénztárca típusra (EARNED, PURCHASED, SERVICE_COINS, VOUCHER)"),
entry_type: Optional[str] = Query(None, description="Szűrés bejegyzés típusra (DEBIT, CREDIT)"),
):
"""
Bejelentkezett felhasználó tranzakciótörténetének lekérdezése (FinancialLedger).
Visszaadja a főkönyvi bejegyzéseket lapozható formában, időrendben csökkenő sorrendben.
"""
try:
# Base query
conditions = [FinancialLedger.user_id == current_user.id]
# Optional wallet_type filter
if wallet_type:
try:
wt = WalletType(wallet_type.upper())
conditions.append(FinancialLedger.wallet_type == wt)
except ValueError:
raise HTTPException(
status_code=400,
detail=f"Érvénytelen wallet_type: {wallet_type}. Használd: {[wt.value for wt in WalletType]}"
)
# Optional entry_type filter
if entry_type:
try:
et = LedgerEntryType(entry_type.upper())
conditions.append(FinancialLedger.entry_type == et)
except ValueError:
raise HTTPException(
status_code=400,
detail=f"Érvénytelen entry_type: {entry_type}. Használd: {[et.value for et in LedgerEntryType]}"
)
# Count total matching records
count_stmt = select(func.count(FinancialLedger.id)).where(and_(*conditions))
count_result = await db.execute(count_stmt)
total_count = count_result.scalar() or 0
# Fetch paginated results
offset = (page - 1) * page_size
stmt = (
select(FinancialLedger)
.where(and_(*conditions))
.order_by(FinancialLedger.created_at.desc())
.offset(offset)
.limit(page_size)
)
result = await db.execute(stmt)
entries = result.scalars().all()
# Build response
transactions = []
for entry in entries:
description = ""
if entry.details and isinstance(entry.details, dict):
description = entry.details.get("description", "")
transactions.append({
"id": entry.id,
"transaction_id": str(entry.transaction_id),
"amount": float(entry.amount),
"entry_type": entry.entry_type.value,
"wallet_type": entry.wallet_type.value if entry.wallet_type else None,
"transaction_type": entry.transaction_type,
"description": description,
"status": entry.status.value if entry.status else None,
"balance_after": float(entry.balance_after) if entry.balance_after else None,
"currency": entry.currency or "EUR",
"created_at": entry.created_at.isoformat() if entry.created_at else None,
})
return {
"success": True,
"data": transactions,
"pagination": {
"page": page,
"page_size": page_size,
"total_count": total_count,
"total_pages": max(1, (total_count + page_size - 1) // page_size),
},
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Tranzakció történet lekérdezési hiba: {e}")
raise HTTPException(status_code=500, detail=f"Belső hiba: {str(e)}")