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

@@ -59,10 +59,34 @@ class SecurityService:
if action.requester_id == approver_id:
raise Exception("Saját kérést nem hagyhatsz jóvá!")
# Üzleti logika (pl. Role változtatás)
# Üzleti logika a művelettípus alapján
if action.action_type == "CHANGE_ROLE":
target_user = (await db.execute(select(User).where(User.id == action.payload.get("user_id")))).scalar_one_or_none()
if target_user: target_user.role = action.payload.get("new_role")
elif action.action_type == "SET_VIP":
target_user = (await db.execute(select(User).where(User.id == action.payload.get("user_id")))).scalar_one_or_none()
if target_user: target_user.is_vip = action.payload.get("is_vip", True)
elif action.action_type == "WALLET_ADJUST":
from app.models.identity import Wallet
wallet = (await db.execute(select(Wallet).where(Wallet.user_id == action.payload.get("user_id")))).scalar_one_or_none()
if wallet:
amount = action.payload.get("amount", 0)
wallet.balance += amount
elif action.action_type == "SOFT_DELETE_USER":
target_user = (await db.execute(select(User).where(User.id == action.payload.get("user_id")))).scalar_one_or_none()
if target_user:
target_user.is_deleted = True
target_user.is_active = False
# Audit log
await self.log_event(
db, user_id=approver_id, action=f"APPROVE_{action.action_type}",
severity=LogSeverity.info, target_type="PendingAction", target_id=str(action_id),
new_data={"action_id": action_id, "action_type": action.action_type}
)
action.status = ActionStatus.approved
action.approver_id = approver_id
@@ -84,6 +108,40 @@ class SecurityService:
return False
return True
async def reject_action(self, db: AsyncSession, approver_id: int, action_id: int, reason: str = None):
""" Művelet elutasítása. """
stmt = select(PendingAction).where(PendingAction.id == action_id)
action = (await db.execute(stmt)).scalar_one_or_none()
if not action or action.status != ActionStatus.pending:
raise Exception("Művelet nem található.")
if action.requester_id == approver_id:
raise Exception("Saját kérést nem utasíthatod el!")
action.status = ActionStatus.rejected
action.approver_id = approver_id
action.processed_at = datetime.now(timezone.utc)
if reason:
action.reason = f"Elutasítva: {reason}"
await self.log_event(
db, user_id=approver_id, action=f"REJECT_{action.action_type}",
severity=LogSeverity.warning, target_type="PendingAction", target_id=str(action_id),
new_data={"action_id": action_id, "reason": reason}
)
await db.commit()
async def get_pending_actions(self, db: AsyncSession, user_id: int = None, action_type: str = None):
""" Függőben lévő műveletek lekérdezése. """
stmt = select(PendingAction).where(PendingAction.status == ActionStatus.pending)
if user_id:
stmt = stmt.where(PendingAction.requester_id == user_id)
if action_type:
stmt = stmt.where(PendingAction.action_type == action_type)
stmt = stmt.order_by(PendingAction.created_at.desc())
result = await db.execute(stmt)
return result.scalars().all()
async def _execute_emergency_lock(self, db: AsyncSession, user_id: int, reason: str):
if not user_id: return
user = (await db.execute(select(User).where(User.id == user_id))).scalar_one_or_none()

View File

