átlagos kiegészítséek jó sok
This commit is contained in:
224
backend/app/models/marketplace/payment.py
Normal file
224
backend/app/models/marketplace/payment.py
Normal file
@@ -0,0 +1,224 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/models/marketplace/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.system.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"<PaymentIntent {self.id}: {self.status} {self.gross_amount}{self.currency}>"
|
||||
|
||||
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"<WithdrawalRequest {self.id}: {self.status} {self.amount}{self.currency}>"
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user