*/ private function values(): array { $result = (new ApprovalRate)->for($this->range()); return array_combine($result->labels, $result->series[0]->values); } private function decision(ApprovalStatusEnum $status, string $decidedAt): void { RouteArticle::factory()->create([ 'approval_status' => $status, 'decided_at' => Carbon::parse($decidedAt), ]); } public function test_it_computes_the_approval_share_of_daily_decisions(): void { $this->decision(ApprovalStatusEnum::APPROVED, '2026-07-10 09:00:00'); $this->decision(ApprovalStatusEnum::APPROVED, '2026-07-10 10:00:00'); $this->decision(ApprovalStatusEnum::APPROVED, '2026-07-10 11:00:00'); $this->decision(ApprovalStatusEnum::REJECTED, '2026-07-10 12:00:00'); $this->assertSame(75.0, $this->values()['2026-07-10']); } public function test_it_excludes_pending_articles_from_the_denominator(): void { $this->decision(ApprovalStatusEnum::APPROVED, '2026-07-10 09:00:00'); RouteArticle::factory()->create([ 'approval_status' => ApprovalStatusEnum::PENDING, 'decided_at' => null, 'created_at' => Carbon::parse('2026-07-10 09:00:00'), ]); $this->assertSame(100.0, $this->values()['2026-07-10']); } public function test_it_reports_no_rate_for_a_day_with_no_decisions(): void { $this->decision(ApprovalStatusEnum::APPROVED, '2026-07-10 09:00:00'); $this->assertNull($this->values()['2026-07-11']); } public function test_it_reports_a_zero_rate_when_everything_was_rejected(): void { $this->decision(ApprovalStatusEnum::REJECTED, '2026-07-10 09:00:00'); $this->decision(ApprovalStatusEnum::REJECTED, '2026-07-10 10:00:00'); $this->assertSame(0.0, $this->values()['2026-07-10']); } public function test_it_buckets_by_decision_time_not_creation_time(): void { RouteArticle::factory()->create([ 'approval_status' => ApprovalStatusEnum::APPROVED, 'created_at' => Carbon::parse('2026-07-05 09:00:00'), 'decided_at' => Carbon::parse('2026-07-20 09:00:00'), ]); $values = $this->values(); $this->assertNull($values['2026-07-05']); $this->assertSame(100.0, $values['2026-07-20']); } public function test_it_ignores_articles_decided_before_the_column_existed(): void { RouteArticle::factory()->create([ 'approval_status' => ApprovalStatusEnum::APPROVED, 'created_at' => Carbon::parse('2026-07-10 09:00:00'), 'decided_at' => null, ]); $this->assertNull($this->values()['2026-07-10']); } public function test_it_excludes_decisions_outside_the_range(): void { $this->decision(ApprovalStatusEnum::APPROVED, '2026-06-30 23:59:59'); $this->decision(ApprovalStatusEnum::APPROVED, '2026-08-01 00:00:00'); $this->assertSame( [], array_filter($this->values(), fn (?float $value): bool => $value !== null) ); } public function test_it_reports_a_meaningful_zero_as_data(): void { $this->decision(ApprovalStatusEnum::REJECTED, '2026-07-10 09:00:00'); $this->assertFalse((new ApprovalRate)->for($this->range())->hasNoData()); } public function test_it_counts_every_decided_status_in_the_denominator(): void { $decided = array_filter( ApprovalStatusEnum::cases(), fn (ApprovalStatusEnum $status): bool => $status->isDecided(), ); foreach ($decided as $status) { $this->decision($status, '2026-07-10 09:00:00'); } $this->assertSame( round(100 / count($decided), 1), $this->values()['2026-07-10'], ); } public function test_it_is_a_daily_series_stat(): void { $stat = new ApprovalRate; $this->assertInstanceOf(DailySeriesStat::class, $stat); $this->assertTrue($stat->for(DateRange::preset('all'))->isTooWide()); } }