Epic 3: Economy & Billing Engine (Pénzügyi Motor)
This commit is contained in:
@@ -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()
|
||||
|
||||
236
backend/app/services/stripe_adapter.py
Normal file
236
backend/app/services/stripe_adapter.py
Normal 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()
|
||||
Reference in New Issue
Block a user