$validated * * @throws ValidationException */ public function register(Event $event, array $validated, ?User $user): Person { if ($event->status !== 'registration_open') { throw ValidationException::withMessages([ 'event' => ['This event is not accepting registrations.'], ]); } $festivalEvent = $this->resolveFestivalEvent($event); $email = strtolower($user?->email ?? $validated['email']); $this->checkDuplicateRegistration($festivalEvent, $email); // Resolve or create user account for unauthenticated registrations if ($user === null) { $user = $this->resolveUserAccount($email, $validated); } $volunteerCrowdType = $this->resolveVolunteerCrowdType($event); $person = DB::transaction(function () use ($festivalEvent, $validated, $user, $email, $volunteerCrowdType): Person { $person = Person::updateOrCreate( [ 'event_id' => $festivalEvent->id, 'email' => $email, ], [ 'user_id' => $user->id, 'crowd_type_id' => $volunteerCrowdType->id, 'first_name' => $validated['first_name'] ?? $user->first_name, 'last_name' => $validated['last_name'] ?? $user->last_name, 'phone' => $validated['phone'] ?? null, 'date_of_birth' => $validated['date_of_birth'] ?? null, 'status' => PersonStatus::PENDING, 'custom_fields' => [ 'tshirt_size' => $validated['tshirt_size'] ?? null, 'first_aid' => $validated['first_aid'] ?? false, 'allergies' => $validated['allergies'] ?? null, 'driving_licence' => $validated['driving_licence'] ?? false, 'motivation' => $validated['motivation'] ?? null, 'motivation_other' => $validated['motivation_other'] ?? null, ], ] ); $this->syncAvailabilities($person, $festivalEvent, $validated['availabilities'] ?? []); if (!empty($validated['field_values'])) { $this->registrationFormFieldService->upsertPersonValues( $person, $validated['field_values'] ); } if (!empty($validated['section_preferences'])) { $this->personSectionPreferenceService->replacePreferences( $person, $validated['section_preferences'] ); } // Trigger tag sync — user_id is always known now $this->tagSyncService->syncFromRegistration($person); $source = auth('sanctum')->check() ? 'authenticated_form' : 'public_form'; $activityLogger = activity('volunteer_registration') ->performedOn($person) ->withProperties([ 'source' => $source, 'event_id' => $festivalEvent->id, 'person_id' => $person->id, 'email' => $email, ]) ->causedBy($user); $activityLogger->log('person.registered'); return $person; }); // Send confirmation email (queued, outside transaction) Mail::to($person->email)->queue(new RegistrationConfirmationMail($person, $festivalEvent)); return $person; } /** * Resolve or create user account for the registering email. * * @param array $validated * * @throws ValidationException */ private function resolveUserAccount(string $email, array $validated): User { $existingUser = User::where('email', $email)->first(); if ($existingUser !== null) { // Returning volunteer: authenticate with provided password if (!Hash::check($validated['password'], $existingUser->password)) { throw ValidationException::withMessages([ 'password' => ['Wachtwoord onjuist.'], ]); } return $existingUser; } // New volunteer: create user account try { return User::create([ 'first_name' => $validated['first_name'], 'last_name' => $validated['last_name'], 'email' => $email, 'password' => Hash::make($validated['password']), ]); } catch (\Illuminate\Database\UniqueConstraintViolationException) { throw ValidationException::withMessages([ 'email' => ['Dit emailadres heeft al een account. Gebruik je bestaande wachtwoord.'], ]); } } private function resolveFestivalEvent(Event $event): Event { if ($event->isSubEvent()) { return $event->parent; } return $event; } /** * @throws ValidationException */ private function checkDuplicateRegistration(Event $festivalEvent, string $email): void { $existing = Person::where('event_id', $festivalEvent->id) ->where('email', $email) ->first(); if ($existing === null) { return; } if ($existing->status !== PersonStatus::REJECTED->value) { throw ValidationException::withMessages([ 'email' => ['Already registered for this event.'], ]); } } /** * @throws \RuntimeException */ private function resolveVolunteerCrowdType(Event $event): CrowdType { $crowdType = CrowdType::where('organisation_id', $event->organisation_id) ->where('system_type', 'VOLUNTEER') ->first(); if ($crowdType === null) { Log::error('No volunteer crowd type configured', [ 'organisation_id' => $event->organisation_id, 'event_id' => $event->id, ]); abort(500, 'No volunteer crowd type configured for this organisation.'); } return $crowdType; } /** * @param array> $availabilities */ private function syncAvailabilities(Person $person, Event $festivalEvent, array $availabilities): void { if (empty($availabilities)) { return; } VolunteerAvailability::where('person_id', $person->id)->delete(); $validTimeSlotIds = $festivalEvent->getAllRelevantTimeSlots() ->where('person_type', 'VOLUNTEER') ->pluck('id') ->toArray(); foreach ($availabilities as $availability) { if (! in_array($availability['time_slot_id'], $validTimeSlotIds, true)) { continue; } VolunteerAvailability::create([ 'person_id' => $person->id, 'time_slot_id' => $availability['time_slot_id'], 'preference_level' => $availability['preference_level'] ?? 3, 'submitted_at' => now(), ]); } } }