diff --git a/migrations/Version20260728074119.php b/migrations/Version20260728074119.php new file mode 100644 index 00000000..35c6ed22 --- /dev/null +++ b/migrations/Version20260728074119.php @@ -0,0 +1,106 @@ +connection instead of addSql() because the + * backfill has to happen between adding the nullable columns and tightening + * them to NOT NULL — addSql() defers everything to the end of up(), which would + * run the guard before the data exists. + * + * MariaDB commits implicitly on DDL, so this migration cannot roll back as a + * unit. The order below is what keeps it safe: the replacement unique index is + * created before the old one is dropped, so the tables are never left without + * uniqueness protection. + */ +final class Version20260728074119 extends AbstractMigration +{ + /** clinic wins when set; NULL means the doctor's own practice. */ + private const BACKFILL = "SET entity_type = IF(clinic_id IS NULL, 'doctor', 'clinic'), + entity_id = IFNULL(clinic_id, doctor_id)"; + + public function getDescription(): string + { + return 'Mark appointments, weekly_schedules and date_overrides with their owning tenant'; + } + + public function up(Schema $schema): void + { + foreach (['appointments', 'weekly_schedules', 'date_overrides'] as $table) { + $this->connection->executeStatement( + "ALTER TABLE {$table} ADD entity_type VARCHAR(10) NULL, ADD entity_id INT NULL" + ); + $this->connection->executeStatement("UPDATE {$table} " . self::BACKFILL); + + // Defensive: a clinic_id pointing at a deleted clinic falls back to + // the doctor's own practice rather than aborting the migration. + $this->connection->executeStatement( + "UPDATE {$table} t LEFT JOIN clinics c ON c.id = t.clinic_id + SET t.entity_type = 'doctor', t.entity_id = t.doctor_id + WHERE t.clinic_id IS NOT NULL AND c.id IS NULL" + ); + + $remaining = (int) $this->connection->fetchOne( + "SELECT COUNT(*) FROM {$table} WHERE entity_type IS NULL OR entity_id IS NULL" + ); + $this->abortIf($remaining > 0, "Backfill left {$remaining} rows in {$table} without a tenant."); + + $this->connection->executeStatement( + "ALTER TABLE {$table} MODIFY entity_type VARCHAR(10) NOT NULL, MODIFY entity_id INT NOT NULL" + ); + } + + $this->connection->executeStatement( + 'CREATE INDEX idx_appointments_tenant_slot ON appointments (entity_type, entity_id, slot_start)' + ); + $this->connection->executeStatement( + 'CREATE INDEX idx_appointments_tenant_status ON appointments (entity_type, entity_id, status)' + ); + + // New unique key first, old one and its generated column afterwards. + $this->connection->executeStatement( + 'CREATE UNIQUE INDEX uniq_weekly_schedule_doctor_tenant ON weekly_schedules (doctor_id, entity_type, entity_id)' + ); + $this->connection->executeStatement('DROP INDEX idx_weekly_schedules_doctor_clinic ON weekly_schedules'); + $this->connection->executeStatement('ALTER TABLE weekly_schedules DROP clinic_key'); + + $this->connection->executeStatement( + 'CREATE UNIQUE INDEX uniq_date_override_doctor_tenant_date ON date_overrides (doctor_id, entity_type, entity_id, date)' + ); + $this->connection->executeStatement('DROP INDEX uniq_date_override_doctor_clinic_date ON date_overrides'); + $this->connection->executeStatement('ALTER TABLE date_overrides DROP clinic_key'); + } + + public function down(Schema $schema): void + { + $this->addSql("ALTER TABLE weekly_schedules ADD clinic_key INT AS (IFNULL(clinic_id, 0)) STORED"); + $this->addSql('CREATE UNIQUE INDEX idx_weekly_schedules_doctor_clinic ON weekly_schedules (doctor_id, clinic_key)'); + $this->addSql('DROP INDEX uniq_weekly_schedule_doctor_tenant ON weekly_schedules'); + + $this->addSql("ALTER TABLE date_overrides ADD clinic_key INT AS (IFNULL(clinic_id, 0)) STORED"); + $this->addSql('CREATE UNIQUE INDEX uniq_date_override_doctor_clinic_date ON date_overrides (doctor_id, clinic_key, date)'); + $this->addSql('DROP INDEX uniq_date_override_doctor_tenant_date ON date_overrides'); + + $this->addSql('DROP INDEX idx_appointments_tenant_slot ON appointments'); + $this->addSql('DROP INDEX idx_appointments_tenant_status ON appointments'); + + foreach (['appointments', 'weekly_schedules', 'date_overrides'] as $table) { + $this->addSql("ALTER TABLE {$table} DROP entity_type, DROP entity_id"); + } + } + + /** DDL on MariaDB commits implicitly; wrapping up() in a transaction would be a lie. */ + public function isTransactional(): bool + { + return false; + } +} diff --git a/src/Admin/Controller/AdminApiController.php b/src/Admin/Controller/AdminApiController.php index 858230b2..4afef556 100644 --- a/src/Admin/Controller/AdminApiController.php +++ b/src/Admin/Controller/AdminApiController.php @@ -1018,6 +1018,7 @@ class AdminApiController extends BaseController } $bookingClinic = $this->bookingContext->resolve($doctor, $data['clinic_uuid'] ?? null); $appointment->setClinic($bookingClinic); + $appointment->assignTenant(\App\Shared\Context\EntityContext::forBooking($doctor, $bookingClinic)); $locationId = $this->slotCalculator->resolveSlotLocationId($doctor, $slotStart, $bookingClinic, true); if ($locationId !== null) $appointment->setAddressId($locationId); diff --git a/src/Appointment/Controller/AppointmentController.php b/src/Appointment/Controller/AppointmentController.php index 584a13fe..f5018e15 100644 --- a/src/Appointment/Controller/AppointmentController.php +++ b/src/Appointment/Controller/AppointmentController.php @@ -519,6 +519,7 @@ class AppointmentController extends BaseController // آدرس نوبت از روی session متناظر در برنامه‌ی هفتگی تعیین می‌شود (location_id). $appointment->setClinic($bookingClinic); + $appointment->assignTenant(\App\Shared\Context\EntityContext::forBooking($doctor, $bookingClinic)); $locationId = $this->slotCalculator->resolveSlotLocationId($doctor, $slotStart, $bookingClinic); if ($locationId !== null) { $appointment->setAddressId($locationId); diff --git a/src/Appointment/Controller/AppointmentSettingsController.php b/src/Appointment/Controller/AppointmentSettingsController.php index 8645fd54..564b3a2a 100644 --- a/src/Appointment/Controller/AppointmentSettingsController.php +++ b/src/Appointment/Controller/AppointmentSettingsController.php @@ -152,6 +152,7 @@ class AppointmentSettingsController extends BaseController $schedule->setSetting($data['schedule'] ?? []); } else { $schedule = new WeeklySchedule($doctor, $data['schedule'] ?? [], $clinic); + $schedule->assignTenant(EntityContext::forBooking($doctor, $clinic)); } if (isset($data['meta']) && is_array($data['meta'])) { @@ -310,6 +311,7 @@ class AppointmentSettingsController extends BaseController } $override = new DateOverride($doctor, $timestamp, (bool) ($data['active'] ?? false), $clinic); + $override->assignTenant(EntityContext::forBooking($doctor, $clinic)); if (isset($data['reason'])) $override->setReason($data['reason']); if (isset($data['custom_slots'])) $override->setSetting($data['custom_slots']); diff --git a/src/Appointment/Controller/MyAppointmentsController.php b/src/Appointment/Controller/MyAppointmentsController.php index 8c302e4a..65aff488 100644 --- a/src/Appointment/Controller/MyAppointmentsController.php +++ b/src/Appointment/Controller/MyAppointmentsController.php @@ -3,6 +3,7 @@ namespace App\Appointment\Controller; use App\Appointment\Entity\Appointment; +use App\Shared\Context\EntityContext; use App\Shared\Constant\ErrorCodes; use App\Appointment\Repository\AppointmentRepository; use App\Appointment\Repository\SlotTakenException; @@ -182,6 +183,7 @@ class MyAppointmentsController extends BaseController // یعنی مطب شخصی، نه «هر برنامه‌ای که پیدا شد». $bookingClinic = $this->bookingContext->resolve($doctor, $data['clinic_uuid'] ?? null); $appointment->setClinic($bookingClinic); + $appointment->assignTenant(EntityContext::forBooking($doctor, $bookingClinic)); $locationId = $this->slotCalculator->resolveSlotLocationId($doctor, $slotStart, $bookingClinic, true); if ($locationId !== null) $appointment->setAddressId($locationId); diff --git a/src/Appointment/Entity/Appointment.php b/src/Appointment/Entity/Appointment.php index d4573de1..1552654b 100644 --- a/src/Appointment/Entity/Appointment.php +++ b/src/Appointment/Entity/Appointment.php @@ -5,6 +5,7 @@ namespace App\Appointment\Entity; use App\Auth\Entity\User; use App\Doctor\Entity\Doctor; use App\Insurance\Enum\ServiceCategory; +use App\Shared\Tenant\TenantOwnedTrait; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; @@ -13,11 +14,22 @@ use Symfony\Component\Uid\Uuid; #[ORM\Entity(repositoryClass: AppointmentRepository::class)] #[ORM\Table(name: 'appointments')] +// tenant پیشرو — لیست‌های پنل همیشه محیط‌محورند +#[ORM\Index(columns: ['entity_type', 'entity_id', 'slot_start'], name: 'idx_appointments_tenant_slot')] +#[ORM\Index(columns: ['entity_type', 'entity_id', 'status'], name: 'idx_appointments_tenant_status')] +// بدون tenant — عمدی: یکتاییِ اسلات و تقویم سطح پزشک‌اند، نه محیط #[ORM\Index(columns: ['doctor_id', 'slot_start'], name: 'idx_appointments_doctor_slot')] #[ORM\Index(columns: ['user_id', 'status'], name: 'idx_appointments_user_status')] #[ORM\Index(columns: ['status', 'expires_at'], name: 'idx_appointments_status_expires')] class Appointment { + /** + * محیطِ مالکِ نوبت. denormalization عمدی روی clinic/doctor موجود: فیلترِ خودکارِ + * tenant و ایندکسِ tenant-پیشرو هر دو به ستون واقعی نیاز دارند و با شرطِ + * IF(clinic_id IS NULL, …) ساخته نمی‌شوند. + */ + use TenantOwnedTrait; + // Status machine: pending → confirmed → completed // ↘ cancelled_by_doctor / cancelled_by_user // pending → expired (cron) @@ -214,7 +226,7 @@ class Appointment * while the appointment occupies the slot; null once it is cancelled. * * کلید عمداً clinic ندارد و فقط doctor+slotStart است: برنامهٔ هفتگی هر محیط - * جداست (WeeklySchedule با UNIQUE(doctor_id, clinic_key)) و می‌تواند با محیط + * جداست (WeeklySchedule با UNIQUE(doctor_id, entity_type, entity_id)) و می‌تواند با محیط * دیگر هم‌پوشانی داشته باشد، ولی پزشک یک نفر است. افزودن clinic به کلید یعنی * اجازهٔ رزرو هم‌زمان همان پزشک در مطب و کلینیک — نه رفع باگ. */ diff --git a/src/Appointment/Entity/DateOverride.php b/src/Appointment/Entity/DateOverride.php index 39df5b6e..59f7d707 100644 --- a/src/Appointment/Entity/DateOverride.php +++ b/src/Appointment/Entity/DateOverride.php @@ -4,6 +4,8 @@ namespace App\Appointment\Entity; use App\Clinic\Entity\Clinic; use App\Doctor\Entity\Doctor; +use App\Shared\Context\EntityContext; +use App\Shared\Tenant\TenantOwnedTrait; use Doctrine\ORM\Mapping as ORM; use App\Appointment\Repository\DateOverrideRepository; use Symfony\Component\Uid\Uuid; @@ -14,9 +16,12 @@ use Symfony\Component\Uid\Uuid; */ #[ORM\Entity(repositoryClass: DateOverrideRepository::class)] #[ORM\Table(name: 'date_overrides')] -#[ORM\UniqueConstraint(name: 'uniq_date_override_doctor_clinic_date', columns: ['doctor_id', 'clinic_key', 'date'])] +// doctor_id در کلید می‌ماند: چند پزشکِ یک کلینیک هر کدام استثنای روزِ خودشان را دارند. +#[ORM\UniqueConstraint(name: 'uniq_date_override_doctor_tenant_date', columns: ['doctor_id', 'entity_type', 'entity_id', 'date'])] class DateOverride { + use TenantOwnedTrait; + #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column(type: 'integer')] @@ -34,10 +39,6 @@ class DateOverride #[ORM\JoinColumn(name: 'clinic_id', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')] private ?Clinic $clinic = null; - /** ستون تولیدشده: IFNULL(clinic_id, 0) — تا یکتایی با clinic_id تهی هم برقرار بماند. */ - #[ORM\Column(name: 'clinic_key', type: 'integer', insertable: false, updatable: false, generated: 'ALWAYS', columnDefinition: 'INT AS (IFNULL(clinic_id, 0)) STORED')] - private int $clinicKey = 0; - #[ORM\Column(type: 'integer')] private int $date; diff --git a/src/Appointment/Entity/Holiday.php b/src/Appointment/Entity/Holiday.php index 080272ea..44a00189 100644 --- a/src/Appointment/Entity/Holiday.php +++ b/src/Appointment/Entity/Holiday.php @@ -12,6 +12,10 @@ use Symfony\Component\Uid\Uuid; * تعطیلی پزشک. برخلاف WeeklySchedule و DateOverride، تعطیلی پیش‌فرضاً سراسری است: * «پزشک آن روز نیست» یک واقعیت فیزیکی است و هم‌زمان روی مطب شخصی و همهٔ کلینیک‌ها * اثر می‌گذارد (clinic = null). مقدار غیر-NULL یعنی پزشک فقط در همان کلینیک نیست. + * + * عمداً جفت (entity_type, entity_id) ندارد: در این جدول clinic = NULL یعنی «همهٔ + * محیط‌ها»، نه «مطب شخصی». جفت tenant نمی‌تواند «همه» را بیان کند و تبدیلش، تعطیلی + * سراسری را به تعطیلی مطب شخصی تنزل می‌دهد. باید در whitelist فیلتر tenant بماند. */ #[ORM\Entity(repositoryClass: HolidayRepository::class)] #[ORM\Table(name: 'holidays')] diff --git a/src/Appointment/Entity/WeeklySchedule.php b/src/Appointment/Entity/WeeklySchedule.php index cbc6d349..e4b922ad 100644 --- a/src/Appointment/Entity/WeeklySchedule.php +++ b/src/Appointment/Entity/WeeklySchedule.php @@ -4,6 +4,8 @@ namespace App\Appointment\Entity; use App\Clinic\Entity\Clinic; use App\Doctor\Entity\Doctor; +use App\Shared\Context\EntityContext; +use App\Shared\Tenant\TenantOwnedTrait; use Doctrine\ORM\Mapping as ORM; use App\Appointment\Repository\WeeklyScheduleRepository; use Symfony\Component\Uid\Uuid; @@ -17,9 +19,13 @@ use Symfony\Component\Uid\Uuid; */ #[ORM\Entity(repositoryClass: WeeklyScheduleRepository::class)] #[ORM\Table(name: 'weekly_schedules')] -#[ORM\UniqueConstraint(name: 'idx_weekly_schedules_doctor_clinic', columns: ['doctor_id', 'clinic_key'])] +// doctor_id در کلید می‌ماند: در یک کلینیک چند پزشک هستند و هر کدام برنامهٔ خودش را +// دارد، پس (entity_type, entity_id) به‌تنهایی برای پزشک دوم نقض یکتایی می‌سازد. +#[ORM\UniqueConstraint(name: 'uniq_weekly_schedule_doctor_tenant', columns: ['doctor_id', 'entity_type', 'entity_id'])] class WeeklySchedule { + use TenantOwnedTrait; + public const DAYS = ['saturday', 'sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday']; public const META_KEY = 'meta'; @@ -55,16 +61,6 @@ class WeeklySchedule #[ORM\JoinColumn(name: 'clinic_id', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')] private ?Clinic $clinic = null; - /** - * ستون تولیدشدهٔ پایگاه‌داده: IFNULL(clinic_id, 0). - * - * MySQL/MariaDB مقادیر NULL را در unique index متمایز می‌شمارند، پس - * UNIQUE(doctor_id, clinic_id) جلوی دو برنامهٔ شخصی برای یک پزشک را نمی‌گرفت. - * این ستون NULL را به 0 نگاشت می‌کند تا یکتایی در سطح دیتابیس تضمین شود. - */ - #[ORM\Column(name: 'clinic_key', type: 'integer', insertable: false, updatable: false, generated: 'ALWAYS', columnDefinition: 'INT AS (IFNULL(clinic_id, 0)) STORED')] - private int $clinicKey = 0; - #[ORM\Column(type: 'json')] private array $setting = []; @@ -94,6 +90,7 @@ class WeeklySchedule { $this->clinic = $clinic; $this->updatedAt = time(); + $this->assignTenant(EntityContext::forBooking($this->doctor, $clinic)); return $this; } public function getSetting(): array { return $this->setting; } diff --git a/src/Shared/Context/EntityContext.php b/src/Shared/Context/EntityContext.php index 5febf6a6..d2e571c0 100644 --- a/src/Shared/Context/EntityContext.php +++ b/src/Shared/Context/EntityContext.php @@ -35,6 +35,15 @@ final class EntityContext return new self(self::TYPE_CLINIC, $clinic->getId(), $clinic); } + /** + * محیط یک رزرو: کلینیکِ داده‌شده، وگرنه مطب شخصی همان پزشک — همان قراردادی که + * {@see \App\Appointment\Service\BookingContextResolver} با ?Clinic بیان می‌کند. + */ + public static function forBooking(Doctor $doctor, ?Clinic $clinic): self + { + return $clinic !== null ? self::forClinic($clinic) : self::forDoctor($doctor); + } + public static function unknown(): self { return new self(self::TYPE_UNKNOWN, null); diff --git a/src/Shared/Tenant/TenantOwnedTrait.php b/src/Shared/Tenant/TenantOwnedTrait.php new file mode 100644 index 00000000..f28632c0 --- /dev/null +++ b/src/Shared/Tenant/TenantOwnedTrait.php @@ -0,0 +1,46 @@ +entityType; } + + public function getEntityId(): int { return $this->entityId; } + + /** @throws \InvalidArgumentException اگر محیط حل نشده باشد */ + public function assignTenant(EntityContext $context): void + { + if (!$context->isResolved()) { + throw new \InvalidArgumentException(sprintf( + 'Cannot assign an unresolved tenant context to %s.', + static::class, + )); + } + + [$this->entityType, $this->entityId] = $context->toEntityPair(); + } +} diff --git a/tests/ApiTestCase.php b/tests/ApiTestCase.php index a6ba86d5..d26aa30b 100644 --- a/tests/ApiTestCase.php +++ b/tests/ApiTestCase.php @@ -2,7 +2,13 @@ namespace App\Tests; +use App\Appointment\Entity\Appointment; +use App\Appointment\Entity\DateOverride; +use App\Appointment\Entity\WeeklySchedule; use App\Auth\Entity\User; +use App\Clinic\Entity\Clinic; +use App\Doctor\Entity\Doctor; +use App\Shared\Context\EntityContext; use App\Subscription\Entity\SubscriptionPlan; use Doctrine\ORM\EntityManagerInterface; use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface; @@ -70,6 +76,74 @@ abstract class ApiTestCase extends WebTestCase return $user; } + /** + * A booking with its owning tenant already assigned — the test-side mirror of + * what the booking controllers do after resolving the environment. Constructing + * an Appointment without it fails at flush, since entity_type/entity_id are NOT + * NULL and have no default. + */ + protected function newAppointment( + Doctor $doctor, + User $patient, + int $slotStart, + int $slotEnd, + ?Clinic $clinic = null, + ): Appointment { + $appointment = new Appointment($doctor, $patient, $slotStart, $slotEnd); + $appointment->setClinic($clinic); + $appointment->assignTenant(EntityContext::forBooking($doctor, $clinic)); + + return $appointment; + } + + /** + * A weekly schedule with its owning tenant assigned. The doctor is flushed + * first when it has no id yet: the tenant pair stores scalars, so the owner + * must already exist in the database. + */ + protected function newWeeklySchedule(Doctor $doctor, array $setting, ?Clinic $clinic = null): WeeklySchedule + { + $this->flushIfNew($doctor, $clinic); + + $schedule = new WeeklySchedule($doctor, $setting, $clinic); + $schedule->assignTenant(EntityContext::forBooking($doctor, $clinic)); + + return $schedule; + } + + protected function newDateOverride(Doctor $doctor, int $date, bool $active = false, ?Clinic $clinic = null): DateOverride + { + $this->flushIfNew($doctor, $clinic); + + $override = new DateOverride($doctor, $date, $active, $clinic); + $override->assignTenant(EntityContext::forBooking($doctor, $clinic)); + + return $override; + } + + /** + * The tenant pair stores scalars, so the owner needs a database id before it + * can be assigned. persist() on an already-managed entity is a no-op, which + * makes this safe for owners the test never persisted itself. + */ + private function flushIfNew(Doctor $doctor, ?Clinic $clinic): void + { + $pending = array_filter( + [$doctor, $clinic], + static fn(?object $owner) => $owner !== null && $owner->getId() === null, + ); + + if ($pending === []) { + return; + } + + foreach ($pending as $owner) { + $this->em->persist($owner); + } + + $this->em->flush(); + } + protected function jwtFor(User $user): string { return static::getContainer() diff --git a/tests/Appointment/AppointmentConfirmFlowTest.php b/tests/Appointment/AppointmentConfirmFlowTest.php index b88f982e..db051981 100644 --- a/tests/Appointment/AppointmentConfirmFlowTest.php +++ b/tests/Appointment/AppointmentConfirmFlowTest.php @@ -54,8 +54,7 @@ class AppointmentConfirmFlowTest extends ApiTestCase // اسلات یکتا به‌ازای هر نوبت: db_test بین اجراها پاک نمی‌شود. $start = strtotime('+30 days') + random_int(0, 500_000) * 7; - $appointment = new Appointment($doctor, $patient, $start, $start + 900); - $appointment->setClinic($clinic); + $appointment = $this->newAppointment($doctor, $patient, $start, $start + 900, $clinic); $appointment->setVisitPriceRials($visitPriceRials); $this->em->persist($appointment); $this->em->flush(); diff --git a/tests/Appointment/AppointmentConfirmInsuranceSharesTest.php b/tests/Appointment/AppointmentConfirmInsuranceSharesTest.php index 40904119..5389c422 100644 --- a/tests/Appointment/AppointmentConfirmInsuranceSharesTest.php +++ b/tests/Appointment/AppointmentConfirmInsuranceSharesTest.php @@ -35,7 +35,7 @@ class AppointmentConfirmInsuranceSharesTest extends ApiTestCase private function makeAppointment(Doctor $doctor): Appointment { $start = strtotime('+30 days') + random_int(0, 500_000) * 7; - $appointment = new Appointment($doctor, $this->createUser(), $start, $start + 900); + $appointment = $this->newAppointment($doctor, $this->createUser(), $start, $start + 900); $appointment->setVisitPriceRials(self::VISIT_RIALS); $this->em->persist($appointment); $this->em->flush(); diff --git a/tests/Appointment/AppointmentExpiryServiceTest.php b/tests/Appointment/AppointmentExpiryServiceTest.php index e10cd921..972293cd 100644 --- a/tests/Appointment/AppointmentExpiryServiceTest.php +++ b/tests/Appointment/AppointmentExpiryServiceTest.php @@ -27,7 +27,7 @@ class AppointmentExpiryServiceTest extends ApiTestCase $patient = $this->createUser(['ROLE_USER']); // distinct past slots — one live booking per (doctor, slot) $slotStart = $past - $i * 1000; - $appt = new Appointment($doctor, $patient, $slotStart, $slotStart + 900); + $appt = $this->newAppointment($doctor, $patient, $slotStart, $slotStart + 900); // مثل مسیر واقعیِ رزرو آنلاین: نگه‌داشتِ موقت تا پرداخت درگاه. $appt->markPendingWithTtl(-1); $this->em->persist($appt); @@ -67,7 +67,7 @@ class AppointmentExpiryServiceTest extends ApiTestCase $this->em->persist($doctor); $slotStart = time() - 7200; - $appt = new Appointment($doctor, $this->createUser(['ROLE_USER']), $slotStart, $slotStart + 900); + $appt = $this->newAppointment($doctor, $this->createUser(['ROLE_USER']), $slotStart, $slotStart + 900); $this->em->persist($appt); $this->em->flush(); diff --git a/tests/Appointment/AppointmentInsuranceSelectionTest.php b/tests/Appointment/AppointmentInsuranceSelectionTest.php index 24ef240a..4c48940d 100644 --- a/tests/Appointment/AppointmentInsuranceSelectionTest.php +++ b/tests/Appointment/AppointmentInsuranceSelectionTest.php @@ -31,7 +31,7 @@ class AppointmentInsuranceSelectionTest extends ApiTestCase { // اسلات یکتا به‌ازای هر نوبت: db_test بین اجراها پاک نمی‌شود. $start = strtotime('+30 days') + random_int(0, 500_000) * 7; - $appointment = new Appointment($doctor, $this->createUser(), $start, $start + 900); + $appointment = $this->newAppointment($doctor, $this->createUser(), $start, $start + 900); $appointment->setVisitPriceRials(5_952_000); $this->em->persist($appointment); $this->em->flush(); diff --git a/tests/Appointment/AppointmentUpdateTest.php b/tests/Appointment/AppointmentUpdateTest.php index 6b5924ec..cb343422 100644 --- a/tests/Appointment/AppointmentUpdateTest.php +++ b/tests/Appointment/AppointmentUpdateTest.php @@ -23,7 +23,7 @@ class AppointmentUpdateTest extends ApiTestCase $this->em->persist($doctor); $this->em->flush(); - $appointment = new Appointment($doctor, $this->createUser(), time() + 86_400, time() + 86_400 + 1_800); + $appointment = $this->newAppointment($doctor, $this->createUser(), time() + 86_400, time() + 86_400 + 1_800); $this->em->persist($appointment); $this->em->flush(); @@ -77,7 +77,7 @@ class AppointmentUpdateTest extends ApiTestCase [$owner, $doctor, $appointment] = $this->booking(); $otherStart = time() + 3 * 86_400; - $this->em->persist(new Appointment($doctor, $this->createUser(), $otherStart, $otherStart + 1_800)); + $this->em->persist($this->newAppointment($doctor, $this->createUser(), $otherStart, $otherStart + 1_800)); $this->em->flush(); $this->authJson('PATCH', '/api/v1/appointment/' . $appointment->getUuid(), $owner, [ diff --git a/tests/Appointment/AppointmentWorkflowFieldsTest.php b/tests/Appointment/AppointmentWorkflowFieldsTest.php index 7e780beb..70ecaa5c 100644 --- a/tests/Appointment/AppointmentWorkflowFieldsTest.php +++ b/tests/Appointment/AppointmentWorkflowFieldsTest.php @@ -24,7 +24,7 @@ class AppointmentWorkflowFieldsTest extends ApiTestCase private function newBooking(Doctor $doctor, int $start): Appointment { - $a = new Appointment($doctor, $this->createUser(), $start, $start + 1_800); + $a = $this->newAppointment($doctor, $this->createUser(), $start, $start + 1_800); $this->em->persist($a); $this->em->flush(); diff --git a/tests/Appointment/BookingLocationValidityTest.php b/tests/Appointment/BookingLocationValidityTest.php index b3f5d802..a5e00e8a 100644 --- a/tests/Appointment/BookingLocationValidityTest.php +++ b/tests/Appointment/BookingLocationValidityTest.php @@ -50,7 +50,7 @@ class BookingLocationValidityTest extends ApiTestCase 'duration_per_patient' => 20, ], fn($v) => $v !== null)]]; - $schedule = new WeeklySchedule( + $schedule = $this->newWeeklySchedule( $doctor, array_fill_keys(array_map('strval', range(0, 6)), $day), $clinic @@ -128,7 +128,7 @@ class BookingLocationValidityTest extends ApiTestCase ]]]; $setting = array_fill_keys(array_map('strval', range(0, 6)), ['sessions' => []]); $setting['0'] = $active; - $this->em->persist(new WeeklySchedule($doctor, $setting)); + $this->em->persist($this->newWeeklySchedule($doctor, $setting)); $this->em->flush(); $saturday = $this->nextWeekday(6); // date('w'): 6 = Saturday @@ -165,7 +165,7 @@ class BookingLocationValidityTest extends ApiTestCase $this->em->flush(); $setting = array_fill_keys(array_map('strval', range(0, 6)), ['sessions' => []]); - $this->em->persist(new WeeklySchedule($doctor, $setting)); + $this->em->persist($this->newWeeklySchedule($doctor, $setting)); $this->em->flush(); $body = $this->authJson('GET', "/api/v1/appointment-slots?doctor_uuid={$doctor->getUuid()}&date={$today}", $doctor->getUser()); diff --git a/tests/Appointment/BookingLocationsScanTest.php b/tests/Appointment/BookingLocationsScanTest.php index 28ddda77..92c48b1c 100644 --- a/tests/Appointment/BookingLocationsScanTest.php +++ b/tests/Appointment/BookingLocationsScanTest.php @@ -43,7 +43,7 @@ class BookingLocationsScanTest extends ApiTestCase $this->em->persist($personalAddress); $this->em->flush(); - $this->em->persist(new WeeklySchedule( + $this->em->persist($this->newWeeklySchedule( $doctor, $this->weekOfSessions($personalAddress->getId(), '09:00', '13:00') )); @@ -59,7 +59,7 @@ class BookingLocationsScanTest extends ApiTestCase $this->em->persist($address); $this->em->flush(); - $this->em->persist(new WeeklySchedule( + $this->em->persist($this->newWeeklySchedule( $doctor, $this->weekOfSessions($address->getId(), '16:00', '20:00'), $clinic diff --git a/tests/Appointment/BookingServicesPublicTest.php b/tests/Appointment/BookingServicesPublicTest.php index dcc06438..d1613b4c 100644 --- a/tests/Appointment/BookingServicesPublicTest.php +++ b/tests/Appointment/BookingServicesPublicTest.php @@ -29,7 +29,7 @@ class BookingServicesPublicTest extends ApiTestCase $this->em->persist($bookable); $this->em->persist($hidden); - $schedule = new WeeklySchedule($doctor, []); + $schedule = $this->newWeeklySchedule($doctor, []); $schedule->setMeta(['booking_mode' => 'service', 'buffer_minutes' => 5]); $this->em->persist($schedule); $this->em->flush(); diff --git a/tests/Appointment/BookingTenantTest.php b/tests/Appointment/BookingTenantTest.php new file mode 100644 index 00000000..f3928fc2 --- /dev/null +++ b/tests/Appointment/BookingTenantTest.php @@ -0,0 +1,137 @@ +createUser(['ROLE_DOCTOR']), 'دکتر تست محیط'); + $this->em->persist($doctor); + $this->em->flush(); + + return $doctor; + } + + private function makeClinic(): Clinic + { + $clinic = new Clinic($this->createUser(['ROLE_CLINIC'])); + $clinic->setName('کلینیک تست محیط'); + $this->em->persist($clinic); + $this->em->flush(); + + return $clinic; + } + + private function uniqueSlot(): int + { + return strtotime('+90 days') + random_int(0, 500_000) * 7; + } + + public function testPersonalBookingIsOwnedByTheDoctor(): void + { + $doctor = $this->makeDoctor(); + $start = $this->uniqueSlot(); + + $appointment = $this->newAppointment($doctor, $this->createUser(), $start, $start + 900); + $this->em->persist($appointment); + $this->em->flush(); + + self::assertSame('doctor', $appointment->getEntityType()); + self::assertSame($doctor->getId(), $appointment->getEntityId()); + } + + public function testClinicBookingIsOwnedByTheClinic(): void + { + $doctor = $this->makeDoctor(); + $clinic = $this->makeClinic(); + $clinic->getDoctors()->add($doctor); + $this->em->flush(); + + $start = $this->uniqueSlot(); + $appointment = $this->newAppointment($doctor, $this->createUser(), $start, $start + 900, $clinic); + $this->em->persist($appointment); + $this->em->flush(); + + self::assertSame('clinic', $appointment->getEntityType()); + self::assertSame($clinic->getId(), $appointment->getEntityId()); + } + + /** یک پزشک در دو محیط = دو مالک متفاوت برای دو نوبت. */ + public function testSameDoctorGetsDifferentOwnersInDifferentEnvironments(): void + { + $doctor = $this->makeDoctor(); + $clinic = $this->makeClinic(); + $clinic->getDoctors()->add($doctor); + $this->em->flush(); + + $personal = $this->newAppointment($doctor, $this->createUser(), $this->uniqueSlot(), $this->uniqueSlot() + 900); + $inClinic = $this->newAppointment($doctor, $this->createUser(), $this->uniqueSlot(), $this->uniqueSlot() + 900, $clinic); + $this->em->persist($personal); + $this->em->persist($inClinic); + $this->em->flush(); + + self::assertSame(['doctor', $doctor->getId()], [$personal->getEntityType(), $personal->getEntityId()]); + self::assertSame(['clinic', $clinic->getId()], [$inClinic->getEntityType(), $inClinic->getEntityId()]); + } + + /** ❌ محیط حل‌نشده هرگز ذخیره نمی‌شود — نه با شناسهٔ صفر، نه بی‌صدا. */ + public function testAssigningAnUnresolvedContextIsRejected(): void + { + $doctor = $this->makeDoctor(); + $start = $this->uniqueSlot(); + $appointment = new Appointment($doctor, $this->createUser(), $start, $start + 900); + + $this->expectException(\InvalidArgumentException::class); + $appointment->assignTenant(EntityContext::unknown()); + } + + /** + * ⚠️ حالت مرزی: دو پزشکِ متفاوت در یک کلینیک، هر کدام برنامهٔ هفتگی خودشان — + * کلید یکتا باید doctor_id را هم داشته باشد وگرنه پزشک دوم رد می‌شود. + */ + public function testTwoDoctorsInOneClinicEachKeepTheirOwnSchedule(): void + { + $clinic = $this->makeClinic(); + $doctorA = $this->makeDoctor(); + $doctorB = $this->makeDoctor(); + $clinic->getDoctors()->add($doctorA); + $clinic->getDoctors()->add($doctorB); + $this->em->flush(); + + $this->em->persist($this->newWeeklySchedule($doctorA, [], $clinic)); + $this->em->persist($this->newWeeklySchedule($doctorB, [], $clinic)); + $this->em->flush(); + + $rows = $this->em->getRepository(WeeklySchedule::class) + ->findBy(['entityType' => 'clinic', 'entityId' => $clinic->getId()]); + + self::assertCount(2, $rows); + } + + /** همان پزشک، همان محیط، دو برنامه — هنوز غیرممکن است. */ + public function testOneSchedulePerEnvironmentPerDoctorIsStillEnforced(): void + { + $doctor = $this->makeDoctor(); + + $this->em->persist($this->newWeeklySchedule($doctor, [])); + $this->em->flush(); + + $this->em->persist($this->newWeeklySchedule($doctor, [])); + $this->expectException(UniqueConstraintViolationException::class); + $this->em->flush(); + } +} diff --git a/tests/Appointment/BookingWindowUnitTest.php b/tests/Appointment/BookingWindowUnitTest.php index 63607a87..f044b303 100644 --- a/tests/Appointment/BookingWindowUnitTest.php +++ b/tests/Appointment/BookingWindowUnitTest.php @@ -30,7 +30,7 @@ class BookingWindowUnitTest extends ApiTestCase ]]]; } - $schedule = new WeeklySchedule($doctor, $days); + $schedule = $this->newWeeklySchedule($doctor, $days); if ($meta !== []) { $schedule->setMeta($meta); } @@ -94,7 +94,7 @@ class BookingWindowUnitTest extends ApiTestCase { $owner = $this->createUser(['ROLE_DOCTOR']); $doctor = new Doctor($owner, 'دکتر واحد نامعتبر'); - $schedule = new WeeklySchedule($doctor, []); + $schedule = $this->newWeeklySchedule($doctor, []); $schedule->setMeta(['booking_window_unit' => 'day', 'booking_window_value' => 10]); $schedule->setMeta(['booking_window_unit' => 'year']); diff --git a/tests/Appointment/ClinicAppointmentAccessTest.php b/tests/Appointment/ClinicAppointmentAccessTest.php index 073ea178..52d8790b 100644 --- a/tests/Appointment/ClinicAppointmentAccessTest.php +++ b/tests/Appointment/ClinicAppointmentAccessTest.php @@ -49,8 +49,7 @@ class ClinicAppointmentAccessTest extends ApiTestCase $patient ??= $this->createUser(); $start = strtotime('+3 days 10:00'); - $appointment = new Appointment($doctor, $patient, $start, $start + 900); - $appointment->setClinic($clinic); + $appointment = $this->newAppointment($doctor, $patient, $start, $start + 900, $clinic); $this->em->persist($appointment); $this->em->flush(); diff --git a/tests/Appointment/DateOverrideOwnershipTest.php b/tests/Appointment/DateOverrideOwnershipTest.php index 7d502ee7..b31c78a3 100644 --- a/tests/Appointment/DateOverrideOwnershipTest.php +++ b/tests/Appointment/DateOverrideOwnershipTest.php @@ -16,7 +16,7 @@ class DateOverrideOwnershipTest extends ApiTestCase $owner = $this->createUser(['ROLE_DOCTOR']); $doctor = new Doctor($owner, 'دکتر تست'); $this->em->persist($doctor); - $override = new DateOverride($doctor, time(), true); + $override = $this->newDateOverride($doctor, time(), true); $this->em->persist($override); $this->em->flush(); diff --git a/tests/Appointment/DoctorAppointmentFilterTest.php b/tests/Appointment/DoctorAppointmentFilterTest.php index dd598482..6861bdac 100644 --- a/tests/Appointment/DoctorAppointmentFilterTest.php +++ b/tests/Appointment/DoctorAppointmentFilterTest.php @@ -29,7 +29,7 @@ class DoctorAppointmentFilterTest extends ApiTestCase private function booking(Doctor $doctor, int $start, ?string $status = null, ?string $name = null): Appointment { - $a = new Appointment($doctor, $this->createUser(), $start, $start + 1_800); + $a = $this->newAppointment($doctor, $this->createUser(), $start, $start + 1_800); if ($name !== null) { $a->setPatientName($name); } diff --git a/tests/Appointment/OnlineBookingManagementTest.php b/tests/Appointment/OnlineBookingManagementTest.php index a629eb31..f757ba51 100644 --- a/tests/Appointment/OnlineBookingManagementTest.php +++ b/tests/Appointment/OnlineBookingManagementTest.php @@ -26,7 +26,7 @@ class OnlineBookingManagementTest extends ApiTestCase $date = date('Y-m-d', strtotime('tomorrow')); $dayKey = (string)(((int) date('w', strtotime($date)) + 1) % 7); - $schedule = new WeeklySchedule($doctor, [ + $schedule = $this->newWeeklySchedule($doctor, [ $dayKey => ['sessions' => [[ 'active' => true, 'start_time' => '10:00', @@ -143,7 +143,7 @@ class OnlineBookingManagementTest extends ApiTestCase $dayKey = (string)(((int) date('w', strtotime($date)) + 1) % 7); // برنامهٔ نوبت‌دهی در محیط کلینیک با نوبت‌دهی آنلاینِ خاموش. - $schedule = new WeeklySchedule($doctor, [ + $schedule = $this->newWeeklySchedule($doctor, [ $dayKey => ['sessions' => [[ 'active' => true, 'start_time' => '10:00', 'end_time' => '12:00', 'duration_per_patient' => 30, 'location_id' => 1, diff --git a/tests/Appointment/ScheduleOwnershipTest.php b/tests/Appointment/ScheduleOwnershipTest.php index ff75d4d3..253e8a93 100644 --- a/tests/Appointment/ScheduleOwnershipTest.php +++ b/tests/Appointment/ScheduleOwnershipTest.php @@ -18,7 +18,7 @@ class ScheduleOwnershipTest extends ApiTestCase $owner = $this->createUser(['ROLE_DOCTOR']); $doctor = new Doctor($owner, 'دکتر تست'); $this->em->persist($doctor); - $schedule = new WeeklySchedule($doctor, ['sat' => []]); + $schedule = $this->newWeeklySchedule($doctor, ['sat' => []]); $this->em->persist($schedule); $this->em->flush(); diff --git a/tests/Appointment/ServiceBasedSlotsTest.php b/tests/Appointment/ServiceBasedSlotsTest.php index 194f5ff8..331857e7 100644 --- a/tests/Appointment/ServiceBasedSlotsTest.php +++ b/tests/Appointment/ServiceBasedSlotsTest.php @@ -24,7 +24,7 @@ class ServiceBasedSlotsTest extends ApiTestCase $date = date('Y-m-d', strtotime('tomorrow')); $dayKey = (string)(((int) date('w', strtotime($date)) + 1) % 7); - $schedule = new WeeklySchedule($doctor, [ + $schedule = $this->newWeeklySchedule($doctor, [ $dayKey => ['sessions' => [[ 'active' => true, 'start_time' => '15:00', @@ -61,7 +61,7 @@ class ServiceBasedSlotsTest extends ApiTestCase $patient = $this->createUser(['ROLE_USER']); $start = strtotime($date . ' 15:00'); - $appt = new Appointment($doctor, $patient, $start, $start + 30 * 60); + $appt = $this->newAppointment($doctor, $patient, $start, $start + 30 * 60); $appt->transitionTo(Appointment::STATUS_CONFIRMED); $this->em->persist($appt); $this->em->flush(); @@ -87,7 +87,7 @@ class ServiceBasedSlotsTest extends ApiTestCase { $owner = $this->createUser(['ROLE_DOCTOR']); $doctor = new Doctor($owner, 'دکتر متا'); - $schedule = new WeeklySchedule($doctor, []); + $schedule = $this->newWeeklySchedule($doctor, []); // پیش‌فرض = اسلاتی $this->assertSame(WeeklySchedule::MODE_SLOT, $schedule->getMeta()['booking_mode']); diff --git a/tests/Appointment/ServiceModeSectionDurationTest.php b/tests/Appointment/ServiceModeSectionDurationTest.php index 87ab63f6..204a6a8c 100644 --- a/tests/Appointment/ServiceModeSectionDurationTest.php +++ b/tests/Appointment/ServiceModeSectionDurationTest.php @@ -24,7 +24,7 @@ class ServiceModeSectionDurationTest extends ApiTestCase $date = date('Y-m-d', strtotime('tomorrow')); $dayKey = (string) (((int) date('w', strtotime($date)) + 1) % 7); - $schedule = new WeeklySchedule($doctor, [ + $schedule = $this->newWeeklySchedule($doctor, [ $dayKey => ['sessions' => [[ 'active' => true, 'start_time' => '15:00', 'end_time' => '19:00', 'duration_per_patient' => 20, 'location_id' => 1, diff --git a/tests/Appointment/SlotUniquenessTest.php b/tests/Appointment/SlotUniquenessTest.php index 8867c884..20c0ecdf 100644 --- a/tests/Appointment/SlotUniquenessTest.php +++ b/tests/Appointment/SlotUniquenessTest.php @@ -27,7 +27,7 @@ class SlotUniquenessTest extends ApiTestCase private function newBooking(Doctor $doctor, int $start): Appointment { - return new Appointment($doctor, $this->createUser(), $start, $start + 1_800); + return $this->newAppointment($doctor, $this->createUser(), $start, $start + 1_800); } public function testTwoLiveBookingsOnSameSlotViolateUniqueKey(): void diff --git a/tests/Dashboard/DashboardChartPeriodTest.php b/tests/Dashboard/DashboardChartPeriodTest.php index cbd7bbe5..b4d5a580 100644 --- a/tests/Dashboard/DashboardChartPeriodTest.php +++ b/tests/Dashboard/DashboardChartPeriodTest.php @@ -88,7 +88,7 @@ class DashboardChartPeriodTest extends ApiTestCase $patient = $this->createUser(['ROLE_USER']); $start = time(); - $this->em->persist(new Appointment($doctor, $patient, $start, $start + 1_800)); + $this->em->persist($this->newAppointment($doctor, $patient, $start, $start + 1_800)); $this->em->flush(); $res = $this->authJson('GET', '/api/v1/dashboard/clinic', $owner); diff --git a/tests/Dashboard/DashboardTodayAppointmentsTest.php b/tests/Dashboard/DashboardTodayAppointmentsTest.php index 342e3f15..056f3600 100644 --- a/tests/Dashboard/DashboardTodayAppointmentsTest.php +++ b/tests/Dashboard/DashboardTodayAppointmentsTest.php @@ -24,7 +24,7 @@ class DashboardTodayAppointmentsTest extends ApiTestCase { // now() is guaranteed within [today midnight, tomorrow midnight-1] $start = time(); - $appt = new Appointment($doctor, $patient, $start, $start + 1_800); + $appt = $this->newAppointment($doctor, $patient, $start, $start + 1_800); $this->em->persist($appt); $this->em->flush(); diff --git a/tests/Database/UniqueConstraintsTest.php b/tests/Database/UniqueConstraintsTest.php index 7f79da94..242c79ad 100644 --- a/tests/Database/UniqueConstraintsTest.php +++ b/tests/Database/UniqueConstraintsTest.php @@ -47,8 +47,8 @@ class UniqueConstraintsTest extends ApiTestCase $this->em->flush(); $date = time(); - $this->em->persist(new DateOverride($doctor, $date)); - $this->em->persist(new DateOverride($doctor, $date)); + $this->em->persist($this->newDateOverride($doctor, $date)); + $this->em->persist($this->newDateOverride($doctor, $date)); $this->expectException(UniqueConstraintViolationException::class); $this->em->flush(); diff --git a/tests/Doctor/DoctorBookingStateAggregationTest.php b/tests/Doctor/DoctorBookingStateAggregationTest.php index 2f1efe5e..d639a9cb 100644 --- a/tests/Doctor/DoctorBookingStateAggregationTest.php +++ b/tests/Doctor/DoctorBookingStateAggregationTest.php @@ -60,8 +60,8 @@ class DoctorBookingStateAggregationTest extends ApiTestCase $this->em->persist($clinicAddress); $this->em->flush(); - $personal = new WeeklySchedule($doctor, $this->week($this->session(false, $personalAddress->getId()))); - $clinicSchedule = new WeeklySchedule( + $personal = $this->newWeeklySchedule($doctor, $this->week($this->session(false, $personalAddress->getId()))); + $clinicSchedule = $this->newWeeklySchedule( $doctor, $this->week($this->session(true, $clinicAddress->getId())), $clinic diff --git a/tests/Doctor/DoctorListBookableSortFilterTest.php b/tests/Doctor/DoctorListBookableSortFilterTest.php index 3cef449c..265e6f95 100644 --- a/tests/Doctor/DoctorListBookableSortFilterTest.php +++ b/tests/Doctor/DoctorListBookableSortFilterTest.php @@ -39,10 +39,10 @@ class DoctorListBookableSortFilterTest extends ApiTestCase $this->flagOff->setActiveDoctorAppointment(false); $days = [['sessions' => [['active' => true, 'start_time' => '09:00', 'end_time' => '13:00']]]]; - $this->em->persist(new WeeklySchedule($this->bookable, $days)); - $this->em->persist(new WeeklySchedule($this->flagOff, $days)); + $this->em->persist($this->newWeeklySchedule($this->bookable, $days)); + $this->em->persist($this->newWeeklySchedule($this->flagOff, $days)); - $disabled = new WeeklySchedule($this->bookingDisabled, $days); + $disabled = $this->newWeeklySchedule($this->bookingDisabled, $days); $disabled->setMeta(['online_booking_enabled' => false]); $this->em->persist($disabled); diff --git a/tests/Patient/AutoCreateSessionOnConfirmTest.php b/tests/Patient/AutoCreateSessionOnConfirmTest.php index b76066b6..a3e5243a 100644 --- a/tests/Patient/AutoCreateSessionOnConfirmTest.php +++ b/tests/Patient/AutoCreateSessionOnConfirmTest.php @@ -47,8 +47,7 @@ class AutoCreateSessionOnConfirmTest extends ApiTestCase private function makeAppointment(Doctor $doctor, ?Clinic $clinic = null): Appointment { - $appointment = new Appointment($doctor, $this->createUser(['ROLE_USER']), 1_790_000_000, 1_790_001_800); - $appointment->setClinic($clinic); + $appointment = $this->newAppointment($doctor, $this->createUser(['ROLE_USER']), 1_790_000_000, 1_790_001_800, $clinic); $appointment->transitionTo(Appointment::STATUS_CONFIRMED); $this->em->persist($appointment); $this->em->flush(); @@ -110,7 +109,7 @@ class AutoCreateSessionOnConfirmTest extends ApiTestCase $this->confirmation()->onConfirmed($first); $this->em->flush(); - $second = new Appointment($doctor, $patient, 1_790_100_000, 1_790_101_800); + $second = $this->newAppointment($doctor, $patient, 1_790_100_000, 1_790_101_800); $second->transitionTo(Appointment::STATUS_CONFIRMED); $this->em->persist($second); $this->em->flush(); diff --git a/tests/Patient/ClinicRecordAccessTest.php b/tests/Patient/ClinicRecordAccessTest.php index 4607739f..879c6e39 100644 --- a/tests/Patient/ClinicRecordAccessTest.php +++ b/tests/Patient/ClinicRecordAccessTest.php @@ -61,8 +61,7 @@ class ClinicRecordAccessTest extends ApiTestCase $this->em->persist($record); $start = strtotime('+60 days') + random_int(0, 500_000) * 7; - $appointment = new Appointment($doctor, $patient, $start, $start + 900); - $appointment->setClinic($clinic); + $appointment = $this->newAppointment($doctor, $patient, $start, $start + 900, $clinic); $this->em->persist($appointment); $this->em->flush(); @@ -221,8 +220,7 @@ class ClinicRecordAccessTest extends ApiTestCase $patient = $this->createUser(); $start = strtotime('+70 days') + random_int(0, 500_000) * 7; - $appointment = new Appointment($doctor, $patient, $start, $start + 900); - $appointment->setClinic($clinic); + $appointment = $this->newAppointment($doctor, $patient, $start, $start + 900, $clinic); $appointment->setVisitPriceRials(3_000_000); $this->em->persist($appointment); $this->em->flush(); diff --git a/tests/Patient/PatientAppointmentsTest.php b/tests/Patient/PatientAppointmentsTest.php index 561095e9..16bb23c2 100644 --- a/tests/Patient/PatientAppointmentsTest.php +++ b/tests/Patient/PatientAppointmentsTest.php @@ -31,7 +31,7 @@ class PatientAppointmentsTest extends ApiTestCase private function appointment(Doctor $doctor, PatientRecord $record, int $slotStart): Appointment { - $appt = new Appointment($doctor, $record->getUser(), $slotStart, $slotStart + 1800); + $appt = $this->newAppointment($doctor, $record->getUser(), $slotStart, $slotStart + 1800); $this->em->persist($appt); $this->em->flush(); diff --git a/tests/Payment/AppointmentPaidConfirmFilesSessionTest.php b/tests/Payment/AppointmentPaidConfirmFilesSessionTest.php index c3bb64c4..2e857d0a 100644 --- a/tests/Payment/AppointmentPaidConfirmFilesSessionTest.php +++ b/tests/Payment/AppointmentPaidConfirmFilesSessionTest.php @@ -37,7 +37,7 @@ class AppointmentPaidConfirmFilesSessionTest extends ApiTestCase $this->em->flush(); $slotStart = strtotime('+30 days') + random_int(0, 500_000) * 7; - $appointment = new Appointment($doctor, $this->createUser(['ROLE_USER']), $slotStart, $slotStart + 1_800); + $appointment = $this->newAppointment($doctor, $this->createUser(['ROLE_USER']), $slotStart, $slotStart + 1_800); // رزرو آنلاین با پنجرهٔ پرداخت ثبت می‌شود؛ پرداخت باید همین را پاک کند. $appointment->markPendingWithTtl(Appointment::PAYMENT_TTL); if ($withClinic !== null) { diff --git a/tests/Secretary/SecretaryAppointmentScopeTest.php b/tests/Secretary/SecretaryAppointmentScopeTest.php index 5d25f665..76aa1347 100644 --- a/tests/Secretary/SecretaryAppointmentScopeTest.php +++ b/tests/Secretary/SecretaryAppointmentScopeTest.php @@ -39,8 +39,8 @@ class SecretaryAppointmentScopeTest extends ApiTestCase // یک نوبت برای هر پزشک $patient = $this->createUser(['ROLE_USER']); $start = time() + 3600; - $this->em->persist(new Appointment($doctorA, $patient, $start, $start + 900)); - $this->em->persist(new Appointment($doctorB, $patient, $start + 1800, $start + 2700)); + $this->em->persist($this->newAppointment($doctorA, $patient, $start, $start + 900)); + $this->em->persist($this->newAppointment($doctorB, $patient, $start + 1800, $start + 2700)); $this->em->flush(); $body = $this->authJson('GET', '/api/v1/my/appointments', $secretaryUser); @@ -66,7 +66,7 @@ class SecretaryAppointmentScopeTest extends ApiTestCase $patient = $this->createUser(['ROLE_USER']); $start = time() + 3600; - $this->em->persist(new Appointment($doctor, $patient, $start, $start + 900)); + $this->em->persist($this->newAppointment($doctor, $patient, $start, $start + 900)); $this->em->flush(); $body = $this->authJson('GET', '/api/v1/my/appointments', $secretaryUser); diff --git a/tests/Secretary/SecretaryOnlineShareTest.php b/tests/Secretary/SecretaryOnlineShareTest.php index 4a8275bb..364b7bd2 100644 --- a/tests/Secretary/SecretaryOnlineShareTest.php +++ b/tests/Secretary/SecretaryOnlineShareTest.php @@ -62,8 +62,7 @@ class SecretaryOnlineShareTest extends ApiTestCase private function makeOnlinePayment(Doctor $doctor, ?Clinic $clinic = null): Payment { $start = strtotime('+30 days') + random_int(0, 500_000) * 7; - $appointment = new Appointment($doctor, $this->createUser(), $start, $start + 900); - $appointment->setClinic($clinic); + $appointment = $this->newAppointment($doctor, $this->createUser(), $start, $start + 900, $clinic); $this->em->persist($appointment); $payment = new Payment($this->createUser(), self::GROSS, 'mock', Payment::TYPE_APPOINTMENT, '');