seed(RoleSeeder::class); $this->orgA = Organisation::factory()->create(); $this->orgB = Organisation::factory()->create(); $schemaA = FormSchema::factory()->create(['organisation_id' => $this->orgA->id]); $schemaB = FormSchema::factory()->create(['organisation_id' => $this->orgB->id]); $this->submissionA = FormSubmission::factory()->create([ 'form_schema_id' => $schemaA->id, 'organisation_id' => $this->orgA->id, ]); $this->submissionB = FormSubmission::factory()->create([ 'form_schema_id' => $schemaB->id, 'organisation_id' => $this->orgB->id, ]); $this->orgAdminA = User::factory()->create(); $this->orgA->users()->attach($this->orgAdminA, ['role' => 'org_admin']); $this->orgAdminB = User::factory()->create(); $this->orgB->users()->attach($this->orgAdminB, ['role' => 'org_admin']); $this->superAdmin = User::factory()->create(); $this->superAdmin->assignRole('super_admin'); } public function test_org_kpis_counts_open_resolved_dismissed_and_total_submissions(): void { FormSubmissionActionFailure::factory()->for($this->submissionA, 'submission')->create(); FormSubmissionActionFailure::factory()->for($this->submissionA, 'submission')->create(); FormSubmissionActionFailure::factory()->for($this->submissionA, 'submission') ->create(['resolved_at' => now()->subDays(5)]); FormSubmissionActionFailure::factory()->for($this->submissionA, 'submission') ->create(['dismissed_at' => now()->subDays(10), 'dismissed_reason_type' => 'schema_deleted']); // Outside-30d window — must NOT count. FormSubmissionActionFailure::factory()->for($this->submissionA, 'submission') ->create(['resolved_at' => now()->subDays(45)]); // Other tenant — must NOT count. FormSubmissionActionFailure::factory()->for($this->submissionB, 'submission')->create(); Sanctum::actingAs($this->orgAdminA); $response = $this->getJson("/api/v1/organisations/{$this->orgA->id}/form-failures/kpis")->assertOk(); $response->assertJsonPath('data.open', 2); $response->assertJsonPath('data.resolved_30d', 1); $response->assertJsonPath('data.dismissed_30d', 1); $response->assertJsonPath('data.total_submissions', 1); } public function test_org_kpis_cross_tenant_returns_404(): void { FormSubmissionActionFailure::factory()->for($this->submissionA, 'submission')->create(); Sanctum::actingAs($this->orgAdminB); $this->getJson("/api/v1/organisations/{$this->orgA->id}/form-failures/kpis") ->assertStatus(404); } public function test_org_kpis_unauthenticated_returns_401(): void { $this->getJson("/api/v1/organisations/{$this->orgA->id}/form-failures/kpis") ->assertStatus(401); } public function test_platform_kpis_aggregates_across_all_orgs(): void { FormSubmissionActionFailure::factory()->for($this->submissionA, 'submission')->create(); FormSubmissionActionFailure::factory()->for($this->submissionB, 'submission')->create(); FormSubmissionActionFailure::factory()->for($this->submissionA, 'submission') ->create(['resolved_at' => now()->subDays(2)]); Sanctum::actingAs($this->superAdmin); $response = $this->getJson('/api/v1/admin/form-failures/kpis')->assertOk(); $response->assertJsonPath('data.open', 2); $response->assertJsonPath('data.resolved_30d', 1); $response->assertJsonPath('data.dismissed_30d', 0); $response->assertJsonPath('data.total_submissions', 2); } public function test_platform_kpis_org_admin_returns_403(): void { Sanctum::actingAs($this->orgAdminA); $this->getJson('/api/v1/admin/form-failures/kpis') ->assertStatus(403); } public function test_platform_kpis_unauthenticated_returns_401(): void { $this->getJson('/api/v1/admin/form-failures/kpis')->assertStatus(401); } public function test_kpis_with_no_data_returns_zeros(): void { $orgC = Organisation::factory()->create(); $orgAdminC = User::factory()->create(); $orgC->users()->attach($orgAdminC, ['role' => 'org_admin']); Sanctum::actingAs($orgAdminC); $response = $this->getJson("/api/v1/organisations/{$orgC->id}/form-failures/kpis")->assertOk(); $response->assertJsonPath('data.open', 0); $response->assertJsonPath('data.resolved_30d', 0); $response->assertJsonPath('data.dismissed_30d', 0); $response->assertJsonPath('data.total_submissions', 0); } }