diff --git a/app/Domains/Event/Views/Partials/SignUpForm/steps/StepSummary.vue b/app/Domains/Event/Views/Partials/SignUpForm/steps/StepSummary.vue
index 64c08ed..e5c985e 100644
--- a/app/Domains/Event/Views/Partials/SignUpForm/steps/StepSummary.vue
+++ b/app/Domains/Event/Views/Partials/SignUpForm/steps/StepSummary.vue
@@ -7,6 +7,7 @@ const PAYMENT_ACCOUNT_TRANSACTION = 'PAYMENT_ACCOUNT_TRANSACTION'
const props = defineProps({
formData: Object,
event: Object,
+ selectedAddons: {type: Object, default: () => ({})},
summaryAmount: String,
summaryAmountValue: {type: Number, default: 0},
summaryLoading: Boolean,
@@ -35,6 +36,16 @@ const paymentConfirmationText = computed(() => {
// Zurück führt zur Zahlungsart (10), sofern dieser Schritt gezeigt wurde; bei 0 € direkt zu Allergien (9).
const backTarget = computed(() => props.summaryAmountValue > 0 ? 10 : 9)
+// Gewählte Zusätze (Event-Addons) für die Zusammenfassung mit Preisanzeige.
+const selectedAddonList = computed(() =>
+ (props.event.addons ?? [])
+ .filter(addon => props.selectedAddons[addon.key])
+ .map(addon => ({
+ title: addon.title,
+ price: addon.free ? 'Kostenfrei' : (addon.priceReadable + (addon.flat ? '' : ' / Tag')),
+ }))
+)
+
// Gewählte Teilnahmeoptionen für die Zusammenfassung (label statt value anzeigen).
const selectedParticipationOptions = computed(() =>
(props.event.participationOptions ?? []).map(question => {
@@ -129,6 +140,11 @@ function eatingHabit() {
| Foto-Erlaubnis: |
diff --git a/app/Models/Event.php b/app/Models/Event.php
index 6a7eb7e..049f2b4 100644
--- a/app/Models/Event.php
+++ b/app/Models/Event.php
@@ -88,6 +88,7 @@ class Event extends InstancedModel
'tax_exemption_reason',
'tax_exemption_note',
'participation_options',
+ 'addons',
];
@@ -117,6 +118,7 @@ class Event extends InstancedModel
'vat_rate' => 'integer',
'participation_options' => 'array',
+ 'addons' => 'array',
];
public function tenant(): BelongsTo
diff --git a/app/Models/EventParticipant.php b/app/Models/EventParticipant.php
index b61aca1..425aca8 100644
--- a/app/Models/EventParticipant.php
+++ b/app/Models/EventParticipant.php
@@ -110,6 +110,12 @@ class EventParticipant extends InstancedModel
return $this->hasMany(EventParticipantOption::class, 'event_participant_id');
}
+ /** Gebuchte Zusätze (Event-Addons) dieser Anmeldung. */
+ public function selectedAddons(): HasMany
+ {
+ return $this->hasMany(EventParticipantAddon::class, 'event_participant_id');
+ }
+
public function user()
{
return $this->belongsTo(User::class);
diff --git a/app/Models/EventParticipantAddon.php b/app/Models/EventParticipantAddon.php
new file mode 100644
index 0000000..574cf91
--- /dev/null
+++ b/app/Models/EventParticipantAddon.php
@@ -0,0 +1,41 @@
+ 'boolean',
+ 'days' => 'integer',
+ 'price' => AmountCast::class,
+ 'amount' => AmountCast::class,
+ ];
+
+ public function event(): BelongsTo
+ {
+ return $this->belongsTo(Event::class);
+ }
+
+ public function participant(): BelongsTo
+ {
+ return $this->belongsTo(EventParticipant::class, 'event_participant_id');
+ }
+}
diff --git a/app/Resources/EventParticipantAddonResource.php b/app/Resources/EventParticipantAddonResource.php
new file mode 100644
index 0000000..41663f9
--- /dev/null
+++ b/app/Resources/EventParticipantAddonResource.php
@@ -0,0 +1,26 @@
+ $this->resource->addon_key,
+ 'title' => $this->resource->title,
+ 'flat' => $this->resource->flat,
+ 'amount' => $this->resource->amount?->getAmount() ?? 0,
+ 'amountReadable' => $this->resource->amount?->toString() ?? '0,00 Euro',
+ 'free' => ($this->resource->amount?->getAmount() ?? 0) <= 0,
+ ];
+ }
+}
diff --git a/app/Resources/EventParticipantResource.php b/app/Resources/EventParticipantResource.php
index 666e043..0a8c185 100644
--- a/app/Resources/EventParticipantResource.php
+++ b/app/Resources/EventParticipantResource.php
@@ -98,6 +98,8 @@ class EventParticipantResource extends JsonResource
'departureDateReadable' => $this->resource->departure_date->format('d.m.Y'),
'participationOptions' => $this->resource->selectedOptions()->get()->map(
fn($option) => new EventParticipantOptionResource($option)->toArray($request))->toArray(),
+ 'selectedAddons' => $this->resource->selectedAddons()->get()->map(
+ fn($addon) => new EventParticipantAddonResource($addon)->toArray($request))->toArray(),
]
);
}
diff --git a/app/Resources/EventResource.php b/app/Resources/EventResource.php
index aa59d21..81324ab 100644
--- a/app/Resources/EventResource.php
+++ b/app/Resources/EventResource.php
@@ -141,6 +141,20 @@ class EventResource extends JsonResource{
// 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'] = [];
@@ -354,6 +368,54 @@ class EventResource extends JsonResource{
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 $selectedKeys
+ * @return array{items: array, total: Amount}
+ */
+ public function resolveAddons(array $selectedKeys, DateTime $arrival, DateTime $departure): array
+ {
+ $days = $arrival->diff($departure)->days + 1;
+ $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];
+ }
}
diff --git a/database/migrations/2026_08_12_150010_add_addons_to_events.php b/database/migrations/2026_08_12_150010_add_addons_to_events.php
new file mode 100644
index 0000000..0131248
--- /dev/null
+++ b/database/migrations/2026_08_12_150010_add_addons_to_events.php
@@ -0,0 +1,23 @@
+json('addons')->nullable();
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::table('events', function (Blueprint $table) {
+ $table->dropColumn('addons');
+ });
+ }
+};
diff --git a/database/migrations/2026_08_12_150020_create_event_participant_addons.php b/database/migrations/2026_08_12_150020_create_event_participant_addons.php
new file mode 100644
index 0000000..af63958
--- /dev/null
+++ b/database/migrations/2026_08_12_150020_create_event_participant_addons.php
@@ -0,0 +1,36 @@
+id();
+ $table->string('tenant');
+
+ $table->foreignId('event_id')->constrained('events', 'id')->cascadeOnDelete()->cascadeOnUpdate();
+ $table->foreignId('event_participant_id')->constrained('event_participants', 'id')->cascadeOnDelete()->cascadeOnUpdate();
+
+ $table->string('addon_key');
+ $table->string('title');
+ $table->float('price')->default(0);
+ $table->boolean('flat')->default(false);
+ $table->integer('days')->default(0);
+ $table->float('amount')->default(0);
+
+ $table->timestamps();
+
+ $table->foreign('tenant')->references('slug')->on('tenants')->restrictOnDelete()->cascadeOnUpdate();
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::dropIfExists('event_participant_addons');
+ }
+};
|