fix(appointments): file the case file on every confirmation path
Confirming an appointment was supposed to create the patient's record and its session, and PatientService already knew how. Only two of the five paths that confirm an appointment ever called it, and the one that mattered most did not: a booking paid for online was confirmed inside the payment callback, which never ran the side-effects. Every Nobat724 booking therefore went unfiled — 7 confirmed appointments in dev had no session at all. The side-effects now run through AppointmentConfirmationService, which every path calls: the payment callback, both PATCH endpoints, and panel/admin bookings. Creating the record can no longer roll back a confirmation or a payment; a failure is logged and can be repaired with the new app:appointment:backfill-sessions command. Two related defects fixed along the way: - A doctor working at a clinic got two records for one appointment, one under the doctor and one under the clinic, so a single visit's revenue was counted twice. The booking context now decides, and it decides once. - That context was inferred from address_id, falling back to "the doctor's only clinic" — a guess that files an appointment under the wrong practice now that schedules are per-context. It is stored as appointments.clinic_id instead. Panel and admin bookings were left pending forever: nothing confirmed them and no payment was expected. They are created confirmed. Repeat confirmations no longer duplicate the session; an archived one still counts as filed, so archiving a mistaken visit does not resurrect it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -44,6 +44,7 @@ class AdminApiController extends BaseController
|
||||
private readonly \App\Payment\Repository\PaymentRepository $paymentRepo,
|
||||
private readonly \App\Patient\Service\PatientResolver $patientResolver,
|
||||
private readonly \App\Insurance\Service\VisitPriceRequirementResolver $visitPriceResolver,
|
||||
private readonly \App\Appointment\Service\AppointmentConfirmationService $appointmentConfirmation,
|
||||
) {}
|
||||
|
||||
// ── Users ─────────────────────────────────────────────────────────────────
|
||||
@@ -932,15 +933,21 @@ class AdminApiController extends BaseController
|
||||
$appointment->addServiceItem($si);
|
||||
}
|
||||
$bookingClinic = $this->bookingContext->resolve($doctor, $data['clinic_uuid'] ?? null);
|
||||
$appointment->setClinic($bookingClinic);
|
||||
$locationId = $this->slotCalculator->resolveSlotLocationId($doctor, $slotStart, $bookingClinic);
|
||||
if ($locationId !== null) $appointment->setAddressId($locationId);
|
||||
|
||||
// نوبتِ ثبتشده توسط ادمین پرداخت آنلاین ندارد و منتظر چیزی نیست؛ قطعی است.
|
||||
$appointment->transitionTo(Appointment::STATUS_CONFIRMED);
|
||||
|
||||
try {
|
||||
$this->em->getRepository(Appointment::class)->bookAtomically($appointment);
|
||||
} catch (SlotTakenException) {
|
||||
return $this->error(ErrorCodes::SLOT_TAKEN, 'این نوبت قبلاً رزرو شده است', 409);
|
||||
}
|
||||
|
||||
$this->appointmentConfirmation->onConfirmed($appointment);
|
||||
|
||||
return $this->success([
|
||||
'uuid' => $appointment->getUuid(),
|
||||
'slot_start' => $slotStart,
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace App\Appointment\Command;
|
||||
|
||||
use App\Appointment\Entity\Appointment;
|
||||
use App\Appointment\Repository\AppointmentRepository;
|
||||
use App\Appointment\Service\AppointmentConfirmationService;
|
||||
use App\Patient\Repository\PatientSessionRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
/**
|
||||
* Finds appointments that were confirmed without getting a patient session.
|
||||
*
|
||||
* Until the confirmation side-effects were funnelled through
|
||||
* AppointmentConfirmationService, only the two PATCH endpoints created records —
|
||||
* anything confirmed by an online payment never did. Those appointments are
|
||||
* still missing their case file, and completed ones need it just as much: a
|
||||
* visit that was never filed does not stop mattering because time passed.
|
||||
*/
|
||||
#[AsCommand(name: 'app:appointment:backfill-sessions', description: 'Report (and optionally create) missing patient sessions for confirmed appointments')]
|
||||
class BackfillAppointmentSessionsCommand extends Command
|
||||
{
|
||||
private const FILED_STATUSES = [
|
||||
Appointment::STATUS_CONFIRMED,
|
||||
Appointment::STATUS_COMPLETED,
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly AppointmentRepository $appointmentRepo,
|
||||
private readonly PatientSessionRepository $sessionRepo,
|
||||
private readonly AppointmentConfirmationService $confirmation,
|
||||
private readonly EntityManagerInterface $em,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addOption('fix', null, InputOption::VALUE_NONE, 'Create the missing sessions instead of only reporting them');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$fix = (bool) $input->getOption('fix');
|
||||
|
||||
$appointments = $this->appointmentRepo->createQueryBuilder('a')
|
||||
->where('a.status IN (:statuses)')
|
||||
->andWhere('a.isReserve = false')
|
||||
->setParameter('statuses', self::FILED_STATUSES)
|
||||
->orderBy('a.id', 'ASC')
|
||||
->getQuery()
|
||||
->getResult();
|
||||
|
||||
$rows = [];
|
||||
$missing = [];
|
||||
|
||||
foreach ($appointments as $appointment) {
|
||||
$clinic = $appointment->getClinic();
|
||||
[$entityType, $entityId] = $clinic !== null
|
||||
? ['clinic', (int) $clinic->getId()]
|
||||
: ['doctor', (int) $appointment->getDoctor()->getId()];
|
||||
|
||||
if ($this->sessionRepo->findByAppointmentAndEntity($appointment, $entityType, $entityId) !== null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$missing[] = [$appointment, $entityType, $entityId];
|
||||
$rows[] = [
|
||||
$appointment->getUuid(),
|
||||
$appointment->getStatus(),
|
||||
date('Y-m-d H:i', $appointment->getSlotStart()),
|
||||
$appointment->getDoctor()->getName(),
|
||||
$clinic !== null ? ($clinic->getName() ?? 'clinic') : 'personal',
|
||||
];
|
||||
}
|
||||
|
||||
if ($rows === []) {
|
||||
$io->success('Every confirmed appointment already has its patient session.');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$io->table(['appointment', 'status', 'slot', 'doctor', 'context'], $rows);
|
||||
|
||||
if (!$fix) {
|
||||
$io->warning(sprintf('%d appointment(s) without a session. Re-run with --fix to create them.', count($rows)));
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$created = 0;
|
||||
foreach ($missing as [$appointment, $entityType, $entityId]) {
|
||||
$this->confirmation->onConfirmed($appointment);
|
||||
$this->em->flush();
|
||||
|
||||
if ($this->sessionRepo->findByAppointmentAndEntity($appointment, $entityType, $entityId) !== null) {
|
||||
$created++;
|
||||
}
|
||||
}
|
||||
|
||||
// آنهایی که ساخته نشدند عمداً رد شدهاند (نبودِ ویژگی patient_records برای آن
|
||||
// tenant)؛ جدا گزارش میشوند تا با شکست اشتباه گرفته نشوند.
|
||||
$skipped = count($missing) - $created;
|
||||
$io->success(sprintf('Created %d session(s).', $created));
|
||||
if ($skipped > 0) {
|
||||
$io->note(sprintf('%d skipped — their tenant has no patient_records feature.', $skipped));
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,12 @@ use App\Appointment\Entity\WeeklySchedule;
|
||||
use App\Appointment\Repository\AppointmentRepository;
|
||||
use App\Appointment\Repository\SlotTakenException;
|
||||
use App\Appointment\Repository\WeeklyScheduleRepository;
|
||||
use App\Appointment\Service\AppointmentConfirmationService;
|
||||
use App\Appointment\Service\SlotCalculatorService;
|
||||
use App\Clinic\Entity\Clinic;
|
||||
use App\Doctor\Entity\Doctor;
|
||||
use App\Auth\Entity\User;
|
||||
use App\Doctor\Repository\DoctorRepository;
|
||||
use App\Patient\Service\PatientService;
|
||||
use App\Shared\Service\InputValidator;
|
||||
use App\Shared\Constant\ErrorCodes;
|
||||
use App\Shared\Controller\BaseController;
|
||||
@@ -31,7 +31,7 @@ class AppointmentController extends BaseController
|
||||
private readonly AppointmentRepository $appointmentRepo,
|
||||
private readonly DoctorRepository $doctorRepo,
|
||||
private readonly SlotCalculatorService $slotCalculator,
|
||||
private readonly PatientService $patientService,
|
||||
private readonly AppointmentConfirmationService $appointmentConfirmation,
|
||||
private readonly WeeklyScheduleRepository $scheduleRepo,
|
||||
private readonly \App\Appointment\Service\BookingContextResolver $bookingContext,
|
||||
private readonly \App\Doctor\Repository\DoctorAddressRepository $addressRepo,
|
||||
@@ -506,6 +506,7 @@ class AppointmentController extends BaseController
|
||||
}
|
||||
|
||||
// آدرس نوبت از روی session متناظر در برنامهی هفتگی تعیین میشود (location_id).
|
||||
$appointment->setClinic($bookingClinic);
|
||||
$locationId = $this->slotCalculator->resolveSlotLocationId($doctor, $slotStart, $bookingClinic);
|
||||
if ($locationId !== null) {
|
||||
$appointment->setAddressId($locationId);
|
||||
@@ -877,7 +878,7 @@ class AppointmentController extends BaseController
|
||||
}
|
||||
|
||||
if ($newStatus === Appointment::STATUS_CONFIRMED) {
|
||||
$this->patientService->autoCreateOnAppointmentConfirm($appointment);
|
||||
$this->appointmentConfirmation->onConfirmed($appointment);
|
||||
}
|
||||
|
||||
if (in_array($newStatus, self::CANCEL_STATUSES, true)) {
|
||||
@@ -980,7 +981,7 @@ class AppointmentController extends BaseController
|
||||
}
|
||||
$appointment->transitionTo($newStatus);
|
||||
if ($newStatus === Appointment::STATUS_CONFIRMED) {
|
||||
$this->patientService->autoCreateOnAppointmentConfirm($appointment);
|
||||
$this->appointmentConfirmation->onConfirmed($appointment);
|
||||
}
|
||||
if (in_array($newStatus, self::CANCEL_STATUSES, true)) {
|
||||
$cancelledTo = $newStatus;
|
||||
|
||||
@@ -45,6 +45,7 @@ class MyAppointmentsController extends BaseController
|
||||
private readonly \App\Auth\Repository\UserRepository $userRepo,
|
||||
private readonly \App\UserProfile\Repository\UserProfileRepository $profileRepo,
|
||||
private readonly VisitPriceRequirementResolver $visitPriceResolver,
|
||||
private readonly \App\Appointment\Service\AppointmentConfirmationService $appointmentConfirmation,
|
||||
) {}
|
||||
|
||||
#[Route('/api/v1/my/appointment', methods: ['POST'])]
|
||||
@@ -145,6 +146,7 @@ class MyAppointmentsController extends BaseController
|
||||
// محل نوبت باید از همان محیطی بیاید که نوبت در آن ثبت میشود؛ بدون clinic_uuid
|
||||
// یعنی مطب شخصی، نه «هر برنامهای که پیدا شد».
|
||||
$bookingClinic = $this->bookingContext->resolve($doctor, $data['clinic_uuid'] ?? null);
|
||||
$appointment->setClinic($bookingClinic);
|
||||
$locationId = $this->slotCalculator->resolveSlotLocationId($doctor, $slotStart, $bookingClinic);
|
||||
if ($locationId !== null) $appointment->setAddressId($locationId);
|
||||
|
||||
@@ -184,6 +186,11 @@ class MyAppointmentsController extends BaseController
|
||||
$appointment->setPatientName($patient->getRealName() ?: $patientName);
|
||||
$appointment->setPatientMobile($mobile);
|
||||
|
||||
// نوبتی که خودِ کلینیک/پزشک ثبت میکند پرداخت آنلاین ندارد و منتظر چیزی نیست؛
|
||||
// قطعی است. transitionTo قبل از ذخیره میآید تا active_slot_key با وضعیت نهایی
|
||||
// محاسبه شود.
|
||||
$appointment->transitionTo(Appointment::STATUS_CONFIRMED);
|
||||
|
||||
if ($isReserve) {
|
||||
// Day-level reserve: no slot occupation, plain save (no atomic slot check).
|
||||
$appointment->rescheduleTo($slotStart, $slotEnd, true);
|
||||
@@ -196,6 +203,8 @@ class MyAppointmentsController extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
$this->appointmentConfirmation->onConfirmed($appointment);
|
||||
|
||||
return $this->success([
|
||||
'uuid' => $appointment->getUuid(),
|
||||
'slot_start' => $slotStart,
|
||||
|
||||
@@ -127,6 +127,15 @@ class Appointment
|
||||
#[ORM\Column(name: 'address_id', type: 'integer', nullable: true)]
|
||||
private ?int $addressId = null;
|
||||
|
||||
/**
|
||||
* محیط رزرو: null یعنی مطب شخصی پزشک، مقدار یعنی همان کلینیک. مبنای واحدِ
|
||||
* تشخیص پرونده — از روی آدرس حدس زده نمیشود، چون با چند برنامهٔ همزمان
|
||||
* حدسزدن یعنی چسباندنِ خاموشِ نوبت به پروندهٔ محیط اشتباه.
|
||||
*/
|
||||
#[ORM\ManyToOne(targetEntity: \App\Clinic\Entity\Clinic::class)]
|
||||
#[ORM\JoinColumn(name: 'clinic_id', nullable: true, onDelete: 'SET NULL')]
|
||||
private ?\App\Clinic\Entity\Clinic $clinic = null;
|
||||
|
||||
#[ORM\Column(name: 'booking_representation_id', type: 'integer', nullable: true)]
|
||||
private ?int $bookingRepresentationId = null;
|
||||
|
||||
@@ -217,11 +226,13 @@ class Appointment
|
||||
public function getPatientGender(): ?string { return $this->patientGender; }
|
||||
public function getPatientReason(): ?string { return $this->patientReason; }
|
||||
public function getAddressId(): ?int { return $this->addressId; }
|
||||
public function getClinic(): ?\App\Clinic\Entity\Clinic { return $this->clinic; }
|
||||
public function getBookingRepresentationId(): ?int { return $this->bookingRepresentationId; }
|
||||
|
||||
public function setNote(?string $v): self { $this->note = $v; return $this; }
|
||||
public function setBookingRepresentationId(?int $v): self { $this->bookingRepresentationId = $v; return $this; }
|
||||
public function setAddressId(?int $v): self { $this->addressId = $v; return $this; }
|
||||
public function setClinic(?\App\Clinic\Entity\Clinic $v): self { $this->clinic = $v; return $this; }
|
||||
public function setPatientName(?string $v): self { $this->patientName = $v; return $this; }
|
||||
public function setPatientMobile(?string $v): self { $this->patientMobile = $v; return $this; }
|
||||
public function setPatientNationalCode(?string $v): self { $this->patientNationalCode = $v; return $this; }
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Appointment\Service;
|
||||
|
||||
use App\Appointment\Entity\Appointment;
|
||||
use App\Patient\Service\PatientService;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
/**
|
||||
* عوارض جانبیِ قطعیشدن نوبت، در یک نقطه.
|
||||
*
|
||||
* قطعیشدن پنج مسیر دارد (پرداخت آنلاین، دو مسیر PATCH، رزرو پنل، رزرو ادمین) و
|
||||
* تا امروز فقط دو تای آنها پرونده میساختند — نوبتهای سایت عمومی که با پرداخت
|
||||
* قطعی میشوند هیچوقت پرونده نداشتند. هر مسیر جدیدی هم باید همین را صدا بزند.
|
||||
*/
|
||||
class AppointmentConfirmationService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PatientService $patientService,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* idempotent: فراخوانی دوباره برای همان نوبت چیزی نمیسازد.
|
||||
*
|
||||
* شکست ساخت پرونده نباید قطعیشدن نوبت یا تأیید پرداخت را برگرداند — نوبت
|
||||
* رزرو شده و پول پرداخت شده است؛ پرونده را میشود با
|
||||
* `app:appointment:backfill-sessions` ساخت، ولی رولبکِ پرداخت برگشتناپذیر است.
|
||||
*/
|
||||
public function onConfirmed(Appointment $appointment): void
|
||||
{
|
||||
// نوبت رزروِ روز-محور اسلات و ساعت مشخص ندارد؛ مراجعهٔ زماندار برایش معنا ندارد.
|
||||
if ($appointment->isReserve()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->patientService->autoCreateOnAppointmentConfirm($appointment);
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->error('Auto-creating the patient record on confirm failed', [
|
||||
'appointment_uuid' => $appointment->getUuid(),
|
||||
'exception' => $e,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Patient\Repository;
|
||||
|
||||
use App\Appointment\Entity\Appointment;
|
||||
use App\Patient\Entity\PatientRecord;
|
||||
use App\Patient\Entity\PatientSession;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
@@ -19,6 +20,28 @@ class PatientSessionRepository extends ServiceEntityRepository
|
||||
return $this->findOneBy(['uuid' => $uuid]);
|
||||
}
|
||||
|
||||
/**
|
||||
* مراجعهٔ ساختهشده برای این نوبت در همین محیط، یا null.
|
||||
*
|
||||
* گاردِ ساختِ خودکار: قطعیشدنِ دوباره (confirmed → cancelled → confirmed) نباید
|
||||
* مراجعهٔ تکراری بسازد. مراجعهٔ آرشیوشده هم «ساختهشده» حساب میشود، وگرنه آرشیو
|
||||
* کردنِ یک مراجعهٔ اشتباه باعث ساخت دوبارهاش میشود.
|
||||
*/
|
||||
public function findByAppointmentAndEntity(Appointment $appointment, string $entityType, int $entityId): ?PatientSession
|
||||
{
|
||||
return $this->createQueryBuilder('s')
|
||||
->join('s.record', 'r')
|
||||
->where('s.appointment = :appointment')
|
||||
->andWhere('r.entityType = :entityType')
|
||||
->andWhere('r.entityId = :entityId')
|
||||
->setParameter('appointment', $appointment)
|
||||
->setParameter('entityType', $entityType)
|
||||
->setParameter('entityId', $entityId)
|
||||
->setMaxResults(1)
|
||||
->getQuery()
|
||||
->getOneOrNullResult();
|
||||
}
|
||||
|
||||
/** $filter: all | active | archived. پیشفرض all برای حفظ رفتار callerهای موجود. */
|
||||
public function findByRecord(PatientRecord $record, int $page = 1, int $limit = 20, string $filter = 'all'): array
|
||||
{
|
||||
|
||||
@@ -29,6 +29,7 @@ use App\Shared\Constant\ErrorCodes;
|
||||
use App\Shared\Exception\AppException;
|
||||
use App\Staff\Repository\ClinicStaffRepository;
|
||||
use App\Subscription\Service\SubscriptionService;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class PatientService
|
||||
{
|
||||
@@ -52,6 +53,7 @@ class PatientService
|
||||
private readonly EntityInsurancePricingRepository $pricingRepo,
|
||||
private readonly \App\Discount\Service\DiscountEngine $discountEngine,
|
||||
private readonly \App\Patient\Repository\SessionAuditLogRepository $auditRepo,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {}
|
||||
|
||||
/** ثبت یک رکورد تاریخچهی تغییر مالی/خدماتی روی مراجعه. */
|
||||
@@ -122,34 +124,38 @@ class PatientService
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* پرونده و مراجعهٔ خودکار برای یک نوبت قطعیشده.
|
||||
*
|
||||
* محیط رزرو تعیینکننده است: کلینیک، یا مطب شخصی پزشک — هرگز هر دو. دو پرونده
|
||||
* برای یک نوبت یعنی درآمد یک ویزیت دو بار شمرده میشود.
|
||||
*/
|
||||
public function autoCreateOnAppointmentConfirm(Appointment $appointment): void
|
||||
{
|
||||
$doctor = $appointment->getDoctor();
|
||||
$clinic = $appointment->getClinic();
|
||||
|
||||
// پروندهی پزشک
|
||||
$this->autoCreateForEntity('doctor', $doctor->getId(), $appointment, $doctor->getId());
|
||||
[$entityType, $entityId] = $clinic !== null
|
||||
? ['clinic', (int) $clinic->getId()]
|
||||
: ['doctor', (int) $appointment->getDoctor()->getId()];
|
||||
|
||||
// کلینیک نوبت را تعیین کن: اول از آدرس انتخابشده، وگرنه اگر دکتر فقط عضو یک کلینیک باشد.
|
||||
$clinicId = null;
|
||||
$addressId = $appointment->getAddressId();
|
||||
if ($addressId !== null) {
|
||||
$clinicId = $this->addressRepo->find($addressId)?->getClinicId();
|
||||
}
|
||||
if ($clinicId === null) {
|
||||
$clinics = $this->clinicRepo->findByDoctor($doctor);
|
||||
if (count($clinics) === 1) {
|
||||
$clinicId = $clinics[0]->getId();
|
||||
}
|
||||
}
|
||||
|
||||
if ($clinicId !== null) {
|
||||
$this->autoCreateForEntity('clinic', $clinicId, $appointment, $clinicId);
|
||||
}
|
||||
$this->autoCreateForEntity($entityType, $entityId, $appointment, $entityId);
|
||||
}
|
||||
|
||||
private function autoCreateForEntity(string $entityType, int $entityId, Appointment $appointment, int $createdById): void
|
||||
{
|
||||
if (!$this->subscriptionService->hasFeature($entityType, $entityId, 'patient_records')) {
|
||||
// بهزور پرونده نمیسازیم، ولی بینشانه هم رد نمیشویم: بدون این لاگ،
|
||||
// «چرا این نوبت پرونده ندارد» غیرقابلتشخیص است.
|
||||
$this->logger->info('Skipped auto-creating the patient record: the tenant has no patient_records feature', [
|
||||
'entity_type' => $entityType,
|
||||
'entity_id' => $entityId,
|
||||
'appointment_uuid' => $appointment->getUuid(),
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->sessionRepo->findByAppointmentAndEntity($appointment, $entityType, $entityId) !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ final class PaymentManager
|
||||
private readonly CommissionService $commissionService,
|
||||
private readonly \App\Representation\Service\DomainContextResolver $domainResolver,
|
||||
private readonly JalaliDateService $jalali,
|
||||
private readonly \App\Appointment\Service\AppointmentConfirmationService $appointmentConfirmation,
|
||||
private readonly string $appBaseUrl,
|
||||
) {}
|
||||
|
||||
@@ -311,6 +312,7 @@ final class PaymentManager
|
||||
|
||||
$appointment->transitionTo(Appointment::STATUS_CONFIRMED);
|
||||
$this->em->persist($appointment);
|
||||
$this->appointmentConfirmation->onConfirmed($appointment);
|
||||
|
||||
$doctor = $appointment->getDoctor();
|
||||
$this->commissionService->processAppointment(
|
||||
|
||||
Reference in New Issue
Block a user