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.phppaymentStatusPaidAction() 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.phppaymentStatusPaidAction() 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.phpcreateAndSalesVolume() 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.phpcancelSalesPointsVolume() 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 NULLrank 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