- 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.
5.7 KiB
دیتابیس — تسک ۱۱
packages — تعریف
| ستون | نوع | توضیح |
|---|---|---|
id |
INT PK AI | |
uuid |
VARCHAR(36) UNIQUE | |
entity_type / entity_id |
VARCHAR(10) / INT NOT NULL | |
name |
VARCHAR(200) NOT NULL | «۶ جلسه لیزر فولبادی» |
session_count |
SMALLINT NOT NULL | تعداد جلسه |
price_rials |
BIGINT NOT NULL | BIGINT — پکیج بزرگ از سقف INT عبور میکند |
validity_days |
SMALLINT NULL | اعتبار از تاریخ خرید؛ NULL = بیپایان |
active |
TINYINT(1) NOT NULL DEFAULT 1 | |
created_at/updated_at |
INT NOT NULL |
KEY idx_packages_tenant (entity_type, entity_id, active)
package_services
CREATE TABLE package_services (
id INT PRIMARY KEY AUTO_INCREMENT,
package_id INT NOT NULL,
service_item_id INT NOT NULL,
UNIQUE KEY uniq_pkg_service (package_id, service_item_id),
CONSTRAINT fk_pkgs_package FOREIGN KEY (package_id) REFERENCES packages(id) ON DELETE CASCADE,
CONSTRAINT fk_pkgs_service FOREIGN KEY (service_item_id) REFERENCES service_items(id) ON DELETE RESTRICT
);
ON DELETE RESTRICT روی سرویس: حذف سرویسی که در پکیج فروختهشده هست، اعتبار بیماران را
بیمعنا میکند.
قید اپلیکیشنی: پکیج باید حداقل یک سرویس داشته باشد → 422 هنگام ساخت.
patient_packages — نمونهٔ خریداریشده
| ستون | نوع | توضیح |
|---|---|---|
id |
INT PK AI | |
uuid |
VARCHAR(36) UNIQUE | |
entity_type / entity_id |
VARCHAR(10) / INT NOT NULL | |
package_id |
INT NOT NULL | FK ON DELETE RESTRICT |
patient_record_id |
INT NOT NULL | FK → patient_records.id ON DELETE RESTRICT |
session_count |
SMALLINT NOT NULL | snapshot تعداد لحظهٔ خرید |
price_paid_rials |
BIGINT NOT NULL | snapshot قیمت پرداختی |
payment_id |
INT NULL | FK → payments.id ON DELETE SET NULL |
purchased_at |
INT NOT NULL | مبنای FIFO |
valid_to |
INT NULL | محاسبهشده از validity_days لحظهٔ خرید |
created_at/updated_at |
INT NOT NULL |
KEY idx_pp_tenant (entity_type, entity_id, purchased_at)
KEY idx_pp_patient (patient_record_id, valid_to)
⛔ هیچ ستون
remaining_sessionsیاused_countنیست و نباید باشد.session_countفقط snapshot تعریف است، نه مانده.
session_count و price_paid_rials کپی میشوند (قانون پنجم مستند): تغییر تعریف پکیج
فردا، پکیج فروختهشدهٔ دیروز را عوض نمیکند.
session_credit_ledger — دفتر
| ستون | نوع | توضیح |
|---|---|---|
id |
BIGINT PK AI | |
uuid |
VARCHAR(36) UNIQUE | |
entity_type / entity_id |
VARCHAR(10) / INT NOT NULL | |
patient_package_id |
INT NOT NULL | FK ON DELETE RESTRICT |
kind |
VARCHAR(15) NOT NULL | purchase|consume|refund|adjustment|expiry |
delta |
SMALLINT NOT NULL | مثبت یا منفی — هرگز صفر |
appointment_id |
INT NULL | FK ON DELETE SET NULL |
service_item_id |
INT NULL | FK ON DELETE SET NULL — کدام سرویس مصرف کرد |
reason |
VARCHAR(255) NULL | اجباری برای adjustment |
created_by |
INT NULL | FK → users.id ON DELETE SET NULL |
created_at |
INT NOT NULL |
KEY idx_scl_package (patient_package_id, created_at)
KEY idx_scl_tenant (entity_type, entity_id, created_at)
KEY idx_scl_appt (appointment_id)
UNIQUE KEY uniq_scl_consume (appointment_id, kind) -- ← جلوگیری از مصرف دوباره
uniq_scl_consume مهم است: confirm تسک ۰۷ idempotent است و اگر دوبار اجرا شود،
دو ردیف consume نباید ثبت شود. NULL های appointment_id در UNIQUE مشکلی ندارند
(چند purchase بدون نوبت مجازند).
ردیفها هرگز حذف یا ویرایش نمیشوند. append-only. اصلاح = ردیف جدید.
هیچ تغییری در جدولهای دیگر
price_snapshot_lines.kind از قبل مقدار package را دارد (تسک ۰۸).
Migration
ddev exec php bin/console doctrine:migrations:diff --no-interaction
ddev exec php bin/console doctrine:migrations:migrate --no-interaction
بدون backfill — هیچ پکیجی از قبل وجود ندارد.
تست schema
// tests/Package/LedgerSchemaTest.php
public function testNoStoredBalanceColumnExists(): void
{
$columns = $this->schemaManager->listTableColumns('patient_packages');
foreach (['remaining', 'remaining_sessions', 'used_count', 'balance'] as $forbidden) {
self::assertArrayNotHasKey($forbidden, $columns,
'مانده باید از دفتر محاسبه شود، نه ذخیره');
}
}
تست عجیبی به نظر میرسد ولی همان چیزی است که شش ماه بعد جلوی «بهینهسازی» میایستد.
طبقهبندی tenant
| جدول | وضعیت |
|---|---|
packages, patient_packages, session_credit_ledger |
جفت tenant |
package_services |
AGGREGATE_CHILDREN → ریشه Package |
⚠️ برخلاف wallet_transactions (که ENTITIES است چون پول مال شخص است)، دفتر اعتبار
جفت tenant واقعی میگیرد: اعتبار جلسهٔ کلینیک الف در کلینیک ب معنا ندارد.
دلیلش را در docs/architecture/tenancy.md کنار توضیح کیف پول اضافه کن.