Files
mareike/app/Resources/EventResource.php
T

468 lines
20 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Resources;
use App\Enumerations\ParticipationFeeType;
use App\Enumerations\ParticipationType;
use App\Enumerations\VatPricingMode;
use App\Models\Event;
use App\Support\DateRange;
use App\ValueObjects\Amount;
use DateTime;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class EventResource extends JsonResource{
private Event $event;
public function __construct(Event $event) {
$this->event = $event;
}
public function toArray(Request $request) : array
{
$duration = DateRange::inclusiveDays($this->event->start_date, $this->event->end_date);
$returnArray = [
'id' => $this->event->id,
'name' => $this->event->name,
'identifier' => $this->event->identifier,
'url' => 'https://' . currentTenant()->url . '/event/' . $this->event->identifier . '/signup',
'urlShort' => 'https://' . currentTenant()->url . '/event/' . $this->event->identifier,
'location' => $this->event->location,
'street' => $this->event->street,
'houseNumber' => $this->event->house_number,
'postalCode' => $this->event->postal_code,
// Fertig formatierte Anschrift für alle Anzeigen -- die Einzelfelder brauchen nur die
// Bearbeitungsformulare.
'fullAddress' => $this->event->getFullAddress(),
'email' => $this->event->email,
'accountOwner' => $this->event->account_owner,
'accountIban' => $this->event->account_iban,
'alcoholicsAge' => $this->event->alcoholics_age,
'sendWeeklyReports' => $this->event->send_weekly_report,
'registrationAllowed' => $this->event->registration_allowed && new \DateTime() <= $this->event->start_date,
'archived' => $this->event->archived,
'earlyBirdEnd' => ['internal' => $this->event->early_bird_end->format('Y-m-d'), 'formatted' => $this->event->early_bird_end->format('d.m.Y')],
'registrationFinalEnd' => ['internal' => $this->event->registration_final_end->format('Y-m-d'), 'formatted' => $this->event->registration_final_end->format('d.m.Y')],
'refundAfterEarlyBirdEnd' => 100 - $this->event->early_bird_end_amount_increase,
];
$returnArray['swimmingPermissions'] = \App\Enumerations\SwimmingPermission::query()
->get()
->map(fn ($permission) => [
'slug' => $permission->slug,
'name' => $permission->name,
])
->toArray();
$returnArray['firstAidPermissions'] = \App\Enumerations\FirstAidPermission::query()
->get()
->map(fn ($permission) => [
'slug' => $permission->slug,
'name' => $permission->name,
])
->toArray();
foreach ([
ParticipationType::PARTICIPATION_TYPE_PARTICIPANT,
ParticipationType::PARTICIPATION_TYPE_TEAM,
ParticipationType::PARTICIPATION_TYPE_VOLUNTEER,
ParticipationType::PARTICIPATION_TYPE_OTHER,
] as $participationType) {
$_t =$this->getParticipants($participationType);
$returnArray['participants'][$participationType] = $_t;
$returnArray['count' . ucfirst($participationType)] = $_t['count'];
}
$returnArray['nameShort'] = $returnArray['name'];
if (strlen($returnArray['nameShort']) > 15) {
$returnArray['nameShort'] = substr($returnArray['nameShort'], 8, 13) . '...';
}
// Steuert im Frontend, welcher Anmelde-Wizard gerendert wird, und ob die Badeerlaubnis abgefragt wird.
$returnArray['shortRegistration'] = (bool)$this->event->short_registration;
$returnArray['swimmingPermissionRequired'] = (bool)$this->event->swimming_permission_required;
$returnArray['siblingReduction'] = $this->event->sibling_reduction ?? true;
$returnArray['costUnit'] = new CostUnitResource($this->event->costUnit()->first())->toArray(true);
$returnArray['solidarityPayment'] = $this->event->participation_fee_type === ParticipationFeeType::PARTICIPATION_FEE_TYPE_SOLIDARITY;
$returnArray['payPerDay'] = $this->event->pay_per_day;
$returnArray['maxAmount'] = $this->event->total_max_amount->getFormattedAmount();
$returnArray['arrivalDefault'] = $this->event->start_date->format('Y-m-d');
$returnArray['departureDefault'] = $this->event->end_date->format('Y-m-d');
$returnArray['eventBegin'] = $this->event->start_date->format('d.m.Y');
$returnArray['eventBeginInternal'] = $this->event->start_date;
$returnArray['eventEnd'] = $this->event->end_date->format('d.m.Y');
$returnArray['eventEndInternal'] = $this->event->end_date;
$returnArray['duration'] = $duration;
$returnArray['totalParticipantCount'] = $this->event->participants()->count();
$returnArray['supportPersonIndex'] = $this->event->support_per_person->toString();
$returnArray['supportPersonValue'] = $this->event->support_per_person->getAmount();
$returnArray['supportPerson'] = $this->calculateSupportPerPerson($returnArray['participants']);
$returnArray['income'] = $this->calculateIncomes($returnArray['participants'], $returnArray['supportPerson']['amount']);
// Eigene Zeile in der Übersicht: In den Zeilen je Teilnahmeart hätte der Betrag nichts zu suchen,
// dort stehen nur aktive Anmeldungen.
$retainedFromUnregistered = $this->sumPaidOfUnregistered();
$returnArray['retainedFromUnregistered'] = [
'value' => $retainedFromUnregistered->getAmount(),
'readable' => $retainedFromUnregistered->toString(),
];
$totalBalanceReal = new Amount(0, 'Euro');
$totalBalanceExpected = new Amount(0, 'Euro');
$totalBalanceEstimated = new Amount(0, 'Euro');
$totalBalanceReal->addAmount($returnArray['income']['real']['amount']);
$totalBalanceExpected->addAmount($returnArray['income']['expected']['amount']);
$totalBalanceEstimated->addAmount($returnArray['income']['expected']['amount']);
$totalBalanceReal->subtractAmount($returnArray['costUnit']['overAllAmount']['value']);
$totalBalanceExpected->subtractAmount($returnArray['costUnit']['overAllAmount']['value']);
$totalBalanceEstimated->subtractAmount($returnArray['costUnit']['overAllEstimatedAmount']['value']);
$returnArray['totalBalance'] = [
'real' => [
'value' => $totalBalanceReal->getAmount(),
'readable' => $totalBalanceReal->toString(),
], 'expected' => [
'value' => $totalBalanceExpected->getAmount(),
'readable' => $totalBalanceExpected->toString(),
],
'estimated' => [
'value' => $totalBalanceEstimated->getAmount(),
'readable' => $totalBalanceEstimated->toString(),
]
];
$returnArray['flatSupport'] = $this->event->support_flat->toString();
$returnArray['flatSupportEdit'] = $this->event->support_flat->getFormattedAmount();
$returnArray['supportPersonEdit'] = $this->event->support_per_person->getFormattedAmount();
$returnArray['managers'] = $this->event->eventManagers()->get()->map(fn($user) => new UserResource($user))->toArray();
$returnArray['supportPersonCalced'] = $this->event->support_per_person->toString();
$returnArray['contributingLocalGroups'] = $this->event->localGroups
->map(fn ($localGroup) => new LocalGroupResource($localGroup)->toArray($request))
->toArray();
$returnArray['eatingHabits'] = $this->event->eatingHabits()->get()->map(
fn($eatingHabit) => new EatingHabitResource($eatingHabit))->toArray();
$returnArray['paymentMethods'] = $this->event->paymentMethods()->get()->map(
fn($paymentMethod) => new AvailablePaymentMethodResource($paymentMethod)->toArray($request))->toArray();
// Veranstaltungsspezifische Pflicht-Auswahlfragen (Teilnahmeoptionen); leeres Array => Schritt entfällt.
$returnArray['participationOptions'] = $this->event->participation_options ?? [];
// Buchbare Zusätze (Event-Addons); leeres Array => Schritt entfällt. Preis 0 => "Kostenfrei".
$returnArray['addons'] = collect($this->event->addons ?? [])->map(function ($addon) {
$price = (float)($addon['price'] ?? 0);
return [
'key' => $addon['key'] ?? '',
'title' => $addon['title'] ?? '',
'description' => $addon['description'] ?? '',
'price' => $price,
'priceReadable' => new Amount($price, 'Euro')->toString(),
'free' => $price <= 0,
'flat' => (bool)($addon['flat'] ?? false),
];
})->toArray();
$returnArray['participationTypes'] = [];
$multiplier = $this->getMultiplier();
for ($i = 1; $i <= 4; $i++) {
$returnArray['participationFee_' . $i] = [
'active' => false,
'name' => '',
'description' => '',
'amount_standard' => null,
'amount_reduced' => null,
'amount_solidarity' => null,
'type' => null,
];
if ($this->event->{'participation_fee_' . $i} === null) {
continue;
}
$participationFee = $this->event->{'participationFee' . $i}()->first();
$feeType = [
'active' => true,
'amount_standard_edit' => $participationFee->amount_standard->getFormattedAmount(),
'amount_standard' => [
'internal' => [
'amount' => $participationFee->amount_standard->getAmount(),
'currency' => $participationFee->amount_standard->getCurrency(),
],
'readable' => $participationFee->amount_standard->multiply($multiplier)->toString(),
],
'amount_reduced_edit' => $participationFee->amount_reduced === null ? null : $participationFee->amount_reduced->getFormattedAmount(),
'amount_reduced' => $participationFee->amount_reduced === null
? null
: [
'internal' => [
'amount' => $participationFee->amount_reduced->getAmount(),
'currency' => $participationFee->amount_reduced->getCurrency(),
],
'readable' => $participationFee->amount_reduced->multiply($multiplier)->toString(),
],
'amount_solidarity_edit' => $participationFee->amount_solidarity === null ? null : $participationFee->amount_solidarity->getFormattedAmount(),
'amount_solidarity' => $participationFee->amount_solidarity === null
? null
: [
'internal' => [
'amount' => $participationFee->amount_solidarity->getAmount(),
'currency' => $participationFee->amount_solidarity->getCurrency(),
],
'readable' => $participationFee->amount_solidarity->multiply($multiplier)->toString(),
],
'name' => $participationFee->name,
'description' => $participationFee->description,
'type' => (new ParticipationTypeResource($participationFee->type()->first()))->toArray($request),
];
$returnArray['participationFee_' . $i] = $feeType;
$returnArray['participationTypes'][] = $feeType;
}
return $returnArray;
}
public function getMultiplier() : float {
$earlyBirdEnd = $this->event->early_bird_end;
if ($earlyBirdEnd > now()) {
return 1;
}
return 1 + $this->event->early_bird_end_amount_increase / 100;
}
public function calculateSupportPerPerson(array $participants) : array {
if ($this->event->support_per_person === null) {
return [
'amount' => new Amount(0, 'Euro'),
'readable' => '0,00'
];
}
$amount = $this->event->support_per_person;
$multiplier = 0;
foreach ($participants as $participationType => $participantData) {
if (isset($participantData['participants']) && count($participantData['participants']) > 0) {
foreach ($participantData['participants'] as $participant) {
$multiplier += $participant->toResource()->toArray(new Request())['presenceDays']['support'];
}
}
}
$amount->multiply($multiplier);
return [
'amount' => $amount,
'readable' => $amount->toString()
];
}
public function calculateIncomes(array $participants, Amount $supportPerPerson) : array {
$expectedAmount = new Amount($supportPerPerson->getAmount(), 'Euro');
$expectedAmount->addAmount($this->event->support_flat);
$realAmount = new Amount($supportPerPerson->getAmount(), 'Euro');
$realAmount->addAmount($this->event->support_flat);
foreach ($participants as $participationType => $participantData) {
$expectedAmount->addAmount(new Amount($participantData['amount']['expected']['value'], 'Euro'));
$realAmount->addAmount(new Amount($participantData['amount']['paid']['value'], 'Euro'));
}
// Was abgemeldete Teilis gezahlt haben und nicht zurückbekommen, gehört in beide Spalten: Das
// Geld liegt beim Verband (real) und fließt nicht mehr ab (erwartet). Ohne diese Zeile stünde
// jede Veranstaltung mit Abmeldungen dauerhaft schlechter da, als sie ist.
$retained = $this->sumPaidOfUnregistered();
$realAmount->addAmount($retained);
$expectedAmount->addAmount($retained);
return ['real' => [
'amount' => $realAmount,
'readable' => $realAmount->toString()
],
'expected' => [
'amount' => $expectedAmount,
'readable' => $expectedAmount->toString(),
]
];
}
/**
* Was von abgemeldeten Teilis beim Verband geblieben ist.
*
* `amount_paid` führt nach einer Erstattung genau den einbehaltenen Rest; wurde nie erstattet, steht
* dort der volle gezahlte Beitrag. Beides ist Geld, das der Veranstaltung zusteht.
*
* Bewusst eine direkte Abfrage wie in {@see self::getParticipants()} nebenan -- ein einzelner
* Repository-Aufruf zwischen den Inline-Queries dieser Klasse würde sie uneinheitlicher machen.
*/
public function sumPaidOfUnregistered() : Amount
{
$sum = new Amount(0, 'Euro');
foreach ($this->event->participants()->whereNotNull('unregistered_at')->get() as $participant) {
if ($participant->amount_paid !== null) {
$sum->addAmount($participant->amount_paid);
}
}
return $sum;
}
public function getParticipants(string $participationType) : array {
$returnData = [];
$returnData['amount'] = [
'expected' => new Amount(0, 'Euro'),
'paid' => new Amount(0, 'Euro')
];
$returnData['count'] = 0;
foreach ($this->event->participants()->where(
[
'participation_type' => $participationType,
'unregistered_at' => null
]
)->get() as $participant)
{
$returnData['count']++;
$returnData['participants'][] = $participant;
if ($participant->amount !== null) {
$returnData['amount']['expected'] = $returnData['amount']['expected']->addAmount($participant->amount);
}
if ($participant->amount_paid !== null) {
$returnData['amount']['paid'] = $returnData['amount']['paid']->addAmount($participant->amount_paid);
}
};
$returnData['amount']['expected'] =
[
'value' => $returnData['amount']['expected']->getAmount(),
'readable' => $returnData['amount']['expected']->toString()
];
$returnData['amount']['paid'] =
[
'value' => $returnData['amount']['paid']->getAmount(),
'readable' => $returnData['amount']['paid']->toString()
];
return $returnData;
}
public function calculateAmount(
string $participationType,
string $feeType,
DateTime $arrival,
DateTime $departure,
bool $hasSibling
) : Amount {
$fee = collect([
$this->event->participationFee1,
$this->event->participationFee2,
$this->event->participationFee3,
$this->event->participationFee4,
])->filter(fn ($participationFee) => $participationFee !== null)
->first(fn ($participationFee) => $participationFee->type === $participationType);
if ($fee === null) {
return new Amount(0, 'Euro');
}
/** @var Amount $basicFee */
$basicFee = $fee['amount_' . $feeType];
$basicFee = $basicFee->multiply($this->getMultiplier());
if ($this->event->pay_per_day) {
$basicFee = $basicFee->multiply(DateRange::inclusiveDays($arrival, $departure));
}
if ($hasSibling && $this->event->sibling_reduction) {
$basicFee = $basicFee->multiply(0.5);
}
// Bei bestehender USt-Pflicht und Netto-Preismodus die USt aufschlagen; gespeichert wird immer der
// finale Brutto-Betrag. Bei "inklusive" oder ohne Steuerpflicht bleibt der Beitrag unverändert.
if ($this->event->tax_liable && $this->event->vat_pricing_mode === VatPricingMode::AddOn->value) {
$basicFee = $basicFee->multiply(1 + $this->event->vat_rate / 100);
}
return new Amount(round($basicFee->getAmount(), 2), 'Euro');
}
/**
* Löst die gewählten Zusätze (Event-Addons) gegen die Event-Definition auf und berechnet je Position den
* Betrag: Pauschale => Preis, sonst Preis × Teilnahmetage. Bei USt-Pflicht + Netto-Preismodus (`add_on`)
* wird die USt aufgeschlagen (analog Beitrag); bei „inclusive"/keiner Pflicht bleibt der Preis unverändert.
* Kein Early-Bird-Aufschlag.
*
* @param array<int, string> $selectedKeys
* @return array{items: array<int, array{key: string, title: string, price: float, flat: bool, days: int, amount: float}>, total: Amount}
*/
public function resolveAddons(array $selectedKeys, DateTime $arrival, DateTime $departure): array
{
$days = DateRange::inclusiveDays($arrival, $departure);
$addsVat = $this->event->tax_liable && $this->event->vat_pricing_mode === VatPricingMode::AddOn->value;
$items = [];
$total = new Amount(0, 'Euro');
foreach ($this->event->addons ?? [] as $addon) {
$key = $addon['key'] ?? null;
if ($key === null || !in_array($key, $selectedKeys, true)) {
continue;
}
$price = (float)($addon['price'] ?? 0);
$flat = (bool)($addon['flat'] ?? false);
$usedDays = $flat ? 1 : $days;
$amount = $flat ? $price : $price * $days;
if ($addsVat) {
$amount *= 1 + $this->event->vat_rate / 100;
}
$amount = round($amount, 2);
$items[] = [
'key' => $key,
'title' => $addon['title'] ?? $key,
'price' => $price,
'flat' => $flat,
'days' => $usedDays,
'amount' => $amount,
];
$total->addAmount(new Amount($amount, 'Euro'));
}
return ['items' => $items, 'total' => $total];
}
}