Fix für Short-Signups
This commit is contained in:
@@ -29,12 +29,20 @@ class ShortSignUpCommand {
|
|||||||
|
|
||||||
// Ohne konfigurierte Teilnahmegruppe gäbe es keinen gültigen `participation_type` -- sauber abbrechen,
|
// Ohne konfigurierte Teilnahmegruppe gäbe es keinen gültigen `participation_type` -- sauber abbrechen,
|
||||||
// statt in einen Fremdschlüssel-Fehler zu laufen.
|
// statt in einen Fremdschlüssel-Fehler zu laufen.
|
||||||
$participationType = $this->request->event->firstParticipationTypeSlug();
|
$participationFee = $this->request->event->firstParticipationFee();
|
||||||
if ($participationType === null) {
|
if ($participationFee === null) {
|
||||||
$response->message = 'Für diese Veranstaltung ist noch keine Teilnahmegruppe hinterlegt.';
|
$response->message = 'Für diese Veranstaltung ist noch keine Teilnahmegruppe hinterlegt.';
|
||||||
return $response;
|
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);
|
$participantAge = new Age($this->request->birthday);
|
||||||
|
|
||||||
// Bei Minderjährigen ist die Kontaktperson Pflicht; der Client prüft das ebenfalls, verlassen wird sich
|
// 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();
|
$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(
|
$amount = $eventResource->calculateAmount(
|
||||||
$participationType,
|
$participationType,
|
||||||
'standard',
|
$this->request->feeType,
|
||||||
$this->request->event->start_date,
|
$this->request->event->start_date,
|
||||||
$this->request->event->end_date,
|
$this->request->event->end_date,
|
||||||
false
|
false
|
||||||
@@ -113,7 +121,7 @@ class ShortSignUpCommand {
|
|||||||
paymentOptions: $this->request->paymentOptions,
|
paymentOptions: $this->request->paymentOptions,
|
||||||
participationOptions: $this->request->participationOptions,
|
participationOptions: $this->request->participationOptions,
|
||||||
addonItems: [],
|
addonItems: [],
|
||||||
feeType: 'standard',
|
feeType: $this->request->feeType,
|
||||||
siblingReduction: false,
|
siblingReduction: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,11 @@ class ShortSignUpRequest {
|
|||||||
public array $paymentOptions = [],
|
public array $paymentOptions = [],
|
||||||
/** Antworten auf die Teilnahmeoptionen: [ question_key => value ]. */
|
/** Antworten auf die Teilnahmeoptionen: [ question_key => value ]. */
|
||||||
public array $participationOptions = [],
|
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',
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,35 +2,45 @@
|
|||||||
|
|
||||||
namespace App\Domains\Event\Controllers;
|
namespace App\Domains\Event\Controllers;
|
||||||
|
|
||||||
|
use App\RelationModels\EventParticipationFee;
|
||||||
use App\Scopes\CommonController;
|
use App\Scopes\CommonController;
|
||||||
use App\ValueObjects\Amount;
|
use App\ValueObjects\Amount;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Beitrag einer Kurzanmeldung. Teilnahmegruppe, Beitragsstufe und Zeitraum stehen fest, deshalb braucht der
|
* Beiträge einer Kurzanmeldung. Teilnahmegruppe und Zeitraum stehen fest; geliefert wird der Betrag je
|
||||||
* Endpunkt keine Eingaben -- und der Betrag lässt sich vom Client nicht beeinflussen.
|
* 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 {
|
class ShortCalculateAmountController extends CommonController {
|
||||||
public function __invoke(int $eventId) : JsonResponse {
|
public function __invoke(int $eventId) : JsonResponse {
|
||||||
$event = $this->events->getById($eventId, false);
|
$event = $this->events->getById($eventId, false);
|
||||||
|
|
||||||
$participationType = $event->firstParticipationTypeSlug();
|
$participationFee = $event->firstParticipationFee();
|
||||||
|
$eventResource = $event->toResource();
|
||||||
|
|
||||||
$amount = $participationType === null
|
$feeTypes = array_map(function (string $feeType) use ($event, $eventResource, $participationFee) {
|
||||||
? new Amount(0, 'Euro')
|
$amount = $participationFee === null
|
||||||
: $event->toResource()->calculateAmount(
|
? new Amount(0, 'Euro')
|
||||||
$participationType,
|
: $eventResource->calculateAmount(
|
||||||
'standard',
|
$participationFee->type,
|
||||||
$event->start_date,
|
$feeType,
|
||||||
$event->end_date,
|
$event->start_date,
|
||||||
false
|
$event->end_date,
|
||||||
);
|
false
|
||||||
|
);
|
||||||
|
|
||||||
return response()->json([
|
return [
|
||||||
'amount' => $amount->toString(),
|
'value' => $feeType,
|
||||||
'amountValue' => $amount->getAmount(),
|
'label' => EventParticipationFee::FEE_TYPE_LABELS[$feeType],
|
||||||
]);
|
'amount' => $amount->toString(),
|
||||||
|
'amountValue' => $amount->getAmount(),
|
||||||
|
];
|
||||||
|
}, $participationFee?->availableFeeTypes() ?? ['standard']);
|
||||||
|
|
||||||
|
return response()->json(['feeTypes' => $feeTypes]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use App\Enumerations\FirstAidPermission;
|
|||||||
use App\Enumerations\SwimmingPermission;
|
use App\Enumerations\SwimmingPermission;
|
||||||
use App\Mail\ParticipantParticipationMails\EventSignUpSuccessfullMail;
|
use App\Mail\ParticipantParticipationMails\EventSignUpSuccessfullMail;
|
||||||
use App\Providers\DoubleCheckEventRegistrationProvider;
|
use App\Providers\DoubleCheckEventRegistrationProvider;
|
||||||
|
use App\RelationModels\EventParticipationFee;
|
||||||
use App\Scopes\CommonController;
|
use App\Scopes\CommonController;
|
||||||
use DateTime;
|
use DateTime;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
@@ -53,6 +54,8 @@ class ShortSignupController extends CommonController {
|
|||||||
'participationOptions' => 'array',
|
'participationOptions' => 'array',
|
||||||
'paymentMethod' => 'nullable|string',
|
'paymentMethod' => 'nullable|string',
|
||||||
'paymentOptions' => 'array',
|
'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()) {
|
if ($validator->fails()) {
|
||||||
@@ -99,6 +102,7 @@ class ShortSignupController extends CommonController {
|
|||||||
paymentMethod: $request->input('paymentMethod') ?: null,
|
paymentMethod: $request->input('paymentMethod') ?: null,
|
||||||
paymentOptions: (array)$request->input('paymentOptions', []),
|
paymentOptions: (array)$request->input('paymentOptions', []),
|
||||||
participationOptions: (array)$request->input('participationOptions', []),
|
participationOptions: (array)$request->input('participationOptions', []),
|
||||||
|
feeType: $request->input('feeType') ?: 'standard',
|
||||||
);
|
);
|
||||||
|
|
||||||
$shortSignUpResponse = new ShortSignUpCommand($shortSignUpRequest)->execute();
|
$shortSignUpResponse = new ShortSignUpCommand($shortSignUpRequest)->execute();
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {computed} from 'vue'
|
|||||||
import {useShortSignupForm} from './composables/useShortSignupForm.js'
|
import {useShortSignupForm} from './composables/useShortSignupForm.js'
|
||||||
import {
|
import {
|
||||||
SHORT_STEP_PERSON,
|
SHORT_STEP_PERSON,
|
||||||
|
SHORT_STEP_FEE,
|
||||||
SHORT_STEP_PARTICIPATION_OPTIONS,
|
SHORT_STEP_PARTICIPATION_OPTIONS,
|
||||||
SHORT_STEP_PHOTO,
|
SHORT_STEP_PHOTO,
|
||||||
SHORT_STEP_PAYMENT,
|
SHORT_STEP_PAYMENT,
|
||||||
@@ -10,6 +11,7 @@ import {
|
|||||||
stepVisible,
|
stepVisible,
|
||||||
} from './composables/shortStepFlow.js'
|
} from './composables/shortStepFlow.js'
|
||||||
import ShortStepPerson from './steps/ShortStepPerson.vue'
|
import ShortStepPerson from './steps/ShortStepPerson.vue'
|
||||||
|
import ShortStepFee from './steps/ShortStepFee.vue'
|
||||||
import ShortStepSummary from './steps/ShortStepSummary.vue'
|
import ShortStepSummary from './steps/ShortStepSummary.vue'
|
||||||
import StepParticipationOptions from '../SignUpForm/steps/StepParticipationOptions.vue'
|
import StepParticipationOptions from '../SignUpForm/steps/StepParticipationOptions.vue'
|
||||||
import StepPhotoPermissions from '../SignUpForm/steps/StepPhotoPermissions.vue'
|
import StepPhotoPermissions from '../SignUpForm/steps/StepPhotoPermissions.vue'
|
||||||
@@ -25,7 +27,7 @@ const props = defineProps({
|
|||||||
const {
|
const {
|
||||||
currentStep, goToStep, goNext, goBack, formData, isMinor, localGroups, needsLocalGroup,
|
currentStep, goToStep, goNext, goBack, formData, isMinor, localGroups, needsLocalGroup,
|
||||||
submit, submitting, submitResult, submitError,
|
submit, submitting, submitResult, submitError,
|
||||||
summaryLoading, summaryAmount, summaryAmountValue,
|
summaryLoading, summaryAmount, feeOptions, selectedFee, flowState,
|
||||||
} = useShortSignupForm(props.event, props.participantData)
|
} = useShortSignupForm(props.event, props.participantData)
|
||||||
|
|
||||||
// Die aus dem langen Prozess übernommenen Schritte emittieren ihre Zielnummer aus jenem Flow mit. Hier zählt
|
// 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 = [
|
const allSteps = [
|
||||||
{step: SHORT_STEP_PERSON, label: 'Person'},
|
{step: SHORT_STEP_PERSON, label: 'Person'},
|
||||||
|
{step: SHORT_STEP_FEE, label: 'Beitrag'},
|
||||||
{step: SHORT_STEP_PARTICIPATION_OPTIONS, label: 'Teilnahmeoptionen'},
|
{step: SHORT_STEP_PARTICIPATION_OPTIONS, label: 'Teilnahmeoptionen'},
|
||||||
{step: SHORT_STEP_PHOTO, label: 'Fotoerlaubnis'},
|
{step: SHORT_STEP_PHOTO, label: 'Fotoerlaubnis'},
|
||||||
{step: SHORT_STEP_PAYMENT, label: 'Zahlungsart'},
|
{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.
|
// Nur die tatsächlich vorkommenden Schritte anzeigen, sonst zeigt die Leiste Schritte, die nie erscheinen.
|
||||||
const steps = computed(() =>
|
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))
|
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"
|
:localGroups="localGroups" :needsLocalGroup="needsLocalGroup"
|
||||||
@next="onNext"
|
@next="onNext"
|
||||||
/>
|
/>
|
||||||
|
<ShortStepFee
|
||||||
|
v-if="currentStep === SHORT_STEP_FEE"
|
||||||
|
:formData="formData" :feeOptions="feeOptions"
|
||||||
|
@next="onNext" @back="onBack"
|
||||||
|
/>
|
||||||
<StepParticipationOptions
|
<StepParticipationOptions
|
||||||
v-if="currentStep === SHORT_STEP_PARTICIPATION_OPTIONS"
|
v-if="currentStep === SHORT_STEP_PARTICIPATION_OPTIONS"
|
||||||
:formData="formData" :event="event"
|
:formData="formData" :event="event"
|
||||||
@@ -122,7 +130,7 @@ const currentIndex = computed(() => steps.value.findIndex(s => s.step === curren
|
|||||||
:event="event"
|
:event="event"
|
||||||
:isMinor="isMinor"
|
:isMinor="isMinor"
|
||||||
:summaryAmount="summaryAmount"
|
:summaryAmount="summaryAmount"
|
||||||
:summaryAmountValue="summaryAmountValue"
|
:feeLabel="feeOptions.length > 1 ? selectedFee?.label : null"
|
||||||
:summaryLoading="summaryLoading"
|
:summaryLoading="summaryLoading"
|
||||||
:submitting="submitting"
|
:submitting="submitting"
|
||||||
:submitError="submitError"
|
:submitError="submitError"
|
||||||
|
|||||||
@@ -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
|
// 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).
|
// Zahlungsart gibt (die wird dann vorausgewählt).
|
||||||
export const SHORT_STEP_PERSON = 1
|
export const SHORT_STEP_PERSON = 1
|
||||||
export const SHORT_STEP_PARTICIPATION_OPTIONS = 2
|
export const SHORT_STEP_FEE = 2
|
||||||
export const SHORT_STEP_PHOTO = 3
|
export const SHORT_STEP_PARTICIPATION_OPTIONS = 3
|
||||||
export const SHORT_STEP_PAYMENT = 4
|
export const SHORT_STEP_PHOTO = 4
|
||||||
export const SHORT_STEP_SUMMARY = 5
|
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) {
|
if (step === SHORT_STEP_PARTICIPATION_OPTIONS) {
|
||||||
return (event.participationOptions?.length ?? 0) > 0
|
return (event.participationOptions?.length ?? 0) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if (step === SHORT_STEP_PAYMENT) {
|
if (step === SHORT_STEP_PAYMENT) {
|
||||||
const activeMethods = (event.paymentMethods ?? []).filter(m => m.active)
|
const activeMethods = (event.paymentMethods ?? []).filter(m => m.active)
|
||||||
return amountValue > 0 && activeMethods.length > 1
|
return state.amountValue > 0 && activeMethods.length > 1
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
export function nextVisibleFrom(event, step, amountValue) {
|
export function nextVisibleFrom(event, step, state) {
|
||||||
let candidate = step + 1
|
let candidate = step + 1
|
||||||
while (candidate < SHORT_STEP_SUMMARY && !stepVisible(event, candidate, amountValue)) {
|
while (candidate < SHORT_STEP_SUMMARY && !stepVisible(event, candidate, state)) {
|
||||||
candidate++
|
candidate++
|
||||||
}
|
}
|
||||||
return candidate
|
return candidate
|
||||||
}
|
}
|
||||||
|
|
||||||
export function prevVisibleFrom(event, step, amountValue) {
|
export function prevVisibleFrom(event, step, state) {
|
||||||
let candidate = step - 1
|
let candidate = step - 1
|
||||||
while (candidate > SHORT_STEP_PERSON && !stepVisible(event, candidate, amountValue)) {
|
while (candidate > SHORT_STEP_PERSON && !stepVisible(event, candidate, state)) {
|
||||||
candidate--
|
candidate--
|
||||||
}
|
}
|
||||||
return candidate
|
return candidate
|
||||||
|
|||||||
+36
-14
@@ -1,4 +1,4 @@
|
|||||||
import {computed, reactive, ref} from 'vue'
|
import {computed, reactive, ref, watch} from 'vue'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import {differenceInYears, parseISO} from 'date-fns'
|
import {differenceInYears, parseISO} from 'date-fns'
|
||||||
import {SHORT_STEP_PERSON, nextVisibleFrom, prevVisibleFrom} from './shortStepFlow.js'
|
import {SHORT_STEP_PERSON, nextVisibleFrom, prevVisibleFrom} from './shortStepFlow.js'
|
||||||
@@ -33,6 +33,8 @@ export function useShortSignupForm(event, participantData) {
|
|||||||
intolerances: participantData.intolerances ?? '',
|
intolerances: participantData.intolerances ?? '',
|
||||||
first_aid: '-1',
|
first_aid: '-1',
|
||||||
badeerlaubnis: '-1',
|
badeerlaubnis: '-1',
|
||||||
|
// Beitragsstufe; wählbar nur, wenn weitere Stufen hinterlegt sind.
|
||||||
|
feeType: 'standard',
|
||||||
// Antworten auf die event-spezifischen Teilnahmeoptionen: { [question.key]: option.value }
|
// Antworten auf die event-spezifischen Teilnahmeoptionen: { [question.key]: option.value }
|
||||||
participationOptions: {},
|
participationOptions: {},
|
||||||
foto: {socialmedia: false, print: false, webseite: false, partner: false, intern: false},
|
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
|
return differenceInYears(new Date(), parseISO(formData.geburtsdatum)) < 18
|
||||||
})
|
})
|
||||||
|
|
||||||
const summaryAmount = ref('')
|
// Beitragsstufen mit fertig berechnetem Betrag: [{ value, label, amount, amountValue }]
|
||||||
const summaryAmountValue = ref(0)
|
const feeOptions = ref([])
|
||||||
const amountLoaded = ref(false)
|
const amountLoaded = ref(false)
|
||||||
|
|
||||||
// Der Betrag steht für die ganze Anmeldung fest (feste Teilnahmegruppe, fester Zeitraum) und wird deshalb
|
const selectedFee = computed(() =>
|
||||||
// genau einmal geholt. Er entscheidet, ob der Zahlungsschritt überhaupt vorkommt.
|
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 () => {
|
const ensureAmount = async () => {
|
||||||
if (amountLoaded.value) {
|
if (amountLoaded.value) {
|
||||||
return
|
return
|
||||||
@@ -67,31 +75,41 @@ export function useShortSignupForm(event, participantData) {
|
|||||||
summaryLoading.value = true
|
summaryLoading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await axios.post('/api/v1/event/' + event.id + '/short-calculate-amount')
|
const res = await axios.post('/api/v1/event/' + event.id + '/short-calculate-amount')
|
||||||
summaryAmount.value = res.data.amount
|
feeOptions.value = res.data.feeTypes ?? []
|
||||||
summaryAmountValue.value = Number(res.data.amountValue ?? 0)
|
|
||||||
amountLoaded.value = true
|
amountLoaded.value = true
|
||||||
|
|
||||||
if (summaryAmountValue.value <= 0) {
|
|
||||||
formData.paymentMethod = null
|
|
||||||
formData.paymentOptions = {}
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
summaryLoading.value = false
|
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) => {
|
const goToStep = (step) => {
|
||||||
currentStep.value = step
|
currentStep.value = step
|
||||||
}
|
}
|
||||||
|
|
||||||
const goNext = async () => {
|
const goNext = async () => {
|
||||||
await ensureAmount()
|
await ensureAmount()
|
||||||
goToStep(nextVisibleFrom(event, currentStep.value, summaryAmountValue.value))
|
goToStep(nextVisibleFrom(event, currentStep.value, flowState.value))
|
||||||
}
|
}
|
||||||
|
|
||||||
const goBack = async () => {
|
const goBack = async () => {
|
||||||
await ensureAmount()
|
await ensureAmount()
|
||||||
goToStep(prevVisibleFrom(event, currentStep.value, summaryAmountValue.value))
|
goToStep(prevVisibleFrom(event, currentStep.value, flowState.value))
|
||||||
}
|
}
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
@@ -120,6 +138,7 @@ export function useShortSignupForm(event, participantData) {
|
|||||||
participationOptions: formData.participationOptions,
|
participationOptions: formData.participationOptions,
|
||||||
paymentMethod: formData.paymentMethod,
|
paymentMethod: formData.paymentMethod,
|
||||||
paymentOptions: formData.paymentOptions,
|
paymentOptions: formData.paymentOptions,
|
||||||
|
feeType: formData.feeType,
|
||||||
})
|
})
|
||||||
|
|
||||||
submitResult.value = {
|
submitResult.value = {
|
||||||
@@ -150,6 +169,9 @@ export function useShortSignupForm(event, participantData) {
|
|||||||
summaryLoading,
|
summaryLoading,
|
||||||
summaryAmount,
|
summaryAmount,
|
||||||
summaryAmountValue,
|
summaryAmountValue,
|
||||||
|
feeOptions,
|
||||||
|
selectedFee,
|
||||||
|
flowState,
|
||||||
ensureAmount,
|
ensureAmount,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<script setup>
|
||||||
|
// Erscheint nur, wenn neben dem Standardbeitrag weitere Stufen hinterlegt sind. Die Beträge gelten für die gesamte
|
||||||
|
// Veranstaltung -- die Kurzanmeldung hat keinen wählbaren Zeitraum.
|
||||||
|
defineProps({
|
||||||
|
formData: Object,
|
||||||
|
feeOptions: {type: Array, default: () => []},
|
||||||
|
})
|
||||||
|
const emit = defineEmits(['next', 'back'])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h3>Teilnahmebeitrag</h3>
|
||||||
|
<p style="margin: 0 0 16px 0; color: #6b7280; font-size: 0.95rem;">
|
||||||
|
Bitte wähle deinen Beitrag für die gesamte Veranstaltung.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<label
|
||||||
|
v-for="option in feeOptions"
|
||||||
|
:key="option.value"
|
||||||
|
style="display: block; margin-bottom: 8px; cursor: pointer;"
|
||||||
|
>
|
||||||
|
<input type="radio" v-model="formData.feeType" :value="option.value" />
|
||||||
|
{{ option.label }}
|
||||||
|
<span style="color: #606060;">({{ option.amount }})</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="btn-row">
|
||||||
|
<button type="button" class="btn-secondary" @click="emit('back')">← Zurück</button>
|
||||||
|
<button type="button" class="btn-primary" @click="emit('next')">Weiter →</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -8,7 +8,8 @@ const props = defineProps({
|
|||||||
event: Object,
|
event: Object,
|
||||||
isMinor: Boolean,
|
isMinor: Boolean,
|
||||||
summaryAmount: String,
|
summaryAmount: String,
|
||||||
summaryAmountValue: {type: Number, default: 0},
|
// Nur gesetzt, wenn zwischen mehreren Beitragsstufen gewählt werden konnte.
|
||||||
|
feeLabel: {type: String, default: null},
|
||||||
summaryLoading: Boolean,
|
summaryLoading: Boolean,
|
||||||
submitting: Boolean,
|
submitting: Boolean,
|
||||||
submitError: {type: String, default: ''},
|
submitError: {type: String, default: ''},
|
||||||
@@ -137,6 +138,10 @@ const canSubmit = computed(() =>
|
|||||||
|
|
||||||
<h4 style="margin: 0 0 8px 0;">Kostenübersicht</h4>
|
<h4 style="margin: 0 0 8px 0;">Kostenübersicht</h4>
|
||||||
<table class="form-table cost-table" style="margin-bottom: 20px;">
|
<table class="form-table cost-table" style="margin-bottom: 20px;">
|
||||||
|
<tr v-if="feeLabel">
|
||||||
|
<td>Beitragsstufe:</td>
|
||||||
|
<td>{{ feeLabel }}</td>
|
||||||
|
</tr>
|
||||||
<tr class="cost-total">
|
<tr class="cost-total">
|
||||||
<td><strong>Teilnahmebeitrag:</strong></td>
|
<td><strong>Teilnahmebeitrag:</strong></td>
|
||||||
<td><strong>{{ summaryAmount }}</strong></td>
|
<td><strong>{{ summaryAmount }}</strong></td>
|
||||||
|
|||||||
@@ -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 --
|
* 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.
|
* 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([
|
return collect([
|
||||||
$this->participationFee1,
|
$this->participationFee1,
|
||||||
$this->participationFee2,
|
$this->participationFee2,
|
||||||
$this->participationFee3,
|
$this->participationFee3,
|
||||||
$this->participationFee4,
|
$this->participationFee4,
|
||||||
])->first(fn ($participationFee) => $participationFee !== null)?->type;
|
])->first(fn ($participationFee) => $participationFee !== null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function firstParticipationTypeSlug() : ?string {
|
||||||
|
return $this->firstParticipationFee()?->type;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<int, string>
|
||||||
|
*/
|
||||||
|
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 {
|
public function tenant() : BelongsTo {
|
||||||
return $this->belongsTo(Tenant::class, 'tenant', 'slug');
|
return $this->belongsTo(Tenant::class, 'tenant', 'slug');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,12 @@ class ShortSignUpTest extends TestCase
|
|||||||
EatingHabit::create(['slug' => EatingHabit::EATING_HABIT_OMNIVOR, 'name' => 'Omnivor']);
|
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([
|
$event = Event::create([
|
||||||
'tenant' => $this->tenant->slug, 'name' => 'Event', 'identifier' => 'evt-1', 'location' => 'Ort',
|
'tenant' => $this->tenant->slug, 'name' => 'Event', 'identifier' => 'evt-1', 'location' => 'Ort',
|
||||||
@@ -65,7 +70,7 @@ class ShortSignUpTest extends TestCase
|
|||||||
$fee = EventParticipationFee::create([
|
$fee = EventParticipationFee::create([
|
||||||
'tenant' => $this->tenant->slug, 'type' => ParticipationType::PARTICIPATION_TYPE_PARTICIPANT,
|
'tenant' => $this->tenant->slug, 'type' => ParticipationType::PARTICIPATION_TYPE_PARTICIPANT,
|
||||||
'name' => 'Teilnahme', 'description' => '', 'amount_standard' => 50.0,
|
'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->participation_fee_1 = $fee->id;
|
||||||
$event->save();
|
$event->save();
|
||||||
@@ -102,6 +107,7 @@ class ShortSignUpTest extends TestCase
|
|||||||
paymentMethod: $overrides['paymentMethod'] ?? PaymentMethod::PAYMENT_ACCOUNT_TRANSACTION,
|
paymentMethod: $overrides['paymentMethod'] ?? PaymentMethod::PAYMENT_ACCOUNT_TRANSACTION,
|
||||||
paymentOptions: [],
|
paymentOptions: [],
|
||||||
participationOptions: $overrides['participationOptions'] ?? [],
|
participationOptions: $overrides['participationOptions'] ?? [],
|
||||||
|
feeType: $overrides['feeType'] ?? 'standard',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +133,7 @@ class ShortSignUpTest extends TestCase
|
|||||||
$this->assertSame(1, $participant->arrival_eating);
|
$this->assertSame(1, $participant->arrival_eating);
|
||||||
$this->assertSame(2, $participant->departure_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(ParticipationType::PARTICIPATION_TYPE_PARTICIPANT, $participant->participation_type);
|
||||||
$this->assertSame('standard', $participant->fee_type);
|
$this->assertSame('standard', $participant->fee_type);
|
||||||
$this->assertSame(50.0, $participant->amount->getAmount());
|
$this->assertSame(50.0, $participant->amount->getAmount());
|
||||||
@@ -142,6 +148,55 @@ class ShortSignUpTest extends TestCase
|
|||||||
$this->assertFalse($participant->foto_print);
|
$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
|
public function test_u18_ohne_abgefragte_badeerlaubnis_bekommt_keine(): void
|
||||||
{
|
{
|
||||||
$event = $this->createEvent(swimmingPermissionRequired: false);
|
$event = $this->createEvent(swimmingPermissionRequired: false);
|
||||||
|
|||||||
Reference in New Issue
Block a user