feat: add date_of_birth field to persons across all layers

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-11 09:06:29 +02:00
parent d2f282eb4c
commit 6dccf87234
18 changed files with 161 additions and 17 deletions

View File

@@ -20,6 +20,7 @@ final class StorePersonRequest extends FormRequest
'crowd_type_id' => ['required', 'ulid', 'exists:crowd_types,id'],
'first_name' => ['required', 'string', 'max:255'],
'last_name' => ['required', 'string', 'max:255'],
'date_of_birth' => ['nullable', 'date', 'before:today'],
'email' => ['required', 'email', 'max:255'],
'phone' => ['nullable', 'string', 'max:30'],
'company_id' => ['nullable', 'ulid', 'exists:companies,id'],

View File

@@ -20,6 +20,7 @@ final class UpdatePersonRequest extends FormRequest
'crowd_type_id' => ['sometimes', 'ulid', 'exists:crowd_types,id'],
'first_name' => ['sometimes', 'string', 'max:255'],
'last_name' => ['sometimes', 'string', 'max:255'],
'date_of_birth' => ['nullable', 'date', 'before:today'],
'email' => ['sometimes', 'email', 'max:255'],
'phone' => ['nullable', 'string', 'max:30'],
'company_id' => ['nullable', 'ulid', 'exists:companies,id'],

View File

@@ -35,6 +35,7 @@ final class VolunteerRegistrationRequest extends FormRequest
'last_name' => ['required_without:_authenticated', 'string', 'max:255'],
'email' => ['required_without:_authenticated', 'email', 'max:255'],
'phone' => ['nullable', 'string', 'max:50'],
'date_of_birth' => ['nullable', 'date', 'before:today'],
'tshirt_size' => ['nullable', 'string', 'in:XS,S,M,L,XL,XXL,XXXL'],
'first_aid' => ['nullable', 'boolean'],

View File

@@ -16,6 +16,7 @@ final class PersonResource extends JsonResource
'event_id' => $this->event_id,
'first_name' => $this->first_name,
'last_name' => $this->last_name,
'date_of_birth' => $this->date_of_birth?->toDateString(),
'full_name' => $this->full_name,
'email' => $this->email,
'phone' => $this->phone,

View File

@@ -30,6 +30,7 @@ final class Person extends Model
'company_id',
'first_name',
'last_name',
'date_of_birth',
'email',
'phone',
'status',
@@ -51,6 +52,7 @@ final class Person extends Model
protected function casts(): array
{
return [
'date_of_birth' => 'date',
'is_blacklisted' => 'boolean',
'custom_fields' => 'array',
];

View File

@@ -53,6 +53,7 @@ final class VolunteerRegistrationService
'first_name' => $user?->first_name ?? $validated['first_name'],
'last_name' => $user?->last_name ?? $validated['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,

View File

@@ -20,6 +20,7 @@ final class PersonFactory extends Factory
'crowd_type_id' => CrowdType::factory(),
'first_name' => fake('nl_NL')->firstName(),
'last_name' => fake('nl_NL')->lastName(),
'date_of_birth' => fake()->dateTimeBetween('-40 years', '-18 years')->format('Y-m-d'),
'email' => fake()->unique()->safeEmail(),
'phone' => fake('nl_NL')->phoneNumber(),
'status' => 'pending',

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('persons', function (Blueprint $table) {
$table->date('date_of_birth')->nullable()->after('last_name');
});
}
public function down(): void
{
Schema::table('persons', function (Blueprint $table) {
$table->dropColumn('date_of_birth');
});
}
};

View File

