29 KiB
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()beipayment_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). SiehewherePaidRegistrationIncludesStarterKit()aufShoppingOrder. - Logik (Kern):
- Neuer User aus
auth_user_id; Sponsor:User::m_sponsor - Aktive Incentives im Registrierungszeitpunkt (Qualifikationszeitraum)
- Sponsor muss
IncentiveParticipanthaben incentive_new_partners(Tracking) +incentive_points_log(Einmalpunkte) +recalculateFromTrackingTables()+ Ranking
- Neuer User aus
trackAboActivated(ShoppingOrder $shopping_order)
- Wird aufgerufen in:
Payment::paymentStatusPaidAction()(nachAboHelper::setAboActive, Zeile 280) - Wichtig: Nur wenn
setAboActivemitstatus=2, paid=trueaufgerufen wird - Bedingung: Abo
is_for === 'ot'(Kundenabo, nicht eigenes Berater-Abo) - Logik:
- Berater ermitteln:
UserAbo->user_id(= auth_user_id des Sponsors) - Prüfen ob Berater Teilnehmer eines aktiven Incentives ist
- Prüfen ob Abo-Abschluss im Qualifikationszeitraum liegt
incentive_points_loganlegen: type=abo, source=UserAbo, points_onetime=400incentive_participants.qualified_abosinkrementieren- Qualifikation + Ranking aktualisieren
- Berater ermitteln:
trackSalesVolume(UserSalesVolume $user_sales_volume)
- Wird aufgerufen in:
InvoiceRepository::createAndSalesVolume()(nach Erzeugung desUserSalesVolume) - Logik:
- Pfad A:
user_sales_volume.user_idist ein getrackter Neupartner (IncentiveNewPartner) → akkumulierte Punkte zum Sponsor-Teilnehmer (Log-Typpartner) - Pfad B: Bestellung gehört zu einem getrackten Kundenabo (
IncentiveNewAboüberUserAbois_for = 'ot') → akkumulierte Punkte (Log-Typabo) - Monat/Jahr im Berechnungs-Scope des jeweiligen Incentives (
isDateInScope) recalculateFromTrackingTables()+ Ranking
- Pfad A:
trackStorno(UserSalesVolume $original, UserSalesVolume $cancellation)
- Wird aufgerufen in:
SalesPointsVolume::cancelSalesPointsVolume()(nach Zeile 312) - Logik:
- Prüfen ob original
user_sales_volume_idinincentive_points_logexistiert - Storno-Eintrag anlegen:
is_storno=true, negative Punkte,storno_of_idverknüpfen - Gesamtpunkte + Qualifikation + Ranking aktualisieren
- Bei Storno eines Partners/Abos:
qualified_partnersbzw.qualified_abosdekrementieren
- Prüfen ob original
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:
- Neupartner ermitteln: Users mit
m_sponsor = participant.user_id, registriert im Qualifikationszeitraum, mit bezahltem Starterpaket - Kundenabos ermitteln:
UserAbomituser_id = participant.user_id,is_for = 'ot',status = 2, erstellt im Qualifikationszeitraum - Akkumulierte Punkte:
UserSalesVolumepro Monat (Qualifikationsstart bis calculation_end) - Stornos berücksichtigen (status=6 Einträge)
incentive_points_logaktualisieren (bei --force: löschen + neu anlegen)incentive_participantsSummen + Qualifikation aktualisieren
- Neupartner ermitteln: Users mit
updateRanking(Incentive $incentive)
- Alle Teilnehmer nach
total_pointsDESC sortieren rankfortlaufend vergeben (1, 2, 3, ...)is_qualifiedprü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 Incentivesresources/views/admin/incentive/create.blade.php– Formular (Spatie\Html)resources/views/admin/incentive/edit.blade.php– Bearbeitungsformularresources/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:
- Header: Statisches Incentive-Bild
- Beschreibung: Erklärungstext aus DB
- Teilnahme-Box (nur wenn noch nicht teilgenommen):
- Checkbox: "Ich akzeptiere die Teilnahmebedingungen" (Link zu Terms)
- Button: "Jetzt teilnehmen"
- 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
IncentiveTrackerService 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
IncentiveCalculationServiceimplementieren (app/Services/Incentive/IncentiveCalculationService.php)- recalculate: Batch-Berechnung aller Teilnehmer via
recalculateFromSource() - recalculateParticipant: Delegiert an
IncentiveParticipant::recalculateFromSource()
- recalculate: Batch-Berechnung aller Teilnehmer via
- Hooks in bestehende Klassen eingebaut:
app/Services/Payment.php: trackNewPartner + trackAboActivatedapp/Repositories/InvoiceRepository.php: trackSalesVolumeapp/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.phpresources/views/admin/incentive/edit.blade.phpresources/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/pintCode 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 TabellenIncentiveParticipant::rebuildFromSourceTables(): Force-Rebuild aus QuelltabellenIncentiveTracker: Insert in Tracking-Tabellen + Log +recalculateFromTrackingTables()IncentiveCalculationService: Normal=TrackingTables, Force=RebuildFromSource- Details-View: Partner/Abos aus Tracking-Tabellen statt aus Log gruppiert
- Neue Tabellen:
- 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_qualifiedabsteigend → Rang mit Wert vorNULL→rankaufsteigend →total_pointsabsteigend (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 odertotal_points > 0gelistet.
Neupartner nur mit Starterpaket
ShoppingOrder::wherePaidRegistrationIncludesStarterKit()/qualifiesForIncentiveTrackedPartner(): Registrierungsbestellung (payment_for = 1) mit bezahlter Zahlung und mindestens einer Position, deren Produktis_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 Teilnehmerincentive_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():
- Leert Tracking-Tabellen + Log
- Scannt Quelltabellen (Users, UserAbos, UserSalesVolumes)
- Neupartner (Block A): nur User mit bezahlter Registrierung, die
wherePaidRegistrationIncludesStarterKit()erfüllt (Starterpaket-Regel wie im Live-Tracking) - Füllt
incentive_new_partners,incentive_new_abos,incentive_points_logneu - Berechnet Totals via
recalculateFromTrackingTables()
Datenfluss bei Events:
- Neupartner: → INSERT
incentive_new_partners+ Log + Recalculate - Abo aktiviert: → INSERT
incentive_new_abos+ Log + Recalculate - Sales Volume: → INSERT Log (akkumulierte Punkte) + Recalculate
- Storno: → INSERT negativer Log-Eintrag + Recalculate