perf(reports): read every resource's calendar in one batch, and close the owed tests

Writing the query-count test that task 14 owed showed the growth was real: one
resource cost 10 queries, six cost 33 — about five per resource, because the
available-minutes figure walked each resource's calendar on its own.

Holidays, tenant overrides and branch hours are identical for every resource in
a report, so they now load once outside the loop; shifts and exceptions load for
all resources in one query each. The batched path is a new method rather than a
change to rawAvailability, which the booking engine also calls. The test pins
the shape of the growth, not an exact count.

Also landed:

- app:segment:seed-templates with beauty, dental and physio presets. Building
  four segments and their requirements by hand is the first thing a new clinic
  must do and the most tedious; this gives them something to edit instead of an
  empty page. It refuses to touch a service that already has segments unless
  --force, and it will not invent resource types the tenant never defined.
- book-all is all-or-nothing, proven rather than asserted: with a calendar open
  one day a week and a 1-2 day protocol gap, session one finds a slot and
  session two cannot, and every session must come back planned.
- credit_refundable: false takes the credit back with a negative adjustment and
  deletes nothing — the ledger stays append-only.
- the segments editor has frontend tests, including that it sends back what the
  user sees and renders read-only without the permission.

useBranches now returns [] for a non-array payload instead of throwing
"branches.map is not a function" and taking the page down with it.

BookingLocationsScanTest built a Clinic around a Doctor loaded from a different
manager, which Doctrine treats as a new entity; it flushed fine most runs and
failed on cascade in others. It now loads the doctor from the same manager.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
hamed
2026-08-01 14:22:22 +03:30
co-authored by Claude Opus 5
parent 92edd175cc
commit b3c331f0cb
20 changed files with 624 additions and 30 deletions
+38
View File
@@ -216,6 +216,44 @@ class PackageLedgerTest extends ApiTestCase
self::assertSame([6, 5, 6], array_column($ledger['data']['rows'], 'running_balance'));
}
/**
* ⭐ `credit_refundable: false` اعتبار برگشته را پس می‌گیرد — **بدون** حذف ردیف.
*
* دفتر append-only است، پس «پس گرفتن» یک ردیف `adjustment` منفی است نه پاک کردن
* `refund`. تاریخچه باید نشان بدهد اعتبار برگشت و بعد طبق سیاست پس گرفته شد.
*/
public function testAPolicyThatDoesNotRefundCreditTakesItBackWithAnAdjustment(): void
{
[$user, $section, $address, $doctor, $patient] = $this->clinicWithPatient();
$service = $this->service($section, 'لیزر');
// نوبت حدود یک روز دیگر است؛ پنجرهٔ ۹۶ ساعته یعنی این لغو **بیرون** بازهٔ رایگان
// نیست بلکه درونِ محدودهٔ جریمه می‌افتد — تنها حالتی که سیاست اعتبار اثر دارد.
$saved = $this->authJson('PUT', '/api/v1/cancellation-policy', $user, [
'free_window_hours' => 96,
'penalty_mode' => 'none',
'credit_refundable' => false,
]);
self::assertSame(200, $this->responseCode(), json_encode($saved, JSON_UNESCAPED_UNICODE));
$sold = $this->sell($user, $patient, $this->definePackage($user, $service)['uuid']);
$appointment = $this->reload($this->appointment($doctor, $patient, $service, (int) $address->getClinicId()));
self::assertTrue($this->consumption()->consumeFor($appointment));
$body = $this->authJson('POST', "/api/v1/appointment/{$appointment->getUuid()}/cancel", $user, ['by' => 'user']);
self::assertSame(200, $this->responseCode(), json_encode($body, JSON_UNESCAPED_UNICODE));
self::assertFalse($body['data']['credit_refundable']);
$ledger = $this->authJson('GET', "/api/v1/patient-package/{$sold['uuid']}/ledger", $user);
$kinds = array_column($ledger['data']['rows'], 'kind');
self::assertSame(['purchase', 'consume', 'refund', 'adjustment'], $kinds, 'هیچ ردیفی حذف نمی‌شود');
$entity = static::getContainer()->get(PatientPackageRepository::class)->findByUuid($sold['uuid']);
self::assertSame(5, $this->ledgerService()->balance($entity), 'جلسه پس گرفته شد');
}
/** `confirm` idempotent است؛ اجرای دومش نباید جلسهٔ دوم بخورد. */
public function testConsumingTwiceForTheSameAppointmentTakesOnlyOneSession(): void
{