mivita/app/Services/Incentive/IncentiveTracker.php
2026-04-10 17:15:27 +02:00

373 lines
15 KiB
PHP

<?php
namespace App\Services\Incentive;
use App\Models\Incentive;
use App\Models\IncentiveNewAbo;
use App\Models\IncentiveNewPartner;
use App\Models\IncentiveParticipant;
use App\Models\IncentivePointsLog;
use App\Models\ShoppingOrder;
use App\Models\UserAbo;
use App\Models\UserSalesVolume;
use App\User;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
class IncentiveTracker
{
/**
* Track a new partner registration (Starterpaket bezahlt).
* Fuegt Partner in Tracking-Tabelle ein + Log-Eintrag + Neuberechnung.
*/
public static function trackNewPartner(ShoppingOrder $shopping_order): void
{
try {
if (! $shopping_order->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,
]);
}
}