confirmWithPayments — the path behind POST /appointment/{uuid}/confirm, which
is how a secretary actually confirms — created the patient session but never
called TreatmentCaseStarter. Only onConfirmed did. So an appointment on a
service with an active protocol was confirmed and paid, and no treatment case
or sessions were ever created; the staff panel had nothing to list.
Every existing test in OpenCaseOnConfirmTest drove onConfirmed, which is why
the gap survived. Added one that drives confirmWithPayments; it fails without
the fix.
Also adds app:treatment:backfill-cases, mirroring
app:appointment:backfill-sessions: it reports confirmed appointments on a
protocol service that have no case, and with --fix replays the starter and
prints the exception the logger would otherwise keep to itself.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
149 lines
6.9 KiB
PHP
149 lines
6.9 KiB
PHP
<?php
|
|
|
|
namespace App\Appointment\Service;
|
|
|
|
use App\Appointment\Entity\Appointment;
|
|
use App\Appointment\Repository\AppointmentRepository;
|
|
use App\Auth\Entity\User;
|
|
use App\Patient\Entity\PatientSession;
|
|
use App\Patient\Service\PatientService;
|
|
use App\Payment\Repository\PaymentRepository;
|
|
use App\Representation\Service\DomainContextResolver;
|
|
use App\Settlement\Service\CommissionService;
|
|
use App\Shared\Constant\ErrorCodes;
|
|
use App\Shared\Exception\AppException;
|
|
use Doctrine\ORM\EntityManagerInterface;
|
|
use Psr\Log\LoggerInterface;
|
|
|
|
/**
|
|
* عوارض جانبیِ قطعیشدن نوبت، در یک نقطه.
|
|
*
|
|
* قطعیشدن پنج مسیر دارد (پرداخت آنلاین، دو مسیر PATCH، رزرو پنل، رزرو ادمین) و
|
|
* تا امروز فقط دو تای آنها پرونده میساختند — نوبتهای سایت عمومی که با پرداخت
|
|
* قطعی میشوند هیچوقت پرونده نداشتند. هر مسیر جدیدی هم باید همین را صدا بزند.
|
|
*/
|
|
class AppointmentConfirmationService
|
|
{
|
|
public function __construct(
|
|
private readonly PatientService $patientService,
|
|
private readonly AppointmentRepository $appointmentRepo,
|
|
private readonly PaymentRepository $paymentRepo,
|
|
private readonly CommissionService $commissionService,
|
|
private readonly DomainContextResolver $domainResolver,
|
|
private readonly \App\Treatment\Service\TreatmentCaseStarter $treatmentCases,
|
|
private readonly EntityManagerInterface $em,
|
|
private readonly LoggerInterface $logger,
|
|
) {}
|
|
|
|
/**
|
|
* تقسیم مالیِ نوبت آنلاین در لحظهٔ **تأیید** انجام میشود، نه لحظهٔ پرداخت: تا وقتی
|
|
* نوبت قطعی نشده، پورسانت نماینده و سهم منشی هم اعتبار نمیشوند. برای نوبتی که
|
|
* پرداخت آنلاین ندارد (ثبتشده در پنل) کاری انجام نمیشود. ثبت idempotent است.
|
|
*/
|
|
private function splitPaymentShares(Appointment $appointment): void
|
|
{
|
|
$payment = $this->paymentRepo->findSuccessfulByAppointment($appointment);
|
|
if ($payment === null) {
|
|
return;
|
|
}
|
|
|
|
$doctor = $appointment->getDoctor();
|
|
$this->commissionService->processAppointment(
|
|
$payment,
|
|
$doctor->getRepresentationId(),
|
|
$this->domainResolver->resolve($payment->getFrontendAddress())->representationId(),
|
|
$doctor->getId(),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* idempotent: فراخوانی دوباره برای همان نوبت چیزی نمیسازد.
|
|
*
|
|
* شکست ساخت پرونده نباید قطعیشدن نوبت یا تأیید پرداخت را برگرداند — نوبت
|
|
* رزرو شده و پول پرداخت شده است؛ پرونده را میشود با
|
|
* `app:appointment:backfill-sessions` ساخت، ولی رولبکِ پرداخت برگشتناپذیر است.
|
|
*/
|
|
public function onConfirmed(Appointment $appointment): ?PatientSession
|
|
{
|
|
// نوبت رزروِ روز-محور اسلات و ساعت مشخص ندارد؛ مراجعهٔ زماندار برایش معنا ندارد.
|
|
if ($appointment->isReserve()) {
|
|
return null;
|
|
}
|
|
|
|
$this->splitPaymentShares($appointment);
|
|
|
|
try {
|
|
$session = $this->patientService->autoCreateOnAppointmentConfirm($appointment);
|
|
|
|
// هسته نمیداند دورهٔ درمان چیست: اگر سرویسِ نوبت پروتکل داشته باشد،
|
|
// workflowِ حوزهٔ فعالیت تصمیم میگیرد چه اتفاقی بیفتد.
|
|
if ($session !== null) {
|
|
$this->treatmentCases->onAppointmentConfirmed($appointment, $session);
|
|
}
|
|
|
|
return $session;
|
|
} catch (\Throwable $e) {
|
|
$this->logger->error('Auto-creating the patient record on confirm failed', [
|
|
'appointment_uuid' => $appointment->getUuid(),
|
|
'exception' => $e,
|
|
]);
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* قطعیکردنِ صریح از پنل: انتقال وضعیت، ثبت پرونده/مراجعه و ثبت پرداختها — همه
|
|
* در یک تراکنش. برخلاف onConfirmed اینجا شکست خاموش نمیماند: کاربر روبهروی
|
|
* مودالی ایستاده که مبلغ نشان داده و منتظر تأیید است؛ «قطعی شد ولی پول ثبت نشد»
|
|
* بدترین خروجیِ ممکن است.
|
|
*
|
|
* @param array<int, array{method: string, amount_rials: int, payment_method_uuid?: ?string, reference?: ?string}> $payments
|
|
* @return PatientSession|null null یعنی این tenant قابلیت پرونده را ندارد
|
|
* (فقط وقتی مجاز است که پرداختی هم ارسال نشده باشد)
|
|
*/
|
|
public function confirmWithPayments(
|
|
Appointment $appointment,
|
|
int $expectedVersion,
|
|
array $payments,
|
|
User $actor,
|
|
): ?PatientSession {
|
|
return $this->em->wrapInTransaction(function () use ($appointment, $expectedVersion, $payments, $actor) {
|
|
$appointment->transitionTo(Appointment::STATUS_CONFIRMED);
|
|
$this->appointmentRepo->saveWithLock($appointment, $expectedVersion);
|
|
|
|
$this->splitPaymentShares($appointment);
|
|
|
|
$session = $this->patientService->autoCreateOnAppointmentConfirm($appointment);
|
|
|
|
if ($session === null) {
|
|
if ($payments !== []) {
|
|
throw new AppException(ErrorCodes::ERR_SUBSCRIPTION_REQUIRED, null, 403);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
foreach ($payments as $payment) {
|
|
$this->patientService->addSessionPayment(
|
|
$session,
|
|
$payment['method'],
|
|
$payment['amount_rials'],
|
|
null,
|
|
$actor,
|
|
$payment['payment_method_uuid'] ?? null,
|
|
$payment['reference'] ?? null,
|
|
);
|
|
}
|
|
|
|
// همان کاری که onConfirmed میکند. نبودنش یعنی نوبتی که از پنل و با
|
|
// پرداخت قطعی شده — یعنی مسیر عادیِ منشی — هرگز پروندهٔ درمان نمیگیرد و
|
|
// جلسهای هم ساخته نمیشود که در پنل پرسنل دیده شود.
|
|
// starter خودش خطا را میگیرد و لاگ میکند، پس تراکنش پرداخت را نمیشکند.
|
|
$this->treatmentCases->onAppointmentConfirmed($appointment, $session);
|
|
|
|
return $session;
|
|
});
|
|
}
|
|
}
|