libra/tests/Feature/Admin/MonthlyReportTest.php

418 lines
13 KiB
PHP

<?php
use App\Models\Consultation;
use App\Models\Post;
use App\Models\Timeline;
use App\Models\TimelineUpdate;
use App\Models\User;
use App\Services\MonthlyReportService;
use Livewire\Volt\Volt;
beforeEach(function () {
$this->admin = User::factory()->admin()->create();
});
// ===========================================
// Access Tests
// ===========================================
test('admin can access monthly report page', function () {
$this->actingAs($this->admin)
->get(route('admin.reports.monthly'))
->assertOk()
->assertSee(__('report.monthly_report'));
});
test('non-admin cannot access monthly report page', function () {
$client = User::factory()->individual()->create();
$this->actingAs($client)
->get(route('admin.reports.monthly'))
->assertForbidden();
});
test('unauthenticated user cannot access monthly report page', function () {
$this->get(route('admin.reports.monthly'))
->assertRedirect(route('login'));
});
// ===========================================
// Component Tests
// ===========================================
test('monthly report component mounts with previous month as default', function () {
$this->actingAs($this->admin);
$previousMonth = now()->subMonth();
Volt::test('admin.reports.monthly-report')
->assertSet('selectedPeriod', $previousMonth->format('Y-m'));
});
test('available months shows only last 12 months', function () {
$this->actingAs($this->admin);
$component = Volt::test('admin.reports.monthly-report');
$availableMonths = $component->get('availableMonths');
expect(count($availableMonths))->toBe(12);
});
test('available months does not include current month', function () {
$this->actingAs($this->admin);
$component = Volt::test('admin.reports.monthly-report');
$availableMonths = $component->get('availableMonths');
$currentMonth = now()->format('Y-m');
$values = array_column($availableMonths, 'value');
expect($values)->not->toContain($currentMonth);
});
test('available months includes previous month', function () {
$this->actingAs($this->admin);
$component = Volt::test('admin.reports.monthly-report');
$availableMonths = $component->get('availableMonths');
$previousMonth = now()->subMonth()->format('Y-m');
$values = array_column($availableMonths, 'value');
expect($values)->toContain($previousMonth);
});
// ===========================================
// PDF Generation Tests
// ===========================================
test('monthly report generates valid PDF', function () {
$this->actingAs($this->admin);
// Create test data for the month
$targetMonth = now()->subMonth();
User::factory()->count(5)->individual()->create([
'created_at' => $targetMonth,
]);
Consultation::factory()->count(10)->create([
'booking_date' => $targetMonth,
]);
Volt::test('admin.reports.monthly-report')
->set('selectedPeriod', $targetMonth->format('Y-m'))
->call('generate')
->assertFileDownloaded("monthly-report-{$targetMonth->year}-{$targetMonth->month}.pdf");
});
test('report handles month with no data gracefully', function () {
$this->actingAs($this->admin);
// Generate for a month with no data (far in the past)
$emptyMonth = now()->subMonths(10);
Volt::test('admin.reports.monthly-report')
->set('selectedPeriod', $emptyMonth->format('Y-m'))
->call('generate')
->assertFileDownloaded();
});
// ===========================================
// Service Tests - User Statistics
// ===========================================
test('user statistics are accurate for selected month', function () {
$targetMonth = now()->subMonth();
$startDate = $targetMonth->copy()->startOfMonth();
$endDate = $targetMonth->copy()->endOfMonth();
// Create 3 users in target month
User::factory()->count(3)->individual()->create([
'created_at' => $targetMonth,
]);
// Create 2 users in different month (should not be counted as new)
User::factory()->count(2)->individual()->create([
'created_at' => now()->subMonths(3),
]);
$service = new MonthlyReportService;
$stats = $service->getUserStats($startDate, $endDate);
expect($stats['new_clients'])->toBe(3);
});
test('user statistics count individual and company clients separately', function () {
$targetMonth = now()->subMonth();
$startDate = $targetMonth->copy()->startOfMonth();
$endDate = $targetMonth->copy()->endOfMonth();
User::factory()->count(4)->individual()->create([
'created_at' => $targetMonth->copy()->subMonth(),
]);
User::factory()->count(2)->company()->create([
'created_at' => $targetMonth->copy()->subMonth(),
]);
$service = new MonthlyReportService;
$stats = $service->getUserStats($startDate, $endDate);
expect($stats['individual'])->toBe(4);
expect($stats['company'])->toBe(2);
expect($stats['total_active'])->toBe(6);
});
// ===========================================
// Service Tests - Consultation Statistics
// ===========================================
test('consultation statistics calculate totals correctly', function () {
$targetMonth = now()->subMonth();
$startDate = $targetMonth->copy()->startOfMonth();
$endDate = $targetMonth->copy()->endOfMonth();
Consultation::factory()->count(5)->completed()->create([
'booking_date' => $targetMonth,
]);
Consultation::factory()->count(3)->pending()->create([
'booking_date' => $targetMonth,
]);
Consultation::factory()->count(2)->cancelled()->create([
'booking_date' => $targetMonth,
]);
$service = new MonthlyReportService;
$stats = $service->getConsultationStats($startDate, $endDate);
expect($stats['total'])->toBe(10);
expect($stats['completed'])->toBe(5);
expect($stats['cancelled'])->toBe(2);
});
test('consultation statistics calculate no-show rate correctly', function () {
$targetMonth = now()->subMonth();
$startDate = $targetMonth->copy()->startOfMonth();
$endDate = $targetMonth->copy()->endOfMonth();
// 8 completed + 2 no-shows = 20% no-show rate
Consultation::factory()->count(8)->completed()->create([
'booking_date' => $targetMonth,
]);
Consultation::factory()->count(2)->noShow()->create([
'booking_date' => $targetMonth,
]);
$service = new MonthlyReportService;
$stats = $service->getConsultationStats($startDate, $endDate);
expect($stats['no_show_rate'])->toBe(20.0);
});
test('consultation statistics handle zero completed consultations', function () {
$targetMonth = now()->subMonth();
$startDate = $targetMonth->copy()->startOfMonth();
$endDate = $targetMonth->copy()->endOfMonth();
// Only pending consultations, no completed ones
Consultation::factory()->count(5)->pending()->create([
'booking_date' => $targetMonth,
]);
$service = new MonthlyReportService;
$stats = $service->getConsultationStats($startDate, $endDate);
expect($stats['no_show_rate'])->toBe(0);
});
test('consultation statistics count free and paid types', function () {
$targetMonth = now()->subMonth();
$startDate = $targetMonth->copy()->startOfMonth();
$endDate = $targetMonth->copy()->endOfMonth();
Consultation::factory()->count(4)->free()->create([
'booking_date' => $targetMonth,
]);
Consultation::factory()->count(6)->paid()->create([
'booking_date' => $targetMonth,
]);
$service = new MonthlyReportService;
$stats = $service->getConsultationStats($startDate, $endDate);
expect($stats['free'])->toBe(4);
expect($stats['paid'])->toBe(6);
});
// ===========================================
// Service Tests - Timeline Statistics
// ===========================================
test('timeline statistics count active timelines correctly', function () {
$targetMonth = now()->subMonth();
$startDate = $targetMonth->copy()->startOfMonth();
$endDate = $targetMonth->copy()->endOfMonth();
Timeline::factory()->count(5)->active()->create([
'created_at' => $targetMonth->copy()->subMonth(),
]);
Timeline::factory()->count(2)->archived()->create([
'created_at' => $targetMonth->copy()->subMonth(),
]);
$service = new MonthlyReportService;
$stats = $service->getTimelineStats($startDate, $endDate);
expect($stats['active'])->toBe(5);
});
test('timeline statistics count new timelines in month', function () {
$targetMonth = now()->subMonth();
$startDate = $targetMonth->copy()->startOfMonth();
$endDate = $targetMonth->copy()->endOfMonth();
Timeline::factory()->count(3)->create([
'created_at' => $targetMonth,
]);
Timeline::factory()->count(2)->create([
'created_at' => now()->subMonths(3),
]);
$service = new MonthlyReportService;
$stats = $service->getTimelineStats($startDate, $endDate);
expect($stats['new'])->toBe(3);
});
test('timeline statistics count updates in month', function () {
$targetMonth = now()->subMonth();
$startDate = $targetMonth->copy()->startOfMonth();
$endDate = $targetMonth->copy()->endOfMonth();
$timeline = Timeline::factory()->create();
TimelineUpdate::factory()->count(5)->create([
'timeline_id' => $timeline->id,
'created_at' => $targetMonth,
]);
TimelineUpdate::factory()->count(2)->create([
'timeline_id' => $timeline->id,
'created_at' => now()->subMonths(3),
]);
$service = new MonthlyReportService;
$stats = $service->getTimelineStats($startDate, $endDate);
expect($stats['updates'])->toBe(5);
});
// ===========================================
// Service Tests - Post Statistics
// ===========================================
test('post statistics count published posts in month', function () {
$targetMonth = now()->subMonth();
$startDate = $targetMonth->copy()->startOfMonth();
$endDate = $targetMonth->copy()->endOfMonth();
Post::factory()->count(4)->published()->create([
'published_at' => $targetMonth,
]);
Post::factory()->count(2)->published()->create([
'published_at' => now()->subMonths(3),
]);
$service = new MonthlyReportService;
$stats = $service->getPostStats($startDate, $endDate);
expect($stats['this_month'])->toBe(4);
expect($stats['total'])->toBe(6);
});
// ===========================================
// Service Tests - Previous Month Comparison
// ===========================================
test('previous month comparison returns null when no prior data exists', function () {
$targetMonth = now()->subMonth()->startOfMonth();
$service = new MonthlyReportService;
$comparison = $service->getPreviousMonthComparison($targetMonth);
expect($comparison)->toBeNull();
});
test('previous month comparison returns data when prior month has data', function () {
$targetMonth = now()->subMonth();
$previousMonth = now()->subMonths(2);
// Create data in the previous month
User::factory()->count(3)->individual()->create([
'created_at' => $previousMonth,
]);
Consultation::factory()->count(5)->create([
'booking_date' => $previousMonth,
]);
$service = new MonthlyReportService;
$comparison = $service->getPreviousMonthComparison($targetMonth->startOfMonth());
expect($comparison)->not->toBeNull();
expect($comparison['consultations'])->toBe(5);
expect($comparison['clients'])->toBe(3);
});
// ===========================================
// Language Tests
// ===========================================
test('report respects admin language preference for Arabic', function () {
$adminArabic = User::factory()->admin()->create(['preferred_language' => 'ar']);
$this->actingAs($adminArabic);
Volt::test('admin.reports.monthly-report')
->assertSee(__('report.monthly_report', [], 'ar'));
});
test('report respects admin language preference for English', function () {
$adminEnglish = User::factory()->admin()->create(['preferred_language' => 'en']);
app()->setLocale('en');
$this->actingAs($adminEnglish);
Volt::test('admin.reports.monthly-report')
->assertSee('Monthly Statistics Report');
});
// ===========================================
// UI Elements Tests
// ===========================================
test('monthly report page shows table of contents preview', function () {
$this->actingAs($this->admin);
Volt::test('admin.reports.monthly-report')
->assertSee(__('report.table_of_contents'))
->assertSee(__('report.executive_summary'))
->assertSee(__('report.user_statistics'))
->assertSee(__('report.consultation_statistics'))
->assertSee(__('report.timeline_statistics'))
->assertSee(__('report.post_statistics'))
->assertSee(__('report.trends_chart'));
});
test('monthly report page shows generate button', function () {
$this->actingAs($this->admin);
Volt::test('admin.reports.monthly-report')
->assertSee(__('report.generate'));
});
test('monthly report page shows period selector', function () {
$this->actingAs($this->admin);
Volt::test('admin.reports.monthly-report')
->assertSee(__('report.select_period'));
});