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

29 KiB
Raw Permalink Blame History

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:

// 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:

// 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:

// 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

  • Migrations erstellen (3 Tabellen: incentives, incentive_participants, incentive_points_log)
  • Models erstellen (Incentive, IncentiveParticipant, IncentivePointsLog)
  • Migrations ausgeführt
  • Factories für Tests

Phase 2: Kern-Logik (Tag 3-5) -- ERLEDIGT 17.03.2026

  • 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
  • IncentiveCalculationService implementieren (app/Services/Incentive/IncentiveCalculationService.php)
    • recalculate: Batch-Berechnung aller Teilnehmer via recalculateFromSource()
    • recalculateParticipant: Delegiert an IncentiveParticipant::recalculateFromSource()
  • Hooks in bestehende Klassen eingebaut:
    • app/Services/Payment.php: trackNewPartner + trackAboActivated
    • app/Repositories/InvoiceRepository.php: trackSalesVolume
    • app/Services/BusinessPlan/SalesPointsVolume.php: trackStorno
  • Artisan Command incentive:calculate (app/Console/Commands/IncentiveCalculate.php)
  • Cron-Eintrag in app/Console/Kernel.php (täglich 05:00)

Phase 3: Admin-Backend (Tag 6-7) -- ERLEDIGT 17.03.2026

  • Admin Controller (app/Http/Controllers/Admin/IncentiveController.php)
    • CRUD: index, create, store, show, edit, update
    • DataTable für Listenansicht
    • Neuberechnung (normal + force)
  • 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)
  • Admin Routes in routes/domains/crm.php (admin_incentive_*)

Phase 4: Berater-Frontend (Tag 8-9) -- ERLEDIGT 17.03.2026

  • User Controller (app/Http/Controllers/User/IncentiveController.php)
    • show: Incentive-Seite mit Ranking
    • participate: Teilnahme mit Terms-Akzeptanz
    • details: Persönliche Berechnungsübersicht
  • 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)
  • 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
  • Teilnahme-Flow (Checkbox + Terms mit Collapse)
  • User Routes in routes/domains/crm.php (user_incentive_*)

Phase 5: Abschluss (Tag 10) -- ERLEDIGT 17.03.2026

  • Localization (de/en/es) in resources/lang/{de,en,es}/incentive.php
  • ./vendor/bin/pint Code formatiert
  • 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
  • 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