10.April 2026
This commit is contained in:
parent
a00c42e770
commit
f58c709945
208 changed files with 19280 additions and 2914 deletions
54
app/Services/Incentive/IncentiveCalculationService.php
Normal file
54
app/Services/Incentive/IncentiveCalculationService.php
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Incentive;
|
||||
|
||||
use App\Models\Incentive;
|
||||
use App\Models\IncentiveParticipant;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class IncentiveCalculationService
|
||||
{
|
||||
/**
|
||||
* Full recalculation of an incentive (batch / cron / manual).
|
||||
* Normal: Neuberechnung aus Tracking-Tabellen + Log.
|
||||
* Force: Kompletter Neuaufbau aus Quelldaten (Users, UserAbos, UserSalesVolumes).
|
||||
*/
|
||||
public function recalculate(Incentive $incentive, bool $force = false): array
|
||||
{
|
||||
$stats = ['participants' => 0, 'errors' => 0];
|
||||
|
||||
$participants = $incentive->participants()->with('user')->get();
|
||||
|
||||
foreach ($participants as $participant) {
|
||||
try {
|
||||
$this->recalculateParticipant($participant, $force);
|
||||
$stats['participants']++;
|
||||
} catch (\Throwable $e) {
|
||||
$stats['errors']++;
|
||||
Log::error('IncentiveCalculation error for participant '.$participant->id.': '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
IncentiveTracker::updateRanking($incentive);
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recalculate a single participant.
|
||||
* Force: Kompletter Neuaufbau aus Quelldaten.
|
||||
* Normal: Neuberechnung aus vorhandenen Tracking-Tabellen + Log.
|
||||
*/
|
||||
public function recalculateParticipant(IncentiveParticipant $participant, bool $force = false): void
|
||||
{
|
||||
if (! $participant->user) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($force) {
|
||||
$participant->rebuildFromSourceTables()->save();
|
||||
} else {
|
||||
$participant->recalculateFromTrackingTables()->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
353
app/Services/Incentive/IncentivePointsLogRepairService.php
Normal file
353
app/Services/Incentive/IncentivePointsLogRepairService.php
Normal file
|
|
@ -0,0 +1,353 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\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\UserAboOrder;
|
||||
use App\Models\UserSalesVolume;
|
||||
use App\User;
|
||||
|
||||
class IncentivePointsLogRepairService
|
||||
{
|
||||
/**
|
||||
* Fehlende Neupartner-Tracking-Zeilen anlegen (Starterpaket / gleiche Regeln wie Neuaufbau A).
|
||||
* Nutzt IncentiveTracker::trackNewPartner, wenn eine qualifizierte Bestellung existiert.
|
||||
*
|
||||
* @return int Anzahl nachgezogener Partner-Trackings fuer diesen Teilnehmer
|
||||
*/
|
||||
public function syncMissingTrackingPartners(IncentiveParticipant $participant): int
|
||||
{
|
||||
$incentive = $participant->incentive;
|
||||
if (! $incentive) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$added = 0;
|
||||
|
||||
$candidates = User::query()
|
||||
->where('m_sponsor', $participant->user_id)
|
||||
->whereBetween('created_at', [
|
||||
$incentive->qualification_start,
|
||||
$incentive->qualification_end->copy()->endOfDay(),
|
||||
])
|
||||
->whereHas('shopping_orders', function ($q) {
|
||||
$q->wherePaidRegistrationIncludesStarterKit();
|
||||
})
|
||||
->get();
|
||||
|
||||
foreach ($candidates as $partner) {
|
||||
if (IncentiveNewPartner::query()
|
||||
->where('participant_id', $participant->id)
|
||||
->where('user_id', $partner->id)
|
||||
->exists()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$order = ShoppingOrder::query()
|
||||
->where('auth_user_id', $partner->id)
|
||||
->wherePaidRegistrationIncludesStarterKit()
|
||||
->orderBy('id')
|
||||
->first();
|
||||
|
||||
if ($order) {
|
||||
IncentiveTracker::trackNewPartner($order);
|
||||
}
|
||||
|
||||
if (! IncentiveNewPartner::query()
|
||||
->where('participant_id', $participant->id)
|
||||
->where('user_id', $partner->id)
|
||||
->exists()) {
|
||||
$newPartner = IncentiveNewPartner::create([
|
||||
'participant_id' => $participant->id,
|
||||
'user_id' => $partner->id,
|
||||
'registered_at' => $partner->created_at,
|
||||
]);
|
||||
|
||||
IncentivePointsLog::create([
|
||||
'participant_id' => $participant->id,
|
||||
'type' => 'partner',
|
||||
'source_type' => User::class,
|
||||
'source_id' => $partner->id,
|
||||
'source_label' => $partner->getFullName() ?: $partner->email ?: ('User #'.$partner->id),
|
||||
'month' => $partner->created_at->month,
|
||||
'year' => $partner->created_at->year,
|
||||
'points_onetime' => $incentive->points_partner_onetime,
|
||||
'points_accumulated' => 0,
|
||||
'incentive_new_partner_id' => $newPartner->id,
|
||||
]);
|
||||
}
|
||||
|
||||
if (IncentiveNewPartner::query()
|
||||
->where('participant_id', $participant->id)
|
||||
->where('user_id', $partner->id)
|
||||
->exists()) {
|
||||
$added++;
|
||||
}
|
||||
}
|
||||
|
||||
return $added;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fehlende Abo-Tracking-Zeilen anlegen (Kundenabo ot / Eigenabo me, wie IncentiveTracker).
|
||||
* Zuerst trackAboActivated ueber Erstbestellung; ohne UserAboOrder manuell wie Neuaufbau B.
|
||||
*
|
||||
* @return int Anzahl nachgezogener Abo-Trackings fuer diesen Teilnehmer
|
||||
*/
|
||||
public function syncMissingTrackingAbos(IncentiveParticipant $participant): int
|
||||
{
|
||||
$incentive = $participant->incentive;
|
||||
if (! $incentive) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$added = 0;
|
||||
|
||||
$qualEnd = $incentive->qualification_end->copy()->endOfDay();
|
||||
|
||||
$candidatesOt = UserAbo::query()
|
||||
->where('is_for', 'ot')
|
||||
->where('status', 2)
|
||||
->where('member_id', $participant->user_id)
|
||||
->whereBetween('created_at', [
|
||||
$incentive->qualification_start,
|
||||
$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);
|
||||
})
|
||||
->get();
|
||||
|
||||
foreach ($candidatesOt->concat($candidatesMe) as $userAbo) {
|
||||
if (IncentiveNewAbo::query()
|
||||
->where('participant_id', $participant->id)
|
||||
->where('user_abo_id', $userAbo->id)
|
||||
->exists()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$order = UserAboOrder::query()
|
||||
->where('user_abo_id', $userAbo->id)
|
||||
->orderBy('id')
|
||||
->with('shopping_order')
|
||||
->first();
|
||||
|
||||
$shoppingOrder = $order?->shopping_order;
|
||||
|
||||
if ($shoppingOrder) {
|
||||
IncentiveTracker::trackAboActivated($shoppingOrder);
|
||||
}
|
||||
|
||||
if (! IncentiveNewAbo::query()
|
||||
->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;
|
||||
|
||||
if ($userAbo->is_for === 'me' && $userAbo->created_at->lt($qualStart)) {
|
||||
$activatedAt = $qualStart->copy();
|
||||
$logMonth = (int) $qualStart->month;
|
||||
$logYear = (int) $qualStart->year;
|
||||
}
|
||||
|
||||
$newAbo = IncentiveNewAbo::create([
|
||||
'participant_id' => $participant->id,
|
||||
'user_abo_id' => $userAbo->id,
|
||||
'activated_at' => $activatedAt,
|
||||
]);
|
||||
|
||||
IncentivePointsLog::create([
|
||||
'participant_id' => $participant->id,
|
||||
'type' => 'abo',
|
||||
'source_type' => UserAbo::class,
|
||||
'source_id' => $userAbo->id,
|
||||
'source_label' => $userAbo->email ?: ('Abo #'.$userAbo->id),
|
||||
'month' => $logMonth,
|
||||
'year' => $logYear,
|
||||
'points_onetime' => $incentive->points_abo_onetime,
|
||||
'points_accumulated' => 0,
|
||||
'incentive_new_abo_id' => $newAbo->id,
|
||||
]);
|
||||
}
|
||||
|
||||
if (IncentiveNewAbo::query()
|
||||
->where('participant_id', $participant->id)
|
||||
->where('user_abo_id', $userAbo->id)
|
||||
->exists()) {
|
||||
$added++;
|
||||
}
|
||||
}
|
||||
|
||||
return $added;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt fehlende incentive_new_partner_id / incentive_new_abo_id an bestehenden Log-Zeilen.
|
||||
*
|
||||
* @return array{partner_fk: int, abo_fk: int, onetime_partner_fk: int, onetime_abo_fk: int}
|
||||
*/
|
||||
public function repairForeignKeys(IncentiveParticipant $participant): array
|
||||
{
|
||||
$stats = [
|
||||
'partner_fk' => 0,
|
||||
'abo_fk' => 0,
|
||||
'onetime_partner_fk' => 0,
|
||||
'onetime_abo_fk' => 0,
|
||||
];
|
||||
|
||||
$newPartnerByUserId = $participant->newPartners()->get()->keyBy('user_id');
|
||||
$newAboByUserAboId = $participant->newAbos()->get()->keyBy('user_abo_id');
|
||||
|
||||
foreach ($participant->pointsLog()->where('type', 'partner')->whereNull('incentive_new_partner_id')->cursor() as $log) {
|
||||
if ($log->source_type === User::class && $log->source_id) {
|
||||
$np = $newPartnerByUserId->get($log->source_id);
|
||||
if ($np) {
|
||||
$log->update(['incentive_new_partner_id' => $np->id]);
|
||||
$stats['onetime_partner_fk']++;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($log->user_sales_volume_id) {
|
||||
$sv = UserSalesVolume::find($log->user_sales_volume_id);
|
||||
if ($sv && $sv->user_id) {
|
||||
$np = $newPartnerByUserId->get($sv->user_id);
|
||||
if ($np) {
|
||||
$log->update(['incentive_new_partner_id' => $np->id]);
|
||||
$stats['partner_fk']++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$orderIdToUserAboId = [];
|
||||
foreach ($participant->pointsLog()->where('type', 'abo')->whereNull('incentive_new_abo_id')->cursor() as $log) {
|
||||
if ($log->source_type === UserAbo::class && $log->source_id) {
|
||||
$na = $newAboByUserAboId->get($log->source_id);
|
||||
if ($na) {
|
||||
$log->update(['incentive_new_abo_id' => $na->id]);
|
||||
$stats['onetime_abo_fk']++;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($log->user_sales_volume_id) {
|
||||
$sv = UserSalesVolume::find($log->user_sales_volume_id);
|
||||
if ($sv && $sv->shopping_order_id) {
|
||||
if (! isset($orderIdToUserAboId[$sv->shopping_order_id])) {
|
||||
$orderIdToUserAboId[$sv->shopping_order_id] = UserAboOrder::where('shopping_order_id', $sv->shopping_order_id)->value('user_abo_id');
|
||||
}
|
||||
$userAboId = $orderIdToUserAboId[$sv->shopping_order_id] ?? null;
|
||||
if ($userAboId) {
|
||||
$na = $newAboByUserAboId->get($userAboId);
|
||||
if ($na) {
|
||||
$log->update(['incentive_new_abo_id' => $na->id]);
|
||||
$stats['abo_fk']++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruft IncentiveTracker::trackSalesVolume fuer Kandidaten-USVs auf, bei denen noch kein Log-Eintrag fuer diesen Teilnehmer existiert.
|
||||
*
|
||||
* @return int Anzahl neu angelegter Log-Zeilen (geschaetzt ueber Vorher/Nachher pro Teilnehmer)
|
||||
*/
|
||||
public function syncMissingSalesVolumeLogs(IncentiveParticipant $participant): int
|
||||
{
|
||||
$incentive = $participant->incentive;
|
||||
if (! $incentive) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$synced = 0;
|
||||
$months = $incentive->getCalculationMonths();
|
||||
|
||||
$newPartnerUserIds = $participant->newPartners()->pluck('user_id')->filter()->values();
|
||||
$trackedUserAboIds = $participant->newAbos()->pluck('user_abo_id')->filter()->values();
|
||||
|
||||
$orderIdsForAbos = $trackedUserAboIds->isNotEmpty()
|
||||
? UserAboOrder::query()->whereIn('user_abo_id', $trackedUserAboIds)->pluck('shopping_order_id')->unique()->values()
|
||||
: collect();
|
||||
|
||||
foreach ($months as $period) {
|
||||
if ($newPartnerUserIds->isNotEmpty()) {
|
||||
$svs = UserSalesVolume::query()
|
||||
->whereIn('user_id', $newPartnerUserIds)
|
||||
->where('month', $period['month'])
|
||||
->where('year', $period['year'])
|
||||
->where('status', '!=', 6)
|
||||
->get();
|
||||
|
||||
foreach ($svs as $sv) {
|
||||
if ((int) abs($sv->points ?? 0) <= 0) {
|
||||
continue;
|
||||
}
|
||||
if ($this->participantHasSalesVolumeLog($participant, $sv->id)) {
|
||||
continue;
|
||||
}
|
||||
IncentiveTracker::trackSalesVolume($sv);
|
||||
if ($this->participantHasSalesVolumeLog($participant, $sv->id)) {
|
||||
$synced++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($orderIdsForAbos->isNotEmpty()) {
|
||||
$svs = UserSalesVolume::query()
|
||||
->whereIn('shopping_order_id', $orderIdsForAbos)
|
||||
->where('month', $period['month'])
|
||||
->where('year', $period['year'])
|
||||
->where('status', '!=', 6)
|
||||
->get();
|
||||
|
||||
foreach ($svs as $sv) {
|
||||
if ((int) abs($sv->points ?? 0) <= 0) {
|
||||
continue;
|
||||
}
|
||||
if ($this->participantHasSalesVolumeLog($participant, $sv->id)) {
|
||||
continue;
|
||||
}
|
||||
IncentiveTracker::trackSalesVolume($sv);
|
||||
if ($this->participantHasSalesVolumeLog($participant, $sv->id)) {
|
||||
$synced++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $synced;
|
||||
}
|
||||
|
||||
private function participantHasSalesVolumeLog(IncentiveParticipant $participant, int $userSalesVolumeId): bool
|
||||
{
|
||||
return IncentivePointsLog::query()
|
||||
->where('participant_id', $participant->id)
|
||||
->where('user_sales_volume_id', $userSalesVolumeId)
|
||||
->where('is_storno', false)
|
||||
->exists();
|
||||
}
|
||||
}
|
||||
373
app/Services/Incentive/IncentiveTracker.php
Normal file
373
app/Services/Incentive/IncentiveTracker.php
Normal file
|
|
@ -0,0 +1,373 @@
|
|||
<?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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue