From 328825293a0b4ce8dc6ab8a6d6b990119f7e8c71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCnrher?= Date: Thu, 10 Sep 2026 22:27:57 +0200 Subject: [PATCH] =?UTF-8?q?Fix=20f=C3=BCr=20Short-Signups?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ShortSignUp/ShortSignUpCommand.php | 18 ++++-- .../ShortSignUp/ShortSignUpRequest.php | 5 ++ .../ShortCalculateAmountController.php | 44 +++++++------ .../Controllers/ShortSignupController.php | 4 ++ .../ShortSignUpForm/ShortSignupForm.vue | 14 ++++- .../composables/shortStepFlow.js | 29 +++++---- .../composables/useShortSignupForm.js | 50 ++++++++++----- .../ShortSignUpForm/steps/ShortStepFee.vue | 33 ++++++++++ .../steps/ShortStepSummary.vue | 7 ++- app/Models/Event.php | 11 +++- app/RelationModels/EventParticipationFee.php | 21 +++++++ tests/Feature/ShortSignUpTest.php | 61 ++++++++++++++++++- version | 2 +- 13 files changed, 242 insertions(+), 57 deletions(-) create mode 100644 app/Domains/Event/Views/Partials/ShortSignUpForm/steps/ShortStepFee.vue diff --git a/app/Domains/Event/Actions/ShortSignUp/ShortSignUpCommand.php b/app/Domains/Event/Actions/ShortSignUp/ShortSignUpCommand.php index cb187f0..d8b651f 100644 --- a/app/Domains/Event/Actions/ShortSignUp/ShortSignUpCommand.php +++ b/app/Domains/Event/Actions/ShortSignUp/ShortSignUpCommand.php @@ -29,12 +29,20 @@ class ShortSignUpCommand { // Ohne konfigurierte Teilnahmegruppe gäbe es keinen gültigen `participation_type` -- sauber abbrechen, // statt in einen Fremdschlüssel-Fehler zu laufen. - $participationType = $this->request->event->firstParticipationTypeSlug(); - if ($participationType === null) { + $participationFee = $this->request->event->firstParticipationFee(); + if ($participationFee === null) { $response->message = 'Für diese Veranstaltung ist noch keine Teilnahmegruppe hinterlegt.'; return $response; } + $participationType = $participationFee->type; + + // Nur hinterlegte Beitragsstufen sind wählbar -- für jede andere hätte calculateAmount() keinen Betrag. + if (!in_array($this->request->feeType, $participationFee->availableFeeTypes(), true)) { + $response->message = 'Der gewählte Beitrag ist für diese Veranstaltung nicht verfügbar.'; + return $response; + } + $participantAge = new Age($this->request->birthday); // Bei Minderjährigen ist die Kontaktperson Pflicht; der Client prüft das ebenfalls, verlassen wird sich @@ -63,10 +71,10 @@ class ShortSignUpCommand { $eventResource = $this->request->event->toResource(); - // Zeitraum, Gruppe und Beitragsstufe stehen bei der Kurzanmeldung fest -- es gibt nichts zu wählen. + // Zeitraum und Gruppe stehen bei der Kurzanmeldung fest, wählbar ist nur die Beitragsstufe. $amount = $eventResource->calculateAmount( $participationType, - 'standard', + $this->request->feeType, $this->request->event->start_date, $this->request->event->end_date, false @@ -113,7 +121,7 @@ class ShortSignUpCommand { paymentOptions: $this->request->paymentOptions, participationOptions: $this->request->participationOptions, addonItems: [], - feeType: 'standard', + feeType: $this->request->feeType, siblingReduction: false, ); diff --git a/app/Domains/Event/Actions/ShortSignUp/ShortSignUpRequest.php b/app/Domains/Event/Actions/ShortSignUp/ShortSignUpRequest.php index 472846b..18b7b46 100644 --- a/app/Domains/Event/Actions/ShortSignUp/ShortSignUpRequest.php +++ b/app/Domains/Event/Actions/ShortSignUp/ShortSignUpRequest.php @@ -42,6 +42,11 @@ class ShortSignUpRequest { public array $paymentOptions = [], /** Antworten auf die Teilnahmeoptionen: [ question_key => value ]. */ public array $participationOptions = [], + /** + * Beitragsstufe (`standard`, `reduced`, `solidarity`). Wählbar nur, wenn die Teilnahmegruppe neben dem + * Standardbeitrag weitere Stufen hinterlegt hat; der Command prüft das. + */ + public string $feeType = 'standard', ) { } } diff --git a/app/Domains/Event/Controllers/ShortCalculateAmountController.php b/app/Domains/Event/Controllers/ShortCalculateAmountController.php index 03a4999..155e955 100644 --- a/app/Domains/Event/Controllers/ShortCalculateAmountController.php +++ b/app/Domains/Event/Controllers/ShortCalculateAmountController.php @@ -2,35 +2,45 @@ namespace App\Domains\Event\Controllers; +use App\RelationModels\EventParticipationFee; use App\Scopes\CommonController; use App\ValueObjects\Amount; use Illuminate\Http\JsonResponse; /** - * Beitrag einer Kurzanmeldung. Teilnahmegruppe, Beitragsstufe und Zeitraum stehen fest, deshalb braucht der - * Endpunkt keine Eingaben -- und der Betrag lässt sich vom Client nicht beeinflussen. + * Beiträge einer Kurzanmeldung. Teilnahmegruppe und Zeitraum stehen fest; geliefert wird der Betrag je + * hinterlegter Beitragsstufe, damit der Client zwischen ihnen wählen kann, ohne erneut zu fragen. Der Endpunkt + * braucht keine Eingaben -- berechnet wird der gespeicherte Betrag bei der Anmeldung ohnehin neu. * - * `amountValue` steuert im Frontend, ob der Schritt "Zahlungsart" gezeigt wird. + * `amountValue` der gewählten Stufe steuert im Frontend, ob der Schritt "Zahlungsart" gezeigt wird; mehr als + * eine Stufe blendet den Schritt "Beitrag" ein. */ class ShortCalculateAmountController extends CommonController { public function __invoke(int $eventId) : JsonResponse { $event = $this->events->getById($eventId, false); - $participationType = $event->firstParticipationTypeSlug(); + $participationFee = $event->firstParticipationFee(); + $eventResource = $event->toResource(); - $amount = $participationType === null - ? new Amount(0, 'Euro') - : $event->toResource()->calculateAmount( - $participationType, - 'standard', - $event->start_date, - $event->end_date, - false - ); + $feeTypes = array_map(function (string $feeType) use ($event, $eventResource, $participationFee) { + $amount = $participationFee === null + ? new Amount(0, 'Euro') + : $eventResource->calculateAmount( + $participationFee->type, + $feeType, + $event->start_date, + $event->end_date, + false + ); - return response()->json([ - 'amount' => $amount->toString(), - 'amountValue' => $amount->getAmount(), - ]); + return [ + 'value' => $feeType, + 'label' => EventParticipationFee::FEE_TYPE_LABELS[$feeType], + 'amount' => $amount->toString(), + 'amountValue' => $amount->getAmount(), + ]; + }, $participationFee?->availableFeeTypes() ?? ['standard']); + + return response()->json(['feeTypes' => $feeTypes]); } } diff --git a/app/Domains/Event/Controllers/ShortSignupController.php b/app/Domains/Event/Controllers/ShortSignupController.php index fd26191..b696f96 100644 --- a/app/Domains/Event/Controllers/ShortSignupController.php +++ b/app/Domains/Event/Controllers/ShortSignupController.php @@ -10,6 +10,7 @@ use App\Enumerations\FirstAidPermission; use App\Enumerations\SwimmingPermission; use App\Mail\ParticipantParticipationMails\EventSignUpSuccessfullMail; use App\Providers\DoubleCheckEventRegistrationProvider; +use App\RelationModels\EventParticipationFee; use App\Scopes\CommonController; use DateTime; use Illuminate\Http\JsonResponse; @@ -53,6 +54,8 @@ class ShortSignupController extends CommonController { 'participationOptions' => 'array', 'paymentMethod' => 'nullable|string', 'paymentOptions' => 'array', + // Ob die Stufe für die Teilnahmegruppe hinterlegt ist, prüft der Command. + 'feeType' => 'nullable|string|in:' . implode(',', array_keys(EventParticipationFee::FEE_TYPE_LABELS)), ]); if ($validator->fails()) { @@ -99,6 +102,7 @@ class ShortSignupController extends CommonController { paymentMethod: $request->input('paymentMethod') ?: null, paymentOptions: (array)$request->input('paymentOptions', []), participationOptions: (array)$request->input('participationOptions', []), + feeType: $request->input('feeType') ?: 'standard', ); $shortSignUpResponse = new ShortSignUpCommand($shortSignUpRequest)->execute(); diff --git a/app/Domains/Event/Views/Partials/ShortSignUpForm/ShortSignupForm.vue b/app/Domains/Event/Views/Partials/ShortSignUpForm/ShortSignupForm.vue index c009637..ce33afb 100644 --- a/app/Domains/Event/Views/Partials/ShortSignUpForm/ShortSignupForm.vue +++ b/app/Domains/Event/Views/Partials/ShortSignUpForm/ShortSignupForm.vue @@ -3,6 +3,7 @@ import {computed} from 'vue' import {useShortSignupForm} from './composables/useShortSignupForm.js' import { SHORT_STEP_PERSON, + SHORT_STEP_FEE, SHORT_STEP_PARTICIPATION_OPTIONS, SHORT_STEP_PHOTO, SHORT_STEP_PAYMENT, @@ -10,6 +11,7 @@ import { stepVisible, } from './composables/shortStepFlow.js' import ShortStepPerson from './steps/ShortStepPerson.vue' +import ShortStepFee from './steps/ShortStepFee.vue' import ShortStepSummary from './steps/ShortStepSummary.vue' import StepParticipationOptions from '../SignUpForm/steps/StepParticipationOptions.vue' import StepPhotoPermissions from '../SignUpForm/steps/StepPhotoPermissions.vue' @@ -25,7 +27,7 @@ const props = defineProps({ const { currentStep, goToStep, goNext, goBack, formData, isMinor, localGroups, needsLocalGroup, submit, submitting, submitResult, submitError, - summaryLoading, summaryAmount, summaryAmountValue, + summaryLoading, summaryAmount, feeOptions, selectedFee, flowState, } = useShortSignupForm(props.event, props.participantData) // Die aus dem langen Prozess übernommenen Schritte emittieren ihre Zielnummer aus jenem Flow mit. Hier zählt @@ -35,6 +37,7 @@ const onBack = () => goBack() const allSteps = [ {step: SHORT_STEP_PERSON, label: 'Person'}, + {step: SHORT_STEP_FEE, label: 'Beitrag'}, {step: SHORT_STEP_PARTICIPATION_OPTIONS, label: 'Teilnahmeoptionen'}, {step: SHORT_STEP_PHOTO, label: 'Fotoerlaubnis'}, {step: SHORT_STEP_PAYMENT, label: 'Zahlungsart'}, @@ -43,7 +46,7 @@ const allSteps = [ // Nur die tatsächlich vorkommenden Schritte anzeigen, sonst zeigt die Leiste Schritte, die nie erscheinen. const steps = computed(() => - allSteps.filter(s => stepVisible(props.event, s.step, summaryAmountValue.value)) + allSteps.filter(s => stepVisible(props.event, s.step, flowState.value)) ) const currentIndex = computed(() => steps.value.findIndex(s => s.step === currentStep.value)) @@ -101,6 +104,11 @@ const currentIndex = computed(() => steps.value.findIndex(s => s.step === curren :localGroups="localGroups" :needsLocalGroup="needsLocalGroup" @next="onNext" /> + steps.value.findIndex(s => s.step === curren :event="event" :isMinor="isMinor" :summaryAmount="summaryAmount" - :summaryAmountValue="summaryAmountValue" + :feeLabel="feeOptions.length > 1 ? selectedFee?.label : null" :summaryLoading="summaryLoading" :submitting="submitting" :submitError="submitError" diff --git a/app/Domains/Event/Views/Partials/ShortSignUpForm/composables/shortStepFlow.js b/app/Domains/Event/Views/Partials/ShortSignUpForm/composables/shortStepFlow.js index 465b472..6bfb4a1 100644 --- a/app/Domains/Event/Views/Partials/ShortSignUpForm/composables/shortStepFlow.js +++ b/app/Domains/Event/Views/Partials/ShortSignUpForm/composables/shortStepFlow.js @@ -1,36 +1,43 @@ -// Schrittreihenfolge der Kurzanmeldung. Zwei Schritte sind optional: die Teilnahmeoptionen entfallen, wenn für +// Schrittreihenfolge der Kurzanmeldung. Drei Schritte sind optional: der Beitrag erscheint nur, wenn neben dem +// Standardbeitrag weitere Stufen (reduziert, Solidarität) hinterlegt sind, die Teilnahmeoptionen entfallen, wenn für // die Veranstaltung keine definiert sind, die Zahlungsart entfällt bei Betrag 0 oder wenn es ohnehin nur eine // Zahlungsart gibt (die wird dann vorausgewählt). export const SHORT_STEP_PERSON = 1 -export const SHORT_STEP_PARTICIPATION_OPTIONS = 2 -export const SHORT_STEP_PHOTO = 3 -export const SHORT_STEP_PAYMENT = 4 -export const SHORT_STEP_SUMMARY = 5 +export const SHORT_STEP_FEE = 2 +export const SHORT_STEP_PARTICIPATION_OPTIONS = 3 +export const SHORT_STEP_PHOTO = 4 +export const SHORT_STEP_PAYMENT = 5 +export const SHORT_STEP_SUMMARY = 6 + +// `state` kommt mit den Beiträgen vom Server: { amountValue: Betrag der gewählten Stufe, feeOptionCount }. +export function stepVisible(event, step, state) { + if (step === SHORT_STEP_FEE) { + return state.feeOptionCount > 1 + } -export function stepVisible(event, step, amountValue) { if (step === SHORT_STEP_PARTICIPATION_OPTIONS) { return (event.participationOptions?.length ?? 0) > 0 } if (step === SHORT_STEP_PAYMENT) { const activeMethods = (event.paymentMethods ?? []).filter(m => m.active) - return amountValue > 0 && activeMethods.length > 1 + return state.amountValue > 0 && activeMethods.length > 1 } return true } -export function nextVisibleFrom(event, step, amountValue) { +export function nextVisibleFrom(event, step, state) { let candidate = step + 1 - while (candidate < SHORT_STEP_SUMMARY && !stepVisible(event, candidate, amountValue)) { + while (candidate < SHORT_STEP_SUMMARY && !stepVisible(event, candidate, state)) { candidate++ } return candidate } -export function prevVisibleFrom(event, step, amountValue) { +export function prevVisibleFrom(event, step, state) { let candidate = step - 1 - while (candidate > SHORT_STEP_PERSON && !stepVisible(event, candidate, amountValue)) { + while (candidate > SHORT_STEP_PERSON && !stepVisible(event, candidate, state)) { candidate-- } return candidate diff --git a/app/Domains/Event/Views/Partials/ShortSignUpForm/composables/useShortSignupForm.js b/app/Domains/Event/Views/Partials/ShortSignUpForm/composables/useShortSignupForm.js index b591bd1..f645f93 100644 --- a/app/Domains/Event/Views/Partials/ShortSignUpForm/composables/useShortSignupForm.js +++ b/app/Domains/Event/Views/Partials/ShortSignUpForm/composables/useShortSignupForm.js @@ -1,4 +1,4 @@ -import {computed, reactive, ref} from 'vue' +import {computed, reactive, ref, watch} from 'vue' import axios from 'axios' import {differenceInYears, parseISO} from 'date-fns' import {SHORT_STEP_PERSON, nextVisibleFrom, prevVisibleFrom} from './shortStepFlow.js' @@ -33,6 +33,8 @@ export function useShortSignupForm(event, participantData) { intolerances: participantData.intolerances ?? '', first_aid: '-1', badeerlaubnis: '-1', + // Beitragsstufe; wählbar nur, wenn weitere Stufen hinterlegt sind. + feeType: 'standard', // Antworten auf die event-spezifischen Teilnahmeoptionen: { [question.key]: option.value } participationOptions: {}, foto: {socialmedia: false, print: false, webseite: false, partner: false, intern: false}, @@ -53,12 +55,18 @@ export function useShortSignupForm(event, participantData) { return differenceInYears(new Date(), parseISO(formData.geburtsdatum)) < 18 }) - const summaryAmount = ref('') - const summaryAmountValue = ref(0) + // Beitragsstufen mit fertig berechnetem Betrag: [{ value, label, amount, amountValue }] + const feeOptions = ref([]) const amountLoaded = ref(false) - // Der Betrag steht für die ganze Anmeldung fest (feste Teilnahmegruppe, fester Zeitraum) und wird deshalb - // genau einmal geholt. Er entscheidet, ob der Zahlungsschritt überhaupt vorkommt. + const selectedFee = computed(() => + feeOptions.value.find(option => option.value === formData.feeType) ?? feeOptions.value[0] ?? null + ) + const summaryAmount = computed(() => selectedFee.value?.amount ?? '') + const summaryAmountValue = computed(() => Number(selectedFee.value?.amountValue ?? 0)) + + // Teilnahmegruppe und Zeitraum stehen für die ganze Anmeldung fest, deshalb werden die Beträge aller Stufen + // genau einmal geholt. Die Wahl der Stufe entscheidet dann nur noch lokal über den Betrag. const ensureAmount = async () => { if (amountLoaded.value) { return @@ -67,31 +75,41 @@ export function useShortSignupForm(event, participantData) { summaryLoading.value = true try { const res = await axios.post('/api/v1/event/' + event.id + '/short-calculate-amount') - summaryAmount.value = res.data.amount - summaryAmountValue.value = Number(res.data.amountValue ?? 0) + feeOptions.value = res.data.feeTypes ?? [] amountLoaded.value = true - - if (summaryAmountValue.value <= 0) { - formData.paymentMethod = null - formData.paymentOptions = {} - } } finally { summaryLoading.value = false } } + // Bei Betrag 0 gibt es nichts zu bezahlen. Wechselt die Stufe zurück auf einen Betrag, wird die einzige + // Zahlungsart wieder vorausgewählt -- sonst fragt sie niemand mehr ab. + watch(summaryAmountValue, (value) => { + if (value <= 0) { + formData.paymentMethod = null + formData.paymentOptions = {} + } else if (formData.paymentMethod === null && activeMethods.length === 1) { + formData.paymentMethod = activeMethods[0].slug + } + }) + + const flowState = computed(() => ({ + amountValue: summaryAmountValue.value, + feeOptionCount: feeOptions.value.length, + })) + const goToStep = (step) => { currentStep.value = step } const goNext = async () => { await ensureAmount() - goToStep(nextVisibleFrom(event, currentStep.value, summaryAmountValue.value)) + goToStep(nextVisibleFrom(event, currentStep.value, flowState.value)) } const goBack = async () => { await ensureAmount() - goToStep(prevVisibleFrom(event, currentStep.value, summaryAmountValue.value)) + goToStep(prevVisibleFrom(event, currentStep.value, flowState.value)) } const submit = async () => { @@ -120,6 +138,7 @@ export function useShortSignupForm(event, participantData) { participationOptions: formData.participationOptions, paymentMethod: formData.paymentMethod, paymentOptions: formData.paymentOptions, + feeType: formData.feeType, }) submitResult.value = { @@ -150,6 +169,9 @@ export function useShortSignupForm(event, participantData) { summaryLoading, summaryAmount, summaryAmountValue, + feeOptions, + selectedFee, + flowState, ensureAmount, } } diff --git a/app/Domains/Event/Views/Partials/ShortSignUpForm/steps/ShortStepFee.vue b/app/Domains/Event/Views/Partials/ShortSignUpForm/steps/ShortStepFee.vue new file mode 100644 index 0000000..e0b4dbc --- /dev/null +++ b/app/Domains/Event/Views/Partials/ShortSignUpForm/steps/ShortStepFee.vue @@ -0,0 +1,33 @@ + + + diff --git a/app/Domains/Event/Views/Partials/ShortSignUpForm/steps/ShortStepSummary.vue b/app/Domains/Event/Views/Partials/ShortSignUpForm/steps/ShortStepSummary.vue index 504eb19..1b1b7c4 100644 --- a/app/Domains/Event/Views/Partials/ShortSignUpForm/steps/ShortStepSummary.vue +++ b/app/Domains/Event/Views/Partials/ShortSignUpForm/steps/ShortStepSummary.vue @@ -8,7 +8,8 @@ const props = defineProps({ event: Object, isMinor: Boolean, summaryAmount: String, - summaryAmountValue: {type: Number, default: 0}, + // Nur gesetzt, wenn zwischen mehreren Beitragsstufen gewählt werden konnte. + feeLabel: {type: String, default: null}, summaryLoading: Boolean, submitting: Boolean, submitError: {type: String, default: ''}, @@ -137,6 +138,10 @@ const canSubmit = computed(() =>

Kostenübersicht

+ + + + diff --git a/app/Models/Event.php b/app/Models/Event.php index 68e6a4b..33b13eb 100644 --- a/app/Models/Event.php +++ b/app/Models/Event.php @@ -234,13 +234,20 @@ class Event extends InstancedModel * jede Anmeldung dieser Gruppe zu. Null, wenn für die Veranstaltung noch keine Gebühr hinterlegt ist -- * dann ist eine Kurzanmeldung nicht möglich, weil `participation_type` am Teilnehmer Pflicht ist. */ - public function firstParticipationTypeSlug() : ?string { + /** + * Die erste hinterlegte Teilnahmegruppe -- die Kurzanmeldung meldet immer in dieser an. + */ + public function firstParticipationFee() : ?EventParticipationFee { return collect([ $this->participationFee1, $this->participationFee2, $this->participationFee3, $this->participationFee4, - ])->first(fn ($participationFee) => $participationFee !== null)?->type; + ])->first(fn ($participationFee) => $participationFee !== null); + } + + public function firstParticipationTypeSlug() : ?string { + return $this->firstParticipationFee()?->type; } } diff --git a/app/RelationModels/EventParticipationFee.php b/app/RelationModels/EventParticipationFee.php index b21634f..6c25bf0 100644 --- a/app/RelationModels/EventParticipationFee.php +++ b/app/RelationModels/EventParticipationFee.php @@ -21,6 +21,27 @@ class EventParticipationFee extends CommonModel ]; + /** Beitragsstufen in Anzeigereihenfolge; die Schlüssel sind die Spaltensuffixe von `amount_*`. */ + public const array FEE_TYPE_LABELS = [ + 'standard' => 'Standardbeitrag', + 'reduced' => 'Reduzierter Beitrag', + 'solidarity' => 'Solidaritätsbeitrag', + ]; + + /** + * Die Beitragsstufen, die für diese Teilnahmegruppe tatsächlich hinterlegt sind. Den Standardbeitrag gibt es + * immer, reduzierter und Solidaritätsbeitrag sind optional. Nur für diese Stufen liefert + * `EventResource::calculateAmount()` einen Betrag. + * + * @return array + */ + public function availableFeeTypes() : array { + return array_values(array_filter( + array_keys(self::FEE_TYPE_LABELS), + fn (string $feeType) => $this->{'amount_' . $feeType} !== null + )); + } + public function tenant() : BelongsTo { return $this->belongsTo(Tenant::class, 'tenant', 'slug'); } diff --git a/tests/Feature/ShortSignUpTest.php b/tests/Feature/ShortSignUpTest.php index 082b887..ed5dbcc 100644 --- a/tests/Feature/ShortSignUpTest.php +++ b/tests/Feature/ShortSignUpTest.php @@ -50,7 +50,12 @@ class ShortSignUpTest extends TestCase EatingHabit::create(['slug' => EatingHabit::EATING_HABIT_OMNIVOR, 'name' => 'Omnivor']); } - private function createEvent(bool $withFee = true, bool $swimmingPermissionRequired = true): Event + private function createEvent( + bool $withFee = true, + bool $swimmingPermissionRequired = true, + ?float $amountReduced = 30.0, + ?float $amountSolidarity = 70.0, + ): Event { $event = Event::create([ 'tenant' => $this->tenant->slug, 'name' => 'Event', 'identifier' => 'evt-1', 'location' => 'Ort', @@ -65,7 +70,7 @@ class ShortSignUpTest extends TestCase $fee = EventParticipationFee::create([ 'tenant' => $this->tenant->slug, 'type' => ParticipationType::PARTICIPATION_TYPE_PARTICIPANT, 'name' => 'Teilnahme', 'description' => '', 'amount_standard' => 50.0, - 'amount_reduced' => 30.0, 'amount_solidarity' => 70.0, + 'amount_reduced' => $amountReduced, 'amount_solidarity' => $amountSolidarity, ]); $event->participation_fee_1 = $fee->id; $event->save(); @@ -102,6 +107,7 @@ class ShortSignUpTest extends TestCase paymentMethod: $overrides['paymentMethod'] ?? PaymentMethod::PAYMENT_ACCOUNT_TRANSACTION, paymentOptions: [], participationOptions: $overrides['participationOptions'] ?? [], + feeType: $overrides['feeType'] ?? 'standard', ); } @@ -127,7 +133,7 @@ class ShortSignUpTest extends TestCase $this->assertSame(1, $participant->arrival_eating); $this->assertSame(2, $participant->departure_eating); - // Gruppe und Beitragsstufe stehen fest. + // Die Gruppe steht fest, ohne Wahl gilt der Standardbeitrag. $this->assertSame(ParticipationType::PARTICIPATION_TYPE_PARTICIPANT, $participant->participation_type); $this->assertSame('standard', $participant->fee_type); $this->assertSame(50.0, $participant->amount->getAmount()); @@ -142,6 +148,55 @@ class ShortSignUpTest extends TestCase $this->assertFalse($participant->foto_print); } + public function test_gewaehlter_reduzierter_beitrag_wird_berechnet(): void + { + $event = $this->createEvent(); + + $response = new ShortSignUpCommand($this->shortRequest($event, ['feeType' => 'reduced']))->execute(); + + $this->assertTrue($response->success, $response->message ?? ''); + $this->assertSame('reduced', $response->participant->fee_type); + $this->assertSame(30.0, $response->participant->amount->getAmount()); + } + + public function test_gewaehlter_solidaritaetsbeitrag_wird_berechnet(): void + { + $event = $this->createEvent(); + + $response = new ShortSignUpCommand($this->shortRequest($event, ['feeType' => 'solidarity']))->execute(); + + $this->assertTrue($response->success, $response->message ?? ''); + $this->assertSame('solidarity', $response->participant->fee_type); + $this->assertSame(70.0, $response->participant->amount->getAmount()); + } + + public function test_nicht_hinterlegte_beitragsstufe_wird_abgelehnt(): void + { + $event = $this->createEvent(amountReduced: null); + + $response = new ShortSignUpCommand($this->shortRequest($event, ['feeType' => 'reduced']))->execute(); + + $this->assertFalse($response->success); + $this->assertNotNull($response->message); + $this->assertSame(0, EventParticipant::count()); + } + + public function test_waehlbar_sind_nur_hinterlegte_beitragsstufen(): void + { + $this->assertSame( + ['standard', 'reduced', 'solidarity'], + $this->createEvent()->firstParticipationFee()->availableFeeTypes() + ); + + $withoutReduced = EventParticipationFee::create([ + 'tenant' => $this->tenant->slug, 'type' => ParticipationType::PARTICIPATION_TYPE_PARTICIPANT, + 'name' => 'Teilnahme', 'description' => '', 'amount_standard' => 50.0, + 'amount_reduced' => null, 'amount_solidarity' => 70.0, + ]); + + $this->assertSame(['standard', 'solidarity'], $withoutReduced->availableFeeTypes()); + } + public function test_u18_ohne_abgefragte_badeerlaubnis_bekommt_keine(): void { $event = $this->createEvent(swimmingPermissionRequired: false); diff --git a/version b/version index 6ed7776..5b341fd 100644 --- a/version +++ b/version @@ -1 +1 @@ -4.9.0 +4.9.1
Beitragsstufe:{{ feeLabel }}
Teilnahmebeitrag: {{ summaryAmount }}