Task 01 planned a new `branches` table with `doctor_addresses.branch_id` bridging to it. That plan was wrong: the branch already exists and is called `DoctorAddress`. It carries name, address, telephone, coordinates, city/province FKs and an owner (`forDoctor` / `forClinic` + `type`), and the whole system already consumes it with exactly that meaning — `WeeklySchedule.sessions[].location_id` points at `doctor_addresses.id`, `appointment-booking-locations` calls each row a booking location, and nine CRUD endpoints plus four admin pages manage them. A parallel table would mean two sources of truth for one physical place and a branch that `location_id` never references. So no `branches` table and no duplicate branch CRUD. Only the three genuinely missing pieces: - `doctor_addresses.active` / `.timezone`, both NOT NULL with a default so existing rows need no backfill and no current behaviour changes. `active` is stored only — applying it to slot calculation is task 03, since touching `SlotCalculatorService` is off limits in this phase. - `branch_working_hours`, keyed to `doctor_addresses.id`. Minutes from midnight rather than "09:00" strings so range intersection stays arithmetic. PUT replaces all seven days; validation of the whole week runs before any DELETE, so an invalid sixth day cannot wipe the five valid ones and then answer 422. - `rooms`, with `capacity` as concurrency (a three-bed injection room is one resource with capacity 3, not three resources) and a deletion-guard iterator so tasks 02 and 07 can add reasons without editing RoomService. `BranchWorkingHours` first registered as an aggregate child of `DoctorAddress`; TenantSchemaCoverageTest rejected it correctly, because that root is itself declared global. It now carries a real tenant pair instead, derived in the constructor from the address's `type` — a total mapping, and the address is only ever listed in its own context, so nothing is hidden wrongly. RoomController checks ownership explicitly rather than trusting TenantFilter: hard isolation only applies to a *chosen* context, so a doctor who had not selected one could PATCH another clinic's room. Caught by RoomCrudTest::testForeignRoomIsNotFound, which failed with 200 before the fix. 35 tests, 97 assertions. Slot-mode frozen contract still green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
71 lines
2.1 KiB
PHP
71 lines
2.1 KiB
PHP
<?php
|
|
|
|
namespace App\Branch\Repository;
|
|
|
|
use App\Branch\Entity\BranchWorkingHours;
|
|
use App\Doctor\Entity\DoctorAddress;
|
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
|
use Doctrine\Persistence\ManagerRegistry;
|
|
|
|
/**
|
|
* @extends ServiceEntityRepository<BranchWorkingHours>
|
|
*/
|
|
class BranchWorkingHoursRepository extends ServiceEntityRepository
|
|
{
|
|
public function __construct(ManagerRegistry $registry)
|
|
{
|
|
parent::__construct($registry, BranchWorkingHours::class);
|
|
}
|
|
|
|
/** @return BranchWorkingHours[] مرتب بر روز و سپس بازه */
|
|
public function findForAddress(DoctorAddress $address): array
|
|
{
|
|
return $this->createQueryBuilder('h')
|
|
->where('h.address = :address')
|
|
->setParameter('address', $address)
|
|
->orderBy('h.dayOfWeek', 'ASC')
|
|
->addOrderBy('h.sequence', 'ASC')
|
|
->getQuery()
|
|
->getResult();
|
|
}
|
|
|
|
public function deleteForAddress(DoctorAddress $address): int
|
|
{
|
|
return (int) $this->createQueryBuilder('h')
|
|
->delete()
|
|
->where('h.address = :address')
|
|
->setParameter('address', $address)
|
|
->getQuery()
|
|
->execute();
|
|
}
|
|
|
|
/**
|
|
* آدرسهایی که ساعت کاری تعریفشده دارند — برای نشان دادن وضعیت در لیست شعبهها
|
|
* بدون N+۱ کوئری.
|
|
*
|
|
* @param int[] $addressIds
|
|
* @return array<int, int> شناسهٔ آدرس => تعداد بازهها
|
|
*/
|
|
public function countByAddressIds(array $addressIds): array
|
|
{
|
|
if ($addressIds === []) {
|
|
return [];
|
|
}
|
|
|
|
$rows = $this->createQueryBuilder('h')
|
|
->select('IDENTITY(h.address) AS address_id, COUNT(h.id) AS total')
|
|
->where('h.address IN (:ids)')
|
|
->setParameter('ids', $addressIds)
|
|
->groupBy('h.address')
|
|
->getQuery()
|
|
->getArrayResult();
|
|
|
|
$counts = [];
|
|
foreach ($rows as $row) {
|
|
$counts[(int) $row['address_id']] = (int) $row['total'];
|
|
}
|
|
|
|
return $counts;
|
|
}
|
|
}
|