Incentive (E1): Recruiting-Challenge mit Geldpraemie, Auszahlungs-Export und parallelen Incentives
Neue Felder amount_per_partner, bonus_rank_1..3 und include_sales_points an incentives. Mit ausgeschalteten Umsatzpunkten zaehlt die Wertung nur die Einmalpunkte je Neupartner, die Rangliste entspricht damit der Anzahl Neupartner mit Starterpaket. IncentivePayoutService rechnet Neupartner mal Betrag plus Rang-Bonus fuer qualifizierte Gewinner; Admin-Detailseite bekommt Praemienspalte, Summen und XLSX-Export fuer die Auszahlung. Sidenav und Dashboard zeigen alle aktiven Incentives, damit Montenegro und die Challenge parallel laufen koennen. Teaser, Rangliste und Details zeigen bei Geldpraemie die Praemien-Kachel und "Deine Praemie bisher" und blenden Abo-Bloecke aus, wenn Abos keine Rolle spielen. Montenegro bleibt unveraendert. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
03416b0afe
commit
e3efa2f5f0
27 changed files with 1418 additions and 129 deletions
137
app/Services/Incentive/IncentivePayoutService.php
Normal file
137
app/Services/Incentive/IncentivePayoutService.php
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Incentive;
|
||||
|
||||
use App\Models\Incentive;
|
||||
use App\Models\IncentiveParticipant;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Geldpraemie eines Incentives (Recruiting-Challenge, Entwicklungsplan E1).
|
||||
*
|
||||
* Praemie = Anzahl Neupartner mit Starterpaket x Betrag je Neupartner, dazu der
|
||||
* Rang-Bonus, wenn der Teilnehmer qualifiziert ist und innerhalb der Gewinnerplaetze
|
||||
* liegt. Grundlage ist der gespeicherte Stand des Teilnehmers (qualified_partners, rank),
|
||||
* also derselbe Stand wie in der Rangliste.
|
||||
*/
|
||||
class IncentivePayoutService
|
||||
{
|
||||
/**
|
||||
* @return array{partners: int, per_partner: float, partner_amount: float, rank_bonus: float, total: float}
|
||||
*/
|
||||
public function forParticipant(Incentive $incentive, IncentiveParticipant $participant): array
|
||||
{
|
||||
$partners = (int) $participant->qualified_partners;
|
||||
$perPartner = (float) $incentive->amount_per_partner;
|
||||
$partnerAmount = round($partners * $perPartner, 2);
|
||||
$rankBonus = $participant->is_qualified ? $incentive->rankBonus($participant->rank) : 0.0;
|
||||
|
||||
return [
|
||||
'partners' => $partners,
|
||||
'per_partner' => $perPartner,
|
||||
'partner_amount' => $partnerAmount,
|
||||
'rank_bonus' => $rankBonus,
|
||||
'total' => round($partnerAmount + $rankBonus, 2),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Auszahlungszeilen: alle Teilnehmer mit Praemie > 0, nach Rang.
|
||||
*
|
||||
* @return Collection<int, array{participant: IncentiveParticipant, payout: array{partners: int, per_partner: float, partner_amount: float, rank_bonus: float, total: float}}>
|
||||
*/
|
||||
public function rows(Incentive $incentive): Collection
|
||||
{
|
||||
return IncentiveParticipant::query()
|
||||
->where('incentive_id', $incentive->id)
|
||||
->with('user', 'user.account')
|
||||
->orderByRankNullsLast()
|
||||
->orderByDesc('qualified_partners')
|
||||
->get()
|
||||
->map(fn (IncentiveParticipant $participant) => [
|
||||
'participant' => $participant,
|
||||
'payout' => $this->forParticipant($incentive, $participant),
|
||||
])
|
||||
->filter(fn (array $row) => $row['payout']['total'] > 0)
|
||||
->values();
|
||||
}
|
||||
|
||||
/**
|
||||
* Summen ueber alle Auszahlungszeilen.
|
||||
*
|
||||
* @param Collection<int, array{participant: IncentiveParticipant, payout: array{partners: int, per_partner: float, partner_amount: float, rank_bonus: float, total: float}}> $rows
|
||||
* @return array{recipients: int, partners: int, partner_amount: float, rank_bonus: float, total: float}
|
||||
*/
|
||||
public function totals(Collection $rows): array
|
||||
{
|
||||
return [
|
||||
'recipients' => $rows->count(),
|
||||
'partners' => (int) $rows->sum(fn (array $row) => $row['payout']['partners']),
|
||||
'partner_amount' => round((float) $rows->sum(fn (array $row) => $row['payout']['partner_amount']), 2),
|
||||
'rank_bonus' => round((float) $rows->sum(fn (array $row) => $row['payout']['rank_bonus']), 2),
|
||||
'total' => round((float) $rows->sum(fn (array $row) => $row['payout']['total']), 2),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Spaltenueberschriften des XLSX-Exports.
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
public function exportHeaders(): array
|
||||
{
|
||||
return [
|
||||
__('incentive.rank'),
|
||||
__('incentive.export_user_id'),
|
||||
__('incentive.consultant'),
|
||||
'E-Mail',
|
||||
__('incentive.admin_terms_accepted'),
|
||||
__('incentive.partners'),
|
||||
__('incentive.amount_per_partner'),
|
||||
__('incentive.payout_partner_amount'),
|
||||
__('incentive.payout_rank_bonus'),
|
||||
__('incentive.payout_total'),
|
||||
__('incentive.total_points'),
|
||||
__('incentive.qualified'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeilen des XLSX-Exports (Betraege als Zahlen, damit Excel summieren kann).
|
||||
*
|
||||
* @return list<array<int, string|int|float>>
|
||||
*/
|
||||
public function exportRows(Incentive $incentive): array
|
||||
{
|
||||
return $this->rows($incentive)->map(function (array $row) {
|
||||
$participant = $row['participant'];
|
||||
$payout = $row['payout'];
|
||||
$user = $participant->user;
|
||||
$name = $user?->account
|
||||
? trim($user->account->first_name.' '.$user->account->last_name)
|
||||
: ($user?->getFullName() ?: '');
|
||||
|
||||
return [
|
||||
$participant->rank ?? '',
|
||||
(int) $participant->user_id,
|
||||
$name,
|
||||
(string) ($user?->email ?? ''),
|
||||
$participant->accepted_terms_at
|
||||
? $participant->accepted_terms_at->format('d.m.Y H:i')
|
||||
: __('incentive.admin_terms_pending'),
|
||||
$payout['partners'],
|
||||
$payout['per_partner'],
|
||||
$payout['partner_amount'],
|
||||
$payout['rank_bonus'],
|
||||
$payout['total'],
|
||||
(int) $participant->total_points,
|
||||
$participant->is_qualified ? __('incentive.yes') : __('incentive.no'),
|
||||
];
|
||||
})->all();
|
||||
}
|
||||
|
||||
public static function formatAmount(float $amount): string
|
||||
{
|
||||
return number_format($amount, 2, ',', '.').' €';
|
||||
}
|
||||
}
|
||||
|
|
@ -350,7 +350,7 @@ class IncentivePointsLogRepairService
|
|||
public function syncMissingSalesVolumeLogs(IncentiveParticipant $participant): int
|
||||
{
|
||||
$incentive = $participant->incentive;
|
||||
if (! $incentive) {
|
||||
if (! $incentive || ! $incentive->includesSalesPoints()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -136,7 +136,8 @@ class IncentiveTracker
|
|||
/**
|
||||
* Track accumulated sales volume points.
|
||||
* Punkte werden NUR gezaehlt wenn der Umsatz von einem gettrackten
|
||||
* Neupartner oder Neuabo stammt.
|
||||
* Neupartner oder Neuabo stammt. Incentives mit `include_sales_points = 0`
|
||||
* (Recruiting-Challenge) bekommen keine Umsatzpunkte.
|
||||
*/
|
||||
public static function trackSalesVolume(UserSalesVolume $user_sales_volume): void
|
||||
{
|
||||
|
|
@ -155,7 +156,7 @@ class IncentiveTracker
|
|||
|
||||
// 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())
|
||||
->whereHas('participant.incentive', fn ($q) => $q->active()->includingSalesPoints())
|
||||
->with('participant.incentive')
|
||||
->get();
|
||||
|
||||
|
|
@ -210,7 +211,7 @@ class IncentiveTracker
|
|||
$abo_trackings = $userAboFromOrder
|
||||
? IncentiveNewAbo::query()
|
||||
->where('user_abo_id', $userAboFromOrder->id)
|
||||
->whereHas('participant.incentive', fn ($q) => $q->active())
|
||||
->whereHas('participant.incentive', fn ($q) => $q->active()->includingSalesPoints())
|
||||
->with('participant.incentive')
|
||||
->get()
|
||||
: collect();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue