find('app:billing:backfill-paid-invoices')); $args = ['--tenant' => 'doctor:' . $doctorId]; if ($dryRun) { $args['--dry-run'] = true; } $tester->execute($args); return $tester->getDisplay(); } /** @return array{0: PatientSession, 1: int} مراجعهٔ پرداخت‌شدهٔ بدون صورتحساب و شناسهٔ پزشکش */ private function makePaidSessionWithoutInvoice(): array { $user = $this->createUser(['ROLE_USER', 'ROLE_DOCTOR']); $doctor = new Doctor($user, 'دکتر بک‌فیل'); $doctor->setMobileNumber($user->getMobileNumber()); $this->em->persist($doctor); $this->em->flush(); $record = new PatientRecord('doctor', $doctor->getId(), $this->createUser(), 'doctor', $doctor->getId()); $this->em->persist($record); $session = new PatientSession($record); $session->setVisitPriceRials(3_000_000); $session->applyShares(3_000_000, 0, 0, 3_000_000); $this->em->persist($session); $this->em->flush(); // مسیر واقعی ثبت پول، ولی صورتحسابی که همان مسیر می‌سازد عمداً حذف می‌شود // تا وضعیتِ پیش از اصلاح بازسازی شود. static::getContainer()->get(PatientService::class) ->addSessionPayment($session, 'cash', 1_000_000); foreach ($this->em->getRepository(Invoice::class)->findBy(['patientSessionId' => $session->getId()]) as $invoice) { $this->em->remove($invoice); } $this->em->flush(); return [$session, $doctor->getId()]; } public function testBackfillCreatesTheMissingFinalizedInvoice(): void { [$session, $doctorId] = $this->makePaidSessionWithoutInvoice(); $this->runCommand($doctorId); $this->em->clear(); $invoices = $this->em->getRepository(Invoice::class)->findBy(['patientSessionId' => $session->getId()]); self::assertCount(1, $invoices); self::assertSame(Invoice::STATUS_FINALIZED, $invoices[0]->getStatus()); self::assertSame(3_000_000, $invoices[0]->getTotalRials()); } public function testDryRunWritesNothing(): void { [$session, $doctorId] = $this->makePaidSessionWithoutInvoice(); $display = $this->runCommand($doctorId, dryRun: true); $this->em->clear(); self::assertStringContainsString((string) $session->getId(), $display); self::assertSame([], $this->em->getRepository(Invoice::class)->findBy(['patientSessionId' => $session->getId()])); } public function testRunningTwiceDoesNotDuplicateInvoices(): void { [$session, $doctorId] = $this->makePaidSessionWithoutInvoice(); $this->runCommand($doctorId); $this->runCommand($doctorId); $this->em->clear(); self::assertCount(1, $this->em->getRepository(Invoice::class)->findBy(['patientSessionId' => $session->getId()])); } /** مرزی: مراجعهٔ بدون پرداخت نباید صورتحساب بگیرد. */ public function testUnpaidSessionIsLeftAlone(): void { $user = $this->createUser(['ROLE_USER', 'ROLE_DOCTOR']); $doctor = new Doctor($user, 'دکتر بدون پرداخت'); $doctor->setMobileNumber($user->getMobileNumber()); $this->em->persist($doctor); $this->em->flush(); $record = new PatientRecord('doctor', $doctor->getId(), $this->createUser(), 'doctor', $doctor->getId()); $this->em->persist($record); $session = new PatientSession($record); $session->setVisitPriceRials(1_000_000); $session->applyShares(1_000_000, 0, 0, 1_000_000); $this->em->persist($session); $this->em->flush(); $this->runCommand($doctor->getId()); $this->em->clear(); self::assertSame([], $this->em->getRepository(Invoice::class)->findBy(['patientSessionId' => $session->getId()])); } /** رفتار سرویس، مستقل از کامند: پرداخت‌دار یعنی صورتحساب لازم. */ public function testServiceCreatesInvoiceOnlyWhenMoneyOrInsuranceExists(): void { $billing = static::getContainer()->get(SessionBillingService::class); [$session] = $this->makePaidSessionWithoutInvoice(); $record = $session->getRecord(); $invoice = $billing->ensureFinalizedInvoice($session, $record->getEntityType(), $record->getEntityId()); self::assertNotNull($invoice); self::assertSame(Invoice::STATUS_FINALIZED, $invoice->getStatus()); } }