mivita/dev/Incentive-Modul/entwicklungsplan.md
2026-04-10 17:15:27 +02:00

530 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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