feat(booking): persist service duration and allow full service replacement
Appointment gains:
- replaceServiceItems(): full replacement that unconditionally syncs the
legacy single serviceItem column. addServiceItem() only fills it when null,
which would leave stale service names in the four consumers that read
service_item (admin lists, public site, desktop app).
- currentServiceUuids(): input-order uuids, falling back to the single column
for appointments created before multi-service support.
- service_total_minutes / service_buffer_minutes (both nullable, NULL in slot
mode). slot_end - slot_start carries the number but cannot say whether it
was intentional, and a reserve entry has slot_start == slot_end so its
duration had nowhere to live.
Existing columns untouched: slot_start, slot_end, active_slot_key, is_reserve
verified unchanged via SHOW COLUMNS.
Task: docs/new_feture/taskes/task-00-service-mode-completion/
Slot-mode contract: unchanged (--group=slot-mode-frozen green)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# چکلیست — تسک ۰۰ (تکمیل نوبتدهی سرویسی در clinicpro)
|
||||
|
||||
**وضعیت کلی:** 🔄 در حال انجام — قابلیت ۱ از ۱۰ (خط سرخ) تمام شد
|
||||
**وضعیت کلی:** 🔄 در حال انجام — قابلیت ۳ از ۱۰ تمام شد (خط سرخ · ServiceBookingCalculator · فیلدهای سرویسی نوبت)
|
||||
**آخرین بازبینی:** ۱۴۰۵/۰۵/۰۸
|
||||
|
||||
قواعد: [_shared/definition-of-done.md](../_shared/definition-of-done.md) ·
|
||||
@@ -37,9 +37,9 @@ UI: [_shared/ui-conventions.md](../_shared/ui-conventions.md)
|
||||
| ۱.۷ | `ReserveConversionService` + `POST /appointment/{uuid}/convert-reserve` | ⏳ | |
|
||||
| ۱.۸ | نوبت رزرو در حالت سرویسی سرویسها را ذخیره میکند | ⏳ | |
|
||||
| ۱.۹ | `excludeAppointmentId` روی `getServiceStartTimes` و `findBusyIntervals` | ⏳ | همان الگوی `isSlotTaken` |
|
||||
| ۱.۱۰ | `Appointment::replaceServiceItems()` + `currentServiceUuids()` | ⏳ | |
|
||||
| ۱.۱۱ | `replaceServiceItems` مقدار `serviceItem` تکی را همگام میکند | ⏳ | چهار مصرفکننده رویش خواندهاند |
|
||||
| ۱.۱۲ | `setIsReserve()` صدا زدن `refreshActiveSlotKey()` | ⏳ | |
|
||||
| ۱.۱۰ | `Appointment::replaceServiceItems()` + `currentServiceUuids()` | ✅ | کنار `addServiceItem()` موجود؛ `currentServiceUuids()` ترتیب ورودی را حفظ میکند و برای نوبت قدیمیِ فقط-تکی هم uuid میدهد |
|
||||
| ۱.۱۱ | `replaceServiceItems` مقدار `serviceItem` تکی را همگام میکند | ✅ | برخلاف `addServiceItem()` که فقط وقتی `null` است ست میکند، این یکی **بیقید** بازنویسی میکند (و با فهرست خالی `null` میشود). چهار مصرفکننده روی `service_item` تکی خواندهاند |
|
||||
| ۱.۱۲ | ~~`setIsReserve()` صدا زدن `refreshActiveSlotKey()`~~ | ✅ | **موردی برای انجام نبود:** `setIsReserve()` وجود ندارد؛ toggle رزرو از قبل با `rescheduleTo($start,$end,$isReserve)` است که خودش `refreshActiveSlotKey()` را صدا میزند |
|
||||
| ۱.۱۳ | اعتبارسنجی زمان با **عضویت در `getServiceStartTimes`**، نه فقط `isSlotTaken` | ⏳ | |
|
||||
| ۱.۱۴ | `TenantOwnershipChecker` روی همهٔ uuid های سرویس، **پیش از** هر بررسی دیگر | ✅ | `belongsToPair()` (نه `belongsTo()`): مسیر عمومی است و کاربر احرازشده ندارد. جای مقایسهٔ دستیِ `getSection()->getEntityType()` نشست — نقطهٔ واحد `tenancy.md`. ⚠️ نام property باید `itemRepo` باشد وگرنه `TenantLookupInventoryTest` آن را **نمیشمارد** و از تور ایمنی فرار میکند |
|
||||
| ۱.۱۵ | `allowInactive` فقط برای سرویسهای موجود نوبت، نه uuid های تازه | ✅ | پرچم روی `calculate()`؛ غیرفعالبودن → `warnings[]` بهجای ۴۲۲. مدتِ نامعتبر همچنان حتی با `allowInactive` رد میشود (تست دارد) |
|
||||
@@ -49,12 +49,12 @@ UI: [_shared/ui-conventions.md](../_shared/ui-conventions.md)
|
||||
|
||||
| # | مورد | وضعیت | یادداشت |
|
||||
|---|---|---|---|
|
||||
| ۲.۱ | `service_total_minutes` و `service_buffer_minutes` (تهیپذیر) | ⏳ | |
|
||||
| ۲.۲ | هیچ ستون موجودی حذف/تغییر نوع/تغییر معنا نداد | ⏳ | |
|
||||
| ۲.۱ | `service_total_minutes` و `service_buffer_minutes` (تهیپذیر) | ✅ | `SMALLINT DEFAULT NULL` هر دو · `migrations/Version20260730091332.php` · `setServiceDuration()` هر دو را با هم ست میکند (بافرِ بیمدت معنا ندارد) |
|
||||
| ۲.۲ | هیچ ستون موجودی حذف/تغییر نوع/تغییر معنا نداد | ✅ | تأیید با `SHOW COLUMNS`: `slot_start` int NOT NULL · `slot_end` int NOT NULL · `active_slot_key` varchar(64) UNI · `is_reserve` tinyint default 0 — همه دستنخورده |
|
||||
| ۲.۳ | دو کد خطای جدید در `ErrorCodes.php` با پیام فارسی | ⏳ | شمارهٔ واقعی از خود فایل |
|
||||
| ۲.۴ | `app:appointment:backfill-service-duration` — dry-run پیشفرض، idempotent | ⏳ | |
|
||||
| ۲.۵ | backfill مقدار را از خود نوبت میگیرد، نه بازمحاسبه از سرویسها | ⏳ | |
|
||||
| ۲.۶ | migration اجرا شد و `TenantSchemaCoverageTest` سبز | ⏳ | |
|
||||
| ۲.۶ | migration اجرا شد و `TenantSchemaCoverageTest` سبز | ✅ | dev با migration؛ **db_test با `ALTER` دستی** — تاریخچهٔ migration آن DB از dev جدا است (`Table users already exists`) و این روش نگهداشتِ موجودِ همان DB است |
|
||||
|
||||
## ۳. UI — پنل ادمین
|
||||
|
||||
@@ -83,13 +83,14 @@ UI: [_shared/ui-conventions.md](../_shared/ui-conventions.md)
|
||||
|
||||
| # | مورد | وضعیت | یادداشت |
|
||||
|---|---|---|---|
|
||||
| ۴.۱ | `SlotModeFrozenTest` — سه سنجه | ⏳ | |
|
||||
| ۴.۱ | `SlotModeFrozenTest` — سه سنجه | ✅ | `OK (3 tests, 8 assertions)` |
|
||||
| ۴.۲ | `ServiceBookingCalculatorTest` — موفق/خطا/مرزی | ✅ | ۱۳ تست / ۲۹ assertion سبز. شامل: جمع مدت + بافر · `endFor` بدون بافر · override منشی بدون تغییر پیشفرض سرویس · چهار مسیر خطا با کد/پیام/فیلد دقیق · uuid ناموجود از سرویسِ محیط دیگر **قابل تفکیک نیست** · `allowInactive` → warning · فهرست خالی → صفر · override نامعتبر (۰ و منفی) → fallback · پزشک بیبرنامه → پیشفرض `slot` |
|
||||
| ۴.۳ | `ServiceRescheduleTest` — شامل «حذف سرویس → مدت خودکار» | ⏳ | |
|
||||
| ۴.۴ | `PatchServiceDurationTest` — شامل «در حالت اسلاتی هیچکدام اجرا نمیشود» | ⏳ | |
|
||||
| ۴.۵ | `ConvertReserveTest` — شامل `active_slot_key` و رقابت | ⏳ | |
|
||||
| ۴.۶ | `ServiceModeSectionDurationTest` موجود سبز ماند | ⏳ | |
|
||||
| ۴.۷ | `BookingTenantTest` موجود سبز ماند | ⏳ | |
|
||||
| ۴.۷ | `BookingTenantTest` موجود سبز ماند | ✅ | داخل `tests/Appointment` — کل ۳۰۹ تست `tests/Appointment` + `tests/Shared` سبز |
|
||||
| ۴.۹ | `AppointmentServiceFieldsTest` — متدها و ستونهای جدید | ✅ | ۹ تست / ۲۴ assertion. شامل: همگامی ستون تکی · حفظ ترتیب · فهرست خالی → `null` · حالت اسلاتی هر دو ستون `null` · مدتِ `null` بافر را هم `null` میکند · تکراریها dedup · نوبت قدیمیِ فقط-تکی · بقای مقادیر پس از flush/clear |
|
||||
| ۴.۸ | `AppointmentEditPage.test.tsx` — دو حالت | ⏳ | |
|
||||
|
||||
## ۵. مستندات
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* حالت نوبتدهی سرویسی: مدت و بافرِ محاسبهشده در لحظهٔ ثبت را روی خودِ نوبت نگه میدارد.
|
||||
*
|
||||
* هر دو ستون تهیپذیرند و در حالت اسلاتی NULL میمانند؛ هیچ ستون موجودی حذف نمیشود،
|
||||
* نوعش عوض نمیشود و معنایش تغییر نمیکند. `slot_start`/`slot_end`/`active_slot_key`
|
||||
* دستنخوردهاند (خط سرخ فاز موتور چندمنبعی).
|
||||
*
|
||||
* @see docs/new_feture/taskes/task-00-service-mode-completion/database.md
|
||||
*/
|
||||
final class Version20260730091332 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add service_total_minutes and service_buffer_minutes to appointments (service booking mode)';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE appointments ADD service_total_minutes SMALLINT DEFAULT NULL, ADD service_buffer_minutes SMALLINT DEFAULT NULL');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE appointments DROP service_total_minutes, DROP service_buffer_minutes');
|
||||
}
|
||||
}
|
||||
@@ -206,6 +206,25 @@ class Appointment
|
||||
#[ORM\Column(name: 'is_reserve', type: 'boolean', options: ['default' => false])]
|
||||
private bool $isReserve = false;
|
||||
|
||||
/**
|
||||
* مدتِ محاسبهشدهٔ ترکیب سرویسها در لحظهٔ ثبت — فقط در حالت نوبتدهی سرویسی.
|
||||
*
|
||||
* `slot_end - slot_start` همین عدد را دارد ولی نمیگوید عمدی بود یا دستی؛ و برای نوبت
|
||||
* رزرو (که `slot_start == slot_end` است) هیچجای دیگری مدت نگهداشته نمیشود، پس تبدیل
|
||||
* رزرو به نوبت زماندار بدون این ستون مدت را از دست میدهد.
|
||||
*
|
||||
* در حالت اسلاتی همیشه NULL میماند.
|
||||
*/
|
||||
#[ORM\Column(name: 'service_total_minutes', type: 'smallint', nullable: true)]
|
||||
private ?int $serviceTotalMinutes = null;
|
||||
|
||||
/**
|
||||
* `buffer_minutes` مؤثر در لحظهٔ ثبت. تغییر بافر در تنظیمات نباید معنای نوبتهای
|
||||
* ثبتشده را عوض کند (قانون پنجم مستند: هر چیزی که ثبت شد همانطور میماند).
|
||||
*/
|
||||
#[ORM\Column(name: 'service_buffer_minutes', type: 'smallint', nullable: true)]
|
||||
private ?int $serviceBufferMinutes = null;
|
||||
|
||||
#[ORM\Column(name: 'created_at', type: 'integer')]
|
||||
private int $createdAt;
|
||||
|
||||
@@ -289,6 +308,51 @@ class Appointment
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* جایگزینی کامل سرویسهای نوبت (برای ویرایش و جابهجایی سرویسآگاه).
|
||||
*
|
||||
* برخلاف {@see addServiceItem()}، ستون تکیِ `serviceItem` را **بیقید** با اولین عضو
|
||||
* همگام میکند: چهار مصرفکننده روی `service_item` تکی خواندهاند (`AppointmentsPage`،
|
||||
* `ReserveAppointmentsPage`، `nobat724_front/services/response.js`،
|
||||
* `clinic-pro-tauri/src/service/response.js`) و رهاکردنش یعنی نوبت با سرویسهای جدید
|
||||
* ولی نامِ سرویس قدیمی در فهرستها. همان الگوی `ServiceItem::setStaffMembers()`.
|
||||
*
|
||||
* @param \App\ClinicService\Entity\ServiceItem[] $items
|
||||
*/
|
||||
public function replaceServiceItems(array $items): self
|
||||
{
|
||||
$this->serviceItems->clear();
|
||||
foreach ($items as $item) {
|
||||
if (!$this->serviceItems->contains($item)) {
|
||||
$this->serviceItems->add($item);
|
||||
}
|
||||
}
|
||||
$this->serviceItem = $items[0] ?? null;
|
||||
$this->updatedAt = time();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* uuid سرویسهای فعلی، به ترتیب. نوبتهای پیش از چند-سرویسیشدن فقط `serviceItem`
|
||||
* تکی دارند، پس آن هم پوشش داده میشود.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function currentServiceUuids(): array
|
||||
{
|
||||
$uuids = array_map(
|
||||
fn(\App\ClinicService\Entity\ServiceItem $i) => $i->getUuid(),
|
||||
$this->serviceItems->toArray(),
|
||||
);
|
||||
|
||||
if ($uuids === [] && $this->serviceItem !== null) {
|
||||
$uuids = [$this->serviceItem->getUuid()];
|
||||
}
|
||||
|
||||
return array_values($uuids);
|
||||
}
|
||||
public function getStaff(): ?\App\Staff\Entity\ClinicStaff { return $this->staff; }
|
||||
public function isDepositRequired(): bool { return $this->depositRequired; }
|
||||
public function getDepositAmountRials(): ?int { return $this->depositAmountRials; }
|
||||
@@ -297,6 +361,18 @@ class Appointment
|
||||
public function getInsuranceBaseId(): ?int { return $this->insuranceBaseId; }
|
||||
public function getInsuranceSupplementaryId(): ?int { return $this->insuranceSupplementaryId; }
|
||||
public function isReserve(): bool { return $this->isReserve; }
|
||||
public function getServiceTotalMinutes(): ?int { return $this->serviceTotalMinutes; }
|
||||
public function getServiceBufferMinutes(): ?int { return $this->serviceBufferMinutes; }
|
||||
|
||||
/** هر دو با هم ست میشوند: مدت بیبافر و بافر بیمدت هیچکدام معنا ندارند. */
|
||||
public function setServiceDuration(?int $totalMinutes, ?int $bufferMinutes): self
|
||||
{
|
||||
$this->serviceTotalMinutes = $totalMinutes;
|
||||
$this->serviceBufferMinutes = $totalMinutes === null ? null : $bufferMinutes;
|
||||
$this->updatedAt = time();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setServiceSection(?\App\ClinicService\Entity\ServiceSection $v): self { $this->serviceSection = $v; return $this; }
|
||||
public function setServiceItem(?\App\ClinicService\Entity\ServiceItem $v): self { $this->serviceItem = $v; return $this; }
|
||||
@@ -430,6 +506,9 @@ class Appointment
|
||||
'insurance_base_id' => $this->insuranceBaseId,
|
||||
'insurance_supplementary_id' => $this->insuranceSupplementaryId,
|
||||
'is_reserve' => $this->isReserve,
|
||||
// فقط در حالت نوبتدهی سرویسی پر میشوند؛ در حالت اسلاتی null.
|
||||
'service_total_minutes' => $this->serviceTotalMinutes,
|
||||
'service_buffer_minutes' => $this->serviceBufferMinutes,
|
||||
'version' => $this->version,
|
||||
'created_at' => $this->createdAt,
|
||||
'updated_at' => $this->updatedAt,
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Appointment;
|
||||
|
||||
use App\ClinicService\Entity\ServiceItem;
|
||||
use App\ClinicService\Entity\ServiceSection;
|
||||
use App\Doctor\Entity\Doctor;
|
||||
use App\Tests\ApiTestCase;
|
||||
|
||||
/**
|
||||
* فیلدهای سرویسیِ نوبت: جایگزینی کامل سرویسها، همگامی ستون تکیِ `serviceItem`، و
|
||||
* مدت/بافرِ ثبتشده در لحظهٔ ثبت.
|
||||
*/
|
||||
class AppointmentServiceFieldsTest extends ApiTestCase
|
||||
{
|
||||
private function doctorWithServices(int $count): array
|
||||
{
|
||||
$owner = $this->createUser(['ROLE_DOCTOR']);
|
||||
$doctor = new Doctor($owner, 'دکتر فیلدهای سرویسی');
|
||||
$this->em->persist($doctor);
|
||||
// بخش به شناسهٔ پزشک نیاز دارد، پس پزشک باید پیش از ساختش flush شود.
|
||||
$this->em->flush();
|
||||
|
||||
$section = new ServiceSection('doctor', $doctor->getId(), 'بخش تست');
|
||||
$this->em->persist($section);
|
||||
|
||||
$items = [];
|
||||
for ($i = 1; $i <= $count; $i++) {
|
||||
$item = new ServiceItem($section, 'سرویس ' . $i, 0);
|
||||
$item->setDurationMinutes(10 * $i)->setBookable(true);
|
||||
$this->em->persist($item);
|
||||
$items[] = $item;
|
||||
}
|
||||
$this->em->flush();
|
||||
|
||||
return [$doctor, $items];
|
||||
}
|
||||
|
||||
private function appointmentFor(Doctor $doctor): \App\Appointment\Entity\Appointment
|
||||
{
|
||||
$patient = $this->createUser(['ROLE_USER']);
|
||||
$start = strtotime('+5 days 10:00');
|
||||
|
||||
return $this->newAppointment($doctor, $patient, $start, $start + 1800);
|
||||
}
|
||||
|
||||
// ── ✅ موفق ──────────────────────────────────────────────────────────────
|
||||
|
||||
public function testReplaceServiceItemsSyncsTheLegacySingleColumn(): void
|
||||
{
|
||||
[$doctor, $items] = $this->doctorWithServices(3);
|
||||
$appt = $this->appointmentFor($doctor);
|
||||
|
||||
$appt->replaceServiceItems([$items[0], $items[1]]);
|
||||
self::assertCount(2, $appt->getServiceItems());
|
||||
self::assertSame($items[0], $appt->getServiceItem(), 'سرویس تکی = اولین عضو');
|
||||
|
||||
// جایگزینی کامل: اعضای قبلی میروند و تکی هم بهروز میشود.
|
||||
$appt->replaceServiceItems([$items[2]]);
|
||||
self::assertCount(1, $appt->getServiceItems());
|
||||
self::assertSame($items[2], $appt->getServiceItem(), 'تکی باید با عضو جدید همگام شود');
|
||||
}
|
||||
|
||||
public function testCurrentServiceUuidsKeepsInputOrder(): void
|
||||
{
|
||||
[$doctor, $items] = $this->doctorWithServices(3);
|
||||
$appt = $this->appointmentFor($doctor);
|
||||
|
||||
$appt->replaceServiceItems([$items[2], $items[0], $items[1]]);
|
||||
|
||||
self::assertSame(
|
||||
[$items[2]->getUuid(), $items[0]->getUuid(), $items[1]->getUuid()],
|
||||
$appt->currentServiceUuids(),
|
||||
);
|
||||
}
|
||||
|
||||
public function testSetServiceDurationIsExposedInToArray(): void
|
||||
{
|
||||
[$doctor] = $this->doctorWithServices(1);
|
||||
$appt = $this->appointmentFor($doctor);
|
||||
|
||||
$appt->setServiceDuration(45, 10);
|
||||
|
||||
self::assertSame(45, $appt->getServiceTotalMinutes());
|
||||
self::assertSame(10, $appt->getServiceBufferMinutes());
|
||||
|
||||
$array = $appt->toArray();
|
||||
self::assertSame(45, $array['service_total_minutes']);
|
||||
self::assertSame(10, $array['service_buffer_minutes']);
|
||||
}
|
||||
|
||||
// ── ❌ خطا / مسیر نادرست ─────────────────────────────────────────────────
|
||||
|
||||
public function testReplaceWithEmptyListClearsTheLegacySingleColumn(): void
|
||||
{
|
||||
[$doctor, $items] = $this->doctorWithServices(2);
|
||||
$appt = $this->appointmentFor($doctor);
|
||||
$appt->replaceServiceItems([$items[0]]);
|
||||
|
||||
$appt->replaceServiceItems([]);
|
||||
|
||||
self::assertCount(0, $appt->getServiceItems());
|
||||
self::assertNull($appt->getServiceItem(), 'سرویس تکی نباید یادگارِ انتخاب قبلی بماند');
|
||||
self::assertSame([], $appt->currentServiceUuids());
|
||||
}
|
||||
|
||||
// ── ⚠️ مرزی ──────────────────────────────────────────────────────────────
|
||||
|
||||
public function testSlotModeAppointmentKeepsBothColumnsNull(): void
|
||||
{
|
||||
[$doctor] = $this->doctorWithServices(1);
|
||||
$appt = $this->appointmentFor($doctor);
|
||||
|
||||
self::assertNull($appt->getServiceTotalMinutes());
|
||||
self::assertNull($appt->getServiceBufferMinutes());
|
||||
self::assertNull($appt->toArray()['service_total_minutes']);
|
||||
}
|
||||
|
||||
public function testClearingDurationAlsoClearsBuffer(): void
|
||||
{
|
||||
[$doctor] = $this->doctorWithServices(1);
|
||||
$appt = $this->appointmentFor($doctor);
|
||||
$appt->setServiceDuration(45, 10);
|
||||
|
||||
// بافرِ بیمدت معنا ندارد؛ حتی اگر مقدار بافر پاس داده شود باید NULL شود.
|
||||
$appt->setServiceDuration(null, 10);
|
||||
|
||||
self::assertNull($appt->getServiceTotalMinutes());
|
||||
self::assertNull($appt->getServiceBufferMinutes());
|
||||
}
|
||||
|
||||
public function testDuplicateItemsInReplaceAreDeduplicated(): void
|
||||
{
|
||||
[$doctor, $items] = $this->doctorWithServices(1);
|
||||
$appt = $this->appointmentFor($doctor);
|
||||
|
||||
$appt->replaceServiceItems([$items[0], $items[0]]);
|
||||
|
||||
self::assertCount(1, $appt->getServiceItems());
|
||||
}
|
||||
|
||||
public function testLegacyAppointmentWithOnlySingleItemStillReportsItsUuid(): void
|
||||
{
|
||||
[$doctor, $items] = $this->doctorWithServices(1);
|
||||
$appt = $this->appointmentFor($doctor);
|
||||
|
||||
// نوبت پیش از چند-سرویسیشدن: فقط ستون تکی پر است.
|
||||
$appt->setServiceItem($items[0]);
|
||||
|
||||
self::assertCount(0, $appt->getServiceItems());
|
||||
self::assertSame([$items[0]->getUuid()], $appt->currentServiceUuids());
|
||||
}
|
||||
|
||||
public function testReplacePersistsThroughFlush(): void
|
||||
{
|
||||
[$doctor, $items] = $this->doctorWithServices(2);
|
||||
$appt = $this->appointmentFor($doctor);
|
||||
$appt->replaceServiceItems([$items[0], $items[1]])->setServiceDuration(30, 5);
|
||||
$this->em->persist($appt);
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$reloaded = static::getContainer()
|
||||
->get(\App\Appointment\Repository\AppointmentRepository::class)
|
||||
->findByUuid($appt->getUuid());
|
||||
|
||||
self::assertNotNull($reloaded);
|
||||
self::assertCount(2, $reloaded->getServiceItems());
|
||||
self::assertSame(30, $reloaded->getServiceTotalMinutes());
|
||||
self::assertSame(5, $reloaded->getServiceBufferMinutes());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user