refactor(tenant): make EntityContextResolver the single context resolver
Phase 1 of the tenant-marking series. The "which environment is this user working in?" decision was reimplemented in six places, each reading UserActiveContext.db_uuid and then guessing whether the uuid belongs to a clinic or a doctor. Every copy was a place the roles could silently diverge. EntityContextResolver already encoded the right precedence (explicit clinic_uuid > stored active context > role) but only five files used it, and it did not recognise secretaries at all: canActInClinic accepted admins, clinic owners and member doctors, so a secretary's active clinic context always collapsed to unknown. That gap is why SecretaryAccessChecker carried its own copy of the logic. - canActInClinic now also accepts an active DoctorSecretary relation, and a matching canActForDoctor covers the personal-practice branch. - AppointmentAccessChecker, ClinicDoctorAccessChecker, SecretaryAccessChecker, PatientRecordScopeResolver, MyAppointmentsController and the secretary dashboard all resolve through it now. - PatientRecordScopeResolver keeps only its real responsibility: which doctors' patients are visible inside the resolved environment. - The resolver answers "where"; ClinicDoctorPermissionChecker and SecretaryPermissionChecker still answer "what may you do". Left deliberately untouched, with the reason recorded at each site: SubscriptionController, InventoryController and TenantTagController check ROLE_DOCTOR unconditionally and ignore the active context, so a member doctor sees personal inventory/tags/subscription even inside a clinic. Switching them changes what users see, which is a product decision, not a refactor. AuthController keeps its repository because it writes the active context. tests/ApiTestCase now seeds the "free" subscription plan. db_test had no such row, so getEffectivePlan returned null, every hasFeature() was false and 83 tests across Patient, ClinicService, Insurance and Appointment failed with 403. No schema, route, request, response or error code changed. Tests: 813 passing (was 730 passing / 83 failing). PHPStan clean on all changed files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,366 @@
|
||||
# فاز ۳ — یکسانسازی نمایشهای ناهمگون tenant
|
||||
|
||||
> پرامپت سوم از سری پنجتایی. **پیشنیاز: فازهای ۱ و ۲ کامل و سبز.**
|
||||
> ۱. `tenant-01-entity-context-unify.md` → ۲. `tenant-02-mark-booking-tables.md` → **۳. یکسانسازی ستونها (همین فایل)** → ۴. `tenant-04-enforce-tenant-filter.md` → ۵. `tenant-05-audit-and-docs.md`
|
||||
|
||||
## زمینه
|
||||
|
||||
بعد از فاز ۲، جدولهای نوبتدهی هم جفت `(entity_type, entity_id)` دارند. اما همان مفهوم در بقیهٔ دیتابیس با **چهار املای متفاوت** نوشته شده و فاز ۴ (فیلتر خودکار) نمیتواند روی چهار املا کار کند — فیلتر Doctrine روی نام فیلد تصمیم میگیرد.
|
||||
|
||||
نقشهٔ وضعیت فعلی:
|
||||
|
||||
| املا | جدولها |
|
||||
|---|---|
|
||||
| `entity_type` + `entity_id` (**هدف**) | `service_sections`, `patient_records`, `clinic_staff`, `clinic_subscriptions`, `sms_wallets`, `sms_settings`, `entity_insurance_pricing`, `tenant_insurances`, `tenant_service_category_settings`, `invoices`, `claims`, `inventory_items`, `inventory_packages`, `tenant_tags` + (از فاز ۲) `appointments`, `weekly_schedules`, `date_overrides` |
|
||||
| `owner_type` + `owner_id` | `discount_rules` |
|
||||
| `owner_type` + `clinic_id` تهیپذیر | `doctor_secretaries` |
|
||||
| `db_uuid` مبهم (کلینیک یا پزشک؟) | `user_active_context` |
|
||||
|
||||
بهعلاوه یک ناهماهنگی طولی: `entity_type` در ۱۱ جدول `VARCHAR(10)` و در ۴ جدول `VARCHAR(20)` است.
|
||||
|
||||
## مشکل / هدف
|
||||
|
||||
**مشکل ۱ — فیلتر خودکار روی چهار املا کار نمیکند.** `TenantFilter` فاز ۴ با `$meta->hasField('entityType')` تصمیم میگیرد؛ `discount_rules` و `doctor_secretaries` بیصدا از فیلتر جا میمانند — یعنی دقیقاً همان نشتی خاموشی که کل این پروژه برای جلوگیری از آن است.
|
||||
|
||||
**مشکل ۲ — `db_uuid` هر بار دو کوئری میخواهد.** الگوی زیر در `EntityContextResolver` اجتنابناپذیر است چون خود ستون نمیگوید uuid مال چیست:
|
||||
|
||||
```php
|
||||
// src/Shared/Context/EntityContextResolver.php — fromActiveContext()
|
||||
$clinic = $this->clinicRepo->findByUuid($active->getDbUuid());
|
||||
if ($clinic !== null) { ... }
|
||||
|
||||
$doctor = $this->doctorRepo->findByUuid($active->getDbUuid());
|
||||
```
|
||||
|
||||
**مشکل ۳ — یک باگ واقعی در یکتایی `doctor_secretaries`.** کلید یکتای فعلی:
|
||||
|
||||
```php
|
||||
#[ORM\Table(name: 'doctor_secretaries')]
|
||||
#[ORM\UniqueConstraint(name: 'idx_doctor_secretary_scope', columns: ['doctor_id', 'secretary_id', 'owner_type'])]
|
||||
```
|
||||
|
||||
`owner_type` فقط `'doctor'` یا `'clinic'` است و `clinic_id` در کلید نیست. نتیجه: **یک منشی نمیتواند به یک پزشک در دو کلینیک متفاوت تخصیص یابد** — ردیف دوم با `(doctor_id, secretary_id, 'clinic')` تکراری میشود و رد میشود. این با سناریوی ۲ ماتریس نقشها (پزشکی که عضو چند کلینیک است) مستقیماً در تضاد است. یکسانسازی این کلید را درست میکند.
|
||||
|
||||
**هدف:** یک املای واحد `(entity_type, entity_id)` با طول یکسان در کل دیتابیس، بهعلاوه رفع باگ یکتایی بالا.
|
||||
|
||||
## معیار پذیرش
|
||||
|
||||
- ✅ موفق: بعد از migration، این دستور خروجی خالی میدهد (هیچ `owner_type` باقی نمانده):
|
||||
`ddev exec grep -rn "owner_type\|ownerType" src --include="*.php"`
|
||||
و همهٔ ستونهای `entity_type` طول یکسان دارند. قوانین تخفیف و منشیهای موجود بدون تغییر رفتار کار میکنند.
|
||||
- ❌ خطا: ساخت `DiscountRule` با محیط حلنشده (`EntityContext::unknown()`) → استثنا، نه ردیف با `entity_id = 0`.
|
||||
- ⚠️ مرزی: **همان منشی، همان پزشک، دو کلینیک متفاوت** → هر دو ردیف با موفقیت ذخیره میشوند (قبل از این فاز، ردیف دوم رد میشد). این تست باید صریحاً نوشته شود چون رفع یک باگ است، نه صرفاً refactor.
|
||||
|
||||
## فایلهای مرتبط
|
||||
|
||||
| فایل | نقش |
|
||||
|------|-----|
|
||||
| `src/Discount/Entity/DiscountRule.php` | `owner_type`/`owner_id` → `entity_type`/`entity_id` |
|
||||
| `src/Discount/Controller/DiscountController.php` | مصرفکنندهٔ ستونهای قدیمی |
|
||||
| `src/Secretary/Entity/DoctorSecretary.php` | `owner_type` + `clinic_id` → جفت + رفع یکتایی |
|
||||
| `src/Secretary/Repository/DoctorSecretaryRepository.php` | کوئریهای `owner_type` |
|
||||
| `src/Secretary/Security/SecretaryAccessChecker.php` | مصرفکننده |
|
||||
| `src/Auth/Entity/UserActiveContext.php` | افزودن `db_type` |
|
||||
| `src/Shared/Context/EntityContextResolver.php` | حذف حدس دوگانه در `fromActiveContext()` |
|
||||
| `src/Tag/Entity/TenantTag.php`, `src/Inventory/Entity/InventoryItem.php`, `src/Inventory/Entity/InventoryPackage.php` | طول `entity_type` = ۲۰ → ۱۰ |
|
||||
| `src/Auth/Entity/MobileVerificationOtp.php` | `entity_type` دارد ولی **معنایش tenant نیست** — بند «نکات مهم» |
|
||||
| `src/Shared/Tenant/TenantOwnedTrait.php` | trait ساختهشده در فاز ۲ |
|
||||
| `migrations/` | migrationهای rename + backfill |
|
||||
|
||||
## وضعیت فعلی
|
||||
|
||||
```php
|
||||
// src/Discount/Entity/DiscountRule.php
|
||||
#[ORM\Table(name: 'discount_rules')]
|
||||
#[ORM\Index(columns: ['owner_type', 'owner_id', 'active'], name: 'idx_discount_rules_owner')]
|
||||
class DiscountRule
|
||||
{
|
||||
#[ORM\Column(name: 'owner_type', type: 'string', length: 10)]
|
||||
private string $ownerType;
|
||||
|
||||
#[ORM\Column(name: 'owner_id', type: 'integer')]
|
||||
private int $ownerId;
|
||||
```
|
||||
|
||||
```php
|
||||
// src/Secretary/Entity/DoctorSecretary.php
|
||||
#[ORM\Table(name: 'doctor_secretaries')]
|
||||
#[ORM\UniqueConstraint(name: 'idx_doctor_secretary_scope', columns: ['doctor_id', 'secretary_id', 'owner_type'])]
|
||||
class DoctorSecretary
|
||||
{
|
||||
public const OWNER_DOCTOR = 'doctor';
|
||||
public const OWNER_CLINIC = 'clinic';
|
||||
|
||||
#[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;
|
||||
```
|
||||
|
||||
```php
|
||||
// src/Auth/Entity/UserActiveContext.php
|
||||
#[ORM\Column(name: 'db_uuid', type: 'string', length: 36)]
|
||||
private string $dbUuid;
|
||||
```
|
||||
|
||||
## وظایف
|
||||
|
||||
### ۱. `discount_rules` — تغییر نام ستونها
|
||||
|
||||
سادهترین مورد سری: هیچ تغییر معنایی ندارد، فقط نام.
|
||||
|
||||
```php
|
||||
// src/Discount/Entity/DiscountRule.php
|
||||
use App\Shared\Tenant\TenantOwnedTrait;
|
||||
|
||||
#[ORM\Table(name: 'discount_rules')]
|
||||
#[ORM\Index(columns: ['entity_type', 'entity_id', 'active'], name: 'idx_discount_rules_tenant')]
|
||||
class DiscountRule
|
||||
{
|
||||
use TenantOwnedTrait;
|
||||
// ownerType / ownerId حذف میشوند
|
||||
```
|
||||
|
||||
migration:
|
||||
|
||||
```sql
|
||||
ALTER TABLE discount_rules
|
||||
CHANGE owner_type entity_type VARCHAR(10) NOT NULL,
|
||||
CHANGE owner_id entity_id INT NOT NULL;
|
||||
|
||||
DROP INDEX idx_discount_rules_owner ON discount_rules;
|
||||
CREATE INDEX idx_discount_rules_tenant ON discount_rules (entity_type, entity_id, active);
|
||||
```
|
||||
|
||||
`CHANGE` بهجای `ADD`+`UPDATE`+`DROP` استفاده میشود چون داده جابهجا نمیشود، فقط نام ستون — بدون ریسک backfill.
|
||||
|
||||
بعد از تغییر entity، همهٔ نقاط مصرف را پیدا و اصلاح کن:
|
||||
|
||||
```bash
|
||||
ddev exec grep -rn "getOwnerType\|getOwnerId\|setOwnerType\|setOwnerId\|owner_type\|owner_id" src assets --include="*.php" --include="*.tsx" --include="*.ts"
|
||||
```
|
||||
|
||||
اگر `owner_type` در پاسخ API تخفیفها ظاهر میشود، **نام فیلد پاسخ را عوض نکن** مگر اینکه پنل ادمین را هم در همان جلسه اصلاح کنی — این یک تغییر قرارداد است.
|
||||
|
||||
**نحوه تست:**
|
||||
```bash
|
||||
ddev exec php bin/console doctrine:migrations:migrate --no-interaction
|
||||
ddev exec php bin/phpunit tests/ # کل سوئیت
|
||||
ddev exec php bin/console dbal:run-sql "SELECT entity_type, COUNT(*) FROM discount_rules GROUP BY 1"
|
||||
```
|
||||
بهعلاوه تست دستی: صفحهٔ تخفیفهای پنل ادمین باز شود، یک قانون ساخته و ویرایش شود.
|
||||
|
||||
---
|
||||
|
||||
### ۲. `doctor_secretaries` — یکسانسازی + رفع باگ یکتایی
|
||||
|
||||
اینجا فقط تغییر نام نیست: `owner_type` + `clinic_id` تهیپذیر باید به جفت واقعی تبدیل شود و کلید یکتا اصلاح گردد.
|
||||
|
||||
نگاشت داده:
|
||||
|
||||
| وضعیت فعلی | جفت جدید |
|
||||
|---|---|
|
||||
| `owner_type='doctor'` (و `clinic_id` تهی) | `('doctor', doctor_id)` |
|
||||
| `owner_type='clinic'` و `clinic_id = X` | `('clinic', X)` |
|
||||
| `owner_type='clinic'` ولی `clinic_id` تهی (دادهٔ خراب) | باید شناسایی و گزارش شود — بند «ردیفهای ناسازگار» |
|
||||
|
||||
```php
|
||||
#[ORM\Table(name: 'doctor_secretaries')]
|
||||
#[ORM\UniqueConstraint(name: 'uniq_doctor_secretary_scope',
|
||||
columns: ['doctor_id', 'secretary_id', 'entity_type', 'entity_id'])]
|
||||
class DoctorSecretary
|
||||
{
|
||||
use TenantOwnedTrait;
|
||||
```
|
||||
|
||||
migration:
|
||||
|
||||
```sql
|
||||
ALTER TABLE doctor_secretaries ADD entity_type VARCHAR(10) NULL, ADD entity_id INT NULL;
|
||||
|
||||
UPDATE doctor_secretaries
|
||||
SET entity_type = owner_type,
|
||||
entity_id = IF(owner_type = 'clinic', clinic_id, doctor_id);
|
||||
|
||||
-- ردیفهای ناسازگار: owner_type='clinic' ولی clinic_id تهی
|
||||
-- SELECT id, doctor_id, secretary_id FROM doctor_secretaries
|
||||
-- WHERE owner_type='clinic' AND clinic_id IS NULL;
|
||||
|
||||
ALTER TABLE doctor_secretaries
|
||||
MODIFY entity_type VARCHAR(10) NOT NULL,
|
||||
MODIFY entity_id INT NOT NULL;
|
||||
|
||||
DROP INDEX idx_doctor_secretary_scope ON doctor_secretaries;
|
||||
CREATE UNIQUE INDEX uniq_doctor_secretary_scope
|
||||
ON doctor_secretaries (doctor_id, secretary_id, entity_type, entity_id);
|
||||
|
||||
ALTER TABLE doctor_secretaries DROP COLUMN owner_type;
|
||||
```
|
||||
|
||||
**ردیفهای ناسازگار:** در `up()` قبل از `NOT NULL` یک `abortIf` بگذار. اگر چنین ردیفهایی وجود داشت، migration باید متوقف شود و شناسهها را در پیام خطا بیاورد — تصمیم دربارهٔ دادهٔ خراب کار انسان است، نه migration:
|
||||
|
||||
```php
|
||||
$broken = $this->connection->fetchFirstColumn(
|
||||
"SELECT id FROM doctor_secretaries WHERE owner_type='clinic' AND clinic_id IS NULL"
|
||||
);
|
||||
$this->abortIf($broken !== [], 'Inconsistent doctor_secretaries rows: ' . implode(',', $broken));
|
||||
```
|
||||
|
||||
`clinic_id` را نگه دار — `SecretaryAccessChecker` و `DoctorSecretaryRepository::findActiveClinicRow()` مستقیم روی رابطهٔ Doctrine کوئری میزنند و حذفش دامنهٔ فاز را میترکاند.
|
||||
|
||||
**نحوه تست:**
|
||||
```bash
|
||||
ddev exec php bin/phpunit tests/Secretary/
|
||||
```
|
||||
بهعلاوه یک تست جدید که باگ رفعشده را تثبیت کند — بدون این تست، وظیفه تمام نیست:
|
||||
|
||||
```php
|
||||
// tests/Secretary/SecretaryMultiClinicScopeTest.php
|
||||
public function testSameSecretaryCanServeSameDoctorInTwoDifferentClinics(): void
|
||||
{
|
||||
// دو کلینیک، یک پزشک عضو هر دو، یک منشی
|
||||
// هر دو ردیف باید ذخیره شوند و findActiveClinicRow برای هر کلینیک
|
||||
// ردیف مخصوص همان کلینیک را برگرداند
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ۳. `user_active_context` — رفع ابهام `db_uuid`
|
||||
|
||||
ستون `db_type` اضافه کن تا `EntityContextResolver` بداند uuid مال چیست و بهجای دو کوئری، یکی بزند.
|
||||
|
||||
```php
|
||||
// src/Auth/Entity/UserActiveContext.php
|
||||
#[ORM\Column(name: 'db_type', type: 'string', length: 10)]
|
||||
private string $dbType; // 'doctor' | 'clinic'
|
||||
|
||||
#[ORM\Column(name: 'db_uuid', type: 'string', length: 36)]
|
||||
private string $dbUuid;
|
||||
```
|
||||
|
||||
backfill از روی خود داده — uuid را در هر دو جدول بگرد:
|
||||
|
||||
```sql
|
||||
ALTER TABLE user_active_context ADD db_type VARCHAR(10) NULL;
|
||||
|
||||
UPDATE user_active_context uac
|
||||
JOIN clinics c ON c.uuid = uac.db_uuid
|
||||
SET uac.db_type = 'clinic';
|
||||
|
||||
UPDATE user_active_context uac
|
||||
JOIN doctors d ON d.uuid = uac.db_uuid
|
||||
SET uac.db_type = 'doctor'
|
||||
WHERE uac.db_type IS NULL;
|
||||
|
||||
-- ردیفهای یتیم (uuid به موجودیت حذفشده اشاره میکند) پاک شوند:
|
||||
DELETE FROM user_active_context WHERE db_type IS NULL;
|
||||
|
||||
ALTER TABLE user_active_context MODIFY db_type VARCHAR(10) NOT NULL;
|
||||
```
|
||||
|
||||
حذف ردیفهای یتیم اینجا بیخطر است: `user_active_context` صرفاً «آخرین محیط انتخابشده» را کش میکند و نبودش یعنی `EntityContextResolver` به `fromRole()` میافتد — همان رفتار کاربر تازهوارد. **این تنها جدول این سری است که حذف ردیف در آن مجاز است**؛ در هیچ migration دیگری `DELETE` ننویس.
|
||||
|
||||
سپس `fromActiveContext()` را ساده کن:
|
||||
|
||||
```php
|
||||
private function fromActiveContext(User $user): ?EntityContext
|
||||
{
|
||||
$active = $this->activeContextRepo->findByUser($user);
|
||||
if ($active === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($active->getDbType() === EntityContext::TYPE_CLINIC) {
|
||||
$clinic = $this->clinicRepo->findByUuid($active->getDbUuid());
|
||||
|
||||
return $clinic !== null && $this->canActInClinic($user, $clinic)
|
||||
? EntityContext::forClinic($clinic)
|
||||
: null;
|
||||
}
|
||||
|
||||
$doctor = $this->doctorRepo->findByUuid($active->getDbUuid());
|
||||
|
||||
return $doctor !== null && $doctor->getUser()->getId() === $user->getId()
|
||||
? EntityContext::forDoctor($doctor)
|
||||
: null;
|
||||
}
|
||||
```
|
||||
|
||||
هر نقطهای که `UserActiveContext` میسازد یا `setDbUuid()` صدا میزند باید `db_type` را هم بدهد. نقاط را پیدا کن:
|
||||
|
||||
```bash
|
||||
ddev exec grep -rn "new UserActiveContext(\|setDbUuid(" src --include="*.php"
|
||||
```
|
||||
|
||||
**نحوه تست:**
|
||||
```bash
|
||||
ddev exec php bin/phpunit tests/Shared/EntityContextResolverTest.php # از فاز ۱
|
||||
ddev exec php bin/console dbal:run-sql "SELECT db_type, COUNT(*) FROM user_active_context GROUP BY 1"
|
||||
```
|
||||
تست دستی: سوییچ محیط از پنل ادمین (کاربر سناریوی ۳ — پزشکِ مالک کلینیک) و بررسی اینکه `db_type` درست ذخیره میشود.
|
||||
|
||||
---
|
||||
|
||||
### ۴. یکسانسازی طول `entity_type`
|
||||
|
||||
چهار جدول `VARCHAR(20)` دارند در حالی که یازده جدول `VARCHAR(10)`:
|
||||
|
||||
| جدول | فایل | اقدام |
|
||||
|---|---|---|
|
||||
| `tenant_tags` | `src/Tag/Entity/TenantTag.php` | → ۱۰ |
|
||||
| `inventory_items` | `src/Inventory/Entity/InventoryItem.php` | → ۱۰ |
|
||||
| `inventory_packages` | `src/Inventory/Entity/InventoryPackage.php` | → ۱۰ |
|
||||
| `mobile_verification_otp` | `src/Auth/Entity/MobileVerificationOtp.php` | **دست نزن** — بند «نکات مهم» |
|
||||
|
||||
قبل از تغییر طول، مطمئن شو هیچ مقدار بلندتری در داده نیست:
|
||||
|
||||
```sql
|
||||
SELECT DISTINCT entity_type, LENGTH(entity_type) FROM tenant_tags;
|
||||
SELECT DISTINCT entity_type, LENGTH(entity_type) FROM inventory_items;
|
||||
SELECT DISTINCT entity_type, LENGTH(entity_type) FROM inventory_packages;
|
||||
```
|
||||
|
||||
اگر مقداری غیر از `doctor`/`clinic` بود، **متوقف شو و گزارش بده** — یعنی این ستون معنای دیگری هم دارد و فرض این فاز غلط است.
|
||||
|
||||
سپس این سه entity هم `TenantOwnedTrait` را بگیرند تا تعریف تکراری حذف شود.
|
||||
|
||||
**نحوه تست:**
|
||||
```bash
|
||||
ddev exec php bin/console doctrine:migrations:diff --no-interaction # باید فقط تغییر طول را ببیند
|
||||
ddev exec php bin/phpunit tests/Inventory/ tests/Tag/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ۵. تست ماتریس نقش، تکرار روی دادههای یکسانشده
|
||||
|
||||
تستهای فاز ۱ فقط رزولور را پوشش میدادند. حالا که ستونها یکسان شدهاند، یک تست یکپارچه بنویس که برای **هر پنج نقش** ماتریس، دادهٔ متعلق به محیط درست را از چند دامنه میگیرد:
|
||||
|
||||
```php
|
||||
// tests/Shared/TenantIsolationMatrixTest.php
|
||||
/**
|
||||
* @dataProvider roleScenarios
|
||||
*/
|
||||
public function testEachRoleSeesOnlyItsOwnTenantData(string $scenario): void
|
||||
{
|
||||
// برای هر نقش: GET روی discounts, patients, services, staff, inventory
|
||||
// هیچ رکورد متعلق به tenant دیگر نباید در پاسخ باشد
|
||||
}
|
||||
```
|
||||
|
||||
این تست ستون فقرات فاز ۴ است — فیلتر خودکار باید همین تست را سبز نگه دارد.
|
||||
|
||||
**نحوه تست:** `ddev exec php bin/phpunit tests/Shared/TenantIsolationMatrixTest.php`
|
||||
|
||||
## نکات مهم
|
||||
|
||||
- **`mobile_verification_otp.entity_type` را دست نزن.** با اینکه نامش یکی است، معنایش tenant نیست — نوع موجودیتی است که کد تأیید برایش صادر شده. قبل از هر تغییری معنایش را از `src/Auth/` تأیید کن؛ اگر واقعاً tenant نبود، در فاز ۴ باید در whitelist استثناها ثبت شود، نه در فیلتر.
|
||||
- **`CHANGE` در برابر `ADD`+`UPDATE`+`DROP`:** برای `discount_rules` که فقط نام عوض میشود، `CHANGE` امن و سریع است. برای `doctor_secretaries` که مقدار جدید از دو ستون ساخته میشود، باید سهمرحلهای باشد. این تفاوت را رعایت کن.
|
||||
- **رفع باگ یکتایی `doctor_secretaries` یک تغییر رفتار عمدی است** — تنها تغییر رفتاری کل این سری. در گزارش پایانی صریحاً ذکرش کن؛ یعنی از این به بعد ترکیبی که قبلاً رد میشد پذیرفته میشود.
|
||||
- **بکاپ قبل از migrate:** `ddev export-db --file=/tmp/pre-tenant-phase3.sql.gz`.
|
||||
- **الگو: Trait مشترک (ادامهٔ فاز ۲).** دلیل: ستونها و منطق `assignTenant()` یکیاند و هفده entity آن را میگیرند؛ تکرار تعریف در هر کدام یعنی هفده نقطهٔ واگرایی برای فیلتر فاز ۴.
|
||||
- **کلاینتهای متأثر:** اگر نام فیلدی در پاسخ API عوض شد (`owner_type` → `entity_type`)، هم پنل ادمین (`assets/admin/`)، هم `nobat724_front/services/response.js` و هم `clinic-pro-tauri/src/service/response.js` باید دستی بررسی شوند. **پیشنهاد کمریسک:** نام ستون دیتابیس عوض شود ولی کلید JSON پاسخ دستنخورده بماند، مگر اینکه هر سه کلاینت در همین جلسه اصلاح شوند.
|
||||
- **مستندات:** `docs/api/` هر دامنهای که شکل پاسخش عوض شده (`discount`, `secretary`) همان جلسه با JSON واقعی بهروز شود.
|
||||
Reference in New Issue
Block a user