# تسک ۰۷ — رزرو موقت و ثبت نهایی چندمنبعی

**فاز:** ۱ (هسته) · **وابستگی:** ۰۶ · **زمان:** ۱۶-۲۰ ساعت

---

## هدف

مستند بند ۱۱ و قانون سوم جمع‌بندی: «جلوگیری از رزرو تکراری کار دیتابیس است، نه کار کد».
سه مرحلهٔ `جستجو → رزرو موقت → ثبت نهایی` روی **چند منبع** پیاده شود، با تضمین یکتایی
در سطح دیتابیس.

## وضعیت فعلی — نقطهٔ قوت پروژه

```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`
- تست همزمانی واقعی
