qualifiesForIncentiveTrackedPartner()) { return; } $new_user = User::find($shopping_order->auth_user_id); if (! $new_user || ! $new_user->m_sponsor) { return; } $sponsor_id = $new_user->m_sponsor; $registration_date = $shopping_order->created_at ?? Carbon::now(); $incentives = Incentive::query() ->active() ->where('qualification_start', '<=', $registration_date) ->where('qualification_end', '>=', $registration_date) ->get(); foreach ($incentives as $incentive) { $participant = IncentiveParticipant::ensureForIncentiveUser($incentive, $sponsor_id); // Tracking-Tabelle: Partner erfassen (keine Duplikate) $newPartner = IncentiveNewPartner::firstOrCreate( ['participant_id' => $participant->id, 'user_id' => $new_user->id], ['registered_at' => $registration_date] ); // Log-Eintrag (Audit-Trail, keine Duplikate) self::writeLog($participant, 'partner', User::class, $new_user->id, $new_user->getFullName() ?: $new_user->email ?: ('User #'.$new_user->id), $registration_date, $incentive->points_partner_onetime, $newPartner->id); // Neuberechnung aus Tracking-Tabellen $participant->recalculateFromTrackingTables()->save(); self::updateRanking($incentive); } } catch (\Throwable $e) { Log::error('IncentiveTracker::trackNewPartner error: '.$e->getMessage(), [ 'shopping_order_id' => $shopping_order->id, ]); } } /** * Track an abo activation (Kundenabo is_for=ot oder Berater-Eigenabo is_for=me, bezahlt + aktiv). * Fuegt Abo in Tracking-Tabelle ein + Log-Eintrag + Neuberechnung. */ /** * Berater-ID für Incentive-Zuordnung: Bei Kundenabos (ot) sitzt der Berater in member_id, * bei Berater-Eigenabo (me) in user_id (vgl. AboHelper::createNewAbo). */ public static function consultantUserIdForAboIncentive(UserAbo $user_abo): ?int { if ($user_abo->is_for === 'ot') { return $user_abo->member_id ? (int) $user_abo->member_id : null; } return $user_abo->user_id ? (int) $user_abo->user_id : null; } public static function trackAboActivated(ShoppingOrder $shopping_order): void { try { $user_abo = $shopping_order->getUserAbo(); if (! $user_abo) { return; } if ($user_abo->is_for === 'ot') { $consultant_id = self::consultantUserIdForAboIncentive($user_abo); } elseif ($user_abo->is_for === 'me') { $consultant_id = $user_abo->user_id ? (int) $user_abo->user_id : null; } else { return; } if (! $consultant_id) { return; } $activation_date = $shopping_order->created_at ?? Carbon::now(); $incentives = Incentive::query() ->active() ->where('qualification_start', '<=', $activation_date) ->where('qualification_end', '>=', $activation_date) ->get(); foreach ($incentives as $incentive) { $participant = IncentiveParticipant::ensureForIncentiveUser($incentive, $consultant_id); // Tracking-Tabelle: Abo erfassen (keine Duplikate) $newAbo = IncentiveNewAbo::firstOrCreate( ['participant_id' => $participant->id, 'user_abo_id' => $user_abo->id], ['activated_at' => $activation_date] ); // Log-Eintrag (Audit-Trail) self::writeLog($participant, 'abo', get_class($user_abo), $user_abo->id, $user_abo->email ?: ('Abo #'.$user_abo->id), $activation_date, $incentive->points_abo_onetime, null, $newAbo->id); // Neuberechnung aus Tracking-Tabellen $participant->recalculateFromTrackingTables()->save(); self::updateRanking($incentive); } } catch (\Throwable $e) { Log::error('IncentiveTracker::trackAboActivated error: '.$e->getMessage(), [ 'shopping_order_id' => $shopping_order->id, ]); } } /** * Track accumulated sales volume points. * Punkte werden NUR gezaehlt wenn der Umsatz von einem gettrackten * Neupartner oder Neuabo stammt. */ public static function trackSalesVolume(UserSalesVolume $user_sales_volume): void { try { $month = $user_sales_volume->month; $year = $user_sales_volume->year; if (! $month || ! $year) { return; } $points = (int) abs($user_sales_volume->points ?? 0); if ($points <= 0) { return; } // A. Pruefen ob der User ein gettrackter Neupartner ist $partner_trackings = IncentiveNewPartner::where('user_id', $user_sales_volume->user_id) ->whereHas('participant.incentive', fn ($q) => $q->active()) ->with('participant.incentive') ->get(); foreach ($partner_trackings as $tracking) { $participant = $tracking->participant; $incentive = $participant->incentive; if (! $incentive->isDateInScope($month, $year)) { continue; } $exists = IncentivePointsLog::where('participant_id', $participant->id) ->where('user_sales_volume_id', $user_sales_volume->id) ->where('is_storno', false) ->exists(); if (! $exists) { IncentivePointsLog::create([ 'participant_id' => $participant->id, 'type' => 'partner', 'source_type' => UserSalesVolume::class, 'source_id' => $user_sales_volume->id, 'source_label' => $user_sales_volume->message ?? ('SV '.$month.'/'.$year), 'month' => $month, 'year' => $year, 'points_onetime' => 0, 'points_accumulated' => $points, 'user_sales_volume_id' => $user_sales_volume->id, 'incentive_new_partner_id' => $tracking->id, ]); } $participant->recalculateFromTrackingTables()->save(); self::updateRanking($incentive); } // B. Pruefen ob die Bestellung zu einem getrackten Neuabo gehoert (Kundenabo ot oder Berater me). // Bei Verlaengerung weicht shopping_order.shopping_user_id oft vom Stamm-user_abos.shopping_user_id ab (Replikat). if ($user_sales_volume->shopping_order_id) { $order = ShoppingOrder::find($user_sales_volume->shopping_order_id); if ($order) { $userAboFromOrder = $order->getUserAbo(); if (! $userAboFromOrder || ! in_array($userAboFromOrder->is_for, ['ot', 'me'], true)) { $userAboFromOrder = null; } $abo_trackings = $userAboFromOrder ? IncentiveNewAbo::query() ->where('user_abo_id', $userAboFromOrder->id) ->whereHas('participant.incentive', fn ($q) => $q->active()) ->with('participant.incentive') ->get() : collect(); foreach ($abo_trackings as $tracking) { $participant = $tracking->participant; $incentive = $participant->incentive; if (! $incentive->isDateInScope($month, $year)) { continue; } $exists = IncentivePointsLog::where('participant_id', $participant->id) ->where('user_sales_volume_id', $user_sales_volume->id) ->where('is_storno', false) ->exists(); if (! $exists) { IncentivePointsLog::create([ 'participant_id' => $participant->id, 'type' => 'abo', 'source_type' => UserSalesVolume::class, 'source_id' => $user_sales_volume->id, 'source_label' => $user_sales_volume->message ?? ('SV '.$month.'/'.$year), 'month' => $month, 'year' => $year, 'points_onetime' => 0, 'points_accumulated' => $points, 'user_sales_volume_id' => $user_sales_volume->id, 'incentive_new_abo_id' => $tracking->id, ]); } $participant->recalculateFromTrackingTables()->save(); self::updateRanking($incentive); } } } } catch (\Throwable $e) { Log::error('IncentiveTracker::trackSalesVolume error: '.$e->getMessage(), [ 'user_sales_volume_id' => $user_sales_volume->id, ]); } } /** * Track a storno (cancellation) of a sales volume. * Storno-Log + Neuberechnung aus Tracking-Tabellen. */ public static function trackStorno(UserSalesVolume $original, UserSalesVolume $cancellation): void { try { // Storno-Log-Eintraege schreiben $original_logs = IncentivePointsLog::where('user_sales_volume_id', $original->id) ->where('is_storno', false) ->get(); $affected_participants = collect(); foreach ($original_logs as $original_log) { IncentivePointsLog::create([ 'participant_id' => $original_log->participant_id, 'type' => $original_log->type, 'source_type' => $original_log->source_type, 'source_id' => $original_log->source_id, 'source_label' => 'Storno: '.$original_log->source_label, 'month' => $cancellation->month ?? $original_log->month, 'year' => $cancellation->year ?? $original_log->year, 'points_onetime' => -$original_log->points_onetime, 'points_accumulated' => -$original_log->points_accumulated, 'is_storno' => true, 'storno_of_id' => $original_log->id, 'user_sales_volume_id' => $cancellation->id, 'incentive_new_partner_id' => $original_log->incentive_new_partner_id, 'incentive_new_abo_id' => $original_log->incentive_new_abo_id, ]); $affected_participants->push($original_log->participant_id); } // Auch ohne Log-Eintraege: alle Teilnehmer dieses Users neu berechnen if ($affected_participants->isEmpty() && $original->user_id) { $affected_participants = IncentiveParticipant::whereHas('incentive', function ($q) { $q->active(); })->where('user_id', $original->user_id)->pluck('id'); } // Neuberechnung fuer alle betroffenen Teilnehmer foreach ($affected_participants->unique() as $participant_id) { $participant = IncentiveParticipant::with('incentive')->find($participant_id); if (! $participant) { continue; } $participant->recalculateFromTrackingTables()->save(); if ($participant->incentive) { self::updateRanking($participant->incentive); } } } catch (\Throwable $e) { Log::error('IncentiveTracker::trackStorno error: '.$e->getMessage(), [ 'original_id' => $original->id, 'cancellation_id' => $cancellation->id, ]); } } /** * Update ranking for all participants of an incentive. */ public static function updateRanking(Incentive $incentive): void { // Nur Teilnehmer mit Punkten bekommen einen Rang (bei Punktgleichstand: Teilnahme bestaetigt vor anonym) $with_points = IncentiveParticipant::where('incentive_id', $incentive->id) ->where('total_points', '>', 0) ->orderByDesc('total_points') ->orderByRaw('accepted_terms_at IS NOT NULL DESC') ->get(); $rank = 1; foreach ($with_points as $participant) { $participant->rank = $rank; $participant->save(); $rank++; } // Teilnehmer ohne Punkte: Rang entfernen IncentiveParticipant::where('incentive_id', $incentive->id) ->where('total_points', '<=', 0) ->whereNotNull('rank') ->update(['rank' => null]); } /** * Log-Eintrag schreiben (Audit-Trail, keine Duplikate). */ private static function writeLog(IncentiveParticipant $participant, string $type, string $source_type, int $source_id, string $source_label, Carbon $date, int $points_onetime, ?int $incentive_new_partner_id = null, ?int $incentive_new_abo_id = null): void { $exists = IncentivePointsLog::where('participant_id', $participant->id) ->where('type', $type) ->where('source_type', $source_type) ->where('source_id', $source_id) ->where('is_storno', false) ->exists(); if ($exists) { return; } IncentivePointsLog::create([ 'participant_id' => $participant->id, 'type' => $type, 'source_type' => $source_type, 'source_id' => $source_id, 'source_label' => $source_label, 'month' => $date->month, 'year' => $date->year, 'points_onetime' => $points_onetime, 'points_accumulated' => 0, 'incentive_new_partner_id' => $incentive_new_partner_id, 'incentive_new_abo_id' => $incentive_new_abo_id, ]); } }