10.April 2026

This commit is contained in:
Kevin Adametz 2026-04-10 17:15:27 +02:00
parent a00c42e770
commit f58c709945
208 changed files with 19280 additions and 2914 deletions

127
dev/2026-03-12/tasks.md Normal file
View file

@ -0,0 +1,127 @@
# Korrekturen & Verbesserungen - 12.03.2026
---
## 1. Bug-Fix: Falsche Zahlungsbeträge bei Abo-Bestellungen
**Datei:** `app/Cron/UserMakeOrder.php` (Zeile 71)
**Problem:** Bei der automatischen Abo-Zahlungsausführung wurde `subtotal_ws` (Nettobetrag + Versand) statt `total_shipping` (Bruttobetrag + Versand) als Zahlungsbetrag an Payone gesendet. Dadurch wurde bei Kunden mit Umsatzsteuer nur der Nettobetrag eingezogen - die MwSt fehlte.
**Fix:**
```php
// Vorher (falsch):
$amount = $this->shopping_order->subtotal_ws * 100;
// Nachher (korrekt):
$amount = $this->shopping_order->total_shipping * 100;
```
**Auswirkung:** Betrifft alle Abo-Bestellungen seit Go-Live, bei denen Umsatzsteuer anfällt. Bei Nettobeträgen (Drittländer, USt-ID hinterlegt) war der Betrag zufällig korrekt.
---
## 2. SysAdmin Tool: Abo-Bestellungen Zahlungsdifferenzen
**Neue Dateien:**
- `app/Services/SyS/AboOrdersOverview.php`
- `resources/views/sys/tools/abo-orders-overview.blade.php`
**Geänderte Dateien:**
- `app/Http/Controllers/SyS/SysController.php` (neuer case `abo_orders_overview`)
- `resources/views/sys/index.blade.php` (neuer Menüpunkt)
**Beschreibung:** Übersichtstool unter `sysadmin/tool/abo_orders_overview` zur Nachverfolgung der durch den Bug betroffenen Abo-Bestellungen. Zeigt pro Bestellung:
- Netto+Versand (`subtotal_ws`) = was eingezogen wurde
- MwSt (`tax`) = was gefehlt hat
- Brutto+Versand (`total_shipping`) = was hätte eingezogen werden sollen
- Differenz = fehlender Betrag (rot markiert)
**Features:**
- Filter: Alle / Berater / Kunden (Button-Gruppe)
- Summary-Karten: Gesamtanzahl, betroffene Bestellungen, Gesamtfehlbetrag
- Betroffene Zeilen rot hervorgehoben
---
## 3. Bug-Fix: Abo-Einstellungen - Vergleichsoperator
**Datei:** `app/Repositories/AboRepository.php` (Zeile 66)
**Problem:** Bei der Reaktivierung eines pausierten Abos wurde statt eines Vergleichs (`==`) eine Zuweisung (`=`) verwendet. Dadurch wurde der Status immer auf 6 gesetzt, bevor er auf 2 geändert wurde.
**Fix:**
```php
// Vorher (falsch - Zuweisung):
if ($this->model->status = 6)
// Nachher (korrekt - Vergleich):
if ($this->model->status == 6)
```
---
## 4. Abo-Liefertag: Validierung und Sperren
**Datei:** `app/Repositories/AboRepository.php`
**Problem:** Beim Ändern des Abo-Liefertags konnte ein Datum gewählt werden, das nur wenige Tage entfernt liegt. Da Pakete vorgepackt werden, muss ein Mindestabstand eingehalten werden.
**Lösung:** Zwei Klassenkonstanten steuern die Sperren:
```php
private const LOCK_DAYS_CHANGE = 10; // Liefertag-Änderung
private const LOCK_DAYS_PAUSE_CANCEL = 3; // Pausieren/Kündigen
```
### Regeln:
| Aktion | Sperre | Beschreibung |
|--------|--------|--------------|
| Liefertag ändern | < 10 Tage vor aktueller Ausführung | `error_change_locked` - Pakete werden vorgepackt |
| Neuen Liefertag wählen | Neues Datum < 10 Tage entfernt | `error_abo_interval_too_soon` - Berechnung via `setNextDate()` |
| Abo pausieren | < 3 Tage vor Ausführung | `error_pause_locked` |
| Abo kündigen | < 3 Tage vor Ausführung | `error_cancel_locked` |
### Validierungslogik:
1. Aktuelles `next_date` prüfen: Wenn < 10 Tage entfernt komplett gesperrt
2. Neues Datum berechnen via `AboHelper::setNextDate()` und prüfen ob >= 10 Tage entfernt
3. Nach erfolgreichem Speichern: Orange Alert mit exaktem nächsten Ausführungsdatum
### Beispiele (heute = 12. März):
| Von | Auf | Berechnet | Tage | Ergebnis |
|-----|-----|-----------|------|----------|
| 5. | 20. | 20. März | 8 | blockiert |
| 5. | 25. | 25. März | 13 | erlaubt |
| 20. | 10. | 10. April | ~29 | erlaubt (nächster Monat) |
---
## 5. Warnungen bei Abo-Erstellung (Bestellformular)
**Geänderte Dateien:**
- `app/Services/HTMLHelper.php` (`getAboDeliveryOptions`)
- `resources/views/user/order/yard_view_form.blade.php`
- `resources/views/portal/abo/_create_check.blade.php`
**Beschreibung:** Beim Erstellen eines neuen Abos zeigt das Liefertag-Dropdown eine dynamische Warnung (orange) an, wenn die erste Abo-Ausführung weniger als 20 Tage entfernt ist. Die Warnung aktualisiert sich beim Wechsel des Liefertags.
**Umsetzung:**
- `HTMLHelper::getAboDeliveryOptions()` gibt pro Option `data-days` und `data-date` Attribute aus
- JavaScript im Formular prüft beim Ändern der Auswahl die Tage und zeigt/versteckt die Warnung
---
## 6. Neue Übersetzungen (DE/EN/ES)
**Dateien:** `resources/lang/{de,en,es}/abo.php`
| Key | Beschreibung |
|-----|-------------|
| `warning_next_date_soon` | Warnung: Nächste Ausführung < 20 Tage (Backend-Flash) |
| `warning_next_date_soon_select` | Warnung: Nächste Ausführung < 20 Tage (Frontend-Select) |
| `warning_next_date_info` | Info nach Speichern: Nächste Ausführung in X Tagen am Datum |
| `error_change_locked` | Fehler: Änderung gesperrt (< 10 Tage) |
| `error_abo_interval_too_soon` | Fehler: Neuer Liefertag zu nah (< 10 Tage) |
| `error_cancel_locked` | Fehler: Kündigung gesperrt (< 3 Tage) |
| `error_pause_locked` | Fehler: Pausierung gesperrt (< 3 Tage) |

