A doctor's office number sat next to the address on the public profile and in the anonymous API payload, so harvesting the phone number of every practice in the country was one unauthenticated request away. Street address and map coordinates stay public — a patient needs those to find the place — but the phone is now opt-in per caller: DoctorAddress::toArray() and the clinic serializers only emit it when told to, and the public doctor/clinic endpoints tell them to only when the caller may edit that profile (the same can_edit they already compute). Owner-facing address CRUD keeps returning it unchanged. The patient still gets the number where it is actually useful — their own appointment. That payload also stops guessing: it used to serialise the doctor's *first* address, so a booking made at the clinic or at a second office showed the wrong street entirely. It now resolves the address recorded on the appointment itself, which works the same for a personal office and a clinic branch, and falls back to the clinic's own number when the address has none. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
299 lines
14 KiB
PHP
299 lines
14 KiB
PHP
<?php
|
|
|
|
namespace App\Clinic\Entity;
|
|
|
|
use App\Auth\Entity\User;
|
|
use App\Doctor\Entity\Doctor;
|
|
use App\DoctorService\Entity\DoctorService;
|
|
use App\Insurance\Entity\Insurance;
|
|
use App\Shared\Util\DisplayName;
|
|
use App\Specialty\Entity\Specialty;
|
|
use Doctrine\Common\Collections\ArrayCollection;
|
|
use Doctrine\Common\Collections\Collection;
|
|
use Doctrine\ORM\Mapping as ORM;
|
|
use App\Clinic\Repository\ClinicRepository;
|
|
use Symfony\Component\Uid\Uuid;
|
|
|
|
#[ORM\Entity(repositoryClass: ClinicRepository::class)]
|
|
#[ORM\Table(name: 'clinics')]
|
|
#[ORM\Index(columns: ['user_id'], name: 'idx_clinics_owner')]
|
|
#[ORM\Index(columns: ['city_id'], name: 'idx_clinics_city')]
|
|
#[ORM\Index(columns: ['province_id'], name: 'idx_clinics_province')]
|
|
class Clinic
|
|
{
|
|
#[ORM\Id]
|
|
#[ORM\GeneratedValue]
|
|
#[ORM\Column(type: 'integer')]
|
|
private ?int $id = null;
|
|
|
|
#[ORM\Column(type: 'string', length: 36, unique: true)]
|
|
private string $uuid;
|
|
|
|
#[ORM\ManyToOne(targetEntity: User::class)]
|
|
#[ORM\JoinColumn(name: 'user_id', referencedColumnName: 'id', nullable: false, onDelete: 'RESTRICT')]
|
|
private User $user;
|
|
|
|
#[ORM\Column(type: 'string', length: 255, nullable: true)]
|
|
private ?string $name = null;
|
|
|
|
/**
|
|
* حوزهٔ فعالیت کلینیک — تعیین میکند کدام `TreatmentWorkflow` صدا زده شود.
|
|
*
|
|
* `null` یعنی تنظیمنشده و رفتار پیشفرض، نه خطا: کلینیکهای موجود بدون انتخاب
|
|
* حوزه باید دقیقاً مثل امروز کار کنند.
|
|
*/
|
|
#[ORM\ManyToOne(targetEntity: \App\PracticeDomain\Entity\PracticeDomain::class)]
|
|
#[ORM\JoinColumn(name: 'practice_domain_id', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
|
|
private ?\App\PracticeDomain\Entity\PracticeDomain $practiceDomain = null;
|
|
|
|
#[ORM\Column(type: 'text', nullable: true)]
|
|
private ?string $info = null;
|
|
|
|
#[ORM\Column(type: 'text', nullable: true)]
|
|
private ?string $address = null;
|
|
|
|
#[ORM\Column(type: 'string', length: 50, nullable: true)]
|
|
private ?string $telephone = null;
|
|
|
|
#[ORM\Column(name: 'is_24_7', type: 'boolean')]
|
|
private bool $is247 = false;
|
|
|
|
#[ORM\Column(name: 'working_days', type: 'string', length: 255, nullable: true)]
|
|
private ?string $workingDays = null;
|
|
|
|
#[ORM\Column(type: 'float', nullable: true)]
|
|
private ?float $latitude = null;
|
|
|
|
#[ORM\Column(type: 'float', nullable: true)]
|
|
private ?float $longitude = null;
|
|
|
|
#[ORM\Column(name: 'city_id', type: 'integer', nullable: true)]
|
|
private ?int $cityId = null;
|
|
|
|
#[ORM\Column(name: 'province_id', type: 'integer', nullable: true)]
|
|
private ?int $provinceId = null;
|
|
|
|
#[ORM\Column(name: 'representation_id', type: 'integer', nullable: true)]
|
|
private ?int $representationId = null;
|
|
|
|
#[ORM\Column(name: 'is_active', type: 'boolean')]
|
|
private bool $isActive = true;
|
|
|
|
#[ORM\Column(name: 'images_clinic', type: 'json', nullable: true)]
|
|
private ?array $imagesClinic = null;
|
|
|
|
#[ORM\Column(name: 'social_media', type: 'json', nullable: true)]
|
|
private ?array $socialMedia = null;
|
|
|
|
#[ORM\Column(name: 'clinic_logo', type: 'string', length: 500, nullable: true)]
|
|
private ?string $clinicLogo = null;
|
|
|
|
#[ORM\Column(name: 'notification_mobile', type: 'string', length: 15, nullable: true)]
|
|
private ?string $notificationMobile = null;
|
|
|
|
#[ORM\Column(name: 'created_at', type: 'integer')]
|
|
private int $createdAt;
|
|
|
|
#[ORM\Column(name: 'updated_at', type: 'integer')]
|
|
private int $updatedAt;
|
|
|
|
#[ORM\ManyToMany(targetEntity: Doctor::class)]
|
|
#[ORM\JoinTable(
|
|
name: 'clinic_doctors',
|
|
joinColumns: [new ORM\JoinColumn(name: 'clinic_id', referencedColumnName: 'id', onDelete: 'CASCADE')],
|
|
inverseJoinColumns: [new ORM\JoinColumn(name: 'doctor_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
|
|
)]
|
|
private Collection $doctors;
|
|
|
|
#[ORM\ManyToMany(targetEntity: Specialty::class)]
|
|
#[ORM\JoinTable(
|
|
name: 'clinic_specialties',
|
|
joinColumns: [new ORM\JoinColumn(name: 'clinic_id', referencedColumnName: 'id', onDelete: 'CASCADE')],
|
|
inverseJoinColumns: [new ORM\JoinColumn(name: 'specialty_id', referencedColumnName: 'id')]
|
|
)]
|
|
private Collection $specialties;
|
|
|
|
#[ORM\ManyToMany(targetEntity: DoctorService::class)]
|
|
#[ORM\JoinTable(
|
|
name: 'clinic_services',
|
|
joinColumns: [new ORM\JoinColumn(name: 'clinic_id', referencedColumnName: 'id', onDelete: 'CASCADE')],
|
|
inverseJoinColumns: [new ORM\JoinColumn(name: 'service_id', referencedColumnName: 'id')]
|
|
)]
|
|
private Collection $services;
|
|
|
|
#[ORM\ManyToMany(targetEntity: Insurance::class)]
|
|
#[ORM\JoinTable(
|
|
name: 'clinic_insurances',
|
|
joinColumns: [new ORM\JoinColumn(name: 'clinic_id', referencedColumnName: 'id', onDelete: 'CASCADE')],
|
|
inverseJoinColumns: [new ORM\JoinColumn(name: 'insurance_id', referencedColumnName: 'id')]
|
|
)]
|
|
private Collection $insurances;
|
|
|
|
public function __construct(User $user)
|
|
{
|
|
$this->uuid = Uuid::v4()->toRfc4122();
|
|
$this->user = $user;
|
|
$this->createdAt = time();
|
|
$this->updatedAt = time();
|
|
$this->doctors = new ArrayCollection();
|
|
$this->specialties = new ArrayCollection();
|
|
$this->services = new ArrayCollection();
|
|
$this->insurances = new ArrayCollection();
|
|
}
|
|
|
|
public function getId(): ?int { return $this->id; }
|
|
public function getUuid(): string { return $this->uuid; }
|
|
public function getUser(): User { return $this->user; }
|
|
public function getName(): ?string { return $this->name; }
|
|
public function getInfo(): ?string { return $this->info; }
|
|
public function getAddress(): ?string { return $this->address; }
|
|
public function getTelephone(): ?string { return $this->telephone; }
|
|
public function isIs247(): bool { return $this->is247; }
|
|
public function getWorkingDays(): ?string { return $this->workingDays; }
|
|
public function getLatitude(): ?float { return $this->latitude; }
|
|
public function getLongitude(): ?float { return $this->longitude; }
|
|
public function getCityId(): ?int { return $this->cityId; }
|
|
public function getProvinceId(): ?int { return $this->provinceId; }
|
|
public function getRepresentationId(): ?int { return $this->representationId; }
|
|
public function isActive(): bool { return $this->isActive; }
|
|
public function getImagesClinic(): ?array { return $this->imagesClinic; }
|
|
public function getSocialMedia(): ?array { return $this->socialMedia; }
|
|
public function getClinicLogo(): ?string { return $this->clinicLogo; }
|
|
public function getNotificationMobile(): ?string { return $this->notificationMobile; }
|
|
public function getCreatedAt(): int { return $this->createdAt; }
|
|
public function getUpdatedAt(): int { return $this->updatedAt; }
|
|
public function getDoctors(): Collection { return $this->doctors; }
|
|
public function getPracticeDomain(): ?\App\PracticeDomain\Entity\PracticeDomain { return $this->practiceDomain; }
|
|
|
|
public function hasDoctor(Doctor $doctor): bool { return $this->doctors->contains($doctor); }
|
|
|
|
public function removeDoctor(Doctor $doctor): self
|
|
{
|
|
$this->doctors->removeElement($doctor);
|
|
$this->touch();
|
|
return $this;
|
|
}
|
|
|
|
public function getSpecialties(): Collection { return $this->specialties; }
|
|
public function getServices(): Collection { return $this->services; }
|
|
public function getInsurances(): Collection { return $this->insurances; }
|
|
|
|
// null مجاز است (کلینیک تازهساخته هنوز نام ندارد)؛ ولی نام آلوده رد میشود.
|
|
public function setName(?string $v): self { if ($v !== null) DisplayName::assertReal($v); $this->name = $v; $this->touch(); return $this; }
|
|
public function setInfo(?string $v): self { $this->info = $v; $this->touch(); return $this; }
|
|
public function setAddress(?string $v): self { $this->address = $v; $this->touch(); return $this; }
|
|
public function setTelephone(?string $v): self { $this->telephone = $v; $this->touch(); return $this; }
|
|
public function setIs247(bool $v): self { $this->is247 = $v; $this->touch(); return $this; }
|
|
public function setWorkingDays(?string $v): self { $this->workingDays = $v; $this->touch(); return $this; }
|
|
public function setLatitude(?float $v): self { $this->latitude = $v; $this->touch(); return $this; }
|
|
public function setLongitude(?float $v): self { $this->longitude = $v; $this->touch(); return $this; }
|
|
public function setCityId(?int $v): self { $this->cityId = $v; $this->touch(); return $this; }
|
|
public function setProvinceId(?int $v): self { $this->provinceId = $v; $this->touch(); return $this; }
|
|
public function setRepresentationId(?int $v): self { $this->representationId = $v; $this->touch(); return $this; }
|
|
public function setIsActive(bool $v): self { $this->isActive = $v; $this->touch(); return $this; }
|
|
public function setImagesClinic(?array $v): self { $this->imagesClinic = $v; $this->touch(); return $this; }
|
|
public function setSocialMedia(?array $v): self { $this->socialMedia = $v; $this->touch(); return $this; }
|
|
public function setClinicLogo(?string $v): self { $this->clinicLogo = $v; $this->touch(); return $this; }
|
|
public function setNotificationMobile(?string $v): self { $this->notificationMobile = $v; $this->touch(); return $this; }
|
|
public function setPracticeDomain(?\App\PracticeDomain\Entity\PracticeDomain $v): self { $this->practiceDomain = $v; $this->touch(); return $this; }
|
|
|
|
private function touch(): void { $this->updatedAt = time(); }
|
|
|
|
/**
|
|
* $street/$telephone come from the clinic's DoctorAddress record. The columns
|
|
* on this entity are the deprecated legacy source and are only a fallback:
|
|
* mixing the two makes a single response describe two different places.
|
|
*/
|
|
public function toDetailArray(
|
|
array $provinceData = [],
|
|
array $cityData = [],
|
|
?array $map = null,
|
|
?string $street = null,
|
|
?string $telephone = null,
|
|
bool $withContact = false
|
|
): array {
|
|
// شمارهٔ کلینیک هم مثل شمارهٔ مطب عمومی نیست. {@see DoctorAddress::toArray()}
|
|
$phone = $withContact ? $this->firstFilled($telephone, $this->telephone) : null;
|
|
$address = $this->firstFilled($street, $this->address);
|
|
|
|
return [
|
|
'id' => (string) $this->id,
|
|
'uuid' => $this->uuid,
|
|
'name' => $this->name,
|
|
'title' => $this->name,
|
|
'is_active' => $this->isActive,
|
|
'phone' => $phone,
|
|
'logo' => $this->clinicLogo,
|
|
'images_clinic' => $this->imagesClinic ?? [],
|
|
'social_media' => $this->socialMedia,
|
|
'clinic_logo' => $this->clinicLogo,
|
|
'phone_number' => $phone,
|
|
'caption' => $this->info,
|
|
'list_bime' => array_map(fn(Insurance $i) => [
|
|
'uuid' => $i->getUuid(), 'id' => (string) $i->getId(), 'name' => $i->getName(),
|
|
], $this->insurances->toArray()),
|
|
'specialties' => array_map(fn(Specialty $s) => [
|
|
'uuid' => $s->getUuid(), 'id' => (string) $s->getId(), 'name' => $s->getName(),
|
|
'parent' => $s->getParent()?->getId() !== null ? (string) $s->getParent()->getId() : null,
|
|
], $this->specialties->toArray()),
|
|
'services' => array_map(fn(DoctorService $ds) => [
|
|
'uuid' => $ds->getUuid(), 'id' => (string) $ds->getId(), 'name' => $ds->getName(),
|
|
], $this->services->toArray()),
|
|
'clinic_specialty' => array_map(fn(Specialty $s) => [
|
|
'uuid' => $s->getUuid(), 'id' => (string) $s->getId(), 'name' => $s->getName(),
|
|
'parent' => $s->getParent()?->getId() !== null ? (string) $s->getParent()->getId() : null,
|
|
], $this->specialties->toArray()),
|
|
'doctors' => $this->doctors->count(),
|
|
'doctor_list' => null,
|
|
'practice_domain' => $this->practiceDomain?->toArray(),
|
|
'city' => $cityData ? [$cityData] : [],
|
|
'state' => $provinceData ? [$provinceData] : [],
|
|
'location' => $address,
|
|
'map' => $map ?? [
|
|
'latitude' => $this->latitude !== null ? (string) $this->latitude : null,
|
|
'longitude' => $this->longitude !== null ? (string) $this->longitude : null,
|
|
],
|
|
'24_7' => $this->is247,
|
|
'field_working_days' => $this->workingDays,
|
|
];
|
|
}
|
|
|
|
private function firstFilled(?string ...$values): ?string
|
|
{
|
|
foreach ($values as $value) {
|
|
if ($value !== null && trim($value) !== '') {
|
|
return $value;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public function toListArray(?string $city = null, ?string $state = null, ?string $telephone = null, bool $withContact = false): array
|
|
{
|
|
$phone = $withContact ? $this->firstFilled($telephone, $this->telephone) : null;
|
|
|
|
return [
|
|
'id' => (string) $this->id,
|
|
'uuid' => $this->uuid,
|
|
'name' => $this->name,
|
|
'title' => $this->name,
|
|
'phone' => $phone,
|
|
'phone_number' => $phone,
|
|
'logo' => $this->clinicLogo,
|
|
'clinic_logo' => $this->clinicLogo,
|
|
'images_clinic' => $this->imagesClinic ?? [],
|
|
'doctors_count' => $this->doctors->count(),
|
|
'is_active' => $this->isActive,
|
|
'created_at' => $this->createdAt,
|
|
'city' => $city,
|
|
'state' => $state,
|
|
'specialties' => array_map(fn(Specialty $s) => [
|
|
'uuid' => $s->getUuid(), 'id' => (string) $s->getId(), 'name' => $s->getName(),
|
|
], $this->specialties->toArray()),
|
|
'24_7' => $this->is247,
|
|
'field_working_days' => $this->workingDays,
|
|
];
|
|
}
|
|
}
|