feat: implement cancellation policy, no-show tracking, and waitlist management

- Add implementation notes for cancellation and waitlist features.
- Create task documentation outlining goals, current status, and acceptance criteria for cancellation policy and resource utilization reporting.
- Establish architecture for domain events and outbox pattern to ensure reliable event publishing.
- Define database schema for domain events and necessary queries for resource utilization and plan accuracy reports.
- Implement detailed implementation notes covering edge cases, testing strategies, and documentation requirements.
This commit is contained in:
hamed
2026-07-30 11:43:58 +03:30
parent 1d338503c8
commit 021d0eb6b2
62 changed files with 8098 additions and 0 deletions
@@ -0,0 +1,81 @@
# تسک ۰۷ — رزرو موقت و ثبت نهایی چندمنبعی
**فاز:** ۱ (هسته) · **وابستگی:** ۰۶ · **زمان:** ۱۶-۲۰ ساعت
---
## هدف
مستند بند ۱۱ و قانون سوم جمع‌بندی: «جلوگیری از رزرو تکراری کار دیتابیس است، نه کار کد».
سه مرحلهٔ `جستجو → رزرو موقت → ثبت نهایی` روی **چند منبع** پیاده شود، با تضمین یکتایی
در سطح دیتابیس.
## وضعیت فعلی — نقطهٔ قوت پروژه
```php
// src/Appointment/Entity/Appointment.php
public const PAYMENT_TTL = 900;
#[ORM\Column(name: 'active_slot_key', length: 64, nullable: true, unique: true)]
private ?string $activeSlotKey = null; // "{doctorId}:{slotStart}" یا NULL
private function refreshActiveSlotKey(): void {
$this->activeSlotKey = !$this->isReserve && in_array($this->status, self::SLOT_OCCUPYING_STATUSES, true)
? sprintf('%d:%d', $this->doctor->getId(), $this->slotStart)
: null;
}
```
سه مرحله و تضمین دیتابیسی **از قبل درست پیاده شده‌اند**. محدودیت: کلید فقط
`doctor + slot_start` است و هیچ منبع دیگری را نمی‌پوشاند، و مدل «یک ردیف = یک بازهٔ پیوسته»
است.
## دامنه
**هست:**
- `resource_occupancy` — تنها مرجع حقیقت اشغال منابع
- `appointment_segments` — بخش‌های نوبت ثبت‌شده
- `HoldService` — رزرو موقت چندمنبعی با TTL
- `BookingService` — ثبت نهایی اتمی
- تضمین یکتایی در MariaDB (بدون `EXCLUDE` — راه‌حل «سطل زمانی»)
- انقضای خودکار hold ها (توسعهٔ `ExpireAppointmentsHandler` موجود)
- وضعیت `rescheduled` و رویداد جابه‌جایی
**نیست:** قیمت‌گذاری تفکیک‌شده (تسک ۰۸)، سیاست لغو و جریمه (تسک ۱۳).
## Endpoint ها
| متد | مسیر | توضیح |
|---|---|---|
| POST | `/api/v1/appointment-hold` | رزرو موقت یک زمان + منابعش |
| DELETE | `/api/v1/appointment-hold/{uuid}` | آزادسازی زودهنگام |
| POST | `/api/v1/appointment-confirm` | ثبت نهایی از یک hold معتبر |
| POST | `/api/v1/appointment/{uuid}/reschedule` | جابه‌جایی (hold جدید + آزادسازی قدیم، اتمی) |
## معیار پذیرش
- ✅ موفق: `POST /appointment-hold` با زمان و `assignment` معتبر → `201` با
`hold_uuid` و `expires_at`؛ ردیف‌های `resource_occupancy` با `status='hold'` برای
**هر بخش × هر منبع** ثبت می‌شوند.
- ✅ موفق: بلافاصله بعد از hold، `POST /appointment-availability` همان زمان را **برنمی‌گرداند**.
- ✅ موفق: `POST /appointment-confirm``200`، وضعیت `confirmed`، همهٔ ردیف‌های اشغال
`status='booked'` و `expires_at = NULL`.
- ✅ موفق (**تست همزمانی — اصلی‌ترین**): دو درخواست hold هم‌زمان روی همان منبع و بازه →
دقیقاً **یکی** `201` و دیگری `409` با `ERR_SLOT_TAKEN`. تست باید با تراکنش واقعی
موازی اجرا شود، نه mock.
- ✅ موفق: hold منقضی‌شده → `POST /appointment-confirm` با `409 ERR_HOLD_EXPIRED` و آن
زمان دوباره در جستجو ظاهر می‌شود.
- ✅ موفق: آزادسازی ظرفیت حفظ می‌شود — اپراتور در بخش انتظار ردیف اشغال **ندارد**.
- ❌ خطا: `confirm` با hold متعلق به کاربر دیگر → `404`.
- ❌ خطا: hold با منبعی که در `assignment` نیست ولی نیازمندی دارد → `422`.
- ⚠️ مرزی: منبع با `capacity=3` → سه hold هم‌زمان موفق، چهارمی `409`.
- ⚠️ مرزی: لغو نوبت → همهٔ ردیف‌های اشغالش آزاد (`status='released'`)، نه حذف فیزیکی.
- ⚠️ مرزی: `reschedule` که hold جدیدش شکست بخورد → نوبت قدیمی **دست‌نخورده** بماند.
- ⚠️ مرزی: نوبت‌های حالت `slot`/`service``active_slot_key` قدیمی همچنان کار می‌کند و
ردیف اشغال هم برایشان ساخته می‌شود (تور ایمنی دوگانه).
## خروجی
- `src/Appointment/Booking/`
- توسعهٔ `Appointment` entity با `segments` و رابطهٔ اشغال
- `docs/api/appointment-booking.md` + به‌روزرسانی `docs/api/appointment.md`
- تست همزمانی واقعی