From 3eb18394dc7c6965c9c3843a7b31804ec9a5495b Mon Sep 17 00:00:00 2001 From: Dat Date: Fri, 10 Jul 2026 16:58:53 +0200 Subject: [PATCH 1/6] Add endpoint for creating a policy acceptance for a specific user --- routes/api.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/routes/api.php b/routes/api.php index f014ffbc..9690d778 100644 --- a/routes/api.php +++ b/routes/api.php @@ -28,6 +28,9 @@ $router->get('auth/login', ['uses' => 'Auth\LoginController@getLogin']); $router->delete('auth/login', ['uses' => 'Auth\LoginController@deleteLogin']); + // policy acceptances + $router->put('policy_acceptances', ['uses' => 'PolicyAcceptanceController@store']); + // user $router->group(['prefix' => 'user'], function () use ($router): void { $router->post('sendVerifyEmail', ['uses' => 'UserVerificationTokenController@createAndSendForUser']); From 99b43cab518833ff7bff36ed31b46115c4bfeb41 Mon Sep 17 00:00:00 2001 From: Dat Date: Fri, 10 Jul 2026 17:07:00 +0200 Subject: [PATCH 2/6] add PolicyAcceptanceController class --- .../PolicyAcceptanceController.php | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 app/Http/Controllers/PolicyAcceptanceController.php diff --git a/app/Http/Controllers/PolicyAcceptanceController.php b/app/Http/Controllers/PolicyAcceptanceController.php new file mode 100644 index 00000000..196e1bc8 --- /dev/null +++ b/app/Http/Controllers/PolicyAcceptanceController.php @@ -0,0 +1,43 @@ +validate([ + 'policy_ids' => ['required', 'array'], + 'policy_ids.*' => ['integer'], + ]); + + $policyIds = $request->input('policy_ids'); + + // Check all policy IDs exist before writing anything + $existingIds = Policy::whereIn('id', $policyIds)->pluck('id')->all(); + $missingIds = array_values(array_diff($policyIds, $existingIds)); + + if (!empty($missingIds)) { + return response()->json([ + 'success' => false, + 'message' => 'Some policy IDs do not exist.', + 'data' => ['missing_policy_ids' => $missingIds], + ], 400); + } + + $userId = $request->user()->id; + + foreach ($policyIds as $policyId) { + // Ignore if the user has already accepted this policy + PolicyAcceptance::firstOrCreate( + ['user_id' => $userId, 'policy_id' => $policyId], + ['accepted_at' => now()], + ); + } + + return response()->json(['success' => true]); + } +} From eb1007657ba6dcb47c9e5328be6b295b0f879b3f Mon Sep 17 00:00:00 2001 From: Dat Date: Mon, 13 Jul 2026 17:33:33 +0200 Subject: [PATCH 3/6] add test cases --- .../Routes/PolicyAcceptanceControllerTest.php | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 tests/Routes/PolicyAcceptanceControllerTest.php diff --git a/tests/Routes/PolicyAcceptanceControllerTest.php b/tests/Routes/PolicyAcceptanceControllerTest.php new file mode 100644 index 00000000..50c5a4c5 --- /dev/null +++ b/tests/Routes/PolicyAcceptanceControllerTest.php @@ -0,0 +1,117 @@ +policy_type = $type; + $policy->active_from = CarbonImmutable::now(); + $policy->content_vue_file = $type . '/version-1.vue'; + $policy->save(); + + return $policy; + } + + public function testUnauthenticatedRequestResponds401(): void { + $this->json('PUT', $this->route) + ->assertStatus(401); + } + + public function testAcceptSinglePolicy(): void { + $user = User::factory()->create(); + $policy = $this->makePolicy(); + + $this->actingAs($user, 'api') + ->json('PUT', $this->route, ['policy_ids' => [$policy->id]]) + ->assertStatus(200) + ->assertJson(['success' => true]); + + $this->assertDatabaseHas('policy_acceptances', [ + 'user_id' => $user->id, + 'policy_id' => $policy->id, + ]); + } + + public function testAcceptMultiplePolicies(): void { + $user = User::factory()->create(); + $termsOfUse = $this->makePolicy('terms-of-use'); + $hostingPolicy = $this->makePolicy('hosting-policy'); + + $this->actingAs($user, 'api') + ->json('PUT', $this->route, ['policy_ids' => [$termsOfUse->id, $hostingPolicy->id]]) + ->assertStatus(200) + ->assertJson(['success' => true]); + + $this->assertDatabaseHas('policy_acceptances', ['user_id' => $user->id, 'policy_id' => $termsOfUse->id]); + $this->assertDatabaseHas('policy_acceptances', ['user_id' => $user->id, 'policy_id' => $hostingPolicy->id]); + } + + public function testAlreadyAcceptedPolicyIsIgnored(): void { + $user = User::factory()->create(); + $policy = $this->makePolicy(); + + PolicyAcceptance::create([ + 'user_id' => $user->id, + 'policy_id' => $policy->id, + 'accepted_at' => now(), + ]); + + $this->actingAs($user, 'api') + ->json('PUT', $this->route, ['policy_ids' => [$policy->id]]) + ->assertStatus(200) + ->assertJson(['success' => true]); + + $this->assertSame(1, PolicyAcceptance::where([ + 'user_id' => $user->id, + 'policy_id' => $policy->id, + ])->count()); + } + + public function testNonExistentPolicyIdReturns400(): void { + $user = User::factory()->create(); + $policy = $this->makePolicy(); + $nonExistentId = 999999; + + $this->actingAs($user, 'api') + ->json('PUT', $this->route, ['policy_ids' => [$policy->id, $nonExistentId]]) + ->assertStatus(400) + ->assertJsonFragment(['success' => false]) + ->assertJsonFragment(['missing_policy_ids' => [$nonExistentId]]); + + // Nothing should have been written + $this->assertDatabaseMissing('policy_acceptances', [ + 'user_id' => $user->id, + 'policy_id' => $policy->id, + ]); + } + + public function testMissingPolicyIdsFieldReturns422(): void { + $user = User::factory()->create(); + + $this->actingAs($user, 'api') + ->json('PUT', $this->route, []) + ->assertStatus(422) + ->assertJsonStructure(['errors' => ['policy_ids']]); + } + + public function testPolicyIdsNotAnArrayReturns422(): void { + $user = User::factory()->create(); + + $this->actingAs($user, 'api') + ->json('PUT', $this->route, ['policy_ids' => 'not-an-array']) + ->assertStatus(422) + ->assertJsonStructure(['errors' => ['policy_ids']]); + } +} From 6aae19d8607d9c94566e1efed19286cbd675f08e Mon Sep 17 00:00:00 2001 From: Dat Date: Tue, 14 Jul 2026 17:56:16 +0200 Subject: [PATCH 4/6] Add tests to check original accepted_at of accepted policy --- .../Routes/PolicyAcceptanceControllerTest.php | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/Routes/PolicyAcceptanceControllerTest.php b/tests/Routes/PolicyAcceptanceControllerTest.php index 50c5a4c5..f2d0c2bc 100644 --- a/tests/Routes/PolicyAcceptanceControllerTest.php +++ b/tests/Routes/PolicyAcceptanceControllerTest.php @@ -5,6 +5,7 @@ use App\Policy; use App\PolicyAcceptance; use App\User; +use Carbon\Carbon; use Carbon\CarbonImmutable; use Illuminate\Foundation\Testing\DatabaseTransactions; use Tests\TestCase; @@ -79,6 +80,30 @@ public function testAlreadyAcceptedPolicyIsIgnored(): void { ])->count()); } + public function testAlreadyAcceptedPolicyKeepsOriginalAcceptedAt(): void { + $user = User::factory()->create(); + $policy = $this->makePolicy(); + + $originalAcceptedAt = CarbonImmutable::create(2026, 7, 1, 10, 0, 0); + Carbon::setTestNow(CarbonImmutable::create(2026, 7, 2, 10, 0, 0)); + + PolicyAcceptance::create([ + 'user_id' => $user->id, + 'policy_id' => $policy->id, + 'accepted_at' => $originalAcceptedAt, + ]); + + $this->actingAs($user, 'api') + ->json('PUT', $this->route, ['policy_ids' => [$policy->id]]) + ->assertStatus(200) + ->assertJson(['success' => true]); + + $this->assertEquals($originalAcceptedAt->toDateTimeString(), PolicyAcceptance::where([ + 'user_id' => $user->id, + 'policy_id' => $policy->id, + ])->first()->accepted_at->toDateTimeString()); + } + public function testNonExistentPolicyIdReturns400(): void { $user = User::factory()->create(); $policy = $this->makePolicy(); From 449163682ed4589daad264f6cf93090d5ef50c2d Mon Sep 17 00:00:00 2001 From: Dat Date: Tue, 14 Jul 2026 18:05:25 +0200 Subject: [PATCH 5/6] Add non-integer policy id test case --- tests/Routes/PolicyAcceptanceControllerTest.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/Routes/PolicyAcceptanceControllerTest.php b/tests/Routes/PolicyAcceptanceControllerTest.php index f2d0c2bc..0033ccf9 100644 --- a/tests/Routes/PolicyAcceptanceControllerTest.php +++ b/tests/Routes/PolicyAcceptanceControllerTest.php @@ -139,4 +139,19 @@ public function testPolicyIdsNotAnArrayReturns422(): void { ->assertStatus(422) ->assertJsonStructure(['errors' => ['policy_ids']]); } + + public function testPolicyIdsContainingNonIntegerReturns422(): void { + $user = User::factory()->create(); + $policy = $this->makePolicy(); + + $this->actingAs($user, 'api') + ->json('PUT', $this->route, ['policy_ids' => [$policy->id, 'abc']]) + ->assertStatus(422) + ->assertJsonStructure(['errors' => ['policy_ids.1']]); + + $this->assertDatabaseMissing('policy_acceptances', [ + 'user_id' => $user->id, + 'policy_id' => $policy->id, + ]); + } } From 80bc9eee38f560acb965ef3f47b3a58d146e22e2 Mon Sep 17 00:00:00 2001 From: Dat Date: Wed, 15 Jul 2026 10:21:40 +0200 Subject: [PATCH 6/6] put endpoint under v1/policy_acceptances instead of root --- routes/api.php | 2 +- tests/Routes/PolicyAcceptanceControllerTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/routes/api.php b/routes/api.php index 9690d778..d4be2c32 100644 --- a/routes/api.php +++ b/routes/api.php @@ -29,7 +29,7 @@ $router->delete('auth/login', ['uses' => 'Auth\LoginController@deleteLogin']); // policy acceptances - $router->put('policy_acceptances', ['uses' => 'PolicyAcceptanceController@store']); + $router->put('v1/policy_acceptances', ['uses' => 'PolicyAcceptanceController@store']); // user $router->group(['prefix' => 'user'], function () use ($router): void { diff --git a/tests/Routes/PolicyAcceptanceControllerTest.php b/tests/Routes/PolicyAcceptanceControllerTest.php index 0033ccf9..243e0962 100644 --- a/tests/Routes/PolicyAcceptanceControllerTest.php +++ b/tests/Routes/PolicyAcceptanceControllerTest.php @@ -11,7 +11,7 @@ use Tests\TestCase; class PolicyAcceptanceControllerTest extends TestCase { - protected $route = 'policy_acceptances'; + protected $route = 'v1/policy_acceptances'; use DatabaseTransactions;