Record which resource an appointment was booked for, and freeze its numbers

An appointment could say which services it was for but not which resource
performed them, so a booking on laser #2 was indistinguishable from one on
laser #1. Both columns are nullable: the appointments that already exist have
no resource and the migration must not break them.

resource_id is not a duplicate of resource_occupancy. Occupancy records what
was held and when — including rooms and devices held for a single segment. This
column records what the appointment is *for*, which is what the panel lists and
what the patient chose.

The option is kept separately from service_item because duration and price
resolve from the resource+service+option triple; without knowing the option,
the stored number cannot be explained later.

Tests: the resource and option survive a round-trip, stored minutes come from
the resolver rather than the service default (15 where the service says 30),
raising the tariff afterwards leaves the earlier snapshot at 8M, and an
appointment with no resource still serialises with nulls instead of failing.

Suite 1290 green, phpstan at its 14-error baseline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
hamed
2026-08-01 21:48:45 +03:30
co-authored by Claude Opus 5
parent 6d7c54508c
commit 0a2ba88808
4 changed files with 295 additions and 7 deletions
+51
View File
@@ -164,6 +164,27 @@ class Appointment
#[ORM\JoinColumn(name: 'service_item_id', nullable: true, onDelete: 'SET NULL')]
private ?\App\ClinicService\Entity\ServiceItem $serviceItem = null;
/**
* منبعی که این نوبت رویش رزرو شده — دستگاه، اتاق، یا خودِ پزشک به‌عنوان منبع.
*
* تهی‌پذیر چون نوبت‌های پیش از مدل منبع‌محور منبعی ندارند و migration نباید آن‌ها
* را بشکند. اشغالِ واقعیِ منابع همچنان در `resource_occupancy` است؛ این ستون
* می‌گوید نوبت **برای** کدام منبع گرفته شده، نه اینکه چه چیزهایی اشغال شده‌اند.
*/
#[ORM\ManyToOne(targetEntity: \App\Resource\Entity\ClinicResource::class)]
#[ORM\JoinColumn(name: 'resource_id', nullable: true, onDelete: 'SET NULL')]
private ?\App\Resource\Entity\ClinicResource $resource = null;
/**
* گزینهٔ سرویس («لیزر پا» زیر «لیزر») — همان `ServiceItem` عضو گروه.
*
* جدا از `serviceItem` نگه داشته می‌شود چون مدت و قیمت از ترکیب منبع+سرویس+گزینه
* حل می‌شوند و بدون دانستن گزینه، بازتولید همان عدد ممکن نیست.
*/
#[ORM\ManyToOne(targetEntity: \App\ClinicService\Entity\ServiceItem::class)]
#[ORM\JoinColumn(name: 'service_option_item_id', nullable: true, onDelete: 'SET NULL')]
private ?\App\ClinicService\Entity\ServiceItem $serviceOptionItem = null;
/** سرویس‌های نوبت — امکان انتخاب چند سرویس. serviceItem بالا همان سرویسِ اول است. */
#[ORM\ManyToMany(targetEntity: \App\ClinicService\Entity\ServiceItem::class)]
#[ORM\JoinTable(name: 'appointment_service_items')]
@@ -296,6 +317,26 @@ class Appointment
public function getServiceSection(): ?\App\ClinicService\Entity\ServiceSection { return $this->serviceSection; }
public function getServiceItem(): ?\App\ClinicService\Entity\ServiceItem { return $this->serviceItem; }
public function getResource(): ?\App\Resource\Entity\ClinicResource { return $this->resource; }
public function setResource(?\App\Resource\Entity\ClinicResource $v): self
{
$this->resource = $v;
$this->updatedAt = time();
return $this;
}
public function getServiceOptionItem(): ?\App\ClinicService\Entity\ServiceItem { return $this->serviceOptionItem; }
public function setServiceOptionItem(?\App\ClinicService\Entity\ServiceItem $v): self
{
$this->serviceOptionItem = $v;
$this->updatedAt = time();
return $this;
}
/** @return Collection<int,\App\ClinicService\Entity\ServiceItem> */
public function getServiceItems(): Collection { return $this->serviceItems; }
@@ -512,6 +553,16 @@ class Appointment
'insurance_base_id' => $this->insuranceBaseId,
'insurance_supplementary_id' => $this->insuranceSupplementaryId,
'is_reserve' => $this->isReserve,
// نوبت‌های پیش از مدل منبع‌محور منبع ندارند؛ کلاینت باید با null کنار بیاید.
'resource' => $this->resource === null ? null : [
'uuid' => $this->resource->getUuid(),
'name' => $this->resource->getName(),
'type' => $this->resource->getType()->getCode(),
],
'service_option' => $this->serviceOptionItem === null ? null : [
'uuid' => $this->serviceOptionItem->getUuid(),
'name' => $this->serviceOptionItem->getName(),
],
// فقط در حالت نوبت‌دهی سرویسی پر می‌شوند؛ در حالت اسلاتی null.
'service_total_minutes' => $this->serviceTotalMinutes,
'service_buffer_minutes' => $this->serviceBufferMinutes,