|null, next_level: array|null, payline_points_qual_kp: float, personal_points: float} */ public function snapshot(User $user, int $month, int $year): array { $stored = UserBusiness::where('user_id', $user->id) ->where('month', $month) ->where('year', $year) ->first(); if ($stored) { return $this->buildSnapshot( (float) ($stored->payline_points ?? 0), $this->paylinesFromQualLevel($stored->qual_user_level), (float) ($stored->commission_pp_total ?? 0), (float) ($stored->commission_shop_sales ?? 0), (float) ($stored->commission_growth_total ?? 0), false, qualLevel: $this->normalizeLevel($stored->qual_user_level), nextLevel: $this->resolveNextLevel( $stored->qual_user_level_next, $stored->next_qual_user_level, $stored->next_can_user_level, ), paylinePointsQualKp: (float) ($stored->payline_points_qual_kp ?? 0), personalPoints: (float) ($stored->sales_volume_points_KP_sum ?? 0), ); } // v3: Payload um Level-/Quali-Daten erweitert (N1/N4, next_level mit // Karriere-Fallback) — alte Cache-Einträge dürfen nicht wiederverwendet werden. $cacheKey = "live_business_v3_{$user->id}_{$month}_{$year}"; $cached = Cache::get($cacheKey); if (is_array($cached)) { return $cached; } $snapshot = $this->calculateLive($user, $month, $year); if ($snapshot !== null) { Cache::put($cacheKey, $snapshot, now()->addHours(self::CACHE_HOURS)); return $snapshot; } return $this->buildSnapshot(0.0, 0, 0.0, 0.0, 0.0, true, failed: true); } /** * Provision "Stand heute" (B1) — alle Bestandteile plus Summe. * * `failed` wird mit durchgereicht: eine fehlgeschlagene Live-Berechnung liefert * Ersatzwerte, aus denen sich rechnerisch 0,00 Euro ergibt. Ohne dieses Kennzeichen * waere ein Berechnungsfehler in der Oberflaeche nicht von einem echten Nullmonat * zu unterscheiden. * * @return array{total: float, pp: float, shop: float, growth: float, is_live: bool, failed: bool} */ public function commission(User $user, int $month, int $year): array { $snapshot = $this->snapshot($user, $month, $year); return [ 'total' => round($snapshot['pp'] + $snapshot['shop'] + $snapshot['growth'], 2), 'pp' => $snapshot['pp'], 'shop' => $snapshot['shop'], 'growth' => $snapshot['growth'], 'is_live' => $snapshot['is_live'], 'failed' => (bool) ($snapshot['failed'] ?? false), ]; } /** * @return array{payline_points: float, paylines: int, pp: float, shop: float, growth: float, is_live: bool, failed: bool}|null */ private function calculateLive(User $user, int $month, int $year): ?array { try { $treeCalcBot = new TreeCalcBotOptimized($month, $year, 'member'); $treeCalcBot->initBusinesslUserDetail($user); $businessUser = $treeCalcBot->business_user; if (! $businessUser) { return null; } // Kein `?? 0` auf den Magic-Properties: die Items haben kein __isset, // isset() wäre dort immer false und das Ergebnis damit immer 0. return $this->buildSnapshot( (float) $businessUser->payline_points, $this->paylinesFromQualLevel($businessUser->qual_user_level), (float) $businessUser->commission_pp_total, (float) $businessUser->commission_shop_sales, (float) $businessUser->commission_growth_total, true, qualLevel: $this->normalizeLevel($businessUser->qual_user_level), nextLevel: $this->resolveNextLevel( $businessUser->qual_user_level_next, $businessUser->next_qual_user_level, $businessUser->next_can_user_level, ), paylinePointsQualKp: (float) $businessUser->payline_points_qual_kp, personalPoints: (float) $businessUser->sales_volume_points_KP_sum, ); } catch (\Throwable $e) { \Log::warning("LiveCommissionService: Live-Berechnung fehlgeschlagen für User {$user->id}: ".$e->getMessage()); return null; } } /** * Payline-Tiefe (Anzahl bezahlter Ebenen) aus dem Qualifikationslevel. * * @param array|object|null $qualUserLevel */ private function paylinesFromQualLevel(mixed $qualUserLevel): int { if (is_array($qualUserLevel)) { return (int) ($qualUserLevel['paylines'] ?? 0); } if (is_object($qualUserLevel)) { return (int) ($qualUserLevel->paylines ?? 0); } return 0; } /** * Level-Fortschritt für die Level-Kachel (N1, F2): monatliche * Qualifikationsstufe plus "zum nächsten Level fehlen X Payline-Punkte / * Y persönliches Volumen"; Karriere-Level als Zusatzinfo. Eine Stelle für * die Rechnung, damit Kachel und Prognose nicht auseinanderlaufen. * * Vor der ersten Monatsqualifikation ist `qual_level` null — dann liefert * {@see BusinessUserItemOptimized::setFirstQualLevel()} das erste erreichbare * Level als `next_level` mit. Der Block darf in diesem Zustand NICHT * verschwinden: genau dann ist "dir fehlen noch X Punkte" die nützlichste * Information der Seite. `is_qualified` unterscheidet die beiden Fälle. * * @return array{name: string, is_qualified: bool, career_name: string, next_name: string|null, missing_pp: float, missing_personal: float, is_live: bool}|null */ public function levelProgress(User $user, int $month, int $year): ?array { $snapshot = $this->snapshot($user, $month, $year); $qualLevel = $snapshot['qual_level']; if (! empty($snapshot['failed'])) { return null; } $nextLevel = $snapshot['next_level']; if (! is_array($qualLevel) && ! is_array($nextLevel)) { return null; } // Punktebasis zum nächsten Level: dessen _calculated-Wert rechnet mit den // Paylines/dem KP-Überschuss DES nächsten Levels — die eigene // payline_points_qual_kp (erreichtes Level) wäre bei mehr Paylines zu klein. $paylineBasis = is_array($nextLevel) ? (float) ($nextLevel['_calculated_payline_points_qual_kp'] ?? $snapshot['payline_points_qual_kp']) : 0.0; return [ 'name' => is_array($qualLevel) ? (string) ($qualLevel['name'] ?? '') : '', 'is_qualified' => is_array($qualLevel), 'career_name' => $user->user_level?->getLang('name') ?? '', 'next_name' => is_array($nextLevel) ? (string) ($nextLevel['name'] ?? '') : null, 'missing_pp' => is_array($nextLevel) ? max(0.0, (float) ($nextLevel['qual_pp'] ?? 0) - $paylineBasis) : 0.0, 'missing_personal' => is_array($nextLevel) ? max(0.0, (float) ($nextLevel['qual_kp'] ?? 0) - (float) $snapshot['personal_points']) : 0.0, 'is_live' => (bool) $snapshot['is_live'], ]; } /** * Nächstes Level für die "fehlen noch X Punkte"-Anzeige: qual_user_level_next * ist im Normalzustand (Quali-Stufe == Karriere-Level) bewusst null — dann * greift der direkt nächste Karriere-Level aus setNextUserLevel() * (next_qual_user_level bzw. next_can_user_level, beide mit _calculated-Werten). * * @return array|null */ private function resolveNextLevel(mixed $qualNext, mixed $nextQual, mixed $nextCan): ?array { return $this->normalizeLevel($qualNext) ?? $this->normalizeLevel($nextQual) ?? $this->normalizeLevel($nextCan); } /** * Level-Daten (JSON aus UserBusiness oder Objekt/Array aus dem Live-Lauf) * in ein einheitliches Array bringen; null wenn nichts vorliegt. * * @return array|null */ private function normalizeLevel(mixed $level): ?array { if (is_object($level)) { $level = method_exists($level, 'toArray') ? $level->toArray() : (array) $level; } return is_array($level) && $level !== [] ? $level : null; } /** * @param array|null $qualLevel * @param array|null $nextLevel * @return array{payline_points: float, paylines: int, pp: float, shop: float, growth: float, is_live: bool, failed: bool, qual_level: array|null, next_level: array|null, payline_points_qual_kp: float, personal_points: float} */ private function buildSnapshot( float $paylinePoints, int $paylines, float $pp, float $shop, float $growth, bool $isLive, bool $failed = false, ?array $qualLevel = null, ?array $nextLevel = null, float $paylinePointsQualKp = 0.0, float $personalPoints = 0.0, ): array { return [ 'payline_points' => round($paylinePoints, 2), 'paylines' => $paylines, 'pp' => round($pp, 2), 'shop' => round($shop, 2), 'growth' => round($growth, 2), 'is_live' => $isLive, 'failed' => $failed, 'qual_level' => $qualLevel, 'next_level' => $nextLevel, 'payline_points_qual_kp' => round($paylinePointsQualKp, 2), 'personal_points' => round($personalPoints, 2), ]; } }