Files
service-finder/docs/sf/epic_3_financial_motor_architecture.md

7.0 KiB

🏛️ 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).