admin_nyelvi_javítas
This commit is contained in:
441
plans/logic_spec_mlm_phase2_commission_wallet_integration.md
Normal file
441
plans/logic_spec_mlm_phase2_commission_wallet_integration.md
Normal 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.
|
||||
Reference in New Issue
Block a user