150 lines
4.7 KiB
PHP
150 lines
4.7 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
namespace Tests\Unit\Dashboard\Stats;
|
||
|
|
|
||
|
|
use App\Dashboard\Stats\ApprovalRate;
|
||
|
|
use App\Dashboard\Stats\DailySeriesStat;
|
||
|
|
use App\Enums\ApprovalStatusEnum;
|
||
|
|
use App\Models\RouteArticle;
|
||
|
|
use App\Support\DateRange;
|
||
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||
|
|
use Illuminate\Support\Carbon;
|
||
|
|
use Tests\TestCase;
|
||
|
|
|
||
|
|
class ApprovalRateTest extends TestCase
|
||
|
|
{
|
||
|
|
use RefreshDatabase;
|
||
|
|
|
||
|
|
private function range(): DateRange
|
||
|
|
{
|
||
|
|
return new DateRange(
|
||
|
|
Carbon::parse('2026-07-01 00:00:00'),
|
||
|
|
Carbon::parse('2026-07-31 23:59:59'),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @return array<string, float|null>
|
||
|
|
*/
|
||
|
|
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());
|
||
|
|
}
|
||
|
|
}
|