mivita/app/Http/Controllers/User/IncentiveController.php
Kevin c4edbf97e4 Recruiting Challenge: Storno nimmt Neupartner raus, Rang und Auszahlung nur mit Bestaetigung
Kundenentscheid 25.09.2026:
- Storno: Ist jede bezahlte Starterpaket-Bestellung eines Neupartners
  storniert (txaction = cancelled), bekommt das Tracking cancelled_at,
  eine Storno-Logzeile nimmt die Einmalpunkte zurueck. Nachtlauf und
  Neuaufbau markieren; "Meine Berechnung" zeigt den Partner durchgestrichen
  mit "Storniert am ..., zaehlt nicht".
- Gezaehlt wird fuer alle, einen Rang bekommen nur bestaetigte Teilnehmer;
  Export und Summen nur fuer sie. Unbestaetigte sehen "Deine Praemie nach
  Bestaetigung", im Admin "Teilnahme nicht bestaetigt, keine Auszahlung".
- Datenschutz: andere Berater sehen Unbestaetigte in der Rangliste nicht
  mehr (auch nicht anonym), VIP-Konten schon, der eigene Eintrag bleibt.
- Teilnahmebedingungen Punkt 2 und 7 und Hinweistexte in de/en/es/fr.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 10:44:49 +00:00

199 lines
6.8 KiB
PHP

<?php
namespace App\Http\Controllers\User;
use App\Http\Controllers\Admin\IncentiveController as AdminIncentiveController;
use App\Http\Controllers\Controller;
use App\Models\Incentive;
use App\Models\IncentiveParticipant;
use App\Models\UserAbo;
use App\Services\Incentive\IncentivePayoutService;
use App\Services\Incentive\IncentivePointsLogRepairService;
use App\Services\Incentive\IncentiveTracker;
use App\User;
use Carbon\Carbon;
use Illuminate\Support\Facades\Auth;
use Request;
class IncentiveController extends Controller
{
/**
* Anzahl Plaetze in der User-Live-Rangliste (Gewinner-Highlight bleibt ueber max_winners, typ. Top 20).
*/
public const USER_RANKING_DISPLAY_LIMIT = 30;
public function __construct()
{
$this->middleware('active.account');
}
/**
* Die eine Incentive-Seite: Teilnahme oder Kopf mit Stand, Titelbild, Text,
* Kacheln, "So funktioniert es", Galerie und Live-Rangliste.
*/
public function teaser($slug)
{
$incentive = Incentive::where('slug', $slug)
->where('status', '!=', 0)
->firstOrFail();
$user = Auth::user();
abort_unless($incentive->isVisibleFor($user), 404);
$participant = IncentiveParticipant::where('incentive_id', $incentive->id)
->where('user_id', $user->id)
->first();
// Datenschutz (Kundenentscheid 25.09.2026): Unbestaetigte Teilnehmer sehen nur VIP-Konten;
// der eigene Eintrag bleibt fuer den Berater selbst sichtbar.
$ranking = IncentiveParticipant::where('incentive_id', $incentive->id)
->withRankingActivity()
->when(! $user->isVIP(), fn ($query) => $query->where(
fn ($visible) => $visible->whereNotNull('accepted_terms_at')->orWhere('user_id', $user->id)
))
->with('user', 'user.account')
->orderByIncentiveLeaderboard()
->paginate(100);
$participateHasTrackableAbos = false;
if (! $participant?->accepted_terms_at && $incentive->includesAbos()) {
$participateHasTrackableAbos = $this->userHasTrackableAbosForIncentive($user, $incentive);
}
$payoutService = new IncentivePayoutService;
$participantPayout = $incentive->hasCashPayout() && $participant
? $payoutService->forParticipant($incentive, $participant)
: null;
$rankingPayouts = $incentive->hasCashPayout()
? $ranking->getCollection()->mapWithKeys(fn (IncentiveParticipant $row) => [
$row->id => $payoutService->forParticipant($incentive, $row),
])
: collect();
return view('user.incentive.teaser', [
'incentive' => $incentive,
'participant' => $participant,
'hasConfirmedParticipation' => $participant && $participant->accepted_terms_at !== null,
'galleryImages' => $this->collectGalleryImages($incentive),
'ranking' => $ranking,
'participateHasTrackableAbos' => $participateHasTrackableAbos,
'isVipView' => $user->isVIP(),
'participantPayout' => $participantPayout,
'rankingPayouts' => $rankingPayouts,
]);
}
/**
* Alte Ranglisten-URL: leitet auf die zusammengefasste Seite mit Sprungmarke zur Rangliste.
*/
public function show($slug)
{
return redirect(route('user_incentive_teaser', [$slug]).'#ranking');
}
public function participate($slug)
{
$incentive = Incentive::where('slug', $slug)
->active()
->firstOrFail();
$user = Auth::user();
abort_unless($incentive->isVisibleFor($user), 404);
if (! Request::has('accept_terms')) {
\Session()->flash('alert-error', __('incentive.terms_required'));
return redirect(route('user_incentive_teaser', [$slug]));
}
$participant = IncentiveParticipant::firstOrCreate(
[
'incentive_id' => $incentive->id,
'user_id' => $user->id,
],
[
'accepted_terms_at' => null,
]
);
if ($participant->accepted_terms_at !== null) {
\Session()->flash('alert-info', __('incentive.already_participating'));
return redirect(route('user_incentive_teaser', [$slug]));
}
$participant->accepted_terms_at = Carbon::now();
$participant->save();
$repair = app(IncentivePointsLogRepairService::class);
$repair->syncMissingTrackingAbos($participant);
$repair->syncMissingSalesVolumeLogs($participant);
$participant->refresh()->recalculateFromTrackingTables()->save();
IncentiveTracker::updateRanking($incentive);
\Session()->flash('alert-success', __('incentive.participation_confirmed'));
return redirect(route('user_incentive_teaser', [$slug]));
}
public function details($slug)
{
$incentive = Incentive::where('slug', $slug)
->where('status', '!=', 0)
->firstOrFail();
$user = Auth::user();
abort_unless($incentive->isVisibleFor($user), 404);
$participant = IncentiveParticipant::with('incentive', 'user', 'user.account')
->where('incentive_id', $incentive->id)
->where('user_id', $user->id)
->firstOrFail();
if ($participant->accepted_terms_at === null) {
\Session()->flash('alert-info', __('incentive.details_requires_confirmation'));
return redirect(route('user_incentive_teaser', [$slug]));
}
$data = AdminIncentiveController::buildParticipantDetailData($participant);
return view('user.incentive.details', $data);
}
/**
* Galerie-Bilder des Incentives aus seinem eigenen Ordner ({@see Incentive::galleryImages()}).
*
* @return list<string>
*/
private function collectGalleryImages(Incentive $incentive): array
{
return $incentive->galleryImages();
}
/**
* Hinweis auf der Teilnehmen-Karte: Es gibt bereits ein Eigenabo oder ein Kundenabo im Qualifikationszeitraum.
*/
private function userHasTrackableAbosForIncentive(User $user, Incentive $incentive): bool
{
$qualEnd = $incentive->qualification_end->copy()->endOfDay();
$hasOwnActiveAbo = UserAbo::query()
->where('user_id', $user->id)
->where('is_for', 'me')
->where('status', 2)
->exists();
$hasCustomerAboInQualification = UserAbo::query()
->where('member_id', $user->id)
->where('is_for', 'ot')
->where('status', 2)
->whereBetween('created_at', [
$incentive->qualification_start,
$qualEnd,
])
->exists();
return $hasOwnActiveAbo || $hasCustomerAboInQualification;
}
}