597 lines
20 KiB
PHP
597 lines
20 KiB
PHP
<?php
|
|
|
|
use App\Enums\ConsultationStatus;
|
|
use App\Enums\ConsultationType;
|
|
use App\Enums\PaymentStatus;
|
|
use App\Models\AdminLog;
|
|
use App\Models\Consultation;
|
|
use App\Models\User;
|
|
use App\Models\WorkingHour;
|
|
use App\Notifications\ConsultationCancelled;
|
|
use App\Notifications\ConsultationRescheduled;
|
|
use App\Services\AvailabilityService;
|
|
use Illuminate\Support\Facades\Notification;
|
|
use Livewire\Volt\Volt;
|
|
|
|
// ==========================================
|
|
// ACCESS CONTROL TESTS
|
|
// ==========================================
|
|
|
|
test('guest cannot access consultations management page', function () {
|
|
$this->get(route('admin.consultations.index'))
|
|
->assertRedirect(route('login'));
|
|
});
|
|
|
|
test('client cannot access consultations management page', function () {
|
|
$client = User::factory()->individual()->create();
|
|
|
|
$this->actingAs($client)
|
|
->get(route('admin.consultations.index'))
|
|
->assertForbidden();
|
|
});
|
|
|
|
test('admin can access consultations management page', function () {
|
|
$admin = User::factory()->admin()->create();
|
|
|
|
$this->actingAs($admin)
|
|
->get(route('admin.consultations.index'))
|
|
->assertOk();
|
|
});
|
|
|
|
// ==========================================
|
|
// CONSULTATIONS LIST VIEW TESTS
|
|
// ==========================================
|
|
|
|
test('consultations list displays all consultations', function () {
|
|
$admin = User::factory()->admin()->create();
|
|
$client = User::factory()->individual()->create(['full_name' => 'Test Client']);
|
|
Consultation::factory()->approved()->create(['user_id' => $client->id]);
|
|
|
|
$this->actingAs($admin);
|
|
|
|
Volt::test('admin.consultations.index')
|
|
->assertSee('Test Client');
|
|
});
|
|
|
|
test('consultations list filters by status', function () {
|
|
$admin = User::factory()->admin()->create();
|
|
$approvedClient = User::factory()->individual()->create(['full_name' => 'Approved Client']);
|
|
$completedClient = User::factory()->individual()->create(['full_name' => 'Completed Client']);
|
|
|
|
Consultation::factory()->approved()->create(['user_id' => $approvedClient->id]);
|
|
Consultation::factory()->completed()->create(['user_id' => $completedClient->id]);
|
|
|
|
$this->actingAs($admin);
|
|
|
|
Volt::test('admin.consultations.index')
|
|
->set('statusFilter', 'approved')
|
|
->assertSee('Approved Client')
|
|
->assertDontSee('Completed Client');
|
|
});
|
|
|
|
test('consultations list filters by consultation type', function () {
|
|
$admin = User::factory()->admin()->create();
|
|
$freeClient = User::factory()->individual()->create(['full_name' => 'Free Client']);
|
|
$paidClient = User::factory()->individual()->create(['full_name' => 'Paid Client']);
|
|
|
|
Consultation::factory()->approved()->free()->create(['user_id' => $freeClient->id]);
|
|
Consultation::factory()->approved()->paid()->create(['user_id' => $paidClient->id]);
|
|
|
|
$this->actingAs($admin);
|
|
|
|
Volt::test('admin.consultations.index')
|
|
->set('typeFilter', 'free')
|
|
->assertSee('Free Client')
|
|
->assertDontSee('Paid Client');
|
|
});
|
|
|
|
test('consultations list filters by payment status', function () {
|
|
$admin = User::factory()->admin()->create();
|
|
$pendingClient = User::factory()->individual()->create(['full_name' => 'Pending Payment']);
|
|
$receivedClient = User::factory()->individual()->create(['full_name' => 'Received Payment']);
|
|
|
|
Consultation::factory()->approved()->create([
|
|
'user_id' => $pendingClient->id,
|
|
'consultation_type' => ConsultationType::Paid,
|
|
'payment_status' => PaymentStatus::Pending,
|
|
]);
|
|
Consultation::factory()->approved()->create([
|
|
'user_id' => $receivedClient->id,
|
|
'consultation_type' => ConsultationType::Paid,
|
|
'payment_status' => PaymentStatus::Received,
|
|
]);
|
|
|
|
$this->actingAs($admin);
|
|
|
|
Volt::test('admin.consultations.index')
|
|
->set('paymentFilter', 'pending')
|
|
->assertSee('Pending Payment')
|
|
->assertDontSee('Received Payment');
|
|
});
|
|
|
|
test('consultations list searches by client name', function () {
|
|
$admin = User::factory()->admin()->create();
|
|
$targetUser = User::factory()->individual()->create(['full_name' => 'John Doe']);
|
|
$otherUser = User::factory()->individual()->create(['full_name' => 'Jane Smith']);
|
|
|
|
Consultation::factory()->approved()->create(['user_id' => $targetUser->id]);
|
|
Consultation::factory()->approved()->create(['user_id' => $otherUser->id]);
|
|
|
|
$this->actingAs($admin);
|
|
|
|
Volt::test('admin.consultations.index')
|
|
->set('search', 'John')
|
|
->assertSee('John Doe')
|
|
->assertDontSee('Jane Smith');
|
|
});
|
|
|
|
test('consultations list filters by date range', function () {
|
|
$admin = User::factory()->admin()->create();
|
|
$oldClient = User::factory()->individual()->create(['full_name' => 'Old Client']);
|
|
$newClient = User::factory()->individual()->create(['full_name' => 'New Client']);
|
|
|
|
Consultation::factory()->approved()->create([
|
|
'user_id' => $oldClient->id,
|
|
'booking_date' => now()->subDays(10),
|
|
]);
|
|
Consultation::factory()->approved()->create([
|
|
'user_id' => $newClient->id,
|
|
'booking_date' => now()->addDays(5),
|
|
]);
|
|
|
|
$this->actingAs($admin);
|
|
|
|
Volt::test('admin.consultations.index')
|
|
->set('dateFrom', now()->format('Y-m-d'))
|
|
->assertSee('New Client')
|
|
->assertDontSee('Old Client');
|
|
});
|
|
|
|
// ==========================================
|
|
// STATUS MANAGEMENT TESTS
|
|
// ==========================================
|
|
|
|
test('admin can mark consultation as completed', function () {
|
|
$admin = User::factory()->admin()->create();
|
|
$consultation = Consultation::factory()->approved()->create();
|
|
|
|
$this->actingAs($admin);
|
|
|
|
Volt::test('admin.consultations.index')
|
|
->call('markCompleted', $consultation->id)
|
|
->assertHasNoErrors();
|
|
|
|
expect($consultation->fresh()->status)->toBe(ConsultationStatus::Completed);
|
|
});
|
|
|
|
test('cannot mark pending consultation as completed', function () {
|
|
$admin = User::factory()->admin()->create();
|
|
$consultation = Consultation::factory()->pending()->create();
|
|
|
|
$this->actingAs($admin);
|
|
|
|
Volt::test('admin.consultations.index')
|
|
->call('markCompleted', $consultation->id);
|
|
|
|
expect($consultation->fresh()->status)->toBe(ConsultationStatus::Pending);
|
|
});
|
|
|
|
test('admin can mark consultation as no-show', function () {
|
|
$admin = User::factory()->admin()->create();
|
|
$consultation = Consultation::factory()->approved()->create();
|
|
|
|
$this->actingAs($admin);
|
|
|
|
Volt::test('admin.consultations.index')
|
|
->call('markNoShow', $consultation->id)
|
|
->assertHasNoErrors();
|
|
|
|
expect($consultation->fresh()->status)->toBe(ConsultationStatus::NoShow);
|
|
});
|
|
|
|
test('cannot mark completed consultation as no-show', function () {
|
|
$admin = User::factory()->admin()->create();
|
|
$consultation = Consultation::factory()->completed()->create();
|
|
|
|
$this->actingAs($admin);
|
|
|
|
Volt::test('admin.consultations.index')
|
|
->call('markNoShow', $consultation->id);
|
|
|
|
expect($consultation->fresh()->status)->toBe(ConsultationStatus::Completed);
|
|
});
|
|
|
|
test('admin can cancel approved consultation', function () {
|
|
Notification::fake();
|
|
|
|
$admin = User::factory()->admin()->create();
|
|
$client = User::factory()->individual()->create();
|
|
$consultation = Consultation::factory()->approved()->create(['user_id' => $client->id]);
|
|
|
|
$this->actingAs($admin);
|
|
|
|
Volt::test('admin.consultations.index')
|
|
->call('cancel', $consultation->id)
|
|
->assertHasNoErrors();
|
|
|
|
expect($consultation->fresh()->status)->toBe(ConsultationStatus::Cancelled);
|
|
Notification::assertSentTo($client, ConsultationCancelled::class);
|
|
});
|
|
|
|
test('admin can cancel pending consultation', function () {
|
|
Notification::fake();
|
|
|
|
$admin = User::factory()->admin()->create();
|
|
$client = User::factory()->individual()->create();
|
|
$consultation = Consultation::factory()->pending()->create(['user_id' => $client->id]);
|
|
|
|
$this->actingAs($admin);
|
|
|
|
Volt::test('admin.consultations.index')
|
|
->call('cancel', $consultation->id)
|
|
->assertHasNoErrors();
|
|
|
|
expect($consultation->fresh()->status)->toBe(ConsultationStatus::Cancelled);
|
|
});
|
|
|
|
test('cannot cancel already cancelled consultation', function () {
|
|
Notification::fake();
|
|
|
|
$admin = User::factory()->admin()->create();
|
|
$consultation = Consultation::factory()->cancelled()->create();
|
|
|
|
$this->actingAs($admin);
|
|
|
|
Volt::test('admin.consultations.index')
|
|
->call('cancel', $consultation->id);
|
|
|
|
Notification::assertNothingSent();
|
|
});
|
|
|
|
// ==========================================
|
|
// PAYMENT TRACKING TESTS
|
|
// ==========================================
|
|
|
|
test('admin can mark payment as received for paid consultation', function () {
|
|
$admin = User::factory()->admin()->create();
|
|
$consultation = Consultation::factory()->approved()->create([
|
|
'consultation_type' => ConsultationType::Paid,
|
|
'payment_amount' => 150.00,
|
|
'payment_status' => PaymentStatus::Pending,
|
|
]);
|
|
|
|
$this->actingAs($admin);
|
|
|
|
Volt::test('admin.consultations.index')
|
|
->call('markPaymentReceived', $consultation->id)
|
|
->assertHasNoErrors();
|
|
|
|
expect($consultation->fresh())
|
|
->payment_status->toBe(PaymentStatus::Received)
|
|
->payment_received_at->not->toBeNull();
|
|
});
|
|
|
|
test('cannot mark payment received for free consultation', function () {
|
|
$admin = User::factory()->admin()->create();
|
|
$consultation = Consultation::factory()->approved()->free()->create();
|
|
|
|
$this->actingAs($admin);
|
|
|
|
Volt::test('admin.consultations.index')
|
|
->call('markPaymentReceived', $consultation->id);
|
|
|
|
expect($consultation->fresh()->payment_status)->toBe(PaymentStatus::NotApplicable);
|
|
});
|
|
|
|
test('cannot mark payment received twice', function () {
|
|
$admin = User::factory()->admin()->create();
|
|
$consultation = Consultation::factory()->approved()->create([
|
|
'consultation_type' => ConsultationType::Paid,
|
|
'payment_status' => PaymentStatus::Received,
|
|
'payment_received_at' => now()->subDay(),
|
|
]);
|
|
|
|
$originalReceivedAt = $consultation->payment_received_at;
|
|
|
|
$this->actingAs($admin);
|
|
|
|
Volt::test('admin.consultations.index')
|
|
->call('markPaymentReceived', $consultation->id);
|
|
|
|
expect($consultation->fresh()->payment_received_at->timestamp)
|
|
->toBe($originalReceivedAt->timestamp);
|
|
});
|
|
|
|
// ==========================================
|
|
// RESCHEDULE TESTS
|
|
// ==========================================
|
|
|
|
test('admin can access consultation detail page', function () {
|
|
$admin = User::factory()->admin()->create();
|
|
$consultation = Consultation::factory()->approved()->create();
|
|
|
|
$this->actingAs($admin)
|
|
->get(route('admin.consultations.show', $consultation))
|
|
->assertOk();
|
|
});
|
|
|
|
test('admin can reschedule consultation to available slot', function () {
|
|
Notification::fake();
|
|
|
|
$admin = User::factory()->admin()->create();
|
|
$client = User::factory()->individual()->create();
|
|
$consultation = Consultation::factory()->approved()->create([
|
|
'user_id' => $client->id,
|
|
'booking_date' => now()->addDays(3),
|
|
'booking_time' => '10:00:00',
|
|
]);
|
|
|
|
// Create working hours for the new date
|
|
WorkingHour::factory()->create([
|
|
'day_of_week' => now()->addDays(5)->dayOfWeek,
|
|
'is_active' => true,
|
|
'start_time' => '09:00',
|
|
'end_time' => '17:00',
|
|
]);
|
|
|
|
$newDate = now()->addDays(5)->format('Y-m-d');
|
|
$newTime = '14:00';
|
|
|
|
$this->actingAs($admin);
|
|
|
|
// Mock the availability service
|
|
$this->mock(AvailabilityService::class)
|
|
->shouldReceive('getAvailableSlots')
|
|
->andReturn(['14:00', '15:00', '16:00']);
|
|
|
|
Volt::test('admin.consultations.show', ['consultation' => $consultation])
|
|
->set('showRescheduleModal', true)
|
|
->set('newDate', $newDate)
|
|
->set('newTime', $newTime)
|
|
->call('reschedule')
|
|
->assertHasNoErrors();
|
|
|
|
expect($consultation->fresh())
|
|
->booking_date->format('Y-m-d')->toBe($newDate)
|
|
->booking_time->toBe($newTime);
|
|
|
|
Notification::assertSentTo($client, ConsultationRescheduled::class);
|
|
});
|
|
|
|
test('cannot reschedule to unavailable slot', function () {
|
|
$admin = User::factory()->admin()->create();
|
|
$consultation = Consultation::factory()->approved()->create();
|
|
|
|
$newDate = now()->addDays(5)->format('Y-m-d');
|
|
$newTime = '14:00';
|
|
|
|
$this->actingAs($admin);
|
|
|
|
// Mock availability service - slot not available
|
|
$this->mock(AvailabilityService::class)
|
|
->shouldReceive('getAvailableSlots')
|
|
->andReturn(['10:00', '11:00']); // 14:00 not in list
|
|
|
|
Volt::test('admin.consultations.show', ['consultation' => $consultation])
|
|
->set('showRescheduleModal', true)
|
|
->set('newDate', $newDate)
|
|
->set('newTime', $newTime)
|
|
->call('reschedule')
|
|
->assertHasErrors(['newTime']);
|
|
});
|
|
|
|
test('cannot reschedule to past date', function () {
|
|
$admin = User::factory()->admin()->create();
|
|
$consultation = Consultation::factory()->approved()->create();
|
|
|
|
$this->actingAs($admin);
|
|
|
|
Volt::test('admin.consultations.show', ['consultation' => $consultation])
|
|
->set('showRescheduleModal', true)
|
|
->set('newDate', now()->subDay()->format('Y-m-d'))
|
|
->set('newTime', '10:00')
|
|
->call('reschedule')
|
|
->assertHasErrors(['newDate']);
|
|
});
|
|
|
|
// ==========================================
|
|
// ADMIN NOTES TESTS
|
|
// ==========================================
|
|
|
|
test('admin can add note to consultation', function () {
|
|
$admin = User::factory()->admin()->create();
|
|
$consultation = Consultation::factory()->approved()->create();
|
|
|
|
$this->actingAs($admin);
|
|
|
|
Volt::test('admin.consultations.show', ['consultation' => $consultation])
|
|
->set('newNote', 'Client requested Arabic documents')
|
|
->call('addNote')
|
|
->assertHasNoErrors();
|
|
|
|
$notes = $consultation->fresh()->admin_notes;
|
|
expect($notes)->toHaveCount(1)
|
|
->and($notes[0]['text'])->toBe('Client requested Arabic documents')
|
|
->and($notes[0]['admin_id'])->toBe($admin->id);
|
|
});
|
|
|
|
test('admin can update note', function () {
|
|
$admin = User::factory()->admin()->create();
|
|
$consultation = Consultation::factory()->approved()->create([
|
|
'admin_notes' => [
|
|
['text' => 'Original note', 'admin_id' => $admin->id, 'created_at' => now()->toISOString()],
|
|
],
|
|
]);
|
|
|
|
$this->actingAs($admin);
|
|
|
|
Volt::test('admin.consultations.show', ['consultation' => $consultation])
|
|
->call('startEditNote', 0)
|
|
->set('editingNoteText', 'Updated note')
|
|
->call('updateNote')
|
|
->assertHasNoErrors();
|
|
|
|
$notes = $consultation->fresh()->admin_notes;
|
|
expect($notes[0]['text'])->toBe('Updated note')
|
|
->and($notes[0])->toHaveKey('updated_at');
|
|
});
|
|
|
|
test('admin can delete note', function () {
|
|
$admin = User::factory()->admin()->create();
|
|
$consultation = Consultation::factory()->approved()->create([
|
|
'admin_notes' => [
|
|
['text' => 'Note 1', 'admin_id' => $admin->id, 'created_at' => now()->toISOString()],
|
|
['text' => 'Note 2', 'admin_id' => $admin->id, 'created_at' => now()->toISOString()],
|
|
],
|
|
]);
|
|
|
|
$this->actingAs($admin);
|
|
|
|
Volt::test('admin.consultations.show', ['consultation' => $consultation])
|
|
->call('deleteNote', 0)
|
|
->assertHasNoErrors();
|
|
|
|
$notes = $consultation->fresh()->admin_notes;
|
|
expect($notes)->toHaveCount(1)
|
|
->and($notes[0]['text'])->toBe('Note 2');
|
|
});
|
|
|
|
// ==========================================
|
|
// AUDIT LOG TESTS
|
|
// ==========================================
|
|
|
|
test('audit log entry created on status change to completed', function () {
|
|
$admin = User::factory()->admin()->create();
|
|
$consultation = Consultation::factory()->approved()->create();
|
|
|
|
$this->actingAs($admin);
|
|
|
|
Volt::test('admin.consultations.index')
|
|
->call('markCompleted', $consultation->id);
|
|
|
|
expect(AdminLog::query()
|
|
->where('admin_id', $admin->id)
|
|
->where('action', 'status_change')
|
|
->where('target_type', 'consultation')
|
|
->where('target_id', $consultation->id)
|
|
->exists()
|
|
)->toBeTrue();
|
|
});
|
|
|
|
test('audit log entry created on payment received', function () {
|
|
$admin = User::factory()->admin()->create();
|
|
$consultation = Consultation::factory()->approved()->create([
|
|
'consultation_type' => ConsultationType::Paid,
|
|
'payment_status' => PaymentStatus::Pending,
|
|
]);
|
|
|
|
$this->actingAs($admin);
|
|
|
|
Volt::test('admin.consultations.index')
|
|
->call('markPaymentReceived', $consultation->id);
|
|
|
|
expect(AdminLog::query()
|
|
->where('admin_id', $admin->id)
|
|
->where('action', 'payment_received')
|
|
->where('target_type', 'consultation')
|
|
->where('target_id', $consultation->id)
|
|
->exists()
|
|
)->toBeTrue();
|
|
});
|
|
|
|
test('audit log entry created on reschedule', function () {
|
|
Notification::fake();
|
|
|
|
$admin = User::factory()->admin()->create();
|
|
$client = User::factory()->individual()->create();
|
|
$consultation = Consultation::factory()->approved()->create([
|
|
'user_id' => $client->id,
|
|
]);
|
|
|
|
$newDate = now()->addDays(5)->format('Y-m-d');
|
|
|
|
$this->actingAs($admin);
|
|
|
|
$this->mock(AvailabilityService::class)
|
|
->shouldReceive('getAvailableSlots')
|
|
->andReturn(['14:00']);
|
|
|
|
Volt::test('admin.consultations.show', ['consultation' => $consultation])
|
|
->set('showRescheduleModal', true)
|
|
->set('newDate', $newDate)
|
|
->set('newTime', '14:00')
|
|
->call('reschedule');
|
|
|
|
expect(AdminLog::query()
|
|
->where('admin_id', $admin->id)
|
|
->where('action', 'reschedule')
|
|
->where('target_type', 'consultation')
|
|
->where('target_id', $consultation->id)
|
|
->exists()
|
|
)->toBeTrue();
|
|
});
|
|
|
|
// ==========================================
|
|
// CLIENT HISTORY TESTS
|
|
// ==========================================
|
|
|
|
test('admin can access client consultation history', function () {
|
|
$admin = User::factory()->admin()->create();
|
|
$client = User::factory()->individual()->create();
|
|
|
|
$this->actingAs($admin)
|
|
->get(route('admin.clients.consultation-history', $client))
|
|
->assertOk();
|
|
});
|
|
|
|
test('client consultation history displays consultations', function () {
|
|
$admin = User::factory()->admin()->create();
|
|
$client = User::factory()->individual()->create();
|
|
|
|
Consultation::factory()->approved()->create([
|
|
'user_id' => $client->id,
|
|
'booking_date' => now()->addDays(5),
|
|
]);
|
|
|
|
$this->actingAs($admin);
|
|
|
|
Volt::test('admin.clients.consultation-history', ['user' => $client])
|
|
->assertOk();
|
|
});
|
|
|
|
test('client consultation history shows statistics', function () {
|
|
$admin = User::factory()->admin()->create();
|
|
$client = User::factory()->individual()->create();
|
|
|
|
Consultation::factory()->completed()->count(2)->create(['user_id' => $client->id]);
|
|
Consultation::factory()->cancelled()->create(['user_id' => $client->id]);
|
|
Consultation::factory()->noShow()->create(['user_id' => $client->id]);
|
|
|
|
$this->actingAs($admin);
|
|
|
|
Volt::test('admin.clients.consultation-history', ['user' => $client])
|
|
->assertSee('4') // total
|
|
->assertSee('2'); // completed
|
|
});
|
|
|
|
// ==========================================
|
|
// BILINGUAL TESTS
|
|
// ==========================================
|
|
|
|
test('cancellation notification sent in client preferred language', function () {
|
|
Notification::fake();
|
|
|
|
$admin = User::factory()->admin()->create();
|
|
$arabicUser = User::factory()->individual()->create(['preferred_language' => 'ar']);
|
|
$consultation = Consultation::factory()->approved()->create(['user_id' => $arabicUser->id]);
|
|
|
|
$this->actingAs($admin);
|
|
|
|
Volt::test('admin.consultations.index')
|
|
->call('cancel', $consultation->id);
|
|
|
|
Notification::assertSentTo($arabicUser, ConsultationCancelled::class, function ($notification) {
|
|
return $notification->consultation->user->preferred_language === 'ar';
|
|
});
|
|
});
|