# 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
