# /opt/docker/dev/service_finder/backend/app/models/payment.py """ Payment Intent modell a Stripe integrációhoz és belső fizetésekhez. Kettős Lakat (Double Lock) biztonságot valósít meg. """ import enum import uuid from datetime import datetime from typing import Any, Optional from sqlalchemy import String, DateTime, JSON, ForeignKey, Numeric, Boolean, Integer, text from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.dialects.postgresql import UUID as PG_UUID, ENUM as PG_ENUM from sqlalchemy.sql import func from app.database import Base from app.models.audit import WalletType class PaymentIntentStatus(str, enum.Enum): """PaymentIntent státuszok.""" PENDING = "PENDING" # Létrehozva, vár Stripe fizetésre PROCESSING = "PROCESSING" # Fizetés folyamatban (belső ajándékozás) COMPLETED = "COMPLETED" # Sikeresen teljesítve FAILED = "FAILED" # Sikertelen (pl. Stripe hiba) CANCELLED = "CANCELLED" # Felhasználó által törölve EXPIRED = "EXPIRED" # Lejárt (pl. Stripe session timeout) class PaymentIntent(Base): """ Fizetési szándék (Prior Intent) a Kettős Lakat biztonsághoz. Minden külső (Stripe) vagy belső fizetés előtt létre kell hozni egy PENDING státuszú PaymentIntent-et. A Stripe metadata tartalmazza az intent_token-t, így a webhook validáció során vissza lehet keresni. Fontos mezők: - net_amount: A kedvezményezett által kapott összeg (pénztárcába kerül) - handling_fee: Kényelmi díj (rendszer bevétele) - gross_amount: net_amount + handling_fee (Stripe-nak küldött összeg) """ __tablename__ = "payment_intents" __table_args__ = {"schema": "finance"} id: Mapped[int] = mapped_column(Integer, primary_key=True) # Egyedi token a Stripe metadata számára intent_token: Mapped[uuid.UUID] = mapped_column( PG_UUID(as_uuid=True), default=uuid.uuid4, unique=True, nullable=False, index=True ) # Fizető felhasználó payer_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=False) payer: Mapped["User"] = relationship("User", foreign_keys=[payer_id], back_populates="payment_intents_as_payer") # Kedvezményezett felhasználó (opcionális, ha None, akkor a rendszernek fizet) beneficiary_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id")) beneficiary: Mapped[Optional["User"]] = relationship("User", foreign_keys=[beneficiary_id], back_populates="payment_intents_as_beneficiary") # Cél pénztárca típusa target_wallet_type: Mapped[WalletType] = mapped_column( PG_ENUM(WalletType, name="wallet_type", schema="finance"), nullable=False ) # Összeg mezők (javított a kényelmi díj kezelésére) net_amount: Mapped[float] = mapped_column(Numeric(18, 4), nullable=False, comment="Kedvezményezett által kapott összeg") handling_fee: Mapped[float] = mapped_column(Numeric(18, 4), default=0.0, comment="Kényelmi díj") gross_amount: Mapped[float] = mapped_column(Numeric(18, 4), nullable=False, comment="Fizetendő összeg (net + fee)") currency: Mapped[str] = mapped_column(String(10), default="EUR", nullable=False) # Státusz status: Mapped[PaymentIntentStatus] = mapped_column( PG_ENUM(PaymentIntentStatus, name="payment_intent_status", schema="finance"), default=PaymentIntentStatus.PENDING, nullable=False, index=True ) # Stripe információk (külső fizetés esetén) stripe_session_id: Mapped[Optional[str]] = mapped_column(String(255), unique=True, index=True) stripe_payment_intent_id: Mapped[Optional[str]] = mapped_column(String(255), index=True) stripe_customer_id: Mapped[Optional[str]] = mapped_column(String(255)) # Metaadatok (metadata foglalt név SQLAlchemy-ban, ezért meta_data) meta_data: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"), name="metadata") # Időbélyegek created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) completed_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) expires_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), comment="Stripe session lejárati ideje") # Tranzakció kapcsolat transaction_id: Mapped[Optional[uuid.UUID]] = mapped_column(PG_UUID(as_uuid=True), comment="Kapcsolódó FinancialLedger transaction_id") # Soft delete is_deleted: Mapped[bool] = mapped_column(Boolean, default=False) deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) def __repr__(self) -> str: return f"" def mark_completed(self, transaction_id: Optional[uuid.UUID] = None) -> None: """PaymentIntent befejezése sikeres fizetés után.""" self.status = PaymentIntentStatus.COMPLETED self.completed_at = datetime.utcnow() if transaction_id: self.transaction_id = transaction_id def mark_failed(self, reason: Optional[str] = None) -> None: """PaymentIntent sikertelen státuszba helyezése.""" self.status = PaymentIntentStatus.FAILED if reason and self.meta_data: self.meta_data = {**self.meta_data, "failure_reason": reason} def is_valid_for_webhook(self) -> bool: """Ellenőrzi, hogy a PaymentIntent érvényes-e webhook feldolgozásra.""" return ( self.status == PaymentIntentStatus.PENDING and not self.is_deleted and (self.expires_at is None or self.expires_at > datetime.utcnow()) ) # Import User modell a relationship-ekhez (circular import elkerülésére) from app.models.identity import User class WithdrawalPayoutMethod(str, enum.Enum): """Kifizetési módok.""" FIAT_BANK = "FIAT_BANK" # Banki átutalás (SEPA) CRYPTO_USDT = "CRYPTO_USDT" # USDT (ERC20/TRC20) class WithdrawalRequestStatus(str, enum.Enum): """Kifizetési kérelem státuszai.""" PENDING = "PENDING" # Beküldve, admin ellenőrzésre vár APPROVED = "APPROVED" # Jóváhagyva, kifizetés folyamatban REJECTED = "REJECTED" # Elutasítva (pl. hiányzó bizonylat) COMPLETED = "COMPLETED" # Kifizetés teljesítve CANCELLED = "CANCELLED" # Felhasználó által visszavonva class WithdrawalRequest(Base): """ Kifizetési kérelem (Withdrawal Request) a felhasználók Earned zsebéből való pénzkivételhez. A felhasználó beküld egy kérést, amely admin jóváhagyást igényel. Ha 14 napon belül nem kerül jóváhagyásra, automatikusan REJECTED lesz és a pénz visszakerül a Earned zsebbe. """ __tablename__ = "withdrawal_requests" __table_args__ = {"schema": "finance"} id: Mapped[int] = mapped_column(Integer, primary_key=True) # Felhasználó aki a kérést benyújtotta user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=False) user: Mapped["User"] = relationship("User", back_populates="withdrawal_requests", foreign_keys=[user_id]) # Összeg és pénznem amount: Mapped[float] = mapped_column(Numeric(18, 4), nullable=False) currency: Mapped[str] = mapped_column(String(10), default="EUR", nullable=False) # Kifizetési mód payout_method: Mapped[WithdrawalPayoutMethod] = mapped_column( PG_ENUM(WithdrawalPayoutMethod, name="withdrawal_payout_method", schema="finance"), nullable=False ) # Státusz status: Mapped[WithdrawalRequestStatus] = mapped_column( PG_ENUM(WithdrawalRequestStatus, name="withdrawal_request_status", schema="finance"), default=WithdrawalRequestStatus.PENDING, nullable=False, index=True ) # Okozata (pl. admin megjegyzés vagy automatikus elutasítás oka) reason: Mapped[Optional[str]] = mapped_column(String(500)) # Admin információk approved_by_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id")) approved_by: Mapped[Optional["User"]] = relationship("User", foreign_keys=[approved_by_id]) approved_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) # Tranzakció kapcsolat (ha a pénz visszakerül a zsebbe) refund_transaction_id: Mapped[Optional[uuid.UUID]] = mapped_column(PG_UUID(as_uuid=True)) # Időbélyegek created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) # Soft delete is_deleted: Mapped[bool] = mapped_column(Boolean, default=False) deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) def __repr__(self) -> str: return f"" def approve(self, admin_user_id: int) -> None: """Admin jóváhagyás.""" self.status = WithdrawalRequestStatus.APPROVED self.approved_by_id = admin_user_id self.approved_at = datetime.utcnow() self.reason = None def reject(self, reason: str) -> None: """Admin elutasítás.""" self.status = WithdrawalRequestStatus.REJECTED self.reason = reason def cancel(self) -> None: """Felhasználó visszavonja a kérést.""" self.status = WithdrawalRequestStatus.CANCELLED self.reason = "User cancelled" def is_expired(self, days: int = 14) -> bool: """Ellenőrzi, hogy a kérelem lejárt-e (14 nap).""" from datetime import timedelta expiry_date = self.created_at + timedelta(days=days) return datetime.utcnow() > expiry_date