330 lines
14 KiB
PHP
330 lines
14 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature\Services\Document;
|
|
|
|
use App\Enums\DocumentStatusEnum;
|
|
use App\Enums\InvoiceStatusEnum;
|
|
use App\Enums\MediaCollectionEnum;
|
|
use App\Enums\ReportStatusEnum;
|
|
use App\Enums\UserEnum;
|
|
use App\Enums\WalletTransactionReasonEnum;
|
|
use App\Jobs\SendPatternSms;
|
|
use App\Models\Document;
|
|
use App\Models\DocumentAssignment;
|
|
use App\Models\Invoice;
|
|
use App\Models\Medium;
|
|
use App\Models\Report;
|
|
use App\Models\Shift;
|
|
use App\Models\User;
|
|
use App\Models\Wallet;
|
|
use App\Services\Document\CaseService;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Carbon;
|
|
use Illuminate\Support\Facades\Queue;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Validation\ValidationException;
|
|
use Tests\TestCase;
|
|
|
|
class CaseServiceTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
protected CaseService $service;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
Queue::fake();
|
|
Storage::fake('public');
|
|
$this->service = app(CaseService::class);
|
|
}
|
|
|
|
protected function doctor(): User
|
|
{
|
|
return User::factory()->create(['level' => UserEnum::Doctor->value]);
|
|
}
|
|
|
|
protected function assignedInProgressDocument(User $doctor): Document
|
|
{
|
|
$document = Document::factory()->inProgress()->create();
|
|
DocumentAssignment::factory()->create(['document_id' => $document->id, 'user_id' => $doctor->id]);
|
|
|
|
return $document;
|
|
}
|
|
|
|
public function test_is_doctor_checks_user_level(): void
|
|
{
|
|
$this->assertTrue($this->service->isDoctor($this->doctor()));
|
|
$this->assertFalse($this->service->isDoctor(User::factory()->create(['level' => UserEnum::Vet->value])));
|
|
}
|
|
|
|
public function test_pending_for_user_returns_registered_and_in_progress_documents_of_requester(): void
|
|
{
|
|
$requester = User::factory()->create();
|
|
$registered = Document::factory()->registered()->create(['requester_id' => $requester->id]);
|
|
$inProgress = Document::factory()->inProgress()->create(['requester_id' => $requester->id]);
|
|
Document::factory()->completed()->create(['requester_id' => $requester->id]);
|
|
Document::factory()->registered()->create();
|
|
|
|
$ids = $this->service->pendingForUser($requester)->pluck('id');
|
|
|
|
$this->assertEqualsCanonicalizing([$registered->id, $inProgress->id], $ids->all());
|
|
}
|
|
|
|
public function test_pending_for_user_includes_documents_assigned_to_non_doctor(): void
|
|
{
|
|
$user = User::factory()->create(['level' => UserEnum::Vet->value]);
|
|
$assigned = Document::factory()->registered()->create();
|
|
DocumentAssignment::factory()->create(['document_id' => $assigned->id, 'user_id' => $user->id]);
|
|
|
|
$this->assertTrue($this->service->pendingForUser($user)->pluck('id')->contains($assigned->id));
|
|
}
|
|
|
|
public function test_pending_for_doctor_returns_only_assigned_in_progress_documents(): void
|
|
{
|
|
$doctor = $this->doctor();
|
|
$inProgress = $this->assignedInProgressDocument($doctor);
|
|
$registered = Document::factory()->registered()->create();
|
|
DocumentAssignment::factory()->create(['document_id' => $registered->id, 'user_id' => $doctor->id]);
|
|
Document::factory()->inProgress()->create(['requester_id' => $doctor->id]);
|
|
|
|
$this->assertSame([$inProgress->id], $this->service->pendingForUser($doctor)->pluck('id')->all());
|
|
}
|
|
|
|
public function test_pending_for_user_orders_by_latest_submitted_at(): void
|
|
{
|
|
$requester = User::factory()->create();
|
|
$older = Document::factory()->registered()->create(['requester_id' => $requester->id, 'submitted_at' => now()->subDay()]);
|
|
$newer = Document::factory()->registered()->create(['requester_id' => $requester->id, 'submitted_at' => now()]);
|
|
|
|
$this->assertSame([$newer->id, $older->id], $this->service->pendingForUser($requester)->pluck('id')->all());
|
|
}
|
|
|
|
public function test_can_access_allows_requester_and_assignees_only(): void
|
|
{
|
|
$document = Document::factory()->create();
|
|
$assignee = User::factory()->create();
|
|
DocumentAssignment::factory()->create(['document_id' => $document->id, 'user_id' => $assignee->id]);
|
|
|
|
$this->assertTrue($this->service->canAccess($document, $document->requester));
|
|
$this->assertTrue($this->service->canAccess($document, $assignee));
|
|
$this->assertFalse($this->service->canAccess($document, User::factory()->create()));
|
|
}
|
|
|
|
public function test_can_access_medium_through_document_attachment(): void
|
|
{
|
|
$document = Document::factory()->create();
|
|
$medium = Medium::factory()->create();
|
|
$document->media()->attach($medium->id, ['collection' => MediaCollectionEnum::Primary->value]);
|
|
|
|
$this->assertTrue($this->service->canAccessMedium($medium, $document->requester));
|
|
$this->assertFalse($this->service->canAccessMedium($medium, User::factory()->create()));
|
|
}
|
|
|
|
public function test_can_access_medium_through_report_attachment(): void
|
|
{
|
|
$report = Report::factory()->create();
|
|
$medium = Medium::factory()->create();
|
|
$report->media()->attach($medium->id, ['collection' => MediaCollectionEnum::Attachments->value]);
|
|
|
|
$this->assertTrue($this->service->canAccessMedium($medium, $report->document->requester));
|
|
}
|
|
|
|
public function test_can_access_medium_denies_unattached_medium(): void
|
|
{
|
|
$this->assertFalse($this->service->canAccessMedium(Medium::factory()->create(), User::factory()->create()));
|
|
}
|
|
|
|
public function test_can_report_requires_doctor_in_progress_status_and_assignment(): void
|
|
{
|
|
$doctor = $this->doctor();
|
|
$document = $this->assignedInProgressDocument($doctor);
|
|
$unassignedDoctor = $this->doctor();
|
|
$nonDoctor = User::factory()->create(['level' => UserEnum::Vet->value]);
|
|
DocumentAssignment::factory()->create(['document_id' => $document->id, 'user_id' => $nonDoctor->id]);
|
|
$registered = Document::factory()->registered()->create();
|
|
DocumentAssignment::factory()->create(['document_id' => $registered->id, 'user_id' => $doctor->id]);
|
|
|
|
$this->assertTrue($this->service->canReport($document, $doctor));
|
|
$this->assertFalse($this->service->canReport($document, $unassignedDoctor));
|
|
$this->assertFalse($this->service->canReport($document, $nonDoctor));
|
|
$this->assertFalse($this->service->canReport($registered, $doctor));
|
|
}
|
|
|
|
public function test_submit_report_creates_approved_report_and_completes_document(): void
|
|
{
|
|
Carbon::setTestNow('2026-09-21 10:00:00');
|
|
$doctor = $this->doctor();
|
|
$document = $this->assignedInProgressDocument($doctor);
|
|
|
|
$report = $this->service->submitReport($document, $doctor, [
|
|
'technique' => 'CT',
|
|
'finding' => 'finding text',
|
|
'diagnose' => 'diagnose text',
|
|
'recommendation' => 'recommendation text',
|
|
]);
|
|
|
|
$this->assertSame(ReportStatusEnum::Approved, $report->status);
|
|
$this->assertDatabaseHas('reports', [
|
|
'id' => $report->id,
|
|
'document_id' => $document->id,
|
|
'doctor_id' => $doctor->id,
|
|
'technique' => 'CT',
|
|
'finding' => 'finding text',
|
|
'diagnose' => 'diagnose text',
|
|
'recommendation' => 'recommendation text',
|
|
]);
|
|
$document->refresh();
|
|
$this->assertSame(DocumentStatusEnum::Completed, $document->status);
|
|
$this->assertSame($doctor->id, $document->finalized_by);
|
|
$this->assertTrue($document->finalized_at->eq(Carbon::parse('2026-09-21 10:00:00')));
|
|
}
|
|
|
|
public function test_submit_report_stores_files_as_report_attachments(): void
|
|
{
|
|
$doctor = $this->doctor();
|
|
$document = $this->assignedInProgressDocument($doctor);
|
|
|
|
$report = $this->service->submitReport($document, $doctor, [], [
|
|
UploadedFile::fake()->image('scan.jpg'),
|
|
UploadedFile::fake()->create('notes.pdf', 10, 'application/pdf'),
|
|
]);
|
|
|
|
$this->assertCount(2, $report->attachments);
|
|
$this->assertDatabaseCount('media', 2);
|
|
Storage::disk('public')->assertExists($report->attachments->first()->path);
|
|
}
|
|
|
|
public function test_submit_report_dispatches_report_ready_sms(): void
|
|
{
|
|
config(['services.iranpayamak.patterns.report_ready' => 'pattern-code']);
|
|
$doctor = $this->doctor();
|
|
$document = $this->assignedInProgressDocument($doctor);
|
|
|
|
$this->service->submitReport($document, $doctor, []);
|
|
|
|
Queue::assertPushed(SendPatternSms::class, 1);
|
|
}
|
|
|
|
public function test_submit_report_throws_when_doctor_cannot_report(): void
|
|
{
|
|
$document = Document::factory()->inProgress()->create();
|
|
|
|
try {
|
|
$this->service->submitReport($document, $this->doctor(), []);
|
|
$this->fail('ValidationException was not thrown');
|
|
} catch (ValidationException $e) {
|
|
$this->assertArrayHasKey('document', $e->errors());
|
|
}
|
|
|
|
$this->assertDatabaseCount('reports', 0);
|
|
$this->assertSame(DocumentStatusEnum::InProgress, $document->fresh()->status);
|
|
}
|
|
|
|
public function test_reject_marks_document_rejected_with_reason(): void
|
|
{
|
|
Carbon::setTestNow('2026-09-21 10:00:00');
|
|
$doctor = $this->doctor();
|
|
$document = $this->assignedInProgressDocument($doctor);
|
|
|
|
$rejected = $this->service->reject($document, $doctor, 'تصاویر ناخوانا');
|
|
|
|
$this->assertSame(DocumentStatusEnum::Rejected, $rejected->status);
|
|
$this->assertSame('تصاویر ناخوانا', $rejected->rejection_reason);
|
|
$this->assertSame($doctor->id, $rejected->finalized_by);
|
|
$this->assertTrue($rejected->finalized_at->eq(Carbon::parse('2026-09-21 10:00:00')));
|
|
}
|
|
|
|
public function test_reject_refunds_paid_invoice_to_requester_wallet(): void
|
|
{
|
|
$doctor = $this->doctor();
|
|
$document = $this->assignedInProgressDocument($doctor);
|
|
$wallet = Wallet::factory()->create(['user_id' => $document->requester_id, 'balance' => 1000]);
|
|
$invoice = Invoice::factory()->paid()->create(['document_id' => $document->id, 'user_id' => $document->requester_id, 'amount' => 500000]);
|
|
|
|
$this->service->reject($document, $doctor, 'reason');
|
|
|
|
$this->assertSame(InvoiceStatusEnum::Canceled, $invoice->fresh()->status);
|
|
$this->assertSame(501000, $wallet->fresh()->balance);
|
|
$this->assertDatabaseHas('wallet_transactions', [
|
|
'wallet_id' => $wallet->id,
|
|
'reason' => WalletTransactionReasonEnum::Refund->value,
|
|
'amount' => 500000,
|
|
]);
|
|
}
|
|
|
|
public function test_reject_does_not_refund_unpaid_invoice(): void
|
|
{
|
|
$doctor = $this->doctor();
|
|
$document = $this->assignedInProgressDocument($doctor);
|
|
$invoice = Invoice::factory()->create(['document_id' => $document->id, 'user_id' => $document->requester_id]);
|
|
|
|
$this->service->reject($document, $doctor, 'reason');
|
|
|
|
$this->assertSame(InvoiceStatusEnum::Pending, $invoice->fresh()->status);
|
|
$this->assertDatabaseCount('wallet_transactions', 0);
|
|
}
|
|
|
|
public function test_reject_throws_when_doctor_cannot_report(): void
|
|
{
|
|
$document = Document::factory()->inProgress()->create();
|
|
|
|
$this->expectException(ValidationException::class);
|
|
|
|
$this->service->reject($document, $this->doctor(), 'reason');
|
|
}
|
|
|
|
public function test_stats_for_requester_counts_pending_monthly_reviewed_and_paid_amount(): void
|
|
{
|
|
Carbon::setTestNow('2026-09-21 10:00:00');
|
|
$requester = User::factory()->create(['level' => UserEnum::Vet->value]);
|
|
Document::factory()->registered()->create(['requester_id' => $requester->id]);
|
|
$thisMonth = Document::factory()->completed()->create(['requester_id' => $requester->id, 'finalized_at' => now()->subDay()]);
|
|
$lastMonth = Document::factory()->completed()->create(['requester_id' => $requester->id, 'finalized_at' => now()->subMonths(2)]);
|
|
Invoice::factory()->paid()->create(['document_id' => $thisMonth->id, 'user_id' => $requester->id, 'amount' => 300000, 'paid_at' => now()->subDay()]);
|
|
Invoice::factory()->paid()->create(['document_id' => $lastMonth->id, 'user_id' => $requester->id, 'amount' => 900000, 'paid_at' => now()->subMonths(2)]);
|
|
|
|
$stats = $this->service->statsForUser($requester);
|
|
|
|
$this->assertSame(1, $stats['pending_cases']);
|
|
$this->assertSame(1, $stats['monthly_reviewed']);
|
|
$this->assertSame(300000, $stats['monthly_amount']);
|
|
$this->assertNull($stats['next_shift_hours']);
|
|
$this->assertFalse($stats['is_doctor']);
|
|
}
|
|
|
|
public function test_stats_for_doctor_counts_finalized_documents_and_next_shift_hours(): void
|
|
{
|
|
Carbon::setTestNow('2026-09-21 10:00:00');
|
|
$doctor = $this->doctor();
|
|
$this->assignedInProgressDocument($doctor);
|
|
$completed = Document::factory()->completed()->create(['finalized_by' => $doctor->id, 'finalized_at' => now()->subDay()]);
|
|
Invoice::factory()->paid()->create(['document_id' => $completed->id, 'user_id' => $completed->requester_id, 'amount' => 700000]);
|
|
Shift::factory()->onDate('2026-09-21')->between('14:30', '18:00')->forDoctor($doctor)->create();
|
|
|
|
$stats = $this->service->statsForUser($doctor);
|
|
|
|
$this->assertSame(1, $stats['pending_cases']);
|
|
$this->assertSame(1, $stats['monthly_reviewed']);
|
|
$this->assertSame(700000, $stats['monthly_amount']);
|
|
$this->assertSame(5, $stats['next_shift_hours']);
|
|
$this->assertTrue($stats['is_doctor']);
|
|
}
|
|
|
|
public function test_stats_for_doctor_reports_zero_hours_when_on_shift(): void
|
|
{
|
|
Carbon::setTestNow('2026-09-21 10:00:00');
|
|
$doctor = $this->doctor();
|
|
Shift::factory()->onDate('2026-09-21')->between('08:00', '16:00')->forDoctor($doctor)->create();
|
|
|
|
$this->assertSame(0, $this->service->statsForUser($doctor)['next_shift_hours']);
|
|
}
|
|
|
|
public function test_stats_for_doctor_reports_null_hours_without_upcoming_shift(): void
|
|
{
|
|
$this->assertNull($this->service->statsForUser($this->doctor())['next_shift_hours']);
|
|
}
|
|
}
|