admin_nyelvi_javítas

This commit is contained in:
Roo
2026-07-25 10:22:03 +00:00
parent 33c4d793db
commit c9118bf52f
29 changed files with 4474 additions and 943 deletions

View File

@@ -0,0 +1,441 @@
# 🏗️ Architecture Plan: Phase 2 - Advanced MLM Logic & Financial System Integration
**Audit Date:** 2026-07-24
**Status:** DRAFT - Awaiting Approval
**Auditor:** Architect Mode
---
## 📋 Executive Summary
A jelenlegi 2-szintű MLM jutalékrendszer (`distribute_commission()`) csak **kiszámolja** a jutalékokat, de **nem írja jóvá** azokat a pénztárcákban (Wallet) és nem naplózza a főkönyvben (FinancialLedger). Emellett hiányzik a **szintenkénti külön plafon**, a **Gen2 inaktivitás ellenőrzés**, a **180 napos reaktivációs szabály**, és a **megújítási jutalék gen2 változata**.
Ez a terv ezeket a hiányosságokat pótolja.
---
## 🔍 TASK 1: Audit Findings Summary
### ✅ Meglévő Rendszer (Current State)
| Komponens | Fájl | Jelenlegi Állapot |
|-----------|------|-------------------|
| **CommissionRule modell** | `backend/app/models/marketplace/commission.py` | Van `commission_max_amount`, DE ez **közös plafon** Gen1 és Gen2 számára |
| **Commission schemas** | `backend/app/schemas/commission.py` | Jó Pydantic modellek, de hiányzik a `gen1_max_amount`, `gen2_max_amount`, `gen2_renewal_percent` |
| **distribute_commission()** | `backend/app/services/commission_service.py:377` | Kiszámolja a jutalékot, de **NEM írja jóvá** a Wallet-ben |
| **Wallet** | `backend/app/models/identity/identity.py:255` | `earned_credits`, `purchased_credits`, `service_coins` - jó. `user.wallet` kapcsolat létezik |
| **FinancialLedger** | `backend/app/models/system/audit.py:78` | Double-entry, `entry_type` (DEBIT/CREDIT), `wallet_type` (EARNED/PURCHASED/SERVICE_COINS/VOUCHER) |
| **GamificationService._add_earned_credits()** | `backend/app/services/gamification_service.py:265` | Bizonyított minta: Wallet frissítés + FinancialLedger bejegyzés |
| **AtomicTransactionManager** | `backend/app/services/billing_engine.py:364` | Bizonyított minta: Atomi tranzakció double-entry könyveléssel |
| **User.is_active** | `backend/app/models/identity/identity.py:169` | Létezik, használható inaktivitás ellenőrzésre |
| **Test rule (DB)** | `marketplace.commission_rules` ID=4 | STANDARD, HU, 5% Gen1, 2% Gen2, max 50000 |
### ❌ Hiányosságok (Gaps)
1. **Közös `commission_max_amount`**: Gen1 és Gen2 ugyanazt a plafont használja
2. **Nincs Wallet írás**: `distribute_commission()` csak visszaad egy CommissionDistributionResponse DTO-t
3. **Nincs inaktivitás ellenőrzés**: Nem nézi, hogy Gen1 aktív-e mielőtt Gen2-nek fizet
4. **Nincs reaktivációs logika**: 180 napos inaktivitás detektálás hiányzik
5. **Nincs `gen2_renewal_percent`**: Csak Gen1-nek van `renewal_commission_percent`
---
## 🏗️ TASK 2: Proposed Architecture Plan
### 2.1 Adatmodell Változtatások (CommissionRule)
#### Cél
A `commission_max_amount` mező szétválasztása szintenként külön plafonra.
#### SQLAlchemy Modell Változtatás
**Fájl:** `backend/app/models/marketplace/commission.py`
**Új mezők hozzáadása (L2_COMMISSION szekcióba, a meglévők után):**
```python
# --- Új: Szintenkénti plafonok ---
gen1_max_amount: Mapped[Optional[float]] = mapped_column(
Numeric(18, 2), nullable=True,
comment="L2: Gen1 maximális jutalék összeg (NULL/0 = korlátlan)"
)
gen2_max_amount: Mapped[Optional[float]] = mapped_column(
Numeric(18, 2), nullable=True,
comment="L2: Gen2 (upline) maximális jutalék összeg (NULL/0 = korlátlan)"
)
gen2_renewal_percent: Mapped[Optional[float]] = mapped_column(
Numeric(5, 2), nullable=True, default=0.00,
comment="L2: Gen2 megújítási jutalék százalék - Gen1-hez hasonlóan, de Gen2 számára"
)
```
**Megjegyzés:** A régi `commission_max_amount` mező **megmarad** backward compatibility miatt, de a logika már az új `gen1_max_amount`-ot és `gen2_max_amount`-ot használja. Ha az új mezők NULL-ok, a `commission_max_amount` értéke lesz a fallback.
#### Sync Engine (Adatbázis Migráció)
A modell módosítása után futtatni kell:
```bash
docker exec -it sf_api python -m app.scripts.sync_engine
```
### 2.2 Schema Változtatások (Pydantic)
**Fájl:** `backend/app/schemas/commission.py`
```python
# Új mezők a CommissionRuleCreate-ben:
gen1_max_amount: Optional[float] = None # NULL = commission_max_amount fallback
gen2_max_amount: Optional[float] = None # NULL = commission_max_amount fallback
gen2_renewal_percent: Optional[float] = None
# Ugyanez a CommissionRuleUpdate-ben és CommissionRuleResponse-ben is
```
### 2.3 CRUD Frissítés (commission_service.py)
**Fájl:** `backend/app/services/commission_service.py`
A `create_commission_rule()` függvényben hozzáadni az új mezők átvételét:
```python
gen1_max_amount=data.gen1_max_amount,
gen2_max_amount=data.gen2_max_amount,
gen2_renewal_percent=data.gen2_renewal_percent,
```
### 2.4 _calculate_commission() Módosítás - Szintenkénti Plafon
A jelenlegi `_calculate_commission()` egyetlen `max_amount` paramétert fogad. Ezt kibővítjük a NULL/0 = korlátlan logikával:
```python
async def _calculate_commission(
amount: float,
percent: Optional[float],
max_amount: Optional[float],
) -> float:
"""
Calculate commission amount from a percentage, capped by max_amount.
NULL or 0 means unlimited (no cap).
"""
if not percent or percent <= 0:
return 0.0
commission = float(Decimal(str(amount)) * Decimal(str(percent)) / Decimal("100"))
# Phase 2: NULL or 0 means unlimited
if max_amount is not None and max_amount > 0:
commission = min(commission, float(max_amount))
return round(commission, 2)
```
**Változás a `distribute_commission()` hívásánál:**
```python
# Gen1: use gen1_max_amount (fallback: commission_max_amount)
gen1_cap = float(rule.gen1_max_amount) if rule.gen1_max_amount is not None else (
float(rule.commission_max_amount) if rule.commission_max_amount else None
)
gen1_amount = await _calculate_commission(
request.transaction_amount,
float(rule.commission_percent) if rule.commission_percent else None,
gen1_cap,
)
# Gen2: use gen2_max_amount (fallback: commission_max_amount)
gen2_cap = float(rule.gen2_max_amount) if rule.gen2_max_amount is not None else (
float(rule.commission_max_amount) if rule.commission_max_amount else None
)
gen2_amount = await _calculate_commission(
request.transaction_amount,
float(rule.upline_commission_percent) if rule.upline_commission_percent else None,
gen2_cap,
)
```
### 2.5 Inaktív Gen2 Megelőzés (Business Rule)
#### Cél
Ha Gen1 (a közvetlen ajánló) inaktív, akkor Gen2 (az upline) **nem kap jutalékot**.
#### Logika menete (`distribute_commission()`-ben)
A Gen2 számítás ELŐTT:
```python
# Phase 2: Inactive Gen2 Prevention
# If Gen1 is inactive (is_active=False or subscription expired),
# Gen2 does NOT receive commission
gen1_is_active = gen1.is_active
gen1_subscription_active = (
gen1.subscription_expires_at is None or
gen1.subscription_expires_at > datetime.utcnow()
)
if not gen1_is_active or not gen1_subscription_active:
logger.info(
"Commission distribution: Gen1 user %d is inactive. Gen2 SKIPPED.",
gen1.id,
)
gen2_amount = 0.0 # Skip Gen2 entirely
```
### 2.6 Reaktivációs Szabály (180 nap)
#### Cél
Ha Gen1 180 napig inaktív volt (nincs tranzakciója), akkor a következő vásárlását **új ügyfélként** kell kezelni.
#### Logika menete
```python
async def _is_user_recently_active(
db: AsyncSession,
user_id: int,
inactivity_days: int = 180,
) -> bool:
"""
Check if a user has been active within the last inactivity_days days.
Checks: User.is_active flag, latest FinancialLedger entry, subscription status.
"""
user_stmt = select(User).where(User.id == user_id)
result = await db.execute(user_stmt)
user = result.scalar_one_or_none()
if not user or user.is_deleted:
return False
# Check if user has any transaction in the last inactivity_days
cutoff = datetime.utcnow() - timedelta(days=inactivity_days)
ledger_stmt = (
select(FinancialLedger)
.where(
FinancialLedger.user_id == user_id,
FinancialLedger.created_at >= cutoff,
)
.limit(1)
)
ledger_result = await db.execute(ledger_stmt)
has_recent_transaction = ledger_result.scalar_one_or_none() is not None
if has_recent_transaction:
return True
# Check subscription
if user.subscription_expires_at and user.subscription_expires_at > datetime.utcnow():
return True
return False
```
### 2.7 Wallet Integráció (A Legfontosabb Változás)
#### Cél
A `distribute_commission()` ne csak számoljon, hanem **ténylegesen írja jóvá** a jutalékot a megfelelő felhasználók Wallet-jében, és **naplózza** a FinancialLedger-ben.
#### Implementáció
A `GamificationService._add_earned_credits()` (bizonyított minta) alapján:
```python
async def _credit_commission_to_wallet(
db: AsyncSession,
user_id: int,
amount: float,
transaction_type: str,
details: dict,
) -> None:
"""
Credit commission to user's Wallet (earned_credits) and log in FinancialLedger.
Follows the proven pattern from GamificationService._add_earned_credits().
"""
wallet_stmt = select(Wallet).where(Wallet.user_id == user_id)
result = await db.execute(wallet_stmt)
wallet = result.scalar_one_or_none()
if not wallet:
logger.warning(f"Wallet not found for user_id={user_id}, cannot credit commission")
return
# 1. Update wallet balance
wallet.earned_credits += Decimal(str(amount))
# 2. Create FinancialLedger CREDIT entry
ledger_entry = FinancialLedger(
user_id=user_id,
amount=Decimal(str(amount)),
entry_type=LedgerEntryType.CREDIT,
wallet_type=WalletType.EARNED,
transaction_type=transaction_type,
details=details,
balance_after=float(wallet.earned_credits),
currency="EUR",
)
db.add(ledger_entry)
```
**A `distribute_commission()` módosítása:**
```python
# Phase 2: Wallet Credit
if gen1_amount > 0:
await _credit_commission_to_wallet(
db,
user_id=gen1.id,
amount=gen1_amount,
transaction_type="COMMISSION_GEN1",
details={
"description": "Gen1 commission on transaction",
"buyer_user_id": request.buyer_user_id,
"transaction_amount": request.transaction_amount,
"rule_id": rule.id,
"commission_percent": float(rule.commission_percent),
},
)
if gen2_amount > 0:
await _credit_commission_to_wallet(
db,
user_id=gen2.id,
amount=gen2_amount,
transaction_type="COMMISSION_GEN2",
details={
"description": "Gen2 (upline) commission on transaction",
"buyer_user_id": request.buyer_user_id,
"gen1_user_id": gen1.id,
"transaction_amount": request.transaction_amount,
"rule_id": rule.id,
"commission_percent": float(rule.upline_commission_percent),
},
)
# Phase 2: Commit
await db.commit()
```
### 2.8 Megújítási Jutalék (Renewal) Logika
#### Cél
A megújítási (renewal) tranzakcióknál:
- Gen1 kapja a `renewal_commission_percent`-et (ha CONTRACTED tier)
- Gen2 kapja a `gen2_renewal_percent`-et (az új mezőből)
#### Logika
```python
# A distribute_commission()-ben, ha a tranzakció típusa "renewal":
is_renewal = getattr(request, 'is_renewal', False)
if is_renewal:
gen1_percent = float(rule.renewal_commission_percent) if rule.renewal_commission_percent else None
gen2_percent = float(rule.gen2_renewal_percent) if rule.gen2_renewal_percent is not None else (
float(rule.upline_commission_percent) if rule.upline_commission_percent else None
)
else:
gen1_percent = float(rule.commission_percent) if rule.commission_percent else None
gen2_percent = float(rule.upline_commission_percent) if rule.upline_commission_percent else None
```
**Schema módosítás:** A `CommissionDistributionRequest`-hez hozzáadni:
```python
class CommissionDistributionRequest(BaseModel):
# ... existing fields ...
is_renewal: bool = Field(False, description="TRUE = renewal transaction, FALSE = first-time purchase")
```
---
## Folyamatábra (Mermaid)
```mermaid
flowchart TD
A[distribute_commission hívás] --> B[Buyer lookup]
B --> C{Van referrer?}
C -->|Nincs| D[Return empty - nincs jutalék]
C -->|Van Gen1| E[Gen1 lookup]
E --> F{Gen1 aktív?}
F -->|Nem| D
F -->|Igen| G[Resolve L2_COMMISSION rule]
G --> H{Rule található?}
H -->|Nem| D
H -->|Igen| I{A tranzakcio renewal?}
I -->|Igen| J[Hasznalj renewal_commission_percent + gen2_renewal_percent]
I -->|Nem| K[Hasznalj commission_percent + upline_commission_percent]
J --> L[Calculate Gen1 commission with gen1_max_amount cap]
K --> L
L --> M{Gen1 recently active? <180 nap?}
M -->|Nem| N[Gen1 inaktiv - normal jutalek, reaktivacio flag]
M -->|Igen| O[Gen1 aktiv - normal folyamat]
N --> P[Calculate Gen2 commission with gen2_max_amount cap]
O --> P
P --> Q{Gen1 aktiv Subscription?}
Q -->|Nem| R[Gen2 jutalek SKIP - inaktiv Gen2 prevention]
Q -->|Igen| S[Gen2 jutalek jar]
R --> T[Credit Gen1 wallet + FinancialLedger]
S --> U[Credit Gen1 wallet + FinancialLedger + Credit Gen2 wallet + FinancialLedger]
T --> V[Commit + Return response]
U --> V
```
---
## Érintett Fájlok (Files to Modify)
| # | Fájl | Módosítás Típusa |
|---|------|-------------------|
| 1 | `backend/app/models/marketplace/commission.py` | Modell bővítés: gen1_max_amount, gen2_max_amount, gen2_renewal_percent |
| 2 | `backend/app/schemas/commission.py` | Schema bővítés: új mezők Create/Update/Response DTO-khoz, is_renewal |
| 3 | `backend/app/services/commission_service.py` | Szolgáltatás bővítés: _credit_commission_to_wallet(), _is_user_recently_active(), módosított logika |
| 4 | `backend/backend/tests/active/seed_commission_test_data.py` | Teszt seed adat frissítés |
| 5 | `backend/backend/tests/active/test_commission_distribution.py` | Teszt bővítés Wallet/FinancialLedger ellenőrzéssel |
---
## Futtatási Sorrend (Execution Order)
1. Modell módosítás -> CommissionRule új mezők
2. Schema módosítás -> Pydantic DTO-k bővítése
3. Service módosítás -> _credit_commission_to_wallet(), _is_user_recently_active(), módosított logika
4. Sync Engine futtatás -> docker exec -it sf_api python -m app.scripts.sync_engine
5. Teszt seed adat frissítés
6. Teszt futtatás -> test_commission_distribution.py
7. Manuális verifikáció -> API hívás + DB ellenőrzés
---
## Tesztelési Terv
### 1. Alap funkcionális tesztek
- Ellenőrizni, hogy a Wallet earned_credits megváltozott a tranzakció után
- Ellenőrizni, hogy a FinancialLedger-ben megjelentek a CREDIT bejegyzések
### 2. Plafon tesztek (Cap)
- gen1_max_amount=3000 esetén Gen1 5000 helyett 3000-et kap
- gen2_max_amount=0 esetén Gen2 korlátlan (nincs plafon)
- gen1_max_amount=NULL esetén a régi commission_max_amount a fallback
### 3. Inaktivitás tesztek
- Ha Gen1 is_active=False, Gen2 nem kap jutalékot
- Ha Gen1 subscription lejárt, Gen2 nem kap jutalékot
### 4. Megújítási teszt
- is_renewal=True esetén Gen1 a renewal_commission_percent-et kapja
- is_renewal=True esetén Gen2 a gen2_renewal_percent-et kapja
---
## Kockázatok és Megfontolások
1. **Adatbázis Migráció**: Az új mezők nullable=True-val történnek, a meglévő rule-ok változatlanul működnek.
2. **Visszamenőleges kompatibilitás**: A régi API hívások továbbra is működnek.
3. **Wallet írás hibakezelés**: Ha a Wallet nem található, logger.warning + a commission response-ba hiba jelzés.
4. **Tranzakció biztonság**: A distribute_commission() jelenleg NEM használ atomi tranzakciót - ez fontos lehet a Wallet írás biztonságához.
---
## Jóváhagyás
**Kérem a felhasználó jóváhagyását a fenti tervhez.** A jóváhagyás után a Code módba kapcsolok és megkezdem az implementációt.