@@ -0,0 +1,236 @@
# /opt/docker/dev/service_finder/backend/app/services/stripe_adapter.py
"""
Stripe integrációs adapter a Payment Router számára.
Kezeli a Stripe Checkout Session létrehozását és a webhook validációt.
"""
import logging
from typing import Dict, Any, Optional, Tuple
from datetime import datetime, timedelta
from decimal import Decimal
from app.core.config import settings
from app.models.payment import PaymentIntent, PaymentIntentStatus
from app.models.audit import WalletType
logger = logging.getLogger("stripe-adapter")
# Try to import stripe, but handle the case when it's not installed
try:
import stripe
STRIPE_AVAILABLE = True
except ImportError:
stripe = None
STRIPE_AVAILABLE = False
logger.warning("Stripe module not installed. Stripe functionality will be disabled.")
class StripeAdapter:
"""Stripe API adapter a fizetési gateway integrációhoz."""
def __init__(self):
"""Inicializálja a Stripe klienst a konfigurációból."""
# Use getattr with defaults for missing settings
self.stripe_api_key = getattr(settings, 'STRIPE_SECRET_KEY', None)
self.webhook_secret = getattr(settings, 'STRIPE_WEBHOOK_SECRET', None)
self.currency = getattr(settings, 'STRIPE_CURRENCY', "EUR")
# Check if stripe module is available
if not STRIPE_AVAILABLE:
logger.warning("Stripe Python module not installed. Stripe adapter disabled.")
self.stripe_available = False
elif not self.stripe_api_key:
logger.warning("STRIPE_SECRET_KEY nincs beállítva, Stripe adapter nem működik")
self.stripe_available = False
else:
stripe.api_key = self.stripe_api_key
self.stripe_available = True
logger.info(f"Stripe adapter inicializálva currency={self.currency}")
async def create_checkout_session(
self,
payment_intent: PaymentIntent,
success_url: str,
cancel_url: str,
metadata: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""
Stripe Checkout Session létrehozása a PaymentIntent alapján.
Args:
payment_intent: A PaymentIntent objektum
success_url: Sikeres fizetés után átirányítási URL
cancel_url: Megszakított fizetés után átirányítási URL
metadata: Extra metadata a Stripe számára
Returns:
Dict: Stripe Checkout Session adatai
"""
if not self.stripe_available:
raise ValueError("Stripe nem elérhető, STRIPE_SECRET_KEY hiányzik")
if payment_intent.status != PaymentIntentStatus.PENDING:
raise ValueError(f"PaymentIntent nem PENDING státuszú: {payment_intent.status}")
# Alap metadata (kötelező: intent_token)
base_metadata = {
"intent_token": str(payment_intent.intent_token),
"payment_intent_id": payment_intent.id,
"payer_id": payment_intent.payer_id,
"target_wallet_type": payment_intent.target_wallet_type.value,
}
if payment_intent.beneficiary_id:
base_metadata["beneficiary_id"] = payment_intent.beneficiary_id
# Egyesített metadata
final_metadata = {**base_metadata, **(metadata or {})}
try:
# Stripe Checkout Session létrehozása
session = stripe.checkout.Session.create(
payment_method_types=["card"],
line_items=[
{
"price_data": {
"currency": self.currency.lower(),
"product_data": {
"name": f"Service Finder - {payment_intent.target_wallet_type.value} feltöltés",
"description": f"Net: {payment_intent.net_amount} {self.currency}, Fee: {payment_intent.handling_fee} {self.currency}",
},
"unit_amount": int(payment_intent.gross_amount * 100), # Stripe centben várja
},
"quantity": 1,
}
],
mode="payment",
success_url=success_url,
cancel_url=cancel_url,
client_reference_id=str(payment_intent.id),
metadata=final_metadata,
expires_at=int((datetime.utcnow() + timedelta(hours=24)).timestamp()),
)
logger.info(
f"Stripe Checkout Session létrehozva: {session.id}, "
f"amount={payment_intent.gross_amount}{self.currency}, "
f"intent_token={payment_intent.intent_token}"
)
return {
"session_id": session.id,
"url": session.url,
"payment_intent_id": session.payment_intent,
"expires_at": datetime.fromtimestamp(session.expires_at),
"metadata": final_metadata,
}
except stripe.error.StripeError as e:
logger.error(f"Stripe hiba Checkout Session létrehozásakor: {e}")
raise ValueError(f"Stripe hiba: {e.user_message if hasattr(e, 'user_message') else str(e)}")
async def verify_webhook_signature(
self,
payload: bytes,
signature: str
) -> Tuple[bool, Optional[Dict[str, Any]]]:
"""
Stripe webhook aláírás validálása (Kettős Lakat - 1. lépés).
Args:
payload: A nyers HTTP request body
signature: A Stripe-Signature header értéke
Returns:
Tuple: (sikeres validáció, event adatok vagy None)
"""
if not self.webhook_secret:
logger.error("STRIPE_WEBHOOK_SECRET nincs beállítva, webhook validáció sikertelen")
return False, None
try:
event = stripe.Webhook.construct_event(
payload, signature, self.webhook_secret
)
logger.info(f"Stripe webhook validálva: {event.type} (id: {event.id})")
return True, event
except stripe.error.SignatureVerificationError as e:
logger.error(f"Stripe webhook aláírás érvénytelen: {e}")
return False, None
except Exception as e:
logger.error(f"Stripe webhook feldolgozási hiba: {e}")
return False, None
async def handle_checkout_completed(
self,
event: Dict[str, Any]
) -> Dict[str, Any]:
"""
checkout.session.completed esemény feldolgozása.
Args:
event: Stripe webhook event
Returns:
Dict: Feldolgozási eredmény
"""
session = event["data"]["object"]
# Metadata kinyerése
metadata = session.get("metadata", {})
intent_token = metadata.get("intent_token")
if not intent_token:
logger.error("Stripe session metadata nem tartalmaz intent_token-t")
return {"success": False, "error": "Missing intent_token in metadata"}
# Összeg ellenőrzése (cent -> valuta)
amount_total = session.get("amount_total", 0) / 100.0 # Centből valuta
logger.info(
f"Stripe checkout completed: session={session['id']}, "
f"amount={amount_total}, intent_token={intent_token}"
)
return {
"success": True,
"session_id": session["id"],
"payment_intent_id": session.get("payment_intent"),
"amount_total": amount_total,
"currency": session.get("currency", "eur").upper(),
"metadata": metadata,
"intent_token": intent_token,
}
async def handle_payment_intent_succeeded(
self,
event: Dict[str, Any]
) -> Dict[str, Any]:
"""
payment_intent.succeeded esemény feldolgozása.
Args:
event: Stripe webhook event
Returns:
Dict: Feldolgozási eredmény
"""
payment_intent = event["data"]["object"]
logger.info(
f"Stripe payment intent succeeded: {payment_intent['id']}, "
f"amount={payment_intent['amount']/100}"
)
return {
"success": True,
"payment_intent_id": payment_intent["id"],
"amount": payment_intent["amount"] / 100.0,
"currency": payment_intent.get("currency", "eur").upper(),
"status": payment_intent.get("status"),
}
# Globális példány
stripe_adapter = StripeAdapter()