Binary file not shown.

View file

@ -0,0 +1,92 @@
# Incentive-Modul (CRM)
Übersichtliche Beschreibung des **wiederverwendbaren Incentive-Systems** in mivita: zeitlich begrenzte Challenges (z.B. Reise-Anreize), eigenes Punkte- und Ranking-Modell, **getrennt** von MLM-Provisionsberechnung (`BusinessPlan` / `TreeCalcBot`).
---
## Zweck
- Mehrere **Incentives** parallel oder nacheinander (pro Jahr/Kampagne anlegbar).
- Berater **opt-in** mit Teilnahmebedingungen; **Punkte**, **Mindestqualifikation** (Partner + Kundenabos) und **Top-N-Gewinner** sind pro Incentive konfigurierbar.
- **Transparenz** für Berater: Live-Rangliste, Detailansicht mit Aufschlüsselung nach Neupartnern und Kundenabos (inkl. Monaten).
---
## Architektur in Kürze
| Schicht | Inhalt |
|--------|--------|
| **Konfiguration** | `incentives` Zeiträume, Punkte, Mindestwerte, `max_winners`, Status, Slug, Übersetzungen |
| **Teilnahme** | `incentive_participants` Opt-in, aggregierte Werte, Rang, Qualifikations-Flags |
| **Tracking** | `incentive_new_partners`, `incentive_new_abos` welche Neupartner/Abos einem Teilnehmer zugeordnet sind |
| **Historie** | `incentive_points_log` Einmal- und Akkumpunkte, optional Verknüpfung zu `UserSalesVolume`, Stornos |
**Kernservice:** `App\Services\Incentive\IncentiveTracker` wird aus Zahlungsfluss, Rechnung/Sales Volume und Storno aufgerufen.
**Batch:** `php artisan incentive:calculate` (`IncentiveCalculate`) + optional `IncentiveCalculationService` / Neuaufbau aus Quellen.
---
## Datenfluss (Events)
1. **Neupartner mit Starterpaket** bezahlte **Wizard-Registrierung** (`payment_for = 1`), Bestellung enthält mindestens ein Produkt **ohne** „reine Mitgliedschaft“ (`Product::is_membership_only = false`). → Eintrag in `incentive_new_partners`, Einmalpunkte im Log, Neuberechnung.
2. **Abo aktiviert** nach `AboHelper::setAboActive`: **Kundenabo** `is_for = 'ot'` (Berater = `member_id`) **oder Berater-Eigenabo** `is_for = 'me'` (Berater = `user_id`). → `incentive_new_abos`, Einmalpunkte, Neuberechnung. Beim Neuaufbau zählen Kundenabos über `member_id` im Qualifikationszeitraum; Eigenabos (`me`) neu im Zeitraum oder **bereits vor Qualifikationsbeginn** aktiv (dann Einmalpunkte mit Wirkung ab Qualifikationsstart).
3. **Akkumulierte Punkte** bei neuer **`UserSalesVolume`**-Zeile (Rechnung): Umsatzpunkte von **getrackten Neupartnern** (SV gehört zur User-ID des Partners) bzw. im **Neuabo-Pfad** über Bestellung/Kundenkontext. → Log `points_accumulated`, Neuberechnung.
4. **Storno** Gegenbuchung im Log, Neuberechnung.
Details und Integrationsstellen: **`entwicklungsplan.md`**.
---
## UI
| Bereich | Beschreibung |
|---------|----------------|
| **Admin** | CRUD Incentives, Konfiguration, Teilnehmer-Ranking-Tabelle |
| **Berater** | Teaser/Show (Info, Live-Ranking, Teilnahme), **Details** mit Monatsaufschlüsselung |
| **Navigation** | Einträge z.B. über Dashboard / Sidenav (je nach Rolle) |
Content-Texte (Marketing) für eine konkrete Kampagne liegen separat in **`site.md`**.
---
## Wichtige Geschäftsregeln (implementiert)
- **Rangliste (User):** Sortierung: zuerst **qualifizierte** Teilnehmer (Mindest-Partner/Abos), dann nach **Rang**, Teilnehmer **ohne** Rang unten, Tiebreaker **Gesamtpunkte**; Anzeige begrenzt auf **`max_winners`**; optional nur Teilnehmer mit **Aktivität** (Partner, Abo oder Punkte > 0) Scope `withRankingActivity()`.
- **Neupartner zählen nur mit Starterpaket** keine reine Mitgliedschafts-Bestellung allein; technisch: `ShoppingOrder::wherePaidRegistrationIncludesStarterKit()` / `qualifiesForIncentiveTrackedPartner()`.
---
## Dokumente in diesem Ordner
| Datei | Inhalt |
|-------|--------|
| **README.md** (diese Datei) | Modulübersicht |
| **entwicklungsplan.md** | Datenmodell, Services, Hooks, Phasen, Dateiliste, Architektur-Tracking |
| **tasks.md** | Anforderungen, Aufgaben- und Statusübersicht |
| **site.md** | Vorgefertigte Textbausteine / Nutzungsbedingungen (Content Montenegro 2026) |
---
## Relevante Code-Pfade (Referenz)
```
app/Models/Incentive.php
app/Models/IncentiveParticipant.php
app/Models/IncentivePointsLog.php
app/Models/IncentiveNewPartner.php
app/Models/IncentiveNewAbo.php
app/Services/Incentive/IncentiveTracker.php
app/Services/Incentive/IncentiveCalculationService.php
app/Services/Incentive/IncentivePointsLogRepairService.php
app/Http/Controllers/Admin/IncentiveController.php
app/Http/Controllers/User/IncentiveController.php
resources/lang/{de,en,es}/incentive.php
tests/Feature/Incentive/
tests/Unit/Incentive/
tests/Unit/Services/Incentive/
```
---
*Letzte inhaltliche Aktualisierung der Modul-Doku: März 2026.*

