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:
Kevin 2026-08-21 10:44:44 +00:00
parent 841d87e161
commit 867dc21b57
87 changed files with 4480 additions and 240 deletions

View file

@ -3,14 +3,30 @@
namespace App;
use App\Mail\MailResetPassword;
use App\Models\Account;
use App\Models\File;
use App\Models\PaymentMethod;
use App\Models\Product;
use App\Models\ShoppingOrder;
use App\Models\ShoppingUser;
use App\Models\UserBusiness;
use App\Models\UserHistory;
use App\Models\UserLevel;
use App\Models\UserSalesVolume;
use App\Models\UserShop;
use App\Models\UserUpdateEmail;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\DatabaseNotification;
use Illuminate\Notifications\DatabaseNotificationCollection;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\Mail;
use Laravel\Passport\Client;
use Laravel\Passport\HasApiTokens;
use Laravel\Passport\Token;
/**
* App\User
@ -23,7 +39,7 @@ use Laravel\Passport\HasApiTokens;
* @property string|null $token
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \Illuminate\Notifications\DatabaseNotificationCollection|\Illuminate\Notifications\DatabaseNotification[] $notifications
* @property-read DatabaseNotificationCollection|DatabaseNotification[] $notifications
*
* @method static \Illuminate\Database\Eloquent\Builder|\App\User whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\User whereEmail($value)
@ -47,8 +63,8 @@ use Laravel\Passport\HasApiTokens;
* @property string|null $notes
* @property string|null $last_login
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property-read \App\Models\Account $account
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\UserUpdateEmail[] $user_update_email
* @property-read Account $account
* @property-read Collection|UserUpdateEmail[] $user_update_email
*
* @method static bool|null forceDelete()
* @method static \Illuminate\Database\Query\Builder|\App\User onlyTrashed()
@ -81,7 +97,7 @@ use Laravel\Passport\HasApiTokens;
* @property string|null $payment_account
* @property string|null $payment_shop
* @property-read int|null $notifications_count
* @property-read \App\Models\UserShop $shop
* @property-read UserShop $shop
* @property-read int|null $user_update_email_count
*
* @method static \Illuminate\Database\Eloquent\Builder|\App\User whereBlocked($value)
@ -91,8 +107,8 @@ use Laravel\Passport\HasApiTokens;
*
* @property int|null $m_level
* @property int|null $m_sponsor
* @property-read \App\Models\UserLevel|null $user_level
* @property-read \App\User|null $user_sponsor
* @property-read UserLevel|null $user_level
* @property-read User|null $user_sponsor
*
* @method static \Illuminate\Database\Eloquent\Builder|\App\User whereMLevel($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\User whereMSponsor($value)
@ -100,32 +116,32 @@ use Laravel\Passport\HasApiTokens;
* @property string|null $release_account
* @property int|null $payment_order_id
* @property int|null $abo_options
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\File[] $files
* @property-read Collection|File[] $files
* @property-read int|null $files_count
* @property-read \App\Models\Product|null $payment_order
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\ShoppingOrder[] $shopping_orders
* @property-read Product|null $payment_order
* @property-read Collection|ShoppingOrder[] $shopping_orders
* @property-read int|null $shopping_orders_count
*
* @method static \Illuminate\Database\Eloquent\Builder|\App\User whereAboOptions($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\User wherePaymentOrderId($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\User whereReleaseAccount($value)
*
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\UserHistory[] $user_histories
* @property-read Collection|UserHistory[] $user_histories
* @property-read int|null $user_histories_count
* @property int|null $test_mode
* @property array|null $settings
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\ShoppingOrder[] $member_shopping_orders
* @property-read Collection|ShoppingOrder[] $member_shopping_orders
* @property-read int|null $member_shopping_orders_count
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\ShoppingUser[] $member_shopping_users
* @property-read Collection|ShoppingUser[] $member_shopping_users
* @property-read int|null $member_shopping_users_count
* @property-read \App\Models\Product|null $payment_order_product
* @property-read Product|null $payment_order_product
*
* @method static \Illuminate\Database\Eloquent\Builder|\App\User whereSettings($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\User whereTestMode($value)
*
* @property-read \Illuminate\Database\Eloquent\Collection|\Laravel\Passport\Client[] $clients
* @property-read Collection|Client[] $clients
* @property-read int|null $clients_count
* @property-read \Illuminate\Database\Eloquent\Collection|\Laravel\Passport\Token[] $tokens
* @property-read Collection|Token[] $tokens
* @property-read int|null $tokens_count
* @property array|null $payment_methods
*
@ -140,7 +156,7 @@ use Laravel\Passport\HasApiTokens;
*
* @method static \Illuminate\Database\Eloquent\Builder<static>|User wherePreDeletedAt($value)
*
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\UserBusiness> $userBusiness
* @property-read Collection<int, UserBusiness> $userBusiness
* @property-read int|null $user_business_count
*
* @mixin \Eloquent
@ -200,6 +216,42 @@ class User extends Authenticatable
return $this->belongsTo('App\User', 'm_sponsor');
}
/**
* Firstlines (direkte Teampartner) eines oder mehrerer Sponsoren —
* die eine autoritative Definition für Dashboard-Kachel, Firstlines-Seite
* und Team-Auswertungen.
*
* @param Builder $query
* @param int|int[] $sponsorIds
* @return Builder
*/
public function scopeFirstlinesOf($query, int|array $sponsorIds)
{
return $query->whereIn('m_sponsor', (array) $sponsorIds)
->whereColumn('id', '!=', 'm_sponsor')
->whereNull('deleted_at');
}
/**
* Neupartner-Definition der Statistik (eine Regel für KPI, Recruiter-Ranking
* und Top-3-Karte): Mitgliedsebene gesetzt, Abrechnungskonto aktuell gültig,
* im Monat aktiv geworden. Achtung: `payment_account > now()` macht die
* Zählung rückwirkend kleiner, sobald Accounts ablaufen (bewusster Ist-Stand).
*
* @param Builder $query
* @return Builder
*/
public function scopeNewPartnerIn($query, int $month, int $year)
{
$startDate = Carbon::create($year, $month, 1)->startOfMonth();
$endDate = $startDate->copy()->endOfMonth();
return $query->whereNotNull('m_level')
->whereNotNull('payment_account')
->where('payment_account', '>', now())
->whereBetween('active_date', [$startDate, $endDate]);
}
public function user_pre_sponsor()
{
return $this->belongsTo('App\User', 'pre_sponsor');