Statistiken: Kundenfeedback 2026-08-19 komplett (N1-N8) + Review-Fixes + VP-/Incentive-Repair gehärtet
Statistik-Änderungen 2026-08-19 (dev/2026-07-22/entwicklungsplan-2026-08-19.md, alle 8 Pakete gemäß beantworteten F-Fragen): - N1 Monatsstatistik: Team-Cluster (Firstlines klickbar -> neue Seite user_team_firstlines, Teampartner gesamt/Payline [nur aktive], Abos Payline mit +N) + Level-Kachel (Quali-Stufe, fehlende Payline-/persönliche Punkte, Karriere-Level); LiveCommissionService-Snapshot v3 reicht Level-/Quali-Daten durch - N2 Abo-Kacheln auf Home monatsbezogen (aboStockCount-Rückrechnung über cancel/deleted/Status-Historie; VIP-Statistik bleibt tagesaktuell) - N3 VIP-Statistik 'Recruiter des Monats' (Neupartner-Definition wie KPI, rekursives Ketten-Volumen als Gesamtpunkte inkl. Starterpaket) + Drilldown/CSV-Spalte 'Punkte gesamt' - N4 Prognose-Block 'Level & Provision laut Prognose (Stand 25.)' mit unverbindlich-Kennzeichnung (ebenengenaue offene Abo-Punkte + TP-Laufstände) - N5 Checkout-Herkunft: Dropdown raus, Pflicht-Freitext (lokalisierte Fehlermeldung) - N6 Berater-Profilfoto (user_accounts.profile_image, Einwilligung als Sprachkey-Platzhalter, DSGVO-Löschung) + teaminterne Top-3-Karte (TopResultsService, Scope-Schalter) - N7 USt-Optionen: 'nicht umsatzsteuerpflichtig' zuerst + '§ 19 UStG'-Textfix - N8 Jahresgebühr-Nachfassliste je Berater (AnnualFeeDueService, überfällig/1-3 Monate, Sidenav) Kontroll-Review (8 Perspektiven) angewendet: next-Level-Fallback im Steady-State (resolveNextLevel — sonst 'Höchstes Level erreicht' für Normalfälle), _calculated-Punktebasis bei mehr Paylines, keine Wiederbelebung deaktivierter Abos in der Monatsrückrechnung, Payline-Abo-Kachel monatskonsistent, Top-3-Nachrücker; Performance: BackofficeDashboardService als Singleton mit lineBuckets-Memo (+flushLineBucketsMemo), level_forecast 6h-Cache; Reuse: User-Scopes firstlinesOf/newPartnerIn, SalesVolumePoints::totalPointsByUser, Slim::storeCroppedImage (3 Upload-Kopien vereinigt), activeAbo-Scope statt Inline-Prädikate, Form::open-Konvention, Return-Types Repair-Härtung (Kundenentscheide): VP-Nachtrepair dedupliziert über shopping_order_id (Live-Pfad schreibt sie jetzt mit), zählende Alt-Zeilen-Erkennung, Vormonats-Scan für Monatswechsel, Firmenaccount-Buchungen (config mivita.company_user_id) werden automatisch auf den Berater umgebucht (in place bzw. per Gegenbuchung bei geschlossenem Monat; Echtfall Anna Schall Order 58797 -> Christina Schuder), Berater-zu-Berater nur Warnung; Incentive-Repair: m_sponsor=NULL löscht nicht mehr, FK-Repair vor Stale-Bereinigung, gebatchte Lookups, zentrales Incentive::qualificationWindow(); .env aus dem Git-Tracking entfernt (Pflege per SFTP) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
841d87e161
commit
867dc21b57
87 changed files with 4480 additions and 240 deletions
|
|
@ -11,6 +11,7 @@ use App\Models\UserAbo;
|
|||
use App\Models\UserAboOrder;
|
||||
use App\Models\UserSalesVolume;
|
||||
use App\User;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class IncentivePointsLogRepairService
|
||||
{
|
||||
|
|
@ -33,8 +34,7 @@ class IncentivePointsLogRepairService
|
|||
|
||||
$added = 0;
|
||||
|
||||
$windowStart = $incentive->qualification_start->copy()->startOfDay();
|
||||
$windowEnd = $incentive->qualification_end->copy()->endOfDay();
|
||||
[$windowStart, $windowEnd] = $incentive->qualificationWindow();
|
||||
|
||||
$candidates = User::query()
|
||||
->where('m_sponsor', $participant->user_id)
|
||||
|
|
@ -44,6 +44,17 @@ class IncentivePointsLogRepairService
|
|||
})
|
||||
->get();
|
||||
|
||||
$firstStarterOrders = $candidates->isNotEmpty()
|
||||
? ShoppingOrder::query()
|
||||
->whereIn('auth_user_id', $candidates->pluck('id'))
|
||||
->wherePaidRegistrationIncludesStarterKit()
|
||||
->whereBetween('created_at', [$windowStart, $windowEnd])
|
||||
->orderBy('id')
|
||||
->get()
|
||||
->groupBy('auth_user_id')
|
||||
->map(fn ($orders) => $orders->first())
|
||||
: collect();
|
||||
|
||||
foreach ($candidates as $partner) {
|
||||
if (IncentiveNewPartner::query()
|
||||
->where('participant_id', $participant->id)
|
||||
|
|
@ -52,12 +63,7 @@ class IncentivePointsLogRepairService
|
|||
continue;
|
||||
}
|
||||
|
||||
$order = ShoppingOrder::query()
|
||||
->where('auth_user_id', $partner->id)
|
||||
->wherePaidRegistrationIncludesStarterKit()
|
||||
->whereBetween('created_at', [$windowStart, $windowEnd])
|
||||
->orderBy('id')
|
||||
->first();
|
||||
$order = $firstStarterOrders->get($partner->id);
|
||||
|
||||
if ($order) {
|
||||
IncentiveTracker::trackNewPartner($order);
|
||||
|
|
@ -107,22 +113,55 @@ class IncentivePointsLogRepairService
|
|||
* {@see syncMissingTrackingPartners} + {@see syncMissingSalesVolumeLogs} im selben
|
||||
* Lauf alles neu an. Geloeschte User bleiben unangetastet.
|
||||
*
|
||||
* Ein leerer Sponsor (m_sponsor = NULL, z. B. Admin-Umbau in Arbeit oder verwaister
|
||||
* Sponsor entfernt) gilt NICHT als Sponsorwechsel: das Tracking bleibt erhalten,
|
||||
* geloescht wird nur bei einem tatsaechlich gesetzten, abweichenden Sponsor.
|
||||
*
|
||||
* @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) {
|
||||
$trackings = $participant->newPartners()->get();
|
||||
if ($trackings->isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$sponsorIdByUserId = User::query()
|
||||
->whereIn('id', $trackings->pluck('user_id'))
|
||||
->pluck('m_sponsor', 'id');
|
||||
|
||||
foreach ($trackings as $tracking) {
|
||||
if (! $sponsorIdByUserId->has($tracking->user_id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((int) $partner->m_sponsor === (int) $participant->user_id) {
|
||||
$currentSponsorId = $sponsorIdByUserId->get($tracking->user_id);
|
||||
|
||||
if ($currentSponsorId === null) {
|
||||
Log::info('Incentive-Repair: Neupartner-Tracking uebersprungen, m_sponsor ist leer (kein Sponsorwechsel)', [
|
||||
'participant_id' => $participant->id,
|
||||
'tracking_id' => $tracking->id,
|
||||
'partner_user_id' => $tracking->user_id,
|
||||
'tracked_sponsor_user_id' => $participant->user_id,
|
||||
]);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((int) $currentSponsorId === (int) $participant->user_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Log::info('Incentive-Repair: Neupartner-Tracking wegen Sponsorwechsel entfernt', [
|
||||
'participant_id' => $participant->id,
|
||||
'tracking_id' => $tracking->id,
|
||||
'partner_user_id' => $tracking->user_id,
|
||||
'old_sponsor_user_id' => $participant->user_id,
|
||||
'new_sponsor_user_id' => (int) $currentSponsorId,
|
||||
]);
|
||||
|
||||
IncentivePointsLog::where('incentive_new_partner_id', $tracking->id)->delete();
|
||||
$tracking->delete();
|
||||
$removed++;
|
||||
|
|
@ -146,27 +185,22 @@ class IncentivePointsLogRepairService
|
|||
|
||||
$added = 0;
|
||||
|
||||
$qualEnd = $incentive->qualification_end->copy()->endOfDay();
|
||||
[$qualStart, $qualEnd] = $incentive->qualificationWindow();
|
||||
|
||||
$candidatesOt = UserAbo::query()
|
||||
->where('is_for', 'ot')
|
||||
->where('status', 2)
|
||||
->where('member_id', $participant->user_id)
|
||||
->whereBetween('created_at', [
|
||||
$incentive->qualification_start,
|
||||
$qualEnd,
|
||||
])
|
||||
->whereBetween('created_at', [$qualStart, $qualEnd])
|
||||
->get();
|
||||
|
||||
$candidatesMe = UserAbo::query()
|
||||
->where('is_for', 'me')
|
||||
->where('status', 2)
|
||||
->where('user_id', $participant->user_id)
|
||||
->where(function ($q) use ($incentive, $qualEnd) {
|
||||
$q->whereBetween('created_at', [
|
||||
$incentive->qualification_start,
|
||||
$qualEnd,
|
||||
])->orWhere('created_at', '<', $incentive->qualification_start);
|
||||
->where(function ($q) use ($qualStart, $qualEnd) {
|
||||
$q->whereBetween('created_at', [$qualStart, $qualEnd])
|
||||
->orWhere('created_at', '<', $qualStart);
|
||||
})
|
||||
->get();
|
||||
|
||||
|
|
@ -194,7 +228,6 @@ class IncentivePointsLogRepairService
|
|||
->where('participant_id', $participant->id)
|
||||
->where('user_abo_id', $userAbo->id)
|
||||
->exists()) {
|
||||
$qualStart = $incentive->qualification_start->copy()->startOfDay();
|
||||
$activatedAt = $userAbo->created_at;
|
||||
$logMonth = (int) $userAbo->created_at->month;
|
||||
$logYear = (int) $userAbo->created_at->year;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue