# تسک ۰۷ — رزرو موقت و ثبت نهایی چندمنبعی **فاز:** ۱ (هسته) · **وابستگی:** ۰۶ · **زمان:** ۱۶-۲۰ ساعت --- ## هدف مستند بند ۱۱ و قانون سوم جمع‌بندی: «جلوگیری از رزرو تکراری کار دیتابیس است، نه کار کد». سه مرحلهٔ `جستجو → رزرو موقت → ثبت نهایی` روی **چند منبع** پیاده شود، با تضمین یکتایی در سطح دیتابیس. ## وضعیت فعلی — نقطهٔ قوت پروژه ```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` - تست همزمانی واقعی