530 lines
29 KiB
Markdown
530 lines
29 KiB
Markdown
# Entwicklungsplan: Incentive-System (Montenegro 2026)
|
||
|
||
**Erstellt**: 17.03.2026
|
||
**Frist**: Fertigstellung innerhalb 2 Wochen (bis 31.03.2026)
|
||
**Ziel**: Wiederverwendbares Incentive-/Gewinnspiel-System, losgelöst von allen anderen Berechnungen.
|
||
|
||
**Kurzüberblick für Einstieg:** siehe **`README.md`** im gleichen Ordner.
|
||
|
||
---
|
||
|
||
## 1. Datenbank-Design (3 neue Tabellen)
|
||
|
||
### Tabelle `incentives` – Master-Konfiguration
|
||
|
||
| Feld | Typ | Beschreibung |
|
||
|-------------------------|------------------|--------------------------------------------------|
|
||
| id | bigint PK | |
|
||
| name | string | z.B. "Montenegro Incentive 2026" |
|
||
| slug | string unique | URL-Slug (Eloquent-Sluggable) |
|
||
| description | text nullable | Erklärungstext für Berater-Seite |
|
||
| image | string nullable | Dateiname des statischen Bildes |
|
||
| terms | text nullable | Teilnahmebedingungen |
|
||
| qualification_start | date | Beginn Qualifikationszeitraum (z.B. 2026-04-01) |
|
||
| qualification_end | date | Ende Qualifikationszeitraum (z.B. 2026-07-31) |
|
||
| calculation_end | date | Ende Punkteberechnung (z.B. 2026-08-31) |
|
||
| points_partner_onetime | int default 600 | Einmalpunkte pro Neupartner |
|
||
| points_abo_onetime | int default 400 | Einmalpunkte pro Kundenabo |
|
||
| min_direct_partners | int default 4 | Mindestanzahl direkte Teampartner |
|
||
| min_customer_abos | int default 6 | Mindestanzahl Kundenabos |
|
||
| max_winners | int default 30 | Maximale Anzahl Gewinner |
|
||
| status | enum | draft / active / closed |
|
||
| timestamps | | |
|
||
| soft_deletes | | |
|
||
|
||
### Tabelle `incentive_participants` – Opt-in + aggregierte Werte
|
||
|
||
| Feld | Typ | Beschreibung |
|
||
|----------------------|------------------|-------------------------------------------|
|
||
| id | bigint PK | |
|
||
| incentive_id | FK | |
|
||
| user_id | FK | Berater |
|
||
| accepted_terms_at | datetime | Zeitpunkt der Teilnahme-Bestätigung |
|
||
| total_points | int default 0 | Gesamtpunkte (Cache für Ranking) |
|
||
| qualified_partners | int default 0 | Anzahl qualifizierter Neupartner |
|
||
| qualified_abos | int default 0 | Anzahl qualifizierter Kundenabos |
|
||
| is_qualified | bool default false | Mindestqualifikation erreicht? |
|
||
| rank | int nullable | Aktuelle Platzierung |
|
||
| timestamps | | |
|
||
| unique(incentive_id, user_id) | | |
|
||
|
||
### Tabelle `incentive_points_log` – Detaillierte Punktehistorie
|
||
|
||
| Feld | Typ | Beschreibung |
|
||
|------------------------|------------------|---------------------------------------------|
|
||
| id | bigint PK | |
|
||
| participant_id | FK | → incentive_participants |
|
||
| type | enum | partner / abo |
|
||
| source_type | string | Polymorphic: User oder UserAbo |
|
||
| source_id | int | ID des Neupartners oder des Abos |
|
||
| source_label | string | Name/Bezeichnung für Anzeige |
|
||
| month | tinyint | Bezugsmonat |
|
||
| year | smallint | Bezugsjahr |
|
||
| points_onetime | int default 0 | Einmalpunkte (600 bzw. 400) |
|
||
| points_accumulated | int default 0 | Akkumulierte Punkte des Monats |
|
||
| is_storno | bool default false | Storno-Eintrag? |
|
||
| storno_of_id | FK nullable | Referenz auf stornierten Eintrag |
|
||
| user_sales_volume_id | FK nullable | Verknüpfung zur Quelle |
|
||
| timestamps | | |
|
||
|
||
---
|
||
|
||
## 2. Models
|
||
|
||
### `Incentive` (app/Models/Incentive.php)
|
||
- Casts: status als Enum, dates als Carbon
|
||
- Relationships: `participants()` hasMany, `activeParticipants()`
|
||
- Scopes: `scopeActive()`, `scopeInQualificationPeriod()`
|
||
- Sluggable-Konfiguration für URL-Slug
|
||
- Hilfsmethoden: `isActive()`, `isInQualificationPeriod()`, `isInCalculationPeriod()`
|
||
|
||
### `IncentiveParticipant` (app/Models/IncentiveParticipant.php)
|
||
- Relationships: `incentive()` belongsTo, `user()` belongsTo, `pointsLog()` hasMany
|
||
- Scopes: `scopeQualified()`, `scopeWinners()` (qualified + rank <= max_winners)
|
||
- Methode: `recalculatePoints()`, `checkQualification()`
|
||
|
||
### `IncentivePointsLog` (app/Models/IncentivePointsLog.php)
|
||
- Relationships: `participant()` belongsTo, `salesVolume()` belongsTo
|
||
- Scopes: `scopePartner()`, `scopeAbo()`, `scopeActive()` (nicht storniert)
|
||
|
||
---
|
||
|
||
## 3. Service-Architektur
|
||
|
||
### `IncentiveTracker` (app/Services/Incentive/IncentiveTracker.php)
|
||
|
||
Wird aus bestehenden Klassen aufgerufen. Enthält statische Methoden:
|
||
|
||
#### `trackNewPartner(ShoppingOrder $shopping_order)`
|
||
- Wird aufgerufen in: `Payment::paymentStatusPaidAction()` bei **`payment_for == 1`** (Wizard-/Registrierungskauf)
|
||
- **Zusätzliche Bedingung:** `ShoppingOrder::qualifiesForIncentiveTrackedPartner()` — bezahlte Registrierung **und** mindestens eine Bestellposition mit Produkt, das **keine** reine Mitgliedschaft ohne Starterpaket ist (`Product::is_membership_only === false`). Siehe `wherePaidRegistrationIncludesStarterKit()` auf `ShoppingOrder`.
|
||
- Logik (Kern):
|
||
1. Neuer User aus `auth_user_id`; Sponsor: `User::m_sponsor`
|
||
2. Aktive Incentives im Registrierungszeitpunkt (Qualifikationszeitraum)
|
||
3. Sponsor muss `IncentiveParticipant` haben
|
||
4. `incentive_new_partners` (Tracking) + `incentive_points_log` (Einmalpunkte) + `recalculateFromTrackingTables()` + Ranking
|
||
|
||
#### `trackAboActivated(ShoppingOrder $shopping_order)`
|
||
- Wird aufgerufen in: `Payment::paymentStatusPaidAction()` (nach `AboHelper::setAboActive`, Zeile 280)
|
||
- **Wichtig**: Nur wenn `setAboActive` mit `status=2, paid=true` aufgerufen wird
|
||
- Bedingung: Abo `is_for === 'ot'` (Kundenabo, nicht eigenes Berater-Abo)
|
||
- Logik:
|
||
1. Berater ermitteln: `UserAbo->user_id` (= auth_user_id des Sponsors)
|
||
2. Prüfen ob Berater Teilnehmer eines aktiven Incentives ist
|
||
3. Prüfen ob Abo-Abschluss im Qualifikationszeitraum liegt
|
||
4. `incentive_points_log` anlegen: type=abo, source=UserAbo, points_onetime=400
|
||
5. `incentive_participants.qualified_abos` inkrementieren
|
||
6. Qualifikation + Ranking aktualisieren
|
||
|
||
#### `trackSalesVolume(UserSalesVolume $user_sales_volume)`
|
||
- Wird aufgerufen in: `InvoiceRepository::createAndSalesVolume()` (nach Erzeugung des `UserSalesVolume`)
|
||
- Logik:
|
||
1. **Pfad A:** `user_sales_volume.user_id` ist ein **getrackter Neupartner** (`IncentiveNewPartner`) → akkumulierte Punkte zum Sponsor-Teilnehmer (Log-Typ `partner`)
|
||
2. **Pfad B:** Bestellung gehört zu einem **getrackten Kundenabo** (`IncentiveNewAbo` über `UserAbo` `is_for = 'ot'`) → akkumulierte Punkte (Log-Typ `abo`)
|
||
3. Monat/Jahr im Berechnungs-Scope des jeweiligen Incentives (`isDateInScope`)
|
||
4. `recalculateFromTrackingTables()` + Ranking
|
||
|
||
#### `trackStorno(UserSalesVolume $original, UserSalesVolume $cancellation)`
|
||
- Wird aufgerufen in: `SalesPointsVolume::cancelSalesPointsVolume()` (nach Zeile 312)
|
||
- Logik:
|
||
1. Prüfen ob original `user_sales_volume_id` in `incentive_points_log` existiert
|
||
2. Storno-Eintrag anlegen: `is_storno=true`, negative Punkte, `storno_of_id` verknüpfen
|
||
3. Gesamtpunkte + Qualifikation + Ranking aktualisieren
|
||
4. Bei Storno eines Partners/Abos: `qualified_partners` bzw. `qualified_abos` dekrementieren
|
||
|
||
---
|
||
|
||
### `IncentiveCalculationService` (app/Services/Incentive/IncentiveCalculationService.php)
|
||
|
||
Batch-Berechnung als Sicherheitsnetz (Cron + manuelle Auslösung):
|
||
|
||
#### `recalculate(Incentive $incentive, bool $force = false)`
|
||
- Iteriert über alle Teilnehmer
|
||
- Für jeden Teilnehmer:
|
||
1. Neupartner ermitteln: Users mit `m_sponsor = participant.user_id`, registriert im Qualifikationszeitraum, mit bezahltem Starterpaket
|
||
2. Kundenabos ermitteln: `UserAbo` mit `user_id = participant.user_id`, `is_for = 'ot'`, `status = 2`, erstellt im Qualifikationszeitraum
|
||
3. Akkumulierte Punkte: `UserSalesVolume` pro Monat (Qualifikationsstart bis calculation_end)
|
||
4. Stornos berücksichtigen (status=6 Einträge)
|
||
5. `incentive_points_log` aktualisieren (bei --force: löschen + neu anlegen)
|
||
6. `incentive_participants` Summen + Qualifikation aktualisieren
|
||
|
||
#### `updateRanking(Incentive $incentive)`
|
||
- Alle Teilnehmer nach `total_points` DESC sortieren
|
||
- `rank` fortlaufend vergeben (1, 2, 3, ...)
|
||
- `is_qualified` prüfen: `qualified_partners >= min_direct_partners AND qualified_abos >= min_customer_abos`
|
||
|
||
---
|
||
|
||
## 4. Integrationspunkte (Hooks in bestehenden Code)
|
||
|
||
### Hook 1: Neupartner-Registrierung bezahlt
|
||
**Datei**: `app/Services/Payment.php` → `paymentStatusPaidAction()`
|
||
**Bedingung**: `payment_for == 1` (entspricht Wizard laut `ShoppingUser::getOrderPaymentFor()`).
|
||
**Intern:** `trackNewPartner` bricht ab, wenn keine Starterpaket-Position vorliegt (`qualifiesForIncentiveTrackedPartner()`).
|
||
|
||
### Hook 2: Kundenabo aktiviert (Zahlung bestätigt)
|
||
**Datei**: `app/Services/Payment.php` → `paymentStatusPaidAction()`
|
||
**Position**: Nach Zeile 280 (nach `AboHelper::setAboActive`)
|
||
**Code**:
|
||
```php
|
||
// Incentive: Track activated customer abo
|
||
if ($shopping_order->is_abo) {
|
||
IncentiveTracker::trackAboActivated($shopping_order);
|
||
}
|
||
```
|
||
|
||
### Hook 3: Sales Volume erstellt
|
||
**Datei**: `app/Repositories/InvoiceRepository.php` → `createAndSalesVolume()`
|
||
**Position**: Nach Zeile 223 (nach Verknüpfung Sales Volume ↔ Invoice)
|
||
**Code**:
|
||
```php
|
||
// Incentive: Track sales volume points
|
||
IncentiveTracker::trackSalesVolume($this->user_sales_volume);
|
||
```
|
||
|
||
### Hook 4: Storno
|
||
**Datei**: `app/Services/BusinessPlan/SalesPointsVolume.php` → `cancelSalesPointsVolume()`
|
||
**Position**: Nach Zeile 312 (nach Recalculation)
|
||
**Code**:
|
||
```php
|
||
// Incentive: Track storno
|
||
IncentiveTracker::trackStorno($original_sales_volume, $cancellation_sales_volume);
|
||
```
|
||
|
||
---
|
||
|
||
## 5. Admin-Bereich
|
||
|
||
### Controller: `app/Http/Controllers/Admin/IncentiveController.php`
|
||
|
||
| Route | Methode | Beschreibung |
|
||
|----------------------------------------|---------------|---------------------------------------|
|
||
| GET `/admin/incentive` | index | Liste aller Incentives (DataTable) |
|
||
| GET `/admin/incentive/create` | create | Anlageformular (Spatie\Html) |
|
||
| POST `/admin/incentive` | store | Speichern |
|
||
| GET `/admin/incentive/{id}` | show | Detail + Teilnehmer-Ranking |
|
||
| GET `/admin/incentive/{id}/edit` | edit | Bearbeiten |
|
||
| PUT `/admin/incentive/{id}` | update | Aktualisieren |
|
||
| POST `/admin/incentive/{id}/recalculate` | recalculate | Neuberechnung auslösen |
|
||
|
||
### Views
|
||
- `resources/views/admin/incentive/index.blade.php` – DataTable mit allen Incentives
|
||
- `resources/views/admin/incentive/create.blade.php` – Formular (Spatie\Html)
|
||
- `resources/views/admin/incentive/edit.blade.php` – Bearbeitungsformular
|
||
- `resources/views/admin/incentive/show.blade.php` – Detail: Konfiguration + Ranking-Tabelle aller Teilnehmer
|
||
|
||
---
|
||
|
||
## 6. Berater-Frontend (User-Bereich)
|
||
|
||
### Controller: `app/Http/Controllers/User/IncentiveController.php`
|
||
|
||
| Route | Methode | Beschreibung |
|
||
|--------------------------------------|---------------|-----------------------------------------|
|
||
| GET `/incentive/{slug}` | show | Incentive-Seite mit Ranking |
|
||
| POST `/incentive/{slug}/participate` | participate | Teilnahme bestätigen (Terms akzeptieren)|
|
||
| GET `/incentive/{slug}/details` | details | Persönliche Berechnungsübersicht |
|
||
|
||
### View: Incentive-Seite (`user/incentive/show.blade.php`)
|
||
|
||
**Layout:**
|
||
1. **Header**: Statisches Incentive-Bild
|
||
2. **Beschreibung**: Erklärungstext aus DB
|
||
3. **Teilnahme-Box** (nur wenn noch nicht teilgenommen):
|
||
- Checkbox: "Ich akzeptiere die Teilnahmebedingungen" (Link zu Terms)
|
||
- Button: "Jetzt teilnehmen"
|
||
4. **Ranking-Tabelle** (nur Teilnehmer sichtbar):
|
||
|
||
| Rang | Name | Punkte | Partner | Abos | Status |
|
||
|------|------------------|--------|---------|-------|------------------|
|
||
| 1 | **Max Muster** | **4800** | **5/4 ✓** | **8/6 ✓** | **🏆 Gewinner** |
|
||
| 2 | **Anna Beispiel** | **4200** | **4/4 ✓** | **7/6 ✓** | **🏆 Gewinner** |
|
||
| ... | ... | ... | ... | ... | ... |
|
||
| 31 | **Klaus Test** | **1200** | **4/4 ✓** | **6/6 ✓** | Qualifiziert |
|
||
| 32 | Peter Demo | 900 | 3/4 | 4/6 | Offen |
|
||
|
||
**Darstellungslogik:**
|
||
- **Normal** (nicht fett): Mindestqualifikation NICHT erreicht
|
||
- **Fett**: Mindestqualifikation erreicht (≥4 Partner + ≥6 Abos)
|
||
- **Fett + farbig hinterlegt (Gold/Grün)**: Qualifiziert + Rang ≤ max_winners → aktueller Gewinner
|
||
|
||
### View: Berechnungsübersicht (`user/incentive/details.blade.php`)
|
||
|
||
**Sektion A: Neupartner-Punkte**
|
||
|
||
| Neupartner | Einstieg | Einmalig | Apr | Mai | Jun | Jul | Aug | Gesamt |
|
||
|-----------------|-----------|----------|------|------|------|------|------|--------|
|
||
| Max Muster | April | 600 | 120 | 150 | 130 | 140 | 110 | 1250 |
|
||
| Anna Beispiel | Juni | 600 | – | – | 80 | 90 | 100 | 870 |
|
||
| **Zwischensumme** | | | | | | | | **2120** |
|
||
|
||
**Sektion B: Kundenabo-Punkte**
|
||
|
||
| Kundenabo | Abschluss | Einmalig | Apr | Mai | Jun | Jul | Aug | Gesamt |
|
||
|-----------------|-----------|----------|------|------|------|------|------|--------|
|
||
| Abo #1 (Müller) | April | 400 | 80 | 80 | 80 | 80 | 80 | 800 |
|
||
| Abo #2 (Schmidt)| Mai | 400 | – | 60 | 60 | 60 | 60 | 640 |
|
||
| **Zwischensumme** | | | | | | | | **1440** |
|
||
|
||
**Gesamtpunkte: 3560**
|
||
|
||
---
|
||
|
||
## 7. Artisan Command
|
||
|
||
### `php artisan incentive:calculate {incentive_id?} [--force]`
|
||
|
||
- Ohne `incentive_id`: Berechnet alle aktiven Incentives
|
||
- Mit `incentive_id`: Berechnet nur das angegebene Incentive
|
||
- `--force`: Löscht bestehende Logs und berechnet komplett neu aus Quelldaten
|
||
- **Cron**: Täglich um 05:00 (nach business:store-optimized um 03:00)
|
||
|
||
---
|
||
|
||
## 8. Localization
|
||
|
||
`resources/lang/{de,en,es}/incentive.php`
|
||
|
||
Enthält alle UI-Strings für Admin- und Berater-Bereich.
|
||
|
||
---
|
||
|
||
## 9. Tests (Pest)
|
||
|
||
| Test | Typ | Beschreibung |
|
||
|-------------------------------|---------|--------------------------------------------------------|
|
||
| Neupartner-Tracking | Feature | Partner registriert + bezahlt → 600 Einmalpunkte |
|
||
| Abo nur bei Aktivierung | Feature | Abo angelegt aber nicht bezahlt → keine Punkte |
|
||
| Abo bei Aktivierung | Feature | Abo bezahlt (status=2) → 400 Einmalpunkte |
|
||
| Nur Kundenabos zählen | Feature | Eigenes Berater-Abo (is_for=me) → keine Punkte |
|
||
| Akkumulierte Punkte | Feature | Monatliche Sales Volume → korrekt pro Monat summiert |
|
||
| Storno | Feature | Storno → negative Punkte, Qualifikation angepasst |
|
||
| Qualifikation erreicht | Unit | 4 Partner + 6 Abos → is_qualified=true |
|
||
| Qualifikation nicht erreicht | Unit | 3 Partner + 6 Abos → is_qualified=false |
|
||
| Ranking-Sortierung | Unit | Teilnehmer korrekt nach Punkten sortiert |
|
||
| Top-N Gewinner | Unit | Nur qualifizierte in Top 30 als Gewinner markiert |
|
||
| Opt-in erforderlich | Feature | Nicht-Teilnehmer nicht im Ranking |
|
||
| Terms-Akzeptanz | Feature | Teilnahme ohne Checkbox → Fehler |
|
||
| Admin CRUD | Feature | Incentive anlegen/bearbeiten/anzeigen |
|
||
| Zeitraum-Validierung | Unit | Events außerhalb Qualifikationszeitraum → ignoriert |
|
||
| Batch-Neuberechnung | Feature | Artisan Command berechnet korrekt |
|
||
|
||
---
|
||
|
||
## 10. Implementierungs-Reihenfolge
|
||
|
||
### Phase 1: Fundament (Tag 1-2) -- ERLEDIGT 17.03.2026
|
||
- [x] Migrations erstellen (3 Tabellen: incentives, incentive_participants, incentive_points_log)
|
||
- [x] Models erstellen (Incentive, IncentiveParticipant, IncentivePointsLog)
|
||
- [x] Migrations ausgeführt
|
||
- [x] Factories für Tests
|
||
|
||
### Phase 2: Kern-Logik (Tag 3-5) -- ERLEDIGT 17.03.2026
|
||
- [x] `IncentiveTracker` Service implementieren (`app/Services/Incentive/IncentiveTracker.php`)
|
||
- trackNewPartner: Hook nach Registration-Payment (payment_for=1)
|
||
- trackAboActivated: Hook nach AboHelper::setAboActive (is_for='ot')
|
||
- trackSalesVolume: Hook nach InvoiceRepository::createAndSalesVolume
|
||
- trackStorno: Hook nach SalesPointsVolume::cancelSalesPointsVolume
|
||
- updateRanking: Ranking-Aktualisierung
|
||
- [x] `IncentiveCalculationService` implementieren (`app/Services/Incentive/IncentiveCalculationService.php`)
|
||
- recalculate: Batch-Berechnung aller Teilnehmer via `recalculateFromSource()`
|
||
- recalculateParticipant: Delegiert an `IncentiveParticipant::recalculateFromSource()`
|
||
- [x] Hooks in bestehende Klassen eingebaut:
|
||
- `app/Services/Payment.php`: trackNewPartner + trackAboActivated
|
||
- `app/Repositories/InvoiceRepository.php`: trackSalesVolume
|
||
- `app/Services/BusinessPlan/SalesPointsVolume.php`: trackStorno
|
||
- [x] Artisan Command `incentive:calculate` (`app/Console/Commands/IncentiveCalculate.php`)
|
||
- [x] Cron-Eintrag in `app/Console/Kernel.php` (täglich 05:00)
|
||
|
||
### Phase 3: Admin-Backend (Tag 6-7) -- ERLEDIGT 17.03.2026
|
||
- [x] Admin Controller (`app/Http/Controllers/Admin/IncentiveController.php`)
|
||
- CRUD: index, create, store, show, edit, update
|
||
- DataTable für Listenansicht
|
||
- Neuberechnung (normal + force)
|
||
- [x] Admin Views:
|
||
- `resources/views/admin/incentive/index.blade.php` (DataTable)
|
||
- `resources/views/admin/incentive/create.blade.php`
|
||
- `resources/views/admin/incentive/edit.blade.php`
|
||
- `resources/views/admin/incentive/_form.blade.php` (Shared Formular)
|
||
- `resources/views/admin/incentive/show.blade.php` (Detail + Ranking)
|
||
- [x] Admin Routes in `routes/domains/crm.php` (admin_incentive_*)
|
||
|
||
### Phase 4: Berater-Frontend (Tag 8-9) -- ERLEDIGT 17.03.2026
|
||
- [x] User Controller (`app/Http/Controllers/User/IncentiveController.php`)
|
||
- show: Incentive-Seite mit Ranking
|
||
- participate: Teilnahme mit Terms-Akzeptanz
|
||
- details: Persönliche Berechnungsübersicht
|
||
- [x] Ranking-View mit Qualifikations-Hervorhebung (`resources/views/user/incentive/show.blade.php`)
|
||
- Normal = nicht qualifiziert, Fett = qualifiziert, Grün+Fett = Gewinner (Top N)
|
||
- Eigener Rang hervorgehoben (table-primary)
|
||
- [x] Berechnungsübersicht-View (`resources/views/user/incentive/details.blade.php`)
|
||
- Sektion A: Neupartner mit monatlicher Aufschlüsselung
|
||
- Sektion B: Kundenabos mit monatlicher Aufschlüsselung
|
||
- Zusammenfassung: Gesamtpunkte, Rang, Qualifikationsstatus
|
||
- [x] Teilnahme-Flow (Checkbox + Terms mit Collapse)
|
||
- [x] User Routes in `routes/domains/crm.php` (user_incentive_*)
|
||
|
||
### Phase 5: Abschluss (Tag 10) -- ERLEDIGT 17.03.2026
|
||
- [x] Localization (de/en/es) in `resources/lang/{de,en,es}/incentive.php`
|
||
- [x] `./vendor/bin/pint` Code formatiert
|
||
- [x] Architektur-Refactoring: Eigene Tracking-Tabellen statt Live-Abfragen auf Quelltabellen
|
||
- Neue Tabellen: `incentive_new_partners`, `incentive_new_abos`
|
||
- Neue Models: `IncentiveNewPartner`, `IncentiveNewAbo`
|
||
- `IncentiveParticipant::recalculateFromTrackingTables()`: Berechnet aus eigenen Tabellen
|
||
- `IncentiveParticipant::rebuildFromSourceTables()`: Force-Rebuild aus Quelltabellen
|
||
- `IncentiveTracker`: Insert in Tracking-Tabellen + Log + `recalculateFromTrackingTables()`
|
||
- `IncentiveCalculationService`: Normal=TrackingTables, Force=RebuildFromSource
|
||
- Details-View: Partner/Abos aus Tracking-Tabellen statt aus Log gruppiert
|
||
- [x] Tests geschrieben + ausgeführt: **27 Tests, 55 Assertions, alle bestanden**
|
||
- `tests/Unit/Incentive/IncentiveModelTest.php` (19 Tests)
|
||
- Zeitraum-Prüfungen (Qualifikation, Berechnung, Scope)
|
||
- Status-Erkennung (draft/active/closed)
|
||
- Qualifikations-Logik (bestanden/nicht bestanden, Grenzwerte)
|
||
- Winner-Logik (Rang-Grenzen, Nicht-Qualifiziert)
|
||
- PointsLog Helpers
|
||
- `tests/Unit/Incentive/IncentiveTrackerTest.php` (8 Tests)
|
||
- Ranking-Sortierung nach Punkten
|
||
- Log-Typ-Erkennung (partner/abo)
|
||
- Qualifikations-Änderungen bei Storno
|
||
- Edge Cases (max_winners=1, negative Punkte)
|
||
- Factories: `IncentiveFactory`, `IncentiveParticipantFactory`, `IncentivePointsLogFactory`
|
||
- Hinweis: Unit-Tests ohne DB (bestehende countries-Migration hat SQLite-Inkompatibilität)
|
||
- [ ] Manueller Test mit Testdaten
|
||
|
||
---
|
||
|
||
## 11. Verfeinerungen nach Erstrelease (März 2026)
|
||
|
||
### Ranglisten & Darstellung (User + Admin)
|
||
|
||
- **`orderByIncentiveLeaderboard()`** (`IncentiveParticipant`): Sortierung: `is_qualified` absteigend → Rang mit Wert vor `NULL` → `rank` aufsteigend → `total_points` absteigend (qualifizierte zuerst; ohne Rang unten).
|
||
- **`orderByRankNullsLast()`**: nur Rang-Sortierung (NULL zuletzt), falls separat benötigt.
|
||
- **User-Rangliste:** `limit` = `max(1, incentive.max_winners)` — dynamisch statt fest 30.
|
||
- **`withRankingActivity()`:** In der User-Tabelle werden nur Teilnehmer mit mindestens einem qualifizierten Partner, einem Kunden-Abo oder `total_points > 0` gelistet.
|
||
|
||
### Neupartner nur mit Starterpaket
|
||
|
||
- **`ShoppingOrder::wherePaidRegistrationIncludesStarterKit()`** / **`qualifiesForIncentiveTrackedPartner()`**: Registrierungsbestellung (`payment_for = 1`) mit bezahlter Zahlung **und** mindestens einer Position, deren Produkt **`is_membership_only = false`**.
|
||
- Verwendung in: `IncentiveTracker::trackNewPartner`, `IncentiveParticipant::rebuildFromSourceTables` (Block Neupartner), `IncentivePointsLogRepairService::syncMissingTrackingPartners`.
|
||
|
||
### Tests (Auswahl)
|
||
|
||
- `tests/Feature/Incentive/IncentiveParticipantRankOrderingTest.php` — Sortierung, Limit, Aktivitätsfilter.
|
||
- `tests/Feature/Incentive/IncentivePartnerRegistrationStarterKitTest.php` — Tracking nur mit Starterpaket-Position.
|
||
|
||
### Dokumentation
|
||
|
||
- **`dev/Incentive-Modul/README.md`** — Modulübersicht für Entwickler/PM.
|
||
- **`dev/Incentive-Modul/tasks.md`** — Anforderungen und Status.
|
||
|
||
---
|
||
|
||
## Datei-Übersicht (erstellt)
|
||
|
||
```
|
||
database/migrations/
|
||
├── 2026_03_17_000001_create_incentives_table.php ✅
|
||
├── 2026_03_17_000002_create_incentive_participants_table.php ✅
|
||
├── 2026_03_17_000003_create_incentive_points_log_table.php ✅
|
||
├── 2026_03_17_000004_create_incentive_new_partners_table.php ✅
|
||
└── 2026_03_17_000005_create_incentive_new_abos_table.php ✅
|
||
|
||
database/factories/
|
||
├── IncentiveFactory.php ✅
|
||
├── IncentiveParticipantFactory.php ✅
|
||
├── IncentivePointsLogFactory.php ✅
|
||
├── IncentiveNewPartnerFactory.php ✅
|
||
└── IncentiveNewAboFactory.php ✅
|
||
|
||
app/Models/
|
||
├── Incentive.php ✅
|
||
├── IncentiveParticipant.php ✅
|
||
├── IncentivePointsLog.php ✅
|
||
├── IncentiveNewPartner.php ✅
|
||
└── IncentiveNewAbo.php ✅
|
||
|
||
app/Services/Incentive/
|
||
├── IncentiveTracker.php ✅
|
||
└── IncentiveCalculationService.php ✅
|
||
|
||
app/Http/Controllers/Admin/
|
||
└── IncentiveController.php ✅
|
||
|
||
app/Http/Controllers/User/
|
||
└── IncentiveController.php ✅
|
||
|
||
app/Console/Commands/
|
||
└── IncentiveCalculate.php ✅
|
||
|
||
resources/views/admin/incentive/
|
||
├── index.blade.php ✅
|
||
├── create.blade.php ✅
|
||
├── edit.blade.php ✅
|
||
├── _form.blade.php ✅
|
||
└── show.blade.php ✅
|
||
|
||
resources/views/user/incentive/
|
||
├── show.blade.php ✅
|
||
└── details.blade.php ✅
|
||
|
||
resources/lang/de/incentive.php ✅
|
||
resources/lang/en/incentive.php ✅
|
||
resources/lang/es/incentive.php ✅
|
||
|
||
tests/Pest.php ✅
|
||
tests/Unit/Incentive/
|
||
├── IncentiveModelTest.php ✅ (19 Tests)
|
||
└── IncentiveTrackerTest.php ✅ (8 Tests)
|
||
|
||
tests/Feature/Incentive/
|
||
├── AboRenewalSalesVolumeIncentiveTest.php ✅
|
||
├── IncentiveParticipantRankOrderingTest.php ✅
|
||
└── IncentivePartnerRegistrationStarterKitTest.php ✅
|
||
```
|
||
|
||
---
|
||
|
||
## Bestehende Dateien mit Änderungen (Hooks)
|
||
|
||
| Datei | Änderung |
|
||
|------------------------------------------------|---------------------------------------------|
|
||
| `app/Services/Payment.php` | 2 Hooks: trackNewPartner (`payment_for==1`), trackAboActivated |
|
||
| `app/Models/ShoppingOrder.php` | Scopes/Methoden: Starterpaket-Registrierung (`wherePaidRegistrationIncludesStarterKit`, `qualifiesForIncentiveTrackedPartner`) |
|
||
| `app/Repositories/InvoiceRepository.php` | 1 Hook: trackSalesVolume |
|
||
| `app/Services/BusinessPlan/SalesPointsVolume.php` | 1 Hook: trackStorno |
|
||
| `routes/domains/crm.php` | Admin + User Routes hinzufügen |
|
||
| `app/Console/Kernel.php` | Cron-Eintrag für incentive:calculate |
|
||
|
||
---
|
||
|
||
## Architektur-Änderung: Eigene Tracking-Tabellen (17.03.2026)
|
||
|
||
**Entscheidung**: Statt bei jedem Event die Quelltabellen (Users, UserAbos, UserSalesVolumes) abzufragen, werden **eigene Tracking-Tabellen** geführt.
|
||
|
||
**Grund**: Performance-Optimierung + saubere Nachverfolgbarkeit, welche Partner/Abos einem Teilnehmer zugeordnet sind.
|
||
|
||
### Neue Tabellen:
|
||
- `incentive_new_partners` (participant_id, user_id, registered_at) - Welche Neupartner gehören zu welchem Teilnehmer
|
||
- `incentive_new_abos` (participant_id, user_abo_id, activated_at) - Welche Kundenabos gehören zu welchem Teilnehmer
|
||
|
||
### Berechnungslogik:
|
||
|
||
**Normal (bei Events + Cron)** via `recalculateFromTrackingTables()`:
|
||
```
|
||
qualified_partners = COUNT(incentive_new_partners)
|
||
qualified_abos = COUNT(incentive_new_abos)
|
||
total_points = SUM(points_onetime + points_accumulated) aus incentive_points_log
|
||
```
|
||
|
||
**Force-Rebuild** via `rebuildFromSourceTables()`:
|
||
1. Leert Tracking-Tabellen + Log
|
||
2. Scannt Quelltabellen (Users, UserAbos, UserSalesVolumes)
|
||
3. **Neupartner (Block A):** nur User mit bezahlter Registrierung, die **`wherePaidRegistrationIncludesStarterKit()`** erfüllt (Starterpaket-Regel wie im Live-Tracking)
|
||
4. Füllt `incentive_new_partners`, `incentive_new_abos`, `incentive_points_log` neu
|
||
5. Berechnet Totals via `recalculateFromTrackingTables()`
|
||
|
||
### Datenfluss bei Events:
|
||
1. **Neupartner**: → INSERT `incentive_new_partners` + Log + Recalculate
|
||
2. **Abo aktiviert**: → INSERT `incentive_new_abos` + Log + Recalculate
|
||
3. **Sales Volume**: → INSERT Log (akkumulierte Punkte) + Recalculate
|
||
4. **Storno**: → INSERT negativer Log-Eintrag + Recalculate
|