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>
199 lines
6.8 KiB
PHP
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;
|
|
}
|
|
}
|