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:
hamed
2026-07-18 16:55:39 +03:30
co-authored by Claude Opus 4.8
parent 7baa4df3d4
commit e6422014d1
15 changed files with 995 additions and 28 deletions
+25 -19
View File
@@ -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;
}