🏛️ EPIC 3: Pénzügyi Motor és Főkönyv (Financial Motor Architecture) Státusz: READY / AUDITED Dátum: 2026-03-08 Hatáskör: Backend (FastAPI, SQLAlchemy 2.0, PostgreSQL) Vezetői Összefoglaló (Executive Summary) A rendszer pénzügyi magja egy szigorú Kettős Könyvvitel (Double-Entry Ledger) elvű, atomi tranzakciókra épülő motor. Célja a "Zero-Sum" (zéró összegű) játékelmélet biztosítása: a rendszerben minden pénz- és kreditmozgásnak (debit/credit) tökéletesen egyeznie kell a felhasználói pénztárcák (Walletek) egyenlegével. Minden tranzakció visszavonhatatlan és auditálható. 📋 Implementált Feladatok és Kártyák 💳 #15 Epic 3 Audit: Pénzügyi Motor és Főkönyv A pénzügyi alapok lefektetése. A többzsebes pénztárca (Quadruple Wallet) és a megmásíthatatlan főkönyv (Financial Ledger) adatbázis sémájának és modelljeinek elkészítése. Architekturális Döntések: Wallet felépítése: Négy különálló zseb (purchased_credits, earned_credits, service_coins, és a FIFO elvű ActiveVouchers). Szigorúan az identity sémában. Főkönyv (Ledger): Az audit sémában tárolva. Nincs description oszlop, helyette egy rugalmas details (JSON) mezőt használunk a metaadatokhoz, és egy transaction_type oszlopot az azonosításhoz. Letisztított Kódstruktúra (Modellek): Python # models/audit.py - Financial Ledger class FinancialLedger(Base): __tablename__ = "financial_ledger" __table_args__ = {"schema": "audit"} id: Mapped[int] = mapped_column(BigInteger, primary_key=True) user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id")) amount: Mapped[float] = mapped_column(Numeric(18, 4), nullable=False) entry_type: Mapped[LedgerEntryType] = mapped_column(PG_ENUM(LedgerEntryType, name="ledger_entry_type", schema="audit")) wallet_type: Mapped[WalletType] = mapped_column(PG_ENUM(WalletType, name="wallet_type", schema="audit")) transaction_type: Mapped[str] = mapped_column(String(50)) details: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb")) balance_after: Mapped[float] = mapped_column(Numeric(18, 4)) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) 🛡️ #16 Fejlesztés: Stripe Webhook implementálása A külső fizetések (Stripe) integrációja a rendszerbe a Kettős Lakat (Double Lock) biztonsági protokollal. A Kettős Lakat folyamata: HMAC Validáció: A Stripe Stripe-Signature ellenőrzése. Intent Egyeztetés: A webhookban kapott azonosító összevetése az adatbázisban lévő PaymentIntent PENDING státuszú rekordjával. Összeg Validáció: A Stripe által küldött összeg cent-pontos egyeztetése a mi gross_amount értékünkkel. Atomi Könyvelés: Csak ha az előző 3 lépés sikeres, akkor kerül meghívásra az AtomicTransactionManager. ⚙️ #17 Fejlesztés: Billing Engine Service létrehozása Az okos levonási logika (Smart Deduction) és a belső fizetések/ajándékozások kezelése. Levonási Prioritás (A "Vízesés" modell): Amikor egy felhasználó fizet a rendszeren belül, a motor a következő sorrendben terheli meg a zsebeket: ActiveVoucher (FIFO elv szerint, a legkorábban lejáró fogy el először) service_coins purchased_credits earned_credits Letisztított Kódstruktúra (Vízesés logika): Python # services/billing_engine.py - Smart Deduction @classmethod async def deduct_from_wallets(cls, db: AsyncSession, user_id: int, amount: float) -> dict: stmt = select(Wallet).where(Wallet.user_id == user_id) wallet = (await db.execute(stmt)).scalar_one() remaining = Decimal(str(amount)) used = {"vouchers": 0, "service_coins": 0, "purchased": 0, "earned": 0} # 1. Voucherek (Kihagyva a rövidség kedvéért, FIFO feldolgozás) # 2. Service Coins if remaining > 0 and wallet.service_coins > 0: deduction = min(wallet.service_coins, remaining) wallet.service_coins -= deduction remaining -= deduction used["service_coins"] = float(deduction) # 3. Purchased Credits if remaining > 0 and wallet.purchased_credits > 0: deduction = min(wallet.purchased_credits, remaining) wallet.purchased_credits -= deduction remaining -= deduction used["purchased"] = float(deduction) if remaining > 0: raise ValueError("Insufficient funds across all wallets.") await db.flush() # Perzisztáljuk az állapotot a fő tranzakció lezárása nélkül! return used ⛓️ #18 Fejlesztés: Atomi tranzakciók bevezetése A rendszer legkritikusabb pontja. A Nested Transactions (egymásba ágyazott tranzakciók) elkerülése SQLAlchemy 2.0 alatt, biztosítva az adatintegritást. Architekturális Szabály: Soha nem használunk db.commit()-ot a szerviz réteg (Service Layer) belső ciklusaiban vagy async with db.begin(): blokkok belsejében. Ehelyett in_transaction() ellenőrzést és flush()-t alkalmazunk. Letisztított Kódstruktúra (Atomic Manager): Python # services/billing_engine.py - AtomicTransactionManager @classmethod async def atomic_billing_transaction(cls, db: AsyncSession, user_id: int, amount: float, transaction_type: str, details: dict): # Ellenőrizzük, hogy van-e már nyitott tranzakció owns_transaction = False if not db.in_transaction(): await db.begin() owns_transaction = True try: # 1. Pénz levonása (Smart Deduction hívása) # 2. Főkönyvi (FinancialLedger) bejegyzések létrehozása Debit/Credit párban await db.flush() # Adatok leírása az adatbázisba, ID-k generálása if owns_transaction: await db.commit() # Csak az zárja le, aki megnyitotta! return {"transaction_id": details.get("transaction_id")} except Exception as e: if owns_transaction: await db.rollback() raise e ⏱️ #19 Fejlesztés: Cron-job ütemező beállítása A lejárt voucherek automatikus feldolgozása, valamint a Network Fee (Rendszerhasználati díj) beszedése. Üzleti logika (Voucher Expiration): Ha egy ActiveVoucher lejár (expires_at < now()), a rendszer: Átmozgatja a fennmaradó összeget a felhasználó purchased_credits zsebébe. Levon 10% "Network Fee"-t a tranzakcióból (a rendszer bevételeként könyvelve). Törli/Inaktiválja az ActiveVoucher rekordot. Ezt a folyamatot egy háttérben futó worker (System-Robot-3) ütemezve hívja meg. ♾️ #20 Fejlesztés: Előfizetés életciklus kezelése B2B és Prémium felhasználók havidíjas/éves előfizetéseinek menedzselése. Renewals (Megújítás): A Cron-job naponta ellenőrzi a subscription_expires_at dátumokat. Grace Period (Türelmi idő): Lejárat után 3 napig a profil még publikus, de a Wallet zárolásra kerül. Downgrade: Sikertelen levonás esetén a rendszer automatikusan visszasorolja a felhasználót a FREE tier-be, és alkalmazza az ehhez tartozó funkcionális korlátozásokat (Quota management).