View file

@ -0,0 +1,530 @@
# 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

View file

@ -0,0 +1,97 @@
> **Dokumentation zum Incentive-Modul (Technik):** siehe [`README.md`](README.md) und [`entwicklungsplan.md`](entwicklungsplan.md).
> **Aufgaben/Status:** [`tasks.md`](tasks.md).
> Die folgende Datei enthält **Content-Texte** (Berater-Oberfläche / Montenegro 2026), keine technische Spezifikation.
---
Das ist eine sehr gute Entscheidung, bei dem Begriff zu bleiben! Für das Frontend im CRM brauchen wir jetzt einen Text, der professionell, motivierend und auf den ersten Blick verständlich ist. Schließlich sollen die Berater direkt Lust bekommen, loszulegen.
Hier ist ein Struktur- und Textentwurf für die Inhaltsseite im System.
---
### [Platzhalter: Großes, ansprechendes Titelbild von einem Luxushotel oder der Küste Montenegros einfügen]
# (icon) Montenegro Incentive 2026 Deine exklusive Auszeit an der Adria!
**Pack deine Koffer, denn mivita belohnt deine Bestleistungen!** Vom **30. September bis zum 4. Oktober 2026** geht es für unsere erfolgreichsten Partner an die malerische Küste Montenegros. Erlebe unvergessliche Tage, tausche dich mit den Top-Leadern aus und feiere deinen Erfolg mit uns!
Gehörst du zu den **besten 30 Partnern**? Dann bist du dabei!
---
### (icon) Der Qualifikationszeitraum
Der Startschuss für die Qualifikation fällt am **1. April**.
* **Regulärer Zeitraum:** April bis Juli
* **Endspurt:** Der Zeitraum wird um 1 Monat verlängert (Punkte zählen bis einschließlich August ).
---
### (icon) Dein Ticket in den Flieger: Die Mindestqualifikation
Um dir deinen Platz im Ranking freizuschalten und überhaupt für die Reise in Frage zu kommen, musst du im Qualifikationszeitraum folgende Basis-Ziele erreichen:
* **4 direkte neue Teampartner** (jeweils mit einem Starterpaket)
* **6 neu abgeschlossene Kundenabos**
*(Hinweis: Im Live-Ranking unten wird dein Name erst dann **fettgedruckt** hervorgehoben, wenn du diese Mindestqualifikation erfolgreich geknackt hast!)*
---
### (icon) So sammelst du deine Incentive-Punkte
Sobald der Startschuss fällt, zählt jeder Umsatz. Deine Punkte setzen sich aus Einmal-Boni und laufenden Umsätzen zusammen:
**1. Punkte für neue Teampartner**
* **600 Punkte einmalig** für jeden direkt gesponserten Neupartner.
* **Zusatz-Boost:** Du erhältst zusätzlich *alle* Kundenumsatzpunkte deines Neupartners ab seinem Startmonat bis einschließlich August!
**2. Punkte für Kundenabos**
* **400 Punkte einmalig** pro neu abgeschlossenem Kundenabo im Qualifikationszeitraum.
* **Zusatz-Boost:** Du erhältst zusätzlich die jeweiligen monatlichen Abopunkte ab dem Abschlussmonat bis einschließlich August!
---
### (icon) Das Live-Ranking
Deinen aktuellen Punktestand und deine Platzierung im Vergleich zu den anderen Beratern findest du jederzeit in unserer separaten Rangliste.
Nur die besten 30 Berater, die auch die Mindestqualifikation erfüllen, sichern sich das Ticket nach Montenegro!
[Hier geht es direkt zur Live-Rangliste] (Link zur neuen Ranking-Route)
### (icon) Deine Teilnahme am Montenegro Incentive 2026
Bist du bereit für die Challenge? Um am Incentive teilzunehmen und im offiziellen Ranking gelistet zu werden, musst du dich einmalig anmelden.
[ ] Ich habe die Nutzungsbedingungen gelesen und akzeptiere diese. Ich möchte am Montenegro Incentive 2026 teilnehmen und stimme zu, dass mein Name und mein Punktestand im internen Ranking für andere Berater sichtbar sind.
[ BUTTON: Jetzt verbindlich teilnehmen ]
---
### (icon) Die Nutzungsbedingungen (Teilnahmebedingungen)
Nutzungsbedingungen (Teilnahmebedingungen)
Diese Bedingungen sichern euch rechtlich ab und fassen die Regeln aus deinem Briefing für die Berater transparent zusammen.
**Nutzungs- und Teilnahmebedingungen: Montenegro Incentive 2026**
**1. Gegenstand des Incentives**
Die mivita veranstaltet das „Montenegro Incentive 2026“. Die erfolgreichsten Berater qualifizieren sich für eine exklusive Reise nach Montenegro vom 30. September bis zum 4. Oktober 2026.
**2. Qualifikationszeitraum**
Der reguläre Qualifikationszeitraum startet am 1. April und läuft bis Ende Juli. Für die finale Punkteberechnung (laufende Kunden- und Aboumsätze) wird der Zeitraum um einen weiteren Monat bis einschließlich August verlängert.
**3. Teilnahmevoraussetzung (Mindestqualifikation)**
Um für die Incentive-Reise infrage zu kommen, muss im Qualifikationszeitraum zwingend folgende Mindestqualifikation erreicht werden:
* Sponsoring von mindestens **4 direkten Teampartnern** (jeweils mit einem Starterpaket).
* Abschluss von mindestens **6 neuen Kundenabos**.
**4. Punkteberechnung**
Teilnehmer sammeln tagesaktuell Punkte, die im Ranking sichtbar sind. Die Punkte setzen sich wie folgt zusammen:
* **Neupartner:** Pro direkt gesponsertem Neupartner im Qualifikationszeitraum gibt es einmalig 600 Punkte. Zusätzlich werden alle Kundenumsatzpunkte dieses Neupartners ab seinem Startmonat bis einschließlich August akkumuliert.
* **Kundenabos:** Pro neu abgeschlossenem Kundenabo im Qualifikationszeitraum gibt es einmalig 400 Punkte. Zusätzlich werden die monatlichen Abopunkte ab Abschluss bis einschließlich August akkumuliert.
**5. Gewinnerermittlung und Ranking**
Die gesammelten Punkte werden in einer Live-Rangliste dargestellt. Die Darstellung des Namens und der Punkte eines Teilnehmers erfolgt erst dann in **Fettdruck**, wenn die Mindestqualifikation (siehe Punkt 3) vollständig erfüllt ist.
Qualifiziert für die Reise sind die **Top 30 Teilnehmer** der Rangliste, die zum Stichtag die Mindestqualifikation erfüllt haben.
**6. Datenschutz und Sichtbarkeit**
Mit der Teilnahme am Incentive stimmt der Berater zu, dass sein Name, seine erreichten Qualifikationen (Anzahl Teampartner/Abos) sowie der Gesamtpunktestand im internen mivita-Ranking für andere Systemnutzer sichtbar sind.

