Files
clinicpro/docs/new_feture/taskes/task-11-package-credit-ledger/database.md
T
hamed 021d0eb6b2 feat: implement cancellation policy, no-show tracking, and waitlist management
- 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.
2026-07-30 11:43:58 +03:30

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 کنار توضیح کیف پول اضافه کن.