@@ -471,21 +471,21 @@ class DevSeeder extends Seeder
}
// 15 named volunteers
$jan = Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'user_id' => $volunteerUsers['jan@gmail.com']->id, 'first_name' => 'Jan', 'last_name' => 'de Vries', 'email' => 'jan@gmail.com', 'phone' => '+31612345001', 'status' => 'approved']);
$lisaB = Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'first_name' => 'Lisa', 'last_name' => 'Bakker', 'email' => 'lisa.bakker@hotmail.com', 'phone' => '+31612345002', 'status' => 'approved']);
$ahmedP = Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'user_id' => $volunteerUsers['ahmed.h@gmail.com']->id, 'first_name' => 'Ahmed', 'last_name' => 'Hassan', 'email' => 'ahmed.h@gmail.com', 'phone' => '+31612345003', 'status' => 'approved']);
$saraJ = Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'first_name' => 'Sara', 'last_name' => 'Jansen', 'email' => 'sara.j@outlook.com', 'phone' => '+31612345004', 'status' => 'approved']);
$tomV = Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'user_id' => $volunteerUsers['tom.visser@gmail.com']->id, 'first_name' => 'Tom', 'last_name' => 'Visser', 'email' => 'tom.visser@gmail.com', 'phone' => '+31612345005', 'status' => 'approved']);
$fatima = Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'first_name' => 'Fatima', 'last_name' => 'El Amrani', 'email' => 'fatima@gmail.com', 'phone' => '+31612345006', 'status' => 'approved']);
$daan = Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'first_name' => 'Daan', 'last_name' => 'Smit', 'email' => 'daan.smit@gmail.com', 'phone' => '+31612345007', 'status' => 'pending']);
Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'first_name' => 'Sophie', 'last_name' => 'Mulder', 'email' => 'sophie.m@hotmail.com', 'phone' => '+31612345008', 'status' => 'pending']);
Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'first_name' => 'Jesse', 'last_name' => 'van Dijk', 'email' => 'jesse@gmail.com', 'phone' => '+31612345009', 'status' => 'applied']);
Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'first_name' => 'Noa', 'last_name' => 'Hendriks', 'email' => 'noa.h@outlook.com', 'phone' => '+31612345010', 'status' => 'applied']);
Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'first_name' => 'Kevin', 'last_name' => 'Bos', 'email' => 'kevin.bos@gmail.com', 'phone' => '+31612345011', 'status' => 'rejected', 'admin_notes' => 'Vorig jaar no-show']);
Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'first_name' => 'Priya', 'last_name' => 'Sharma', 'email' => 'priya@gmail.com', 'phone' => '+31612345012', 'status' => 'invited']);
$lotte = Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'user_id' => $volunteerUsers['lotte@gmail.com']->id, 'first_name' => 'Lotte', 'last_name' => 'de Jong', 'email' => 'lotte@gmail.com', 'phone' => '+31612345013', 'status' => 'approved']);
$robin = Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'first_name' => 'Robin', 'last_name' => 'Peters', 'email' => 'robin.p@hotmail.com', 'phone' => '+31612345014', 'status' => 'approved']);
$emma = Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'first_name' => 'Emma', 'last_name' => 'Willems', 'email' => 'emma.w@gmail.com', 'phone' => '+31612345015', 'status' => 'no_show', 'is_blacklisted' => true]);
$jan = Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'user_id' => $volunteerUsers['jan@gmail.com']->id, 'first_name' => 'Jan', 'last_name' => 'de Vries', 'email' => 'jan@gmail.com', 'phone' => '+31612345001', 'status' => 'approved', 'date_of_birth' => '1995-03-15']);
$lisaB = Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'first_name' => 'Lisa', 'last_name' => 'Bakker', 'email' => 'lisa.bakker@hotmail.com', 'phone' => '+31612345002', 'status' => 'approved', 'date_of_birth' => '1998-07-22']);
$ahmedP = Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'user_id' => $volunteerUsers['ahmed.h@gmail.com']->id, 'first_name' => 'Ahmed', 'last_name' => 'Hassan', 'email' => 'ahmed.h@gmail.com', 'phone' => '+31612345003', 'status' => 'approved', 'date_of_birth' => '1992-11-08']);
$saraJ = Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'first_name' => 'Sara', 'last_name' => 'Jansen', 'email' => 'sara.j@outlook.com', 'phone' => '+31612345004', 'status' => 'approved', 'date_of_birth' => '2000-01-30']);
$tomV = Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'user_id' => $volunteerUsers['tom.visser@gmail.com']->id, 'first_name' => 'Tom', 'last_name' => 'Visser', 'email' => 'tom.visser@gmail.com', 'phone' => '+31612345005', 'status' => 'approved', 'date_of_birth' => '1997-06-14']);
$fatima = Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'first_name' => 'Fatima', 'last_name' => 'El Amrani', 'email' => 'fatima@gmail.com', 'phone' => '+31612345006', 'status' => 'approved', 'date_of_birth' => '1996-05-20']);
$daan = Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'first_name' => 'Daan', 'last_name' => 'Smit', 'email' => 'daan.smit@gmail.com', 'phone' => '+31612345007', 'status' => 'pending', 'date_of_birth' => '1993-08-11']);
Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'first_name' => 'Sophie', 'last_name' => 'Mulder', 'email' => 'sophie.m@hotmail.com', 'phone' => '+31612345008', 'status' => 'pending', 'date_of_birth' => '2001-02-28']);
Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'first_name' => 'Jesse', 'last_name' => 'van Dijk', 'email' => 'jesse@gmail.com', 'phone' => '+31612345009', 'status' => 'applied', 'date_of_birth' => '1999-10-05']);
Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'first_name' => 'Noa', 'last_name' => 'Hendriks', 'email' => 'noa.h@outlook.com', 'phone' => '+31612345010', 'status' => 'applied', 'date_of_birth' => '2000-07-19']);
Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'first_name' => 'Kevin', 'last_name' => 'Bos', 'email' => 'kevin.bos@gmail.com', 'phone' => '+31612345011', 'status' => 'rejected', 'admin_notes' => 'Vorig jaar no-show', 'date_of_birth' => '1994-12-01']);
Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'first_name' => 'Priya', 'last_name' => 'Sharma', 'email' => 'priya@gmail.com', 'phone' => '+31612345012', 'status' => 'invited', 'date_of_birth' => '1998-03-27']);
$lotte = Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'user_id' => $volunteerUsers['lotte@gmail.com']->id, 'first_name' => 'Lotte', 'last_name' => 'de Jong', 'email' => 'lotte@gmail.com', 'phone' => '+31612345013', 'status' => 'approved', 'date_of_birth' => '1994-09-25']);
$robin = Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'first_name' => 'Robin', 'last_name' => 'Peters', 'email' => 'robin.p@hotmail.com', 'phone' => '+31612345014', 'status' => 'approved', 'date_of_birth' => '1991-12-03']);
$emma = Person::create(['event_id' => $festival->id, 'crowd_type_id' => $vol, 'first_name' => 'Emma', 'last_name' => 'Willems', 'email' => 'emma.w@gmail.com', 'phone' => '+31612345015', 'status' => 'no_show', 'is_blacklisted' => true, 'date_of_birth' => '1999-04-17']);
// 6 named crew
$klaas = Person::create(['event_id' => $festival->id, 'crowd_type_id' => $crew, 'company_id' => $this->companies['SecureEvent BV']->id, 'first_name' => 'Klaas', 'last_name' => 'Veilig Jr.', 'email' => 'klaas.jr@secureevent.nl', 'phone' => '+31612345016', 'status' => 'approved']);

