# دیتابیس — تسک ۰۹ ## `policies` | ستون | نوع | توضیح | |---|---|---| | `id` | INT PK AI | | | `uuid` | VARCHAR(36) UNIQUE | | | `entity_type` / `entity_id` | VARCHAR(10) / INT NOT NULL | | | `branch_id` | INT NULL | NULL = همهٔ شعب — FK ON DELETE CASCADE | | `name` | VARCHAR(200) NOT NULL | «خدمات جراحی به جراح نیاز دارند» | | `category` | VARCHAR(20) NOT NULL | یکی از شش دسته | | `priority` | SMALLINT NOT NULL DEFAULT 0 | | | `specificity` | SMALLINT NOT NULL DEFAULT 0 | محاسبه‌شده هنگام ذخیره | | `version` | SMALLINT NOT NULL DEFAULT 1 | | | `valid_from` | INT NULL | | | `valid_to` | INT NULL | | | `active` | TINYINT(1) NOT NULL DEFAULT 0 | **پیش‌فرض غیرفعال** | | `conditions` | JSON NOT NULL | | | `effects` | JSON NOT NULL | | | `created_at`/`updated_at` | INT NOT NULL | | ```sql KEY idx_policies_lookup (entity_type, entity_id, category, active, valid_from) KEY idx_policies_branch (branch_id, category, active) ``` `idx_policies_lookup` کوئری داغ است: «قوانین فعال دستهٔ X این محیط که امروز معتبرند». با ۶ دسته و معمولاً < ۵۰ قانون per محیط، این کوئری همیشه ارزان است — و باید بماند. اگر روزی قوانین به هزاران رسیدند، کش per (محیط، دسته) اضافه شود، نه ایندکس پیچیده‌تر. ## `policy_version_log` ```sql CREATE TABLE policy_version_log ( id INT PRIMARY KEY AUTO_INCREMENT, policy_id INT NOT NULL, version SMALLINT NOT NULL, snapshot JSON NOT NULL, -- کل قانون در آن نسخه، نه diff valid_from INT NULL, valid_to INT NULL, changed_by INT NULL, -- FK users ON DELETE SET NULL changed_at INT NOT NULL, UNIQUE KEY uniq_policy_version (policy_id, version), KEY idx_pvl_policy (policy_id, version), CONSTRAINT fk_pvl_policy FOREIGN KEY (policy_id) REFERENCES policies(id) ON DELETE CASCADE ); ``` `snapshot` کامل است، نه diff: بازسازی نسخهٔ ۳ از یک قانون که الان نسخهٔ ۹ است، باید یک `SELECT` باشد. حجم ناچیز (JSON چند کیلوبایتی × چند ده نسخه). ## هیچ تغییری در `discount_rules` جدول و entity موجود دست‌نخورده می‌مانند. `PricingPolicyEngine` **هر دو** را می‌خواند: ```php $effects = array_merge( $this->discountEngine->evaluate($ctx), // DiscountRule موجود $this->pricingPolicies->evaluate($ctx), // Policy دستهٔ pricing ); $combined = $this->combiner->combine($effects); // ترکیب واحد ``` دلیل عدم مهاجرت در implementation_notes بند ۱. ## `applied_policy_ids` — تسک ۰۸ ستون از قبل در `price_snapshots` هست. قرارداد مقدارش اینجا تعیین می‌شود: ```json [ { "source": "policy", "id": 12, "version": 3, "name": "تخفیف VIP" }, { "source": "discount_rule", "id": 5, "version": 1, "name": "تخفیف تولد" } ] ``` `name` کپی متنی — همان دلیل `price_snapshot_lines.label`: قانون ممکن است حذف شود. ## ذخیرهٔ قوانین اعمال‌شده روی خودِ نوبت قوانین غیرقیمتی هم باید ثبت شوند (مستند: «هر نوبت فهرست قانون‌هایی که رویش اعمال شده را ذخیره می‌کند»)، ولی `price_snapshots` جای قوانین منبع و زمان نیست. ```sql ALTER TABLE appointments ADD COLUMN applied_policies JSON NULL; ``` همان قرارداد بالا. یک ستون JSON کافی است — هیچ کوئری‌ای روی آن زده نمی‌شود، فقط برای آدیت و پاسخ به «چرا این نوبت ۷۵ دقیقه شد؟» خوانده می‌شود. ## Migration ```bash ddev exec php bin/console doctrine:migrations:diff --no-interaction ddev exec php bin/console doctrine:migrations:migrate --no-interaction ``` بدون backfill. هیچ قانونی از قبل وجود ندارد و `DiscountRule` ها سر جایشان می‌مانند. ## نمونهٔ داده برای تست ```bash ddev exec php bin/console app:policy:seed-examples --tenant=clinic:12 --force ``` پنج قانون نمونه (یکی از هر دسته جز `selection`)، همه با `active=false` تا کلینیک اول آزمایششان کند: ``` resource | خدمات جراحی به جراح نیاز دارند timing | حداقل مدت درمان پیچیده یک ساعت است spacing | حداقل ۲۱ روز از جلسهٔ قبلی لیزر eligibility | بیمار زیر ۱۸ سال بدون رضایت والدین نمی‌شود pricing | بیمار VIP ده درصد تخفیف ``` این نمونه‌ها هم مستندات زنده‌اند و هم داده تست تسک ۱۰. ## طبقه‌بندی tenant | جدول | وضعیت | |---|---| | `policies` | جفت tenant | | `policy_version_log` | `AGGREGATE_CHILDREN` → ریشه `Policy` |