View file

@ -0,0 +1,49 @@
# Incentive-Modul Anforderungen & Aufgaben
Auszug aus dem Kundenbriefing (`dev/2026-03-17/Incentive-montenegro.pdf`) und Nachverfolgung der Umsetzung.
---
## Produktziele
| # | Anforderung | Status |
|---|-------------|--------|
| P1 | Eigenes Punkte-/Ranking-System, **losgelöst** von anderen MLM-Berechnungen | Erledigt |
| P2 | **Wiederverwendbar**: mehrere Incentives (z.B. jährlich wechselnd) anlegbar | Erledigt |
| P3 | Parameter pro Incentive: Zeiträume, Punkte, Mindest-Partner/Abos, **Anzahl Gewinner** (`max_winners`) | Erledigt |
| P4 | UI: Inhaltsseite mit Bild, Erklärung, **Rangliste**; Qualifikation und Gewinner **visuell** hervorheben | Erledigt |
| P5 | **Detailübersicht** für Berater: Neupartner- und Abo-Berechnung, nach Monaten, nachvollziehbar | Erledigt |
| P6 | Neupartner zählen nur mit **Starterpaket** (nicht nur Mitgliedschaft) | Erledigt |
| P7 | Rangliste: Qualifizierte oben; sinnvolle Sortierung Rang/Null-Rang; User-Liste begrenzt auf `max_winners`; optionale Filter „ohne Aktivität“ | Erledigt |
---
## Technische Arbeitspakete (Referenz)
| Paket | Beschreibung | Status |
|-------|--------------|--------|
| T1 | Migrationen `incentives`, `incentive_participants`, `incentive_points_log`, Tracking-Tabellen | Erledigt |
| T2 | Models, Factories, `IncentiveTracker`, Hooks in `Payment`, `InvoiceRepository`, `SalesPointsVolume` | Erledigt |
| T3 | Admin CRUD + DataTable + Views | Erledigt |
| T4 | User-Routen: Show, Participate, Details, Teaser | Erledigt |
| T5 | Artisan `incentive:calculate`, Cron | Erledigt |
| T6 | Übersetzungen `de` / `en` / `es` | Erledigt |
| T7 | Tests (Pest): Model, Tracker, Feature (Ranking, Starterkit, Abo, …) | Laufend erweitert |
| T8 | `IncentivePointsLogRepairService`, Debug-Commands (optional) | Vorhanden |
---
## Offen / manuell
| Aufgabe | Notiz |
|---------|--------|
| **Manueller End-to-End-Test** | Mit realitätsnahen Testdaten (Zahlung, Registrierung, Abo, Shop) im Staging prüfen |
| **Content-Pflege** | `site.md` Bilder, finale Texte, Links zur Live-Route je nach Deployment |
---
## Verwandte Dateien
- Technischer Fahrplan: **`entwicklungsplan.md`**
- Modul-Kurzüberblick: **`README.md`**
- Marketing-/Nutzertexte: **`site.md`**