View File

@@ -170,6 +170,48 @@ class VolunteerRegistrationTest extends TestCase
$this->assertNotEmpty($customFields['section_preferences']);
}
public function test_volunteer_can_register_with_date_of_birth(): void
{
$response = $this->postJson("/api/v1/events/{$this->event->id}/volunteer-register", [
'first_name' => 'Mila',
'last_name' => 'de Boer',
'email' => 'mila@voorbeeld.nl',
'date_of_birth' => '1998-05-12',
]);
$response->assertStatus(201);
$person = Person::where('email', 'mila@voorbeeld.nl')->first();
$this->assertEquals('1998-05-12', $person->date_of_birth->format('Y-m-d'));
}
public function test_volunteer_can_register_without_date_of_birth(): void
{
$response = $this->postJson("/api/v1/events/{$this->event->id}/volunteer-register", [
'first_name' => 'Sem',
'last_name' => 'van Beek',
'email' => 'sem@voorbeeld.nl',
]);
$response->assertStatus(201);
$person = Person::where('email', 'sem@voorbeeld.nl')->first();
$this->assertNull($person->date_of_birth);
}
public function test_date_of_birth_must_be_before_today(): void
{
$response = $this->postJson("/api/v1/events/{$this->event->id}/volunteer-register", [
'first_name' => 'Tijn',
'last_name' => 'Kuiper',
'email' => 'tijn@voorbeeld.nl',
'date_of_birth' => now()->addDay()->format('Y-m-d'),
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors('date_of_birth');
}
public function test_duplicate_email_rejected(): void
{
$this->postJson("/api/v1/events/{$this->event->id}/volunteer-register", [