# 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