refactor(tenant): give every table one spelling of the tenant pair
Phase 3 of the tenant-marking series. The same concept was written four ways, and the Doctrine filter arriving in phase 4 keys on the field name — so the tables using a different spelling would have been skipped silently, which is exactly the leak this work exists to prevent. - discount_rules: owner_type/owner_id renamed to entity_type/entity_id. Pure rename, no data moves. - doctor_secretaries: owner_type plus a nullable clinic_id replaced by the shared pair. The environment now comes from the clinic argument alone, so the inconsistent combination (owner_type='clinic', clinic_id=NULL) can no longer be constructed, and the redundant constructor parameter is gone. - user_active_context: added db_type, so resolving an environment is one lookup instead of "try clinics, then try doctors". Filled from the type already present in available_contexts. - entity_type is VARCHAR(10) in all twenty tenant tables; four of them were 20. Behaviour change, the only one in this series: the doctor_secretaries unique key went from (doctor_id, secretary_id, owner_type) to (doctor_id, secretary_id, entity_type, entity_id). With clinic_id outside the key, one secretary could not be assigned to the same doctor in two clinics — the second row collided on owner_type='clinic'. The duplicate check in SecretaryController had the same blind spot and would have rejected the request before the database saw it; both are fixed together. Correcting an assumption from the phase-3 plan: mobile_verification_otp.entity_type really is a tenant pair. NotificationMobileController validates the target against ['doctor','clinic'] and stores that entity's id, so the column was normalised with the rest rather than treated as unrelated. TenantOwnedTrait gained assignTenantPair() for callers that resolved the pair as scalars and hold no entity — building an EntityContext from scalars would produce one where isClinic() is true but ->clinic is null, breaking consumers silently. tests/ApiTestCase::createUser now retries on a duplicate mobile. db_test is never reset and already holds ~38k users, so the 9-digit random draw collided often enough to fail unrelated tests a few percent of runs. Tests: 830 passing. PHPStan reports no new errors on the changed files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -597,7 +597,11 @@ class AuthController extends BaseController
|
||||
// اگر یک context داری، خودکار فعال کن
|
||||
$activeCtx = $this->contextRepo->findByUser($user);
|
||||
if ($activeCtx === null && count($availableContexts) === 1) {
|
||||
$activeCtx = $this->contextRepo->upsert($user, $availableContexts[0]['db_uuid']);
|
||||
$activeCtx = $this->contextRepo->upsert(
|
||||
$user,
|
||||
$availableContexts[0]['db_uuid'],
|
||||
$availableContexts[0]['type'],
|
||||
);
|
||||
}
|
||||
|
||||
$dbUuid = $activeCtx?->getDbUuid();
|
||||
@@ -663,7 +667,7 @@ class AuthController extends BaseController
|
||||
return $this->error(ErrorCodes::ERR_AUTH_006, 'دسترسی به این محیط کاری مجاز نیست', 403);
|
||||
}
|
||||
|
||||
$this->contextRepo->upsert($user, $dbUuid);
|
||||
$this->contextRepo->upsert($user, $dbUuid, $matched['type']);
|
||||
|
||||
return $this->success([
|
||||
'db_uuid' => $dbUuid,
|
||||
|
||||
@@ -14,7 +14,7 @@ class MobileVerificationOtp
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(name: 'entity_type', type: 'string', length: 20)]
|
||||
#[ORM\Column(name: 'entity_type', type: 'string', length: 10)]
|
||||
private string $entityType;
|
||||
|
||||
#[ORM\Column(name: 'entity_id', type: 'integer')]
|
||||
|
||||
@@ -2,9 +2,16 @@
|
||||
|
||||
namespace App\Auth\Entity;
|
||||
|
||||
use App\Shared\Context\EntityContext;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
use App\Auth\Repository\UserActiveContextRepository;
|
||||
|
||||
/**
|
||||
* آخرین محیط کاری انتخابشدهٔ کاربر. db_uuid بهتنهایی نمیگوید مالِ کلینیک است یا
|
||||
* پزشک، پس db_type هم ذخیره میشود تا EntityContextResolver با یک lookup به نتیجه
|
||||
* برسد بهجای اینکه اول کلینیک را امتحان کند و بعد پزشک را.
|
||||
*/
|
||||
#[ORM\Entity(repositoryClass: UserActiveContextRepository::class)]
|
||||
#[ORM\Table(name: 'user_active_context')]
|
||||
class UserActiveContext
|
||||
@@ -17,24 +24,38 @@ class UserActiveContext
|
||||
#[ORM\Column(name: 'db_uuid', type: 'string', length: 36)]
|
||||
private string $dbUuid;
|
||||
|
||||
/** 'doctor' یا 'clinic' — کدام موجودیت را db_uuid آدرس میدهد. */
|
||||
#[ORM\Column(name: 'db_type', type: 'string', length: 10)]
|
||||
private string $dbType;
|
||||
|
||||
#[ORM\Column(name: 'updated_at', type: 'integer')]
|
||||
private int $updatedAt;
|
||||
|
||||
public function __construct(User $user, string $dbUuid)
|
||||
public function __construct(User $user, string $dbUuid, string $dbType)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->dbUuid = $dbUuid;
|
||||
$this->updatedAt = time();
|
||||
$this->setContext($dbUuid, $dbType);
|
||||
}
|
||||
|
||||
public function getUser(): User { return $this->user; }
|
||||
public function getUser(): User { return $this->user; }
|
||||
public function getDbUuid(): string { return $this->dbUuid; }
|
||||
public function getDbType(): string { return $this->dbType; }
|
||||
public function getUpdatedAt(): int { return $this->updatedAt; }
|
||||
|
||||
public function setDbUuid(string $dbUuid): self
|
||||
public function isClinic(): bool { return $this->dbType === EntityContext::TYPE_CLINIC; }
|
||||
|
||||
/** @throws \InvalidArgumentException اگر نوع محیط یکی از doctor/clinic نباشد */
|
||||
public function setContext(string $dbUuid, string $dbType): self
|
||||
{
|
||||
if (!in_array($dbType, [EntityContext::TYPE_DOCTOR, EntityContext::TYPE_CLINIC], true)) {
|
||||
throw new \InvalidArgumentException("Unknown active-context type: {$dbType}");
|
||||
}
|
||||
|
||||
$this->dbUuid = $dbUuid;
|
||||
$this->dbType = $dbType;
|
||||
$this->updatedAt = time();
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,14 +19,14 @@ class UserActiveContextRepository extends ServiceEntityRepository
|
||||
return $this->findOneBy(['user' => $user]);
|
||||
}
|
||||
|
||||
public function upsert(User $user, string $dbUuid): UserActiveContext
|
||||
public function upsert(User $user, string $dbUuid, string $dbType): UserActiveContext
|
||||
{
|
||||
$ctx = $this->findByUser($user);
|
||||
if ($ctx === null) {
|
||||
$ctx = new UserActiveContext($user, $dbUuid);
|
||||
$ctx = new UserActiveContext($user, $dbUuid, $dbType);
|
||||
$this->getEntityManager()->persist($ctx);
|
||||
} else {
|
||||
$ctx->setDbUuid($dbUuid);
|
||||
$ctx->setContext($dbUuid, $dbType);
|
||||
}
|
||||
$this->getEntityManager()->flush();
|
||||
return $ctx;
|
||||
|
||||
@@ -3,18 +3,21 @@
|
||||
namespace App\Discount\Entity;
|
||||
|
||||
use App\Discount\Repository\DiscountRuleRepository;
|
||||
use App\Shared\Tenant\TenantOwnedTrait;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
|
||||
/**
|
||||
* قانون تخفیف عمومی، per-tenant (owner = doctor|clinic). موتور تخفیف
|
||||
* قانون تخفیف عمومی، per-tenant (پزشک یا کلینیک). موتور تخفیف
|
||||
* (DiscountEngine) این قوانین را برای یک پرونده ارزیابی میکند.
|
||||
*/
|
||||
#[ORM\Entity(repositoryClass: DiscountRuleRepository::class)]
|
||||
#[ORM\Table(name: 'discount_rules')]
|
||||
#[ORM\Index(columns: ['owner_type', 'owner_id', 'active'], name: 'idx_discount_rules_owner')]
|
||||
#[ORM\Index(columns: ['entity_type', 'entity_id', 'active'], name: 'idx_discount_rules_tenant')]
|
||||
class DiscountRule
|
||||
{
|
||||
use TenantOwnedTrait;
|
||||
|
||||
public const TYPE_PATIENT_TAG = 'patient_tag';
|
||||
public const TYPE_INVOICE_AMOUNT = 'invoice_amount';
|
||||
public const TYPE_SPECIFIC_PATIENT = 'specific_patient';
|
||||
@@ -45,12 +48,6 @@ class DiscountRule
|
||||
#[ORM\Column(type: 'string', length: 36, unique: true)]
|
||||
private string $uuid;
|
||||
|
||||
#[ORM\Column(name: 'owner_type', type: 'string', length: 10)]
|
||||
private string $ownerType;
|
||||
|
||||
#[ORM\Column(name: 'owner_id', type: 'integer')]
|
||||
private int $ownerId;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 120)]
|
||||
private string $name;
|
||||
|
||||
@@ -104,21 +101,18 @@ class DiscountRule
|
||||
#[ORM\Column(name: 'updated_at', type: 'integer')]
|
||||
private int $updatedAt;
|
||||
|
||||
public function __construct(string $ownerType, int $ownerId, string $name, string $type)
|
||||
public function __construct(string $entityType, int $entityId, string $name, string $type)
|
||||
{
|
||||
$this->uuid = Uuid::v4()->toRfc4122();
|
||||
$this->ownerType = $ownerType;
|
||||
$this->ownerId = $ownerId;
|
||||
$this->name = $name;
|
||||
$this->type = $type;
|
||||
$this->createdAt = time();
|
||||
$this->updatedAt = time();
|
||||
$this->assignTenantPair($entityType, $entityId);
|
||||
}
|
||||
|
||||
public function getId(): ?int { return $this->id; }
|
||||
public function getUuid(): string { return $this->uuid; }
|
||||
public function getOwnerType(): string { return $this->ownerType; }
|
||||
public function getOwnerId(): int { return $this->ownerId; }
|
||||
public function getName(): string { return $this->name; }
|
||||
public function getType(): string { return $this->type; }
|
||||
public function getDiscountType(): string { return $this->discountType; }
|
||||
|
||||
@@ -26,15 +26,15 @@ class DiscountRuleRepository extends ServiceEntityRepository
|
||||
}
|
||||
}
|
||||
|
||||
public function findByUuidForOwner(string $uuid, string $ownerType, int $ownerId): ?DiscountRule
|
||||
public function findByUuidForOwner(string $uuid, string $entityType, int $entityId): ?DiscountRule
|
||||
{
|
||||
return $this->findOneBy(['uuid' => $uuid, 'ownerType' => $ownerType, 'ownerId' => $ownerId]);
|
||||
return $this->findOneBy(['uuid' => $uuid, 'entityType' => $entityType, 'entityId' => $entityId]);
|
||||
}
|
||||
|
||||
/** @return DiscountRule[] قوانین فعالِ یک owner (برای موتور). */
|
||||
public function findActiveForOwner(string $ownerType, int $ownerId): array
|
||||
public function findActiveForOwner(string $entityType, int $entityId): array
|
||||
{
|
||||
return $this->findBy(['ownerType' => $ownerType, 'ownerId' => $ownerId, 'active' => true], ['priority' => 'DESC']);
|
||||
return $this->findBy(['entityType' => $entityType, 'entityId' => $entityId, 'active' => true], ['priority' => 'DESC']);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,11 +42,11 @@ class DiscountRuleRepository extends ServiceEntityRepository
|
||||
* موجودیتها برمیگردند تا کنترلر با toArray() قرارداد snake_case فرانت را بدهد.
|
||||
* @return DiscountRule[]
|
||||
*/
|
||||
public function findAllForOwner(string $ownerType, int $ownerId): array
|
||||
public function findAllForOwner(string $entityType, int $entityId): array
|
||||
{
|
||||
return $this->createQueryBuilder('r')
|
||||
->where('r.ownerType = :t')->setParameter('t', $ownerType)
|
||||
->andWhere('r.ownerId = :i')->setParameter('i', $ownerId)
|
||||
->where('r.entityType = :t')->setParameter('t', $entityType)
|
||||
->andWhere('r.entityId = :i')->setParameter('i', $entityId)
|
||||
->orderBy('r.priority', 'DESC')->addOrderBy('r.id', 'DESC')
|
||||
->getQuery()->getResult();
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ class InventoryItem
|
||||
#[ORM\Column(type: 'string', length: 36, unique: true)]
|
||||
private string $uuid;
|
||||
|
||||
#[ORM\Column(name: 'entity_type', type: 'string', length: 20)]
|
||||
#[ORM\Column(name: 'entity_type', type: 'string', length: 10)]
|
||||
private string $entityType;
|
||||
|
||||
#[ORM\Column(name: 'entity_id', type: 'integer')]
|
||||
|
||||
@@ -26,7 +26,7 @@ class InventoryPackage
|
||||
#[ORM\Column(type: 'string', length: 36, unique: true)]
|
||||
private string $uuid;
|
||||
|
||||
#[ORM\Column(name: 'entity_type', type: 'string', length: 20)]
|
||||
#[ORM\Column(name: 'entity_type', type: 'string', length: 10)]
|
||||
private string $entityType;
|
||||
|
||||
#[ORM\Column(name: 'entity_id', type: 'integer')]
|
||||
|
||||
@@ -7,6 +7,7 @@ use App\Auth\Repository\UserRepository;
|
||||
use App\Clinic\Repository\ClinicRepository;
|
||||
use App\Doctor\Repository\DoctorRepository;
|
||||
use App\Secretary\Entity\DoctorSecretary;
|
||||
use App\Shared\Context\EntityContext;
|
||||
use App\Secretary\Repository\DoctorSecretaryRepository;
|
||||
use App\Secretary\Service\SecretaryService;
|
||||
use App\Shared\Constant\ErrorCodes;
|
||||
@@ -240,8 +241,6 @@ class SecretaryController extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
$ownerType = $ownerClinic !== null ? DoctorSecretary::OWNER_CLINIC : DoctorSecretary::OWNER_DOCTOR;
|
||||
|
||||
// Check plan limit
|
||||
$limit = $this->subscriptionService->getSecretaryLimit('doctor', $doctor->getId());
|
||||
$activeCount = $this->secretaryRepo->countActiveByDoctor($doctor);
|
||||
@@ -270,17 +269,21 @@ class SecretaryController extends BaseController
|
||||
}
|
||||
$this->userRepo->save($secretaryUser);
|
||||
|
||||
// Check duplicate within same scope
|
||||
// تکراری فقط در همان محیط. entityId هم لازم است، وگرنه تخصیص همان منشی به
|
||||
// همان پزشک در کلینیک دوم — که کلید یکتا اجازهاش را میدهد — اینجا رد میشد.
|
||||
[$ownerEntityType, $ownerEntityId] = EntityContext::forBooking($doctor, $ownerClinic)->toEntityPair();
|
||||
|
||||
$existing = $this->secretaryRepo->findOneBy([
|
||||
'doctor' => $doctor,
|
||||
'secretary' => $secretaryUser,
|
||||
'ownerType' => $ownerType,
|
||||
'doctor' => $doctor,
|
||||
'secretary' => $secretaryUser,
|
||||
'entityType' => $ownerEntityType,
|
||||
'entityId' => $ownerEntityId,
|
||||
]);
|
||||
if ($existing !== null) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'این منشی قبلاً اضافه شده است', 409);
|
||||
}
|
||||
|
||||
$secretary = new DoctorSecretary($doctor, $secretaryUser, $ownerType, $ownerClinic);
|
||||
$secretary = new DoctorSecretary($doctor, $secretaryUser, $ownerClinic);
|
||||
|
||||
if (array_key_exists('national_code', $data)) {
|
||||
$secretary->setNationalCode($data['national_code'] !== null ? trim((string) $data['national_code']) : null);
|
||||
|
||||
@@ -6,16 +6,22 @@ use App\Auth\Entity\User;
|
||||
use App\Clinic\Entity\Clinic;
|
||||
use App\Doctor\Entity\Doctor;
|
||||
use App\Secretary\Repository\DoctorSecretaryRepository;
|
||||
use App\Shared\Context\EntityContext;
|
||||
use App\Shared\Tenant\TenantOwnedTrait;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
|
||||
#[ORM\Entity(repositoryClass: DoctorSecretaryRepository::class)]
|
||||
#[ORM\Table(name: 'doctor_secretaries')]
|
||||
#[ORM\UniqueConstraint(name: 'idx_doctor_secretary_scope', columns: ['doctor_id', 'secretary_id', 'owner_type'])]
|
||||
// entity_id در کلید هست تا یک منشی بتواند به همان پزشک در چند کلینیک تخصیص یابد؛
|
||||
// کلید قبلی فقط owner_type را داشت و ردیف دومِ 'clinic' را تکراری میشمرد.
|
||||
#[ORM\UniqueConstraint(name: 'uniq_doctor_secretary_scope', columns: ['doctor_id', 'secretary_id', 'entity_type', 'entity_id'])]
|
||||
class DoctorSecretary
|
||||
{
|
||||
public const OWNER_DOCTOR = 'doctor';
|
||||
public const OWNER_CLINIC = 'clinic';
|
||||
use TenantOwnedTrait;
|
||||
|
||||
public const OWNER_DOCTOR = EntityContext::TYPE_DOCTOR;
|
||||
public const OWNER_CLINIC = EntityContext::TYPE_CLINIC;
|
||||
|
||||
public const DEFAULT_PERMISSIONS = [
|
||||
'version' => 1,
|
||||
@@ -54,9 +60,6 @@ class DoctorSecretary
|
||||
#[ORM\JoinColumn(name: 'secretary_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||
private User $secretary;
|
||||
|
||||
#[ORM\Column(name: 'owner_type', type: 'string', length: 10, options: ['default' => 'doctor'])]
|
||||
private string $ownerType;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Clinic::class)]
|
||||
#[ORM\JoinColumn(name: 'clinic_id', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
|
||||
private ?Clinic $clinic = null;
|
||||
@@ -90,23 +93,27 @@ class DoctorSecretary
|
||||
#[ORM\Column(name: 'updated_at', type: 'integer')]
|
||||
private int $updatedAt;
|
||||
|
||||
public function __construct(Doctor $doctor, User $secretary, string $ownerType = self::OWNER_DOCTOR, ?Clinic $clinic = null)
|
||||
/** محیط از $clinic میآید: null یعنی مطب شخصی همان پزشک. */
|
||||
public function __construct(Doctor $doctor, User $secretary, ?Clinic $clinic = null)
|
||||
{
|
||||
$this->uuid = Uuid::v4()->toRfc4122();
|
||||
$this->doctor = $doctor;
|
||||
$this->secretary = $secretary;
|
||||
$this->ownerType = $ownerType;
|
||||
$this->clinic = $clinic;
|
||||
$this->permissions = self::DEFAULT_PERMISSIONS;
|
||||
$this->createdAt = time();
|
||||
$this->updatedAt = time();
|
||||
|
||||
// clinic تعیینکننده است، نه $ownerType: ترکیب ناسازگارِ ('clinic', clinic=null)
|
||||
// در گذشته ممکن بود ساخته شود و همین ردیفها بودند که یکتایی را میشکستند.
|
||||
$this->assignTenant(EntityContext::forBooking($doctor, $clinic));
|
||||
}
|
||||
|
||||
public function getId(): ?int { return $this->id; }
|
||||
public function getUuid(): string { return $this->uuid; }
|
||||
public function getDoctor(): Doctor { return $this->doctor; }
|
||||
public function getSecretary(): User { return $this->secretary; }
|
||||
public function getOwnerType(): string { return $this->ownerType; }
|
||||
public function getOwnerType(): string { return $this->entityType; }
|
||||
public function getClinic(): ?Clinic { return $this->clinic; }
|
||||
public function getPermissions(): array { return $this->permissions ?? self::DEFAULT_PERMISSIONS; }
|
||||
public function getNationalCode(): ?string { return $this->nationalCode; }
|
||||
@@ -161,7 +168,8 @@ class DoctorSecretary
|
||||
'mobile_number' => $this->secretary->getMobileNumber(),
|
||||
'doctor_name' => $this->doctor->getName(),
|
||||
'doctor_uuid' => $this->doctor->getUuid(),
|
||||
'owner_type' => $this->ownerType,
|
||||
// کلید پاسخ عمداً owner_type مانده تا قرارداد کلاینتها نشکند.
|
||||
'owner_type' => $this->entityType,
|
||||
'clinic_uuid' => $this->clinic?->getUuid(),
|
||||
'is_active' => $this->active,
|
||||
'online_share_enabled' => $this->onlineShareEnabled,
|
||||
|
||||
@@ -54,7 +54,7 @@ class DoctorSecretaryRepository extends ServiceEntityRepository
|
||||
{
|
||||
return $this->listWithRelations()
|
||||
->where('s.doctor = :doctor')
|
||||
->andWhere('s.ownerType = :type')
|
||||
->andWhere('s.entityType = :type')
|
||||
->setParameter('doctor', $doctor)
|
||||
->setParameter('type', DoctorSecretary::OWNER_DOCTOR)
|
||||
->getQuery()
|
||||
@@ -67,7 +67,7 @@ class DoctorSecretaryRepository extends ServiceEntityRepository
|
||||
return $this->findOneBy([
|
||||
'secretary' => $user,
|
||||
'doctor' => $doctor,
|
||||
'ownerType' => DoctorSecretary::OWNER_DOCTOR,
|
||||
'entityType' => DoctorSecretary::OWNER_DOCTOR,
|
||||
'active' => true,
|
||||
]);
|
||||
}
|
||||
@@ -78,7 +78,7 @@ class DoctorSecretaryRepository extends ServiceEntityRepository
|
||||
return $this->createQueryBuilder('s')
|
||||
->where('s.secretary = :user')
|
||||
->andWhere('s.clinic = :clinic')
|
||||
->andWhere('s.ownerType = :type')
|
||||
->andWhere('s.entityType = :type')
|
||||
->andWhere('s.active = true')
|
||||
->setParameter('user', $user)
|
||||
->setParameter('clinic', $clinic)
|
||||
@@ -95,7 +95,7 @@ class DoctorSecretaryRepository extends ServiceEntityRepository
|
||||
'secretary' => $user,
|
||||
'clinic' => $clinic,
|
||||
'doctor' => $doctor,
|
||||
'ownerType' => DoctorSecretary::OWNER_CLINIC,
|
||||
'entityType' => DoctorSecretary::OWNER_CLINIC,
|
||||
'active' => true,
|
||||
]);
|
||||
}
|
||||
@@ -109,7 +109,7 @@ class DoctorSecretaryRepository extends ServiceEntityRepository
|
||||
->join(DoctorSecretary::class, 's', 'WITH', 's.doctor = d')
|
||||
->where('s.secretary = :user')
|
||||
->andWhere('s.clinic = :clinic')
|
||||
->andWhere('s.ownerType = :type')
|
||||
->andWhere('s.entityType = :type')
|
||||
->andWhere('s.active = true')
|
||||
->setParameter('user', $user)
|
||||
->setParameter('clinic', $clinic)
|
||||
@@ -143,7 +143,7 @@ class DoctorSecretaryRepository extends ServiceEntityRepository
|
||||
->join('s.doctor', 'doc')
|
||||
->where('s.clinic = :clinic')
|
||||
->andWhere('s.secretary = :secretary')
|
||||
->andWhere('s.ownerType = :type')
|
||||
->andWhere('s.entityType = :type')
|
||||
->setParameter('clinic', $clinic)
|
||||
->setParameter('secretary', $secretary)
|
||||
->setParameter('type', DoctorSecretary::OWNER_CLINIC)
|
||||
@@ -156,7 +156,7 @@ class DoctorSecretaryRepository extends ServiceEntityRepository
|
||||
{
|
||||
return $this->listWithRelations()
|
||||
->where('s.clinic = :clinic')
|
||||
->andWhere('s.ownerType = :type')
|
||||
->andWhere('s.entityType = :type')
|
||||
->setParameter('clinic', $clinic)
|
||||
->setParameter('type', DoctorSecretary::OWNER_CLINIC)
|
||||
->getQuery()
|
||||
@@ -180,12 +180,12 @@ class DoctorSecretaryRepository extends ServiceEntityRepository
|
||||
|
||||
if ($clinic !== null) {
|
||||
$qb->andWhere('s.clinic = :clinic')
|
||||
->andWhere('s.ownerType = :type')
|
||||
->andWhere('s.entityType = :type')
|
||||
->setParameter('clinic', $clinic)
|
||||
->setParameter('type', DoctorSecretary::OWNER_CLINIC);
|
||||
} else {
|
||||
$qb->andWhere('s.doctor = :doctor')
|
||||
->andWhere('s.ownerType = :type')
|
||||
->andWhere('s.entityType = :type')
|
||||
->setParameter('doctor', $doctor)
|
||||
->setParameter('type', DoctorSecretary::OWNER_DOCTOR);
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ class SecretaryService
|
||||
$existing = $this->secretaryRepo->findOneBy([
|
||||
'doctor' => $doctor,
|
||||
'secretary' => $secretary,
|
||||
'ownerType' => DoctorSecretary::OWNER_CLINIC,
|
||||
'entityType' => DoctorSecretary::OWNER_CLINIC,
|
||||
]);
|
||||
if ($existing !== null) {
|
||||
if ($existing->isActive()) {
|
||||
@@ -104,7 +104,7 @@ class SecretaryService
|
||||
$skippedLimit[] = $doctorUuid;
|
||||
continue;
|
||||
}
|
||||
$row = new DoctorSecretary($doctor, $secretary, DoctorSecretary::OWNER_CLINIC, $clinic);
|
||||
$row = new DoctorSecretary($doctor, $secretary, $clinic);
|
||||
$this->applyMeta($row, $meta);
|
||||
$this->secretaryRepo->save($row, false);
|
||||
$created[] = $row;
|
||||
@@ -164,7 +164,7 @@ class SecretaryService
|
||||
$skippedLimit[] = $doctorUuid;
|
||||
continue;
|
||||
}
|
||||
$row = new DoctorSecretary($doctor, $secretary, DoctorSecretary::OWNER_CLINIC, $clinic);
|
||||
$row = new DoctorSecretary($doctor, $secretary, $clinic);
|
||||
if ($template !== null) {
|
||||
$row->setNationalCode($template->getNationalCode())
|
||||
->setAddress($template->getAddress())
|
||||
|
||||
@@ -95,10 +95,7 @@ class EntityContextResolver
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* محیط فعالِ ذخیرهشده. db_uuid یا uuid کلینیک است یا uuid پزشک؛ کلینیک اول
|
||||
* بررسی میشود چون پزشکِ دعوتشده هم db_uuid کلینیک را ذخیره میکند.
|
||||
*/
|
||||
/** محیط فعالِ ذخیرهشده؛ db_type میگوید db_uuid کدام موجودیت را آدرس میدهد. */
|
||||
private function fromActiveContext(User $user): ?EntityContext
|
||||
{
|
||||
$active = $this->activeContextRepo->findByUser($user);
|
||||
@@ -106,9 +103,12 @@ class EntityContextResolver
|
||||
return null;
|
||||
}
|
||||
|
||||
$clinic = $this->clinicRepo->findByUuid($active->getDbUuid());
|
||||
if ($clinic !== null) {
|
||||
return $this->canActInClinic($user, $clinic) ? EntityContext::forClinic($clinic) : null;
|
||||
if ($active->isClinic()) {
|
||||
$clinic = $this->clinicRepo->findByUuid($active->getDbUuid());
|
||||
|
||||
return $clinic !== null && $this->canActInClinic($user, $clinic)
|
||||
? EntityContext::forClinic($clinic)
|
||||
: null;
|
||||
}
|
||||
|
||||
$doctor = $this->doctorRepo->findByUuid($active->getDbUuid());
|
||||
|
||||
@@ -43,4 +43,27 @@ trait TenantOwnedTrait
|
||||
|
||||
[$this->entityType, $this->entityId] = $context->toEntityPair();
|
||||
}
|
||||
|
||||
/**
|
||||
* برای فراخوانیهایی که جفت را از قبل حل کردهاند و موجودیت را در دست ندارند —
|
||||
* مثل DiscountController که owner را بهصورت اسکالر از resolveOwner میگیرد.
|
||||
* ساختن EntityContext از روی اسکالر ممکن نیست: چنین contextی isClinic() درست
|
||||
* میدهد ولی ->clinic تهی دارد و مصرفکننده را بیصدا میشکند.
|
||||
*
|
||||
* @throws \InvalidArgumentException اگر جفت معتبر نباشد
|
||||
*/
|
||||
public function assignTenantPair(string $entityType, int $entityId): void
|
||||
{
|
||||
if (!in_array($entityType, [EntityContext::TYPE_DOCTOR, EntityContext::TYPE_CLINIC], true) || $entityId <= 0) {
|
||||
throw new \InvalidArgumentException(sprintf(
|
||||
'Invalid tenant pair (%s, %d) for %s.',
|
||||
$entityType,
|
||||
$entityId,
|
||||
static::class,
|
||||
));
|
||||
}
|
||||
|
||||
$this->entityType = $entityType;
|
||||
$this->entityId = $entityId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ class TenantTag
|
||||
#[ORM\Column(type: 'string', length: 36, unique: true)]
|
||||
private string $uuid;
|
||||
|
||||
#[ORM\Column(name: 'entity_type', type: 'string', length: 20)]
|
||||
#[ORM\Column(name: 'entity_type', type: 'string', length: 10)]
|
||||
private string $entityType;
|
||||
|
||||
#[ORM\Column(name: 'entity_id', type: 'integer')]
|
||||
|
||||
Reference in New Issue
Block a user