feat(waitlist): make the day-part preference, the conversion and the expiry real
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>
This commit is contained in:
@@ -26,6 +26,9 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
class WaitlistController extends BaseController
|
||||
{
|
||||
/** همان افق رزرو تسک ۱۲؛ بازهٔ بلندتر یعنی ردیفی که هرگز خودش را پاک نمیکند. */
|
||||
private const MAX_RANGE_DAYS = 90;
|
||||
|
||||
public function __construct(
|
||||
private readonly WaitlistEntryRepository $entries,
|
||||
private readonly ServiceItemRepository $items,
|
||||
@@ -79,6 +82,17 @@ class WaitlistController extends BaseController
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'پایان بازه باید بعد از شروع آن باشد', 422, 'desired_to');
|
||||
}
|
||||
|
||||
// بازهٔ باز تا ابد یعنی ردیفی که هرگز منقضی نمیشود و برای همیشه در هر تطبیقی
|
||||
// میآید؛ سقف همان افق رزرو است.
|
||||
if ($to - $from > self::MAX_RANGE_DAYS * 86400) {
|
||||
return $this->error(
|
||||
ErrorCodes::ERR_VALIDATION_001,
|
||||
sprintf('بازهٔ انتظار حداکثر %d روز است', self::MAX_RANGE_DAYS),
|
||||
422,
|
||||
'desired_to',
|
||||
);
|
||||
}
|
||||
|
||||
$patient = $this->requirePatient($user, $data['patient_uuid']);
|
||||
$service = $this->requireItem($user, $data['service_uuid']);
|
||||
|
||||
@@ -91,7 +105,16 @@ class WaitlistController extends BaseController
|
||||
$entry = new WaitlistEntry($patient, $service, $from, $to, $branchId);
|
||||
|
||||
if (is_array($data['preferred_day_parts'] ?? null)) {
|
||||
$entry->setPreferredDayParts($data['preferred_day_parts']);
|
||||
try {
|
||||
$entry->setPreferredDayParts($data['preferred_day_parts']);
|
||||
} catch (\InvalidArgumentException) {
|
||||
return $this->error(
|
||||
ErrorCodes::ERR_VALIDATION_001,
|
||||
sprintf('بخش روز باید یکی از اینها باشد: %s', implode('، ', array_keys(WaitlistEntry::DAY_PARTS))),
|
||||
422,
|
||||
'preferred_day_parts',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (is_numeric($data['priority'] ?? null)) {
|
||||
|
||||
Reference in New Issue
Block a user