Three rows of task 13 were storing data nothing ever read. `preferred_day_parts` was saved and displayed but never applied when matching. It was deferred because "evening" has no fixed meaning — but branches already carry a timezone (DoctorAddress::getTimezone), so the boundaries can be pinned: morning [6,12), afternoon [12,17), evening [17,22), in the branch's local hour. The list is now closed and validated; an unknown part is a 422 rather than a preference that silently matches nothing. The filter runs *before* the cut to ten recipients — otherwise the first ten slots go to people who did not want that hour and the real eleventh person is never told. `markConverted()` was dead code: nothing called it. It now runs off the AppointmentBooked domain event rather than from inside BookingService, because converting is a side effect of booking — inside the booking transaction a waitlist error could roll back the patient's actual appointment. The match is deliberately narrow (same patient, same service, start inside the window); a loose match closes a row the patient is still waiting on. It is idempotent, so redelivery is harmless. Expiry now exists as a service, a daily scheduled message and `app:waitlist:expire`. Expired rows were already excluded from matching, so this is display hygiene, not a behaviour fix: without it the waitlist page fills with dead entries and the operator cannot tell which are still live. It sets a status rather than deleting — who waited and never got a slot is data. Also: a waitlist window is capped at 90 days, matching the booking horizon. An unbounded window is a row that never expires and shows up in every match. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
50 lines
1.7 KiB
PHP
50 lines
1.7 KiB
PHP
<?php
|
|
|
|
namespace App\Waitlist\MessageHandler;
|
|
|
|
use App\Appointment\Entity\Appointment;
|
|
use App\Shared\Event\DomainEvents;
|
|
use App\Shared\Event\Message\DomainEventMessage;
|
|
use App\Waitlist\Service\WaitlistConverter;
|
|
use Doctrine\ORM\EntityManagerInterface;
|
|
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
|
|
|
/**
|
|
* وقتی بیمار بالاخره رزرو کرد، ردیف انتظارش `converted` میشود.
|
|
*
|
|
* از رویداد میآید نه از خودِ `BookingService`: تبدیل، اثر جانبیِ رزرو است نه بخشی از
|
|
* آن، و اگر داخل تراکنش رزرو مینشست یک خطای لیست انتظار میتوانست نوبت واقعی بیمار
|
|
* را برگرداند.
|
|
*
|
|
* **idempotent** است: `WaitlistConverter` ردیفِ از قبل تبدیلشده را رد میکند، پس تحویل
|
|
* دوبارهٔ پیام چیزی را خراب نمیکند.
|
|
*/
|
|
#[AsMessageHandler]
|
|
final class WaitlistConversionHandler
|
|
{
|
|
public function __construct(
|
|
private readonly WaitlistConverter $converter,
|
|
private readonly EntityManagerInterface $em,
|
|
) {}
|
|
|
|
public function __invoke(DomainEventMessage $message): void
|
|
{
|
|
if ($message->name !== DomainEvents::APPOINTMENT_BOOKED) {
|
|
return;
|
|
}
|
|
|
|
$uuid = $message->payload['appointment_uuid'] ?? null;
|
|
|
|
if (!is_string($uuid)) {
|
|
return;
|
|
}
|
|
|
|
$appointment = $this->em->getRepository(Appointment::class)->findOneBy(['uuid' => $uuid]);
|
|
|
|
// نوبتِ لغوشده بین انتشار و مصرف: چیزی برای تبدیل نمانده.
|
|
if ($appointment !== null) {
|
|
$this->converter->convertFor($appointment);
|
|
}
|
|
}
|
|
}
|