From b79ebf555026ff24a47866acbfb6ae6586e2bdc3 Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Fri, 17 Apr 2026 10:26:44 +0200 Subject: [PATCH] feat(organisation): add contact fields to model and API Add contact_name, contact_email, phone, website columns. Wire the new fields through the Organisation model, update request validation, response resource, and the TypeScript Organisation interface. Needed by the upcoming dashboard + form-builder binding registry. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Api/V1/UpdateOrganisationRequest.php | 4 ++ .../Resources/Api/V1/OrganisationResource.php | 4 ++ api/app/Models/Organisation.php | 4 ++ ..._contact_fields_to_organisations_table.php | 27 ++++++++ .../Feature/Organisation/OrganisationTest.php | 61 +++++++++++++++++++ apps/app/src/types/organisation.ts | 8 +++ 6 files changed, 108 insertions(+) create mode 100644 api/database/migrations/2026_04_18_110000_add_contact_fields_to_organisations_table.php diff --git a/api/app/Http/Requests/Api/V1/UpdateOrganisationRequest.php b/api/app/Http/Requests/Api/V1/UpdateOrganisationRequest.php index 3180eb9c..94cd28aa 100644 --- a/api/app/Http/Requests/Api/V1/UpdateOrganisationRequest.php +++ b/api/app/Http/Requests/Api/V1/UpdateOrganisationRequest.php @@ -23,6 +23,10 @@ final class UpdateOrganisationRequest extends FormRequest 'sometimes', 'string', 'max:255', 'regex:/^[a-z0-9-]+$/', Rule::unique('organisations', 'slug')->ignore($this->route('organisation')), ], + 'contact_name' => ['sometimes', 'nullable', 'string', 'max:255'], + 'contact_email' => ['sometimes', 'nullable', 'email', 'max:255'], + 'phone' => ['sometimes', 'nullable', 'string', 'max:255'], + 'website' => ['sometimes', 'nullable', 'url', 'max:255'], 'settings' => ['sometimes', 'array'], 'email_logo_url' => ['nullable', 'url', 'max:500'], 'email_primary_color' => ['nullable', 'string', 'regex:/^#[0-9A-Fa-f]{6}$/'], diff --git a/api/app/Http/Resources/Api/V1/OrganisationResource.php b/api/app/Http/Resources/Api/V1/OrganisationResource.php index 768ed976..c8f1331e 100644 --- a/api/app/Http/Resources/Api/V1/OrganisationResource.php +++ b/api/app/Http/Resources/Api/V1/OrganisationResource.php @@ -15,6 +15,10 @@ final class OrganisationResource extends JsonResource 'id' => $this->id, 'name' => $this->name, 'slug' => $this->slug, + 'contact_name' => $this->contact_name, + 'contact_email' => $this->contact_email, + 'phone' => $this->phone, + 'website' => $this->website, 'billing_status' => $this->billing_status, 'settings' => $this->settings, 'email_logo_url' => $this->email_logo_url, diff --git a/api/app/Models/Organisation.php b/api/app/Models/Organisation.php index 9d2b929e..5be35671 100644 --- a/api/app/Models/Organisation.php +++ b/api/app/Models/Organisation.php @@ -21,6 +21,10 @@ final class Organisation extends Model protected $fillable = [ 'name', 'slug', + 'contact_name', + 'contact_email', + 'phone', + 'website', 'billing_status', 'settings', 'email_logo_url', diff --git a/api/database/migrations/2026_04_18_110000_add_contact_fields_to_organisations_table.php b/api/database/migrations/2026_04_18_110000_add_contact_fields_to_organisations_table.php new file mode 100644 index 00000000..56c5a05b --- /dev/null +++ b/api/database/migrations/2026_04_18_110000_add_contact_fields_to_organisations_table.php @@ -0,0 +1,27 @@ +string('contact_name')->nullable()->after('slug'); + $table->string('contact_email')->nullable()->after('contact_name'); + $table->string('phone')->nullable()->after('contact_email'); + $table->string('website')->nullable()->after('phone'); + }); + } + + public function down(): void + { + Schema::table('organisations', function (Blueprint $table) { + $table->dropColumn(['contact_name', 'contact_email', 'phone', 'website']); + }); + } +}; diff --git a/api/tests/Feature/Organisation/OrganisationTest.php b/api/tests/Feature/Organisation/OrganisationTest.php index 39c4bd3b..5ba30238 100644 --- a/api/tests/Feature/Organisation/OrganisationTest.php +++ b/api/tests/Feature/Organisation/OrganisationTest.php @@ -221,4 +221,65 @@ class OrganisationTest extends TestCase $response->assertForbidden(); } + + public function test_org_admin_can_update_all_contact_fields(): void + { + $user = User::factory()->create(); + $org = Organisation::factory()->create(); + $org->users()->attach($user, ['role' => 'org_admin']); + + Sanctum::actingAs($user); + + $response = $this->putJson("/api/v1/organisations/{$org->id}", [ + 'contact_name' => 'Bert Hausmans', + 'contact_email' => 'bert@example.com', + 'phone' => '+31 6 12345678', + 'website' => 'https://example.com', + ]); + + $response->assertOk() + ->assertJson(['data' => [ + 'contact_name' => 'Bert Hausmans', + 'contact_email' => 'bert@example.com', + 'phone' => '+31 6 12345678', + 'website' => 'https://example.com', + ]]); + + $this->assertDatabaseHas('organisations', [ + 'id' => $org->id, + 'contact_email' => 'bert@example.com', + ]); + } + + public function test_update_returns_422_for_invalid_contact_email(): void + { + $user = User::factory()->create(); + $org = Organisation::factory()->create(); + $org->users()->attach($user, ['role' => 'org_admin']); + + Sanctum::actingAs($user); + + $response = $this->putJson("/api/v1/organisations/{$org->id}", [ + 'contact_email' => 'not-an-email', + ]); + + $response->assertUnprocessable() + ->assertJsonValidationErrors(['contact_email']); + } + + public function test_update_returns_422_for_invalid_website_url(): void + { + $user = User::factory()->create(); + $org = Organisation::factory()->create(); + $org->users()->attach($user, ['role' => 'org_admin']); + + Sanctum::actingAs($user); + + $response = $this->putJson("/api/v1/organisations/{$org->id}", [ + 'website' => 'not a url', + ]); + + $response->assertUnprocessable() + ->assertJsonValidationErrors(['website']); + } } diff --git a/apps/app/src/types/organisation.ts b/apps/app/src/types/organisation.ts index 812af2f0..62d16130 100644 --- a/apps/app/src/types/organisation.ts +++ b/apps/app/src/types/organisation.ts @@ -2,6 +2,10 @@ export interface Organisation { id: string name: string slug: string + contact_name: string | null + contact_email: string | null + phone: string | null + website: string | null billing_status: 'trial' | 'active' | 'suspended' | 'cancelled' settings: Record | null email_logo_url: string | null @@ -23,6 +27,10 @@ export interface OrganisationMember { export interface UpdateOrganisationPayload { name?: string slug?: string + contact_name?: string | null + contact_email?: string | null + phone?: string | null + website?: string | null billing_status?: Organisation['billing_status'] settings?: Record email_logo_url?: string | null