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:
parent
ac3bd6e8ea
commit
841d87e161
11 changed files with 689 additions and 115 deletions
|
|
@ -325,6 +325,8 @@ class BusinessUserItem
|
|||
->where('users.m_sponsor', '=', $this->b_user->user_id) // <- need the id for parents / sponsors
|
||||
->where('users.payment_account', '!=', null)
|
||||
->where('users.active_date', '<=', $this->date->end_date) // wurde in dem Monat freigeschaltet
|
||||
->orderBy('users.active_date')
|
||||
->orderBy('users.id')
|
||||
->get();
|
||||
|
||||
if ($users) {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ namespace App\Services\BusinessPlan;
|
|||
use App\Models\UserAccount;
|
||||
use App\Models\UserBusiness;
|
||||
use App\Models\UserLevel;
|
||||
use App\Models\UserSalesVolume;
|
||||
use App\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
|
@ -393,7 +394,7 @@ class BusinessUserItemOptimized
|
|||
\Log::info("BusinessUserItem: No UserSalesVolume found for user {$user->id} in {$this->date->month}/{$this->date->year}");
|
||||
|
||||
// Prüfe neueste verfügbare Daten
|
||||
$latestVolume = \App\Models\UserSalesVolume::where('user_id', $user->id)
|
||||
$latestVolume = UserSalesVolume::where('user_id', $user->id)
|
||||
->orderBy('year', 'desc')
|
||||
->orderBy('month', 'desc')
|
||||
->first();
|
||||
|
|
@ -1086,6 +1087,8 @@ class BusinessUserItemOptimized
|
|||
->where('users.m_sponsor', '=', $this->b_user->user_id)
|
||||
->where('users.payment_account', '!=', null)
|
||||
->where('users.active_date', '<=', $this->date->end_date)
|
||||
->orderBy('users.active_date')
|
||||
->orderBy('users.id')
|
||||
->get();
|
||||
|
||||
if ($users->isNotEmpty()) {
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ class TreeHtmlRenderer
|
|||
$this->loadAboBadges($this->collectUserIds($businessUsers));
|
||||
|
||||
$html = '<ol class="dd-list">';
|
||||
foreach ($businessUsers as $businessUser) {
|
||||
foreach (self::sortByActivation($businessUsers) as $businessUser) {
|
||||
$html .= $this->renderUserItem($businessUser, 0);
|
||||
}
|
||||
$html .= '</ol>';
|
||||
|
|
@ -49,6 +49,35 @@ class TreeHtmlRenderer
|
|||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sortiert Business-User-Items nach Partner-Aktivierung (active_date, dann user_id).
|
||||
*
|
||||
* Die Loader liefern die Downline in users.id-Reihenfolge (= Konto-Registrierung);
|
||||
* gespeicherte Monats-Snapshots konservieren diese Reihenfolge zusaetzlich.
|
||||
* Bestandskunden, die erst spaeter zum Berater upgraden, stuenden damit mitten in
|
||||
* der Liste statt am Ende - deshalb wird hier zentral beim Rendern sortiert.
|
||||
*
|
||||
* Achtung: kein `$item->active_date ?? null` verwenden - BusinessUserItem(Optimized)
|
||||
* hat __get ohne __isset, damit liefert `??` immer null.
|
||||
*
|
||||
* @param array<int, object> $items
|
||||
* @return array<int, object>
|
||||
*/
|
||||
public static function sortByActivation(array $items): array
|
||||
{
|
||||
usort($items, function ($a, $b): int {
|
||||
$aDate = $a->active_date;
|
||||
$bDate = $b->active_date;
|
||||
|
||||
$aKey = $aDate ? strtotime((string) $aDate) : PHP_INT_MAX;
|
||||
$bKey = $bDate ? strtotime((string) $bDate) : PHP_INT_MAX;
|
||||
|
||||
return $aKey <=> $bKey ?: ((int) $a->user_id <=> (int) $b->user_id);
|
||||
});
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendert parentlose User als HTML
|
||||
*/
|
||||
|
|
@ -221,7 +250,7 @@ class TreeHtmlRenderer
|
|||
$childrenHtml = '';
|
||||
if ($item->businessUserItems && count($item->businessUserItems) > 0) {
|
||||
$childrenHtml = '<ol class="dd-list dd-nodrag">';
|
||||
foreach ($item->businessUserItems as $child) {
|
||||
foreach (self::sortByActivation($item->businessUserItems) as $child) {
|
||||
$childrenHtml .= $this->renderUserItem($child, $deep + 1);
|
||||
}
|
||||
$childrenHtml .= '</ol>';
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -37,8 +37,8 @@ class IncentiveTracker
|
|||
|
||||
$incentives = Incentive::query()
|
||||
->active()
|
||||
->where('qualification_start', '<=', $registration_date)
|
||||
->where('qualification_end', '>=', $registration_date)
|
||||
->whereDate('qualification_start', '<=', $registration_date)
|
||||
->whereDate('qualification_end', '>=', $registration_date)
|
||||
->get();
|
||||
|
||||
foreach ($incentives as $incentive) {
|
||||
|
|
@ -106,8 +106,8 @@ class IncentiveTracker
|
|||
|
||||
$incentives = Incentive::query()
|
||||
->active()
|
||||
->where('qualification_start', '<=', $activation_date)
|
||||
->where('qualification_end', '>=', $activation_date)
|
||||
->whereDate('qualification_start', '<=', $activation_date)
|
||||
->whereDate('qualification_end', '>=', $activation_date)
|
||||
->get();
|
||||
|
||||
foreach ($incentives as $incentive) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue