Incentive: Ticket-Fixes 2026-08-12 + Team-Struktur nach active_date sortiert

Incentive (Montenegro 2026):
- Live-Tracker: whereDate() für Zeitraum-Vergleiche – Bestellungen am letzten
  Tag des Qualifikationszeitraums gingen verloren (DATE-Spalte vs. Datetime)
- Repair/Rebuild: Neupartner-Definition auf Starterpaket-Bestelldatum im
  Zeitraum umgestellt (statt users.created_at) – Bestandskunden-Upgrades
  zählen jetzt wie im Live-Tracking (Fall Dino Portaro → Steffi Karrer)
- Neu: removeStaleSponsorTrackings() – Partner-Trackings wandern nach
  Sponsorwechsel zum aktuellen Sponsor (Fall Anna Schall lag beim Firmenkonto)
- incentive:calculate: Sponsorwechsel-Bereinigung eingebunden + Statistik

Team-Struktur (/user/team/structure):
- Downline sortiert nach active_date (Partner-Aktivierung) statt users.id:
  zentral in TreeHtmlRenderer::sortByActivation() (wirkt sofort, auch für
  gespeicherte Monats-Snapshots) + orderBy in beiden Downline-Queries

Doku: dev/2026-08-12-intensiv-tickets/README.md (4 Kundentickets analysiert,
Lücke durch verspätete Zeitraum-Verlängerung hatte sich per Nacht-Cron geheilt)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin 2026-08-12 10:28:06 +00:00
parent ac3bd6e8ea
commit 841d87e161
11 changed files with 689 additions and 115 deletions

View file

@ -18,6 +18,10 @@ class IncentivePointsLogRepairService
* Fehlende Neupartner-Tracking-Zeilen anlegen (Starterpaket / gleiche Regeln wie Neuaufbau A).
* Nutzt IncentiveTracker::trackNewPartner, wenn eine qualifizierte Bestellung existiert.
*
* Neupartner = Starterpaket im Qualifikationszeitraum bezahlt (Bestelldatum, wie Live-Tracking).
* Das User-Registrierungsdatum ist bewusst irrelevant: auch Bestandskunden, die im Zeitraum
* zum Berater upgraden, zaehlen.
*
* @return int Anzahl nachgezogener Partner-Trackings fuer diesen Teilnehmer
*/
public function syncMissingTrackingPartners(IncentiveParticipant $participant): int
@ -29,14 +33,14 @@ class IncentivePointsLogRepairService
$added = 0;
$windowStart = $incentive->qualification_start->copy()->startOfDay();
$windowEnd = $incentive->qualification_end->copy()->endOfDay();
$candidates = User::query()
->where('m_sponsor', $participant->user_id)
->whereBetween('created_at', [
$incentive->qualification_start,
$incentive->qualification_end->copy()->endOfDay(),
])
->whereHas('shopping_orders', function ($q) {
$q->wherePaidRegistrationIncludesStarterKit();
->whereHas('shopping_orders', function ($q) use ($windowStart, $windowEnd) {
$q->wherePaidRegistrationIncludesStarterKit()
->whereBetween('created_at', [$windowStart, $windowEnd]);
})
->get();
@ -51,6 +55,7 @@ class IncentivePointsLogRepairService
$order = ShoppingOrder::query()
->where('auth_user_id', $partner->id)
->wherePaidRegistrationIncludesStarterKit()
->whereBetween('created_at', [$windowStart, $windowEnd])
->orderBy('id')
->first();
@ -62,10 +67,12 @@ class IncentivePointsLogRepairService
->where('participant_id', $participant->id)
->where('user_id', $partner->id)
->exists()) {
$registeredAt = $order?->created_at ?? $partner->created_at;
$newPartner = IncentiveNewPartner::create([
'participant_id' => $participant->id,
'user_id' => $partner->id,
'registered_at' => $partner->created_at,
'registered_at' => $registeredAt,
]);
IncentivePointsLog::create([
@ -74,8 +81,8 @@ class IncentivePointsLogRepairService
'source_type' => User::class,
'source_id' => $partner->id,
'source_label' => $partner->getFullName() ?: $partner->email ?: ('User #'.$partner->id),
'month' => $partner->created_at->month,
'year' => $partner->created_at->year,
'month' => $registeredAt->month,
'year' => $registeredAt->year,
'points_onetime' => $incentive->points_partner_onetime,
'points_accumulated' => 0,
'incentive_new_partner_id' => $newPartner->id,
@ -93,6 +100,37 @@ class IncentivePointsLogRepairService
return $added;
}
/**
* Entfernt Neupartner-Trackings, deren Partner inzwischen einen anderen Sponsor hat
* (z. B. nach Umhaengen in der Teamstruktur). Die zugehoerigen Log-Zeilen (Einmalpunkte
* und akkumulierte Umsatzpunkte) werden mitgeloescht; beim aktuellen Sponsor legt
* {@see syncMissingTrackingPartners} + {@see syncMissingSalesVolumeLogs} im selben
* Lauf alles neu an. Geloeschte User bleiben unangetastet.
*
* @return int Anzahl entfernter Partner-Trackings fuer diesen Teilnehmer
*/
public function removeStaleSponsorTrackings(IncentiveParticipant $participant): int
{
$removed = 0;
foreach ($participant->newPartners()->get() as $tracking) {
$partner = User::find($tracking->user_id);
if (! $partner) {
continue;
}
if ((int) $partner->m_sponsor === (int) $participant->user_id) {
continue;
}
IncentivePointsLog::where('incentive_new_partner_id', $tracking->id)->delete();
$tracking->delete();
$removed++;
}
return $removed;
}
/**
* Fehlende Abo-Tracking-Zeilen anlegen (Kundenabo ot / Eigenabo me, wie IncentiveTracker).
* Zuerst trackAboActivated ueber Erstbestellung; ohne UserAboOrder manuell wie Neuaufbau B.