- Add implementation notes for cancellation and waitlist features. - Create task documentation outlining goals, current status, and acceptance criteria for cancellation policy and resource utilization reporting. - Establish architecture for domain events and outbox pattern to ensure reliable event publishing. - Define database schema for domain events and necessary queries for resource utilization and plan accuracy reports. - Implement detailed implementation notes covering edge cases, testing strategies, and documentation requirements.
6.1 KiB
دیتابیس — تسک ۰۸
price_lists
| ستون | نوع | توضیح |
|---|---|---|
id |
INT PK AI | |
uuid |
VARCHAR(36) UNIQUE | |
entity_type / entity_id |
VARCHAR(10) / INT NOT NULL | |
branch_id |
INT NULL | NULL = همهٔ شعب محیط |
name |
VARCHAR(150) NOT NULL | «نیمهٔ دوم ۱۴۰۵» |
valid_from |
INT NOT NULL | نیمهشب روز شروع |
valid_to |
INT NULL | NULL = بیپایان |
status |
VARCHAR(10) NOT NULL DEFAULT 'draft' | draft|active|archived |
created_at/updated_at |
INT NOT NULL |
KEY idx_price_lists_tenant (entity_type, entity_id, status, valid_from)
KEY idx_price_lists_branch (branch_id, status, valid_from)
تداخل بازه در سطح اپلیکیشن بررسی میشود (activate)، نه DB — MariaDB محدودیت بازهای ندارد
و راه سطل زمانی تسک ۰۷ اینجا بیمورد است چون تعداد لیستها کم و تغییرشان نادر است.
price_list_items
CREATE TABLE price_list_items (
id INT PRIMARY KEY AUTO_INCREMENT,
price_list_id INT NOT NULL,
service_item_id INT NULL, -- قیمت سرویس
service_option_id INT NULL, -- قیمت آیتم
price_rials INT NOT NULL,
UNIQUE KEY uniq_pli_service (price_list_id, service_item_id),
UNIQUE KEY uniq_pli_option (price_list_id, service_option_id),
KEY idx_pli_list (price_list_id),
CONSTRAINT fk_pli_list FOREIGN KEY (price_list_id) REFERENCES price_lists(id) ON DELETE CASCADE,
CONSTRAINT fk_pli_service FOREIGN KEY (service_item_id) REFERENCES service_items(id) ON DELETE CASCADE,
CONSTRAINT fk_pli_option FOREIGN KEY (service_option_id) REFERENCES service_options(id) ON DELETE CASCADE
);
دقیقاً یکی از service_item_id / service_option_id غیر-NULL (قید اپلیکیشنی).
فرزند aggregate با ریشهٔ PriceList.
price_snapshots
| ستون | نوع | توضیح |
|---|---|---|
id |
INT PK AI | |
uuid |
VARCHAR(36) UNIQUE | |
entity_type / entity_id |
VARCHAR(10) / INT NOT NULL | |
appointment_id |
INT NOT NULL UNIQUE | FK ON DELETE CASCADE — یک snapshot per نوبت |
base_rials |
INT NOT NULL | |
options_rials |
INT NOT NULL DEFAULT 0 | |
discount_rials |
INT NOT NULL DEFAULT 0 | |
insurance_base_rials |
INT NOT NULL DEFAULT 0 | |
insurance_supplementary_rials |
INT NOT NULL DEFAULT 0 | |
tax_rials |
INT NOT NULL DEFAULT 0 | |
final_rials |
INT NOT NULL | |
deposit_rials |
INT NOT NULL DEFAULT 0 | |
applied_policy_ids |
JSON NULL | [{id, version}] |
price_list_id |
INT NULL | FK SET NULL — کدام لیست مبنا بود |
created_at |
INT NOT NULL |
UNIQUE KEY uniq_snapshot_appointment (appointment_id)
KEY idx_snapshot_tenant (entity_type, entity_id, created_at)
UNIQUE روی appointment_id: یک نوبت یک فاکتور رزرو دارد. reschedule نوبت جدید
میسازد (تسک ۰۷) پس snapshot جدید هم میگیرد و قدیمی سالم میماند.
⚠️ همهٔ مبالغ INT ریال. DECIMAL یا FLOAT ننویس — بقیهٔ پروژه (price_rials,
visit_price_rials, deposit_amount_rials) همه INT ریالاند و قاطی کردن دو نوع یعنی
خطای گردکردن در جمع فاکتور.
price_snapshot_lines
CREATE TABLE price_snapshot_lines (
id INT PRIMARY KEY AUTO_INCREMENT,
snapshot_id INT NOT NULL,
kind VARCHAR(15) NOT NULL, -- base|option|discount|insurance|tax|package
label VARCHAR(200) NOT NULL, -- snapshot متنی — نام لحظهٔ ثبت
amount_rials INT NOT NULL, -- منفی برای تخفیف و سهم بیمه
source_type VARCHAR(20) NULL, -- service|option|discount_rule|policy|insurance
source_id INT NULL, -- بدون FK — منبع ممکن است حذف شود
sort_order SMALLINT NOT NULL DEFAULT 0,
KEY idx_psl_snapshot (snapshot_id, sort_order),
CONSTRAINT fk_psl_snapshot FOREIGN KEY (snapshot_id) REFERENCES price_snapshots(id) ON DELETE CASCADE
);
source_id بدون FK عمدی: قانون تخفیف ممکن است فردا حذف شود ولی فاکتور دیروز باید
همانطور بماند. label هم به همین دلیل کپی متنی است، نه JOIN.
deposit_policies
CREATE TABLE deposit_policies (
id INT PRIMARY KEY AUTO_INCREMENT,
uuid VARCHAR(36) NOT NULL UNIQUE,
entity_type VARCHAR(10) NOT NULL,
entity_id INT NOT NULL,
service_item_id INT NULL, -- NULL = پیشفرض محیط
mode VARCHAR(10) NOT NULL, -- none|fixed|percent
value INT NOT NULL DEFAULT 0,
min_rials INT NULL,
max_rials INT NULL,
active TINYINT(1) NOT NULL DEFAULT 1,
created_at INT NOT NULL,
updated_at INT NOT NULL,
UNIQUE KEY uniq_deposit_scope (entity_type, entity_id, service_item_id),
KEY idx_deposit_tenant (entity_type, entity_id, active)
);
تغییر جدول موجود
هیچ. appointments.visit_price_rials, deposit_required, deposit_amount_rials,
insurance_base_id, insurance_supplementary_id همه استفاده میشوند و کافیاند.
Tariff هم دستنخورده میماند و در زنجیرهٔ PriceResolver سطر ۴ است.
Migration
ddev exec php bin/console doctrine:migrations:diff --no-interaction
ddev exec php bin/console doctrine:migrations:migrate --no-interaction
ddev exec php bin/console app:pricing:backfill-snapshots --force
app:pricing:backfill-snapshots برای نوبتهای confirmed آینده که snapshot ندارند، یکی
از visit_price_rials موجود میسازد (یک ردیف base). بدون آن، صفحهٔ فاکتور برای
نوبتهای موجود خالی است.
طبقهبندی tenant
| جدول | وضعیت |
|---|---|
price_lists, price_snapshots, deposit_policies |
جفت tenant |
price_list_items, price_snapshot_lines |
AGGREGATE_CHILDREN |