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']); } }