From 3c31e8bd4746fab1b5c60579935fab21501b87b6 Mon Sep 17 00:00:00 2001 From: Dat Date: Fri, 17 Jul 2026 16:54:34 +0200 Subject: [PATCH 01/12] Added controller method that returns only current policies not accepted by user Bug: T432337 --- app/Http/Controllers/PoliciesController.php | 22 +++++++++++++++++++++ routes/api.php | 1 + 2 files changed, 23 insertions(+) diff --git a/app/Http/Controllers/PoliciesController.php b/app/Http/Controllers/PoliciesController.php index 1a3dfd65..2d67ee6c 100644 --- a/app/Http/Controllers/PoliciesController.php +++ b/app/Http/Controllers/PoliciesController.php @@ -4,7 +4,9 @@ use App\Http\Resources\PoliciesCollection; use App\Policy; +use App\PolicyAcceptance; use Carbon\CarbonImmutable; +use Illuminate\Http\Request; class PoliciesController extends Controller { public function getCurrentPolicies(): PoliciesCollection { @@ -20,4 +22,24 @@ public function getCurrentPolicies(): PoliciesCollection { return new PoliciesCollection($currentPolicies); } + + public function getMissingPolicies(Request $request): PoliciesCollection { + $now = CarbonImmutable::now(); + + // This works based on the assumption that the latest policy has the highest id given that id is AUTO_INCREMENT + $latestPolicyIds = Policy::where('active_from', '<', $now) + ->selectRaw('MAX(id) as id') + ->groupBy('policy_type') + ->pluck('id'); + + $acceptedPolicyIds = PolicyAcceptance::where('user_id', $request->user()->id) + ->whereIn('policy_id', $latestPolicyIds) + ->pluck('policy_id'); + + $missingPolicies = Policy::whereIn('id', $latestPolicyIds) + ->whereNotIn('id', $acceptedPolicyIds) + ->get(); + + return new PoliciesCollection($missingPolicies); + } } diff --git a/routes/api.php b/routes/api.php index 125bc994..9f8c4acb 100644 --- a/routes/api.php +++ b/routes/api.php @@ -28,6 +28,7 @@ $router->group(['middleware' => ['auth:api']], function () use ($router): void { $router->get('auth/login', ['uses' => 'Auth\LoginController@getLogin']); $router->delete('auth/login', ['uses' => 'Auth\LoginController@deleteLogin']); + $router->get('v1/policies/missing', ['uses' => 'PoliciesController@getMissingPolicies']); // policy acceptances $router->put('v1/policy_acceptances', ['uses' => 'PolicyAcceptanceController@store']); From 191fac253f2dd4d7fdedb84081f938be0d3cfc79 Mon Sep 17 00:00:00 2001 From: Dat Date: Mon, 20 Jul 2026 15:38:20 +0200 Subject: [PATCH 02/12] Add test class --- tests/Routes/PoliciesControllerTest.php | 136 ++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 tests/Routes/PoliciesControllerTest.php diff --git a/tests/Routes/PoliciesControllerTest.php b/tests/Routes/PoliciesControllerTest.php new file mode 100644 index 00000000..19fbfd9e --- /dev/null +++ b/tests/Routes/PoliciesControllerTest.php @@ -0,0 +1,136 @@ + $type, + 'active_from' => $activeFrom, + 'content_vue_file' => $content, + ]); + } + + public function testMissingPoliciesRequiresAuthentication(): void { + $this->json('GET', $this->missingPoliciesRoute) + ->assertStatus(401); + } + + public function testMissingPoliciesReturnsOnlyLatestCurrentPolicyPerType(): void { + $user = User::factory()->create(); + $now = CarbonImmutable::now(); + + $olderTerms = $this->createPolicy('terms-of-use', $now->subDays(10), 'terms-of-use/version-1.vue'); + $latestTerms = $this->createPolicy('terms-of-use', $now->subDays(1), 'terms-of-use/version-2.vue'); + $this->createPolicy('terms-of-use', $now->addDays(1), 'terms-of-use/version-3.vue'); + + $olderHosting = $this->createPolicy('hosting-policy', $now->subDays(20), 'hosting-policy/version-1.vue'); + $latestHosting = $this->createPolicy('hosting-policy', $now->subDays(2), 'hosting-policy/version-2.vue'); + + $response = $this->actingAs($user, 'api') + ->json('GET', $this->missingPoliciesRoute) + ->assertStatus(200) + ->assertJsonStructure([ + 'items' => [ + ['metadata' => ['policy_id', 'type', 'active_from', 'content_vue_file']], + ], + ]); + + $items = collect($response->json('items')); + + $this->assertCount(2, $items); + + $this->assertTrue($items->contains(function (array $item) use ($latestTerms): bool { + return $item['metadata']['policy_id'] === $latestTerms->id + && $item['metadata']['type'] === 'terms-of-use' + && $item['metadata']['active_from'] === $latestTerms->active_from->format('Y-m-d') + && $item['metadata']['content_vue_file'] === 'terms-of-use/version-2.vue'; + })); + + $this->assertTrue($items->contains(function (array $item) use ($latestHosting): bool { + return $item['metadata']['policy_id'] === $latestHosting->id + && $item['metadata']['type'] === 'hosting-policy' + && $item['metadata']['active_from'] === $latestHosting->active_from->format('Y-m-d') + && $item['metadata']['content_vue_file'] === 'hosting-policy/version-2.vue'; + })); + + $this->assertFalse($items->contains(function (array $item) use ($olderTerms): bool { + return $item['metadata']['policy_id'] === $olderTerms->id; + })); + + $this->assertFalse($items->contains(function (array $item) use ($olderHosting): bool { + return $item['metadata']['policy_id'] === $olderHosting->id; + })); + + $this->assertCount(1, $items->where('metadata.type', 'terms-of-use')); + $this->assertCount(1, $items->where('metadata.type', 'hosting-policy')); + } + + public function testMissingPoliciesExcludesAlreadyAcceptedPoliciesForCurrentUser(): void { + $user = User::factory()->create(); + $anotherUser = User::factory()->create(); + $now = CarbonImmutable::now(); + + $terms = $this->createPolicy('terms-of-use', $now->subDays(1), 'terms-of-use/version-2.vue'); + $hosting = $this->createPolicy('hosting-policy', $now->subDays(1), 'hosting-policy/version-1.vue'); + + PolicyAcceptance::create([ + 'user_id' => $user->id, + 'policy_id' => $terms->id, + 'accepted_at' => $now, + ]); + + // Acceptance by another user must not affect current user response + PolicyAcceptance::create([ + 'user_id' => $anotherUser->id, + 'policy_id' => $hosting->id, + 'accepted_at' => $now, + ]); + + $response = $this->actingAs($user, 'api') + ->json('GET', $this->missingPoliciesRoute) + ->assertStatus(200); + + $items = collect($response->json('items')); + + $this->assertCount(1, $items); + $this->assertSame($hosting->id, $items->first()['metadata']['policy_id']); + $this->assertSame('hosting-policy', $items->first()['metadata']['type']); + } + + public function testMissingPoliciesReturnsEmptyListWhenAllCurrentPoliciesAccepted(): void { + $user = User::factory()->create(); + $now = CarbonImmutable::now(); + + $terms = $this->createPolicy('terms-of-use', $now->subDays(1), 'terms-of-use/version-2.vue'); + $hosting = $this->createPolicy('hosting-policy', $now->subDays(1), 'hosting-policy/version-1.vue'); + + PolicyAcceptance::create([ + 'user_id' => $user->id, + 'policy_id' => $terms->id, + 'accepted_at' => $now, + ]); + + PolicyAcceptance::create([ + 'user_id' => $user->id, + 'policy_id' => $hosting->id, + 'accepted_at' => $now, + ]); + + $this->actingAs($user, 'api') + ->json('GET', $this->missingPoliciesRoute) + ->assertStatus(200) + ->assertJson(['items' => []]); + } +} From ea4924f0ed6da72fffa9fd0307cf14ce0be47ed2 Mon Sep 17 00:00:00 2001 From: Dat Date: Mon, 20 Jul 2026 16:22:24 +0200 Subject: [PATCH 03/12] change query condition and variables' names --- app/Http/Controllers/PoliciesController.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/Http/Controllers/PoliciesController.php b/app/Http/Controllers/PoliciesController.php index 2d67ee6c..17c0558e 100644 --- a/app/Http/Controllers/PoliciesController.php +++ b/app/Http/Controllers/PoliciesController.php @@ -26,17 +26,17 @@ public function getCurrentPolicies(): PoliciesCollection { public function getMissingPolicies(Request $request): PoliciesCollection { $now = CarbonImmutable::now(); - // This works based on the assumption that the latest policy has the highest id given that id is AUTO_INCREMENT - $latestPolicyIds = Policy::where('active_from', '<', $now) + // This works based on the assumption that the active policy has the highest id given that id is AUTO_INCREMENT + $activePolicyIds = Policy::where('active_from', '<=', $now) ->selectRaw('MAX(id) as id') ->groupBy('policy_type') ->pluck('id'); $acceptedPolicyIds = PolicyAcceptance::where('user_id', $request->user()->id) - ->whereIn('policy_id', $latestPolicyIds) + ->whereIn('policy_id', $activePolicyIds) ->pluck('policy_id'); - $missingPolicies = Policy::whereIn('id', $latestPolicyIds) + $missingPolicies = Policy::whereIn('id', $activePolicyIds) ->whereNotIn('id', $acceptedPolicyIds) ->get(); From fe2a622be73218ad1a5b7ca4c0899c44203e1fa3 Mon Sep 17 00:00:00 2001 From: Dat Date: Mon, 20 Jul 2026 16:23:18 +0200 Subject: [PATCH 04/12] verify that future policies are excluded --- tests/Routes/PoliciesControllerTest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/Routes/PoliciesControllerTest.php b/tests/Routes/PoliciesControllerTest.php index 19fbfd9e..631fb37b 100644 --- a/tests/Routes/PoliciesControllerTest.php +++ b/tests/Routes/PoliciesControllerTest.php @@ -31,6 +31,9 @@ public function testMissingPoliciesReturnsOnlyLatestCurrentPolicyPerType(): void $user = User::factory()->create(); $now = CarbonImmutable::now(); + // Future policy (should be ignored because it is not active yet) + $this->createPolicy('terms-of-use', $now->addDays(10), 'terms-of-use/version-future.vue'); + $olderTerms = $this->createPolicy('terms-of-use', $now->subDays(10), 'terms-of-use/version-1.vue'); $latestTerms = $this->createPolicy('terms-of-use', $now->subDays(1), 'terms-of-use/version-2.vue'); $this->createPolicy('terms-of-use', $now->addDays(1), 'terms-of-use/version-3.vue'); From 6ef72d91f7f68e8b92b3809eac5489f3098a8b21 Mon Sep 17 00:00:00 2001 From: Dat Date: Mon, 20 Jul 2026 16:45:41 +0200 Subject: [PATCH 05/12] remove unrelated comment --- app/Http/Controllers/PoliciesController.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/Http/Controllers/PoliciesController.php b/app/Http/Controllers/PoliciesController.php index 17c0558e..331dc176 100644 --- a/app/Http/Controllers/PoliciesController.php +++ b/app/Http/Controllers/PoliciesController.php @@ -26,7 +26,6 @@ public function getCurrentPolicies(): PoliciesCollection { public function getMissingPolicies(Request $request): PoliciesCollection { $now = CarbonImmutable::now(); - // This works based on the assumption that the active policy has the highest id given that id is AUTO_INCREMENT $activePolicyIds = Policy::where('active_from', '<=', $now) ->selectRaw('MAX(id) as id') ->groupBy('policy_type') From 779c6d89fea4a1fd1c8c4433c28555b42ba783ec Mon Sep 17 00:00:00 2001 From: Dat Date: Fri, 17 Jul 2026 16:54:34 +0200 Subject: [PATCH 06/12] Added controller method that returns only current policies not accepted by user Bug: T432337 --- app/Http/Controllers/PoliciesController.php | 22 +++++++++++++++++++++ routes/api.php | 1 + 2 files changed, 23 insertions(+) diff --git a/app/Http/Controllers/PoliciesController.php b/app/Http/Controllers/PoliciesController.php index 1a3dfd65..2d67ee6c 100644 --- a/app/Http/Controllers/PoliciesController.php +++ b/app/Http/Controllers/PoliciesController.php @@ -4,7 +4,9 @@ use App\Http\Resources\PoliciesCollection; use App\Policy; +use App\PolicyAcceptance; use Carbon\CarbonImmutable; +use Illuminate\Http\Request; class PoliciesController extends Controller { public function getCurrentPolicies(): PoliciesCollection { @@ -20,4 +22,24 @@ public function getCurrentPolicies(): PoliciesCollection { return new PoliciesCollection($currentPolicies); } + + public function getMissingPolicies(Request $request): PoliciesCollection { + $now = CarbonImmutable::now(); + + // This works based on the assumption that the latest policy has the highest id given that id is AUTO_INCREMENT + $latestPolicyIds = Policy::where('active_from', '<', $now) + ->selectRaw('MAX(id) as id') + ->groupBy('policy_type') + ->pluck('id'); + + $acceptedPolicyIds = PolicyAcceptance::where('user_id', $request->user()->id) + ->whereIn('policy_id', $latestPolicyIds) + ->pluck('policy_id'); + + $missingPolicies = Policy::whereIn('id', $latestPolicyIds) + ->whereNotIn('id', $acceptedPolicyIds) + ->get(); + + return new PoliciesCollection($missingPolicies); + } } diff --git a/routes/api.php b/routes/api.php index d6dc19fa..c7b306f7 100644 --- a/routes/api.php +++ b/routes/api.php @@ -29,6 +29,7 @@ $router->group(['middleware' => ['auth:api']], function () use ($router): void { $router->get('auth/login', ['uses' => 'Auth\LoginController@getLogin']); $router->delete('auth/login', ['uses' => 'Auth\LoginController@deleteLogin']); + $router->get('v1/policies/missing', ['uses' => 'PoliciesController@getMissingPolicies']); // policy acceptances $router->put('v1/policy_acceptances', ['uses' => 'PolicyAcceptanceController@store']); From c7780da28741392b8a2b0b3d820b956bce129309 Mon Sep 17 00:00:00 2001 From: Dat Date: Mon, 20 Jul 2026 15:38:20 +0200 Subject: [PATCH 07/12] Add test class --- tests/Routes/PoliciesControllerTest.php | 136 ++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 tests/Routes/PoliciesControllerTest.php diff --git a/tests/Routes/PoliciesControllerTest.php b/tests/Routes/PoliciesControllerTest.php new file mode 100644 index 00000000..19fbfd9e --- /dev/null +++ b/tests/Routes/PoliciesControllerTest.php @@ -0,0 +1,136 @@ + $type, + 'active_from' => $activeFrom, + 'content_vue_file' => $content, + ]); + } + + public function testMissingPoliciesRequiresAuthentication(): void { + $this->json('GET', $this->missingPoliciesRoute) + ->assertStatus(401); + } + + public function testMissingPoliciesReturnsOnlyLatestCurrentPolicyPerType(): void { + $user = User::factory()->create(); + $now = CarbonImmutable::now(); + + $olderTerms = $this->createPolicy('terms-of-use', $now->subDays(10), 'terms-of-use/version-1.vue'); + $latestTerms = $this->createPolicy('terms-of-use', $now->subDays(1), 'terms-of-use/version-2.vue'); + $this->createPolicy('terms-of-use', $now->addDays(1), 'terms-of-use/version-3.vue'); + + $olderHosting = $this->createPolicy('hosting-policy', $now->subDays(20), 'hosting-policy/version-1.vue'); + $latestHosting = $this->createPolicy('hosting-policy', $now->subDays(2), 'hosting-policy/version-2.vue'); + + $response = $this->actingAs($user, 'api') + ->json('GET', $this->missingPoliciesRoute) + ->assertStatus(200) + ->assertJsonStructure([ + 'items' => [ + ['metadata' => ['policy_id', 'type', 'active_from', 'content_vue_file']], + ], + ]); + + $items = collect($response->json('items')); + + $this->assertCount(2, $items); + + $this->assertTrue($items->contains(function (array $item) use ($latestTerms): bool { + return $item['metadata']['policy_id'] === $latestTerms->id + && $item['metadata']['type'] === 'terms-of-use' + && $item['metadata']['active_from'] === $latestTerms->active_from->format('Y-m-d') + && $item['metadata']['content_vue_file'] === 'terms-of-use/version-2.vue'; + })); + + $this->assertTrue($items->contains(function (array $item) use ($latestHosting): bool { + return $item['metadata']['policy_id'] === $latestHosting->id + && $item['metadata']['type'] === 'hosting-policy' + && $item['metadata']['active_from'] === $latestHosting->active_from->format('Y-m-d') + && $item['metadata']['content_vue_file'] === 'hosting-policy/version-2.vue'; + })); + + $this->assertFalse($items->contains(function (array $item) use ($olderTerms): bool { + return $item['metadata']['policy_id'] === $olderTerms->id; + })); + + $this->assertFalse($items->contains(function (array $item) use ($olderHosting): bool { + return $item['metadata']['policy_id'] === $olderHosting->id; + })); + + $this->assertCount(1, $items->where('metadata.type', 'terms-of-use')); + $this->assertCount(1, $items->where('metadata.type', 'hosting-policy')); + } + + public function testMissingPoliciesExcludesAlreadyAcceptedPoliciesForCurrentUser(): void { + $user = User::factory()->create(); + $anotherUser = User::factory()->create(); + $now = CarbonImmutable::now(); + + $terms = $this->createPolicy('terms-of-use', $now->subDays(1), 'terms-of-use/version-2.vue'); + $hosting = $this->createPolicy('hosting-policy', $now->subDays(1), 'hosting-policy/version-1.vue'); + + PolicyAcceptance::create([ + 'user_id' => $user->id, + 'policy_id' => $terms->id, + 'accepted_at' => $now, + ]); + + // Acceptance by another user must not affect current user response + PolicyAcceptance::create([ + 'user_id' => $anotherUser->id, + 'policy_id' => $hosting->id, + 'accepted_at' => $now, + ]); + + $response = $this->actingAs($user, 'api') + ->json('GET', $this->missingPoliciesRoute) + ->assertStatus(200); + + $items = collect($response->json('items')); + + $this->assertCount(1, $items); + $this->assertSame($hosting->id, $items->first()['metadata']['policy_id']); + $this->assertSame('hosting-policy', $items->first()['metadata']['type']); + } + + public function testMissingPoliciesReturnsEmptyListWhenAllCurrentPoliciesAccepted(): void { + $user = User::factory()->create(); + $now = CarbonImmutable::now(); + + $terms = $this->createPolicy('terms-of-use', $now->subDays(1), 'terms-of-use/version-2.vue'); + $hosting = $this->createPolicy('hosting-policy', $now->subDays(1), 'hosting-policy/version-1.vue'); + + PolicyAcceptance::create([ + 'user_id' => $user->id, + 'policy_id' => $terms->id, + 'accepted_at' => $now, + ]); + + PolicyAcceptance::create([ + 'user_id' => $user->id, + 'policy_id' => $hosting->id, + 'accepted_at' => $now, + ]); + + $this->actingAs($user, 'api') + ->json('GET', $this->missingPoliciesRoute) + ->assertStatus(200) + ->assertJson(['items' => []]); + } +} From 3c96fd5f964985f12e189ecee1019efb6efcb9ca Mon Sep 17 00:00:00 2001 From: Dat Date: Mon, 20 Jul 2026 16:22:24 +0200 Subject: [PATCH 08/12] change query condition and variables' names --- app/Http/Controllers/PoliciesController.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/Http/Controllers/PoliciesController.php b/app/Http/Controllers/PoliciesController.php index 2d67ee6c..17c0558e 100644 --- a/app/Http/Controllers/PoliciesController.php +++ b/app/Http/Controllers/PoliciesController.php @@ -26,17 +26,17 @@ public function getCurrentPolicies(): PoliciesCollection { public function getMissingPolicies(Request $request): PoliciesCollection { $now = CarbonImmutable::now(); - // This works based on the assumption that the latest policy has the highest id given that id is AUTO_INCREMENT - $latestPolicyIds = Policy::where('active_from', '<', $now) + // This works based on the assumption that the active policy has the highest id given that id is AUTO_INCREMENT + $activePolicyIds = Policy::where('active_from', '<=', $now) ->selectRaw('MAX(id) as id') ->groupBy('policy_type') ->pluck('id'); $acceptedPolicyIds = PolicyAcceptance::where('user_id', $request->user()->id) - ->whereIn('policy_id', $latestPolicyIds) + ->whereIn('policy_id', $activePolicyIds) ->pluck('policy_id'); - $missingPolicies = Policy::whereIn('id', $latestPolicyIds) + $missingPolicies = Policy::whereIn('id', $activePolicyIds) ->whereNotIn('id', $acceptedPolicyIds) ->get(); From 0bacc5cccd9716a10f96d539647ec51a3a916fb3 Mon Sep 17 00:00:00 2001 From: Dat Date: Mon, 20 Jul 2026 16:23:18 +0200 Subject: [PATCH 09/12] verify that future policies are excluded --- tests/Routes/PoliciesControllerTest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/Routes/PoliciesControllerTest.php b/tests/Routes/PoliciesControllerTest.php index 19fbfd9e..631fb37b 100644 --- a/tests/Routes/PoliciesControllerTest.php +++ b/tests/Routes/PoliciesControllerTest.php @@ -31,6 +31,9 @@ public function testMissingPoliciesReturnsOnlyLatestCurrentPolicyPerType(): void $user = User::factory()->create(); $now = CarbonImmutable::now(); + // Future policy (should be ignored because it is not active yet) + $this->createPolicy('terms-of-use', $now->addDays(10), 'terms-of-use/version-future.vue'); + $olderTerms = $this->createPolicy('terms-of-use', $now->subDays(10), 'terms-of-use/version-1.vue'); $latestTerms = $this->createPolicy('terms-of-use', $now->subDays(1), 'terms-of-use/version-2.vue'); $this->createPolicy('terms-of-use', $now->addDays(1), 'terms-of-use/version-3.vue'); From f2a65e6234b47b70e6cf25d606c7c0628d63228f Mon Sep 17 00:00:00 2001 From: Dat Date: Mon, 20 Jul 2026 16:45:41 +0200 Subject: [PATCH 10/12] remove unrelated comment --- app/Http/Controllers/PoliciesController.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/Http/Controllers/PoliciesController.php b/app/Http/Controllers/PoliciesController.php index 17c0558e..331dc176 100644 --- a/app/Http/Controllers/PoliciesController.php +++ b/app/Http/Controllers/PoliciesController.php @@ -26,7 +26,6 @@ public function getCurrentPolicies(): PoliciesCollection { public function getMissingPolicies(Request $request): PoliciesCollection { $now = CarbonImmutable::now(); - // This works based on the assumption that the active policy has the highest id given that id is AUTO_INCREMENT $activePolicyIds = Policy::where('active_from', '<=', $now) ->selectRaw('MAX(id) as id') ->groupBy('policy_type') From 7b950eb4519ff5796d8ed9c1954cb1d84769f727 Mon Sep 17 00:00:00 2001 From: Dat Date: Tue, 21 Jul 2026 17:22:42 +0200 Subject: [PATCH 11/12] add getLatestPolicies() function --- app/Http/Controllers/PoliciesController.php | 25 ++++++++++++--------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/app/Http/Controllers/PoliciesController.php b/app/Http/Controllers/PoliciesController.php index 331dc176..48c28632 100644 --- a/app/Http/Controllers/PoliciesController.php +++ b/app/Http/Controllers/PoliciesController.php @@ -13,10 +13,7 @@ public function getCurrentPolicies(): PoliciesCollection { $now = CarbonImmutable::now(); // This works based on the assumption that the latest policy has the highest id given that id is AUTO_INCREMENT - $latestPolicyIds = Policy::where('active_from', '<', $now) - ->selectRaw('MAX(id) as id') - ->groupBy('policy_type') - ->pluck('id'); + $latestPolicyIds = $this->getLatestPolicies(); $currentPolicies = Policy::whereIn('id', $latestPolicyIds)->get(); @@ -26,19 +23,27 @@ public function getCurrentPolicies(): PoliciesCollection { public function getMissingPolicies(Request $request): PoliciesCollection { $now = CarbonImmutable::now(); - $activePolicyIds = Policy::where('active_from', '<=', $now) - ->selectRaw('MAX(id) as id') - ->groupBy('policy_type') - ->pluck('id'); + $activePolicyIds = $this->getLatestPolicies(); - $acceptedPolicyIds = PolicyAcceptance::where('user_id', $request->user()->id) + $userAcceptedPolicyIds = PolicyAcceptance::where('user_id', $request->user()->id) ->whereIn('policy_id', $activePolicyIds) ->pluck('policy_id'); $missingPolicies = Policy::whereIn('id', $activePolicyIds) - ->whereNotIn('id', $acceptedPolicyIds) + ->whereNotIn('id', $userAcceptedPolicyIds) ->get(); return new PoliciesCollection($missingPolicies); } + + private function getLatestPolicies() { + $now = CarbonImmutable::now(); + + $latestPolicyIds = Policy::where('active_from', '<', $now) + ->selectRaw('MAX(id) as id') + ->groupBy('policy_type') + ->pluck('id'); + + return $latestPolicyIds; + } } From cb5470761d6d94cd9c3b1066187af07e642542dc Mon Sep 17 00:00:00 2001 From: Dat Date: Tue, 21 Jul 2026 17:31:49 +0200 Subject: [PATCH 12/12] merge PoliciesControllerTest class into one file --- .../Controllers/PoliciesControllerTest.php | 60 ------------------- tests/Routes/PoliciesControllerTest.php | 41 +++++++++++++ 2 files changed, 41 insertions(+), 60 deletions(-) delete mode 100644 tests/Http/Controllers/PoliciesControllerTest.php diff --git a/tests/Http/Controllers/PoliciesControllerTest.php b/tests/Http/Controllers/PoliciesControllerTest.php deleted file mode 100644 index 7c9e030b..00000000 --- a/tests/Http/Controllers/PoliciesControllerTest.php +++ /dev/null @@ -1,60 +0,0 @@ -create([ - 'policy_type' => 'terms-of-use', - 'active_from' => $currentTime->addDay(), - ]); - // Old policy - Policy::factory()->create([ - 'policy_type' => 'hosting-policy', - 'active_from' => $currentTime->subMonth(), - ]); - // Active policies - $latestActiveToUPolicy = Policy::factory()->create([ - 'policy_type' => 'terms-of-use', - 'active_from' => $currentTime->subMonth(), - ]); - $latestActiveHostingPolicy = Policy::factory()->create([ - 'policy_type' => 'hosting-policy', - 'active_from' => $currentTime->subWeek(), - ]); - - $response = $this->getJson('/v1/policies/current'); - - $response->assertOk(); - $response->assertJsonStructure([ - 'items' => [ - '*' => [ - 'metadata' => [ - 'policy_id', - 'active_from', - 'content_vue_file', - 'type', - ], - ], - ], - ]); - - $response->assertJsonFragment([ - 'policy_id' => $latestActiveToUPolicy->id, - 'active_from' => $latestActiveToUPolicy->active_from->format('Y-m-d'), - ]); - $response->assertJsonFragment([ - 'policy_id' => $latestActiveHostingPolicy->id, - 'active_from' => $latestActiveHostingPolicy->active_from->format('Y-m-d'), - ]); - } -} diff --git a/tests/Routes/PoliciesControllerTest.php b/tests/Routes/PoliciesControllerTest.php index 631fb37b..9eb6f348 100644 --- a/tests/Routes/PoliciesControllerTest.php +++ b/tests/Routes/PoliciesControllerTest.php @@ -12,6 +12,8 @@ class PoliciesControllerTest extends TestCase { use DatabaseTransactions; + private string $currentPoliciesRoute = 'v1/policies/current'; + private string $missingPoliciesRoute = 'v1/policies/missing'; private function createPolicy(string $type, CarbonImmutable $activeFrom, string $content): Policy { @@ -27,6 +29,45 @@ public function testMissingPoliciesRequiresAuthentication(): void { ->assertStatus(401); } + public function testGetCurrentPolicies(): void { + $now = CarbonImmutable::now(); + + // Future policy + $this->createPolicy('terms-of-use', $now->addDay(), 'terms-of-use/version-future.vue'); + + // Older active policy of the same type should be excluded + $this->createPolicy('hosting-policy', $now->subMonths(2), 'hosting-policy/version-1.vue'); + + // Active policies + $latestActiveToUPolicy = $this->createPolicy('terms-of-use', $now->subMonth(), 'terms-of-use/version-2.vue'); + $latestActiveHostingPolicy = $this->createPolicy('hosting-policy', $now->subWeek(), 'hosting-policy/version-2.vue'); + + $response = $this->json('GET', $this->currentPoliciesRoute); + + $response->assertOk(); + $response->assertJsonStructure([ + 'items' => [ + '*' => [ + 'metadata' => [ + 'policy_id', + 'active_from', + 'content_vue_file', + 'type', + ], + ], + ], + ]); + + $response->assertJsonFragment([ + 'policy_id' => $latestActiveToUPolicy->id, + 'active_from' => $latestActiveToUPolicy->active_from->format('Y-m-d'), + ]); + $response->assertJsonFragment([ + 'policy_id' => $latestActiveHostingPolicy->id, + 'active_from' => $latestActiveHostingPolicy->active_from->format('Y-m-d'), + ]); + } + public function testMissingPoliciesReturnsOnlyLatestCurrentPolicyPerType(): void { $user = User::factory()->create(); $now = CarbonImmutable::now();