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.
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
# دیتابیس — تسک ۱۱
|
||||
|
||||
## `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 | |
|
||||
|
||||
```sql
|
||||
KEY idx_packages_tenant (entity_type, entity_id, active)
|
||||
```
|
||||
|
||||
## `package_services`
|
||||
|
||||
```sql
|
||||
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 | |
|
||||
|
||||
```sql
|
||||
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 | |
|
||||
|
||||
```sql
|
||||
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
|
||||
|
||||
```bash
|
||||
ddev exec php bin/console doctrine:migrations:diff --no-interaction
|
||||
ddev exec php bin/console doctrine:migrations:migrate --no-interaction
|
||||
```
|
||||
|
||||
بدون backfill — هیچ پکیجی از قبل وجود ندارد.
|
||||
|
||||
## تست schema
|
||||
|
||||
```php
|
||||
// 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` کنار توضیح کیف پول اضافه کن.
|
||||
Reference in New Issue
Block a user