- 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.
157 lines
6.7 KiB
Markdown
157 lines
6.7 KiB
Markdown
# نکات پیادهسازی — تسک ۱۱
|
||
|
||
## ۱. `quote` نمایش میدهد، `confirm` مصرف میکند
|
||
|
||
بدترین باگ ممکن در این تسک:
|
||
|
||
```php
|
||
// ❌ بیمار صفحه را سه بار رفرش میکند، سه جلسه از دست میدهد
|
||
public function quote(QuoteRequest $req): PriceQuote {
|
||
$this->packages->consume(…);
|
||
}
|
||
```
|
||
|
||
```php
|
||
// ✅
|
||
public function quote(…): PriceQuote {
|
||
$pkg = $this->finder->firstUsable(…);
|
||
return $quote->withPackagePreview($pkg); // فقط نمایش
|
||
}
|
||
// و در BookingService::confirm() داخل تراکنش:
|
||
$this->packages->consume($pkg, $appointment);
|
||
```
|
||
|
||
تست اجباری: ده بار `quote` → مانده بدون تغییر.
|
||
|
||
## ۲. مانده صفر خطا نیست
|
||
|
||
```php
|
||
if ($this->ledger->balance($pkg) <= 0) {
|
||
return false; // ✅ مبلغ کامل محاسبه میشود
|
||
// نه: throw new AppException(...)
|
||
}
|
||
```
|
||
|
||
بیمار با پکیج تمامشده باید بتواند نقدی نوبت بگیرد. `422` یعنی بنبست بیدلیل.
|
||
UI پیام بدهد: «اعتبار پکیج شما تمام شده؛ این نوبت نقدی محاسبه میشود.»
|
||
|
||
## ۳. `uniq_scl_consume` و idempotency
|
||
|
||
`confirm` تسک ۰۷ idempotent است. اگر دوبار صدا زده شود:
|
||
|
||
```php
|
||
try {
|
||
$this->ledger->record($pkg, KIND_CONSUME, -1, $meta);
|
||
} catch (UniqueConstraintViolationException) {
|
||
// قبلاً مصرف شده — همان رفتار idempotent، نه خطا
|
||
}
|
||
```
|
||
|
||
با کلید یکتای `(appointment_id, kind)` این تضمین از دیتابیس میآید. همان الگوی تسک ۰۷.
|
||
|
||
## ۴. لغو = ردیف `refund`، نه حذف `consume`
|
||
|
||
```php
|
||
// ❌ تاریخ را پاک میکند
|
||
$this->em->remove($consumeRow);
|
||
|
||
// ✅
|
||
$this->ledger->record($pkg, KIND_REFUND, +1, LedgerMeta::forCancellation($appt));
|
||
```
|
||
|
||
دفتر append-only است. بعد از سه ماه، سؤال «چند بار این بیمار نوبتش را لغو کرد؟» فقط از
|
||
دفتر جواب دارد.
|
||
|
||
⚠️ بازگشت اعتبار **مشروط به سیاست لغو** است (تسک ۱۳). تا آن تسک نیامده، همیشه برگردان و
|
||
یک `TODO` با ارجاع به تسک ۱۳ بگذار — نه یک پرچم نیمکاره.
|
||
|
||
## ۵. FIFO و انقضا
|
||
|
||
```php
|
||
->orderBy('pp.purchasedAt', 'ASC')
|
||
```
|
||
|
||
قدیمیترین اول. اگر LIFO باشد، پکیج قدیمی منقضی میشود و بیمار پولش را از دست میدهد —
|
||
و شکایتش درست است.
|
||
|
||
`valid_to` هنگام **خرید** محاسبه و ذخیره میشود (`purchased_at + validity_days * 86400`)،
|
||
نه در زمان اجرا: تغییر `validity_days` تعریف پکیج نباید اعتبار خریدهای قبلی را عوض کند.
|
||
|
||
## ۶. قفل بدبینانه اینجا درست است
|
||
|
||
برخلاف تسک ۰۷ که قفل را رد کردیم:
|
||
|
||
```php
|
||
$locked = $this->em->find(PatientPackage::class, $id, LockMode::PESSIMISTIC_WRITE);
|
||
```
|
||
|
||
نرخ رقابت اینجا ناچیز است (یک بیمار، یک پکیج) و یک ردیف قفل میشود، نه دهها سطل.
|
||
جدول مقایسه در `architecture.md` را در `docs/api/package.md` هم بنویس، وگرنه کسی روزی
|
||
«برای یکدستی» یکی را به دیگری تبدیل میکند.
|
||
|
||
## ۷. `adjustment` فقط با نقش مدیر و با دلیل
|
||
|
||
```php
|
||
#[IsGranted('ROLE_CLINIC_OWNER')] // نه منشی، نه پرسنل
|
||
public function adjust(string $uuid, Request $request): JsonResponse
|
||
{
|
||
$reason = trim((string) $data['reason'] ?? '');
|
||
if ($reason === '') {
|
||
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'ذکر دلیل اصلاح الزامی است', 422, 'reason');
|
||
}
|
||
}
|
||
```
|
||
|
||
اصلاح دستی بدون دلیل، دفتر را به همان شمارندهٔ غیرقابلردیابی تبدیل میکند که مستند
|
||
هشدار داده.
|
||
|
||
## ۸. edge case ها
|
||
|
||
| حالت | رفتار درست |
|
||
|---|---|
|
||
| بیمار دو پکیج معتبر برای یک سرویس | FIFO — قدیمیترِ منقضینشده |
|
||
| پکیج معتبر ولی سرویس نوبت پوشش داده نمیشود | اعمال نمیشود، مبلغ کامل |
|
||
| پکیج منقضی با مانده ۳ | ردیف `expiry -3` توسط cron؛ مانده صفر، دفتر کامل |
|
||
| `confirm` دوباره | `uniq_scl_consume` → idempotent |
|
||
| لغو نوبتی که پکیج نداشت | هیچ ردیفی ثبت نمیشود |
|
||
| `delta = 0` | `422` — ردیف بیاثر ننویس |
|
||
| حذف تعریف پکیجی که فروخته شده | `422` (FK RESTRICT) — `active=false` مسیر درست |
|
||
| پکیج بدون سرویس | `422` هنگام ساخت |
|
||
| مبلغ پکیج بزرگتر از سقف INT | `BIGINT` — از قبل حل شده |
|
||
| بیمار مهمان بدون `patient_record` | پکیج فروش نمیرود — `422` با پیام «ابتدا پروندهٔ بیمار را ثبت کنید» |
|
||
|
||
## ۹. تست
|
||
|
||
```
|
||
tests/Package/CreditLedgerTest.php ← ⭐
|
||
- مانده = SUM(delta) در همهٔ سناریوها
|
||
- purchase → consume → refund → مانده اولیه
|
||
- append-only: هیچ remove/update روی ردیفها
|
||
tests/Package/LedgerSchemaTest.php ← ⭐
|
||
- هیچ ستون remaining/used_count در schema
|
||
tests/Package/QuoteDoesNotConsumeTest.php ← ⭐
|
||
- ده بار quote → مانده بدون تغییر
|
||
tests/Package/ConcurrentConsumeTest.php
|
||
- دو نوبت همزمان روی آخرین اعتبار → یکی میگیرد، مانده منفی نمیشود
|
||
tests/Package/IdempotentConsumeTest.php
|
||
- confirm دوبار → یک ردیف consume
|
||
tests/Package/FifoTest.php
|
||
- قدیمیترین پکیج اول مصرف میشود
|
||
tests/Package/ExpiryTest.php
|
||
- cron ردیف expiry با delta = -balance میسازد
|
||
- پکیج منقضی در finder نمیآید
|
||
tests/Package/AdjustmentAuthTest.php
|
||
- منشی → 403 · مدیر بدون دلیل → 422 · مدیر با دلیل → 200
|
||
tests/Package/PackageTenantTest.php
|
||
- پکیج محیط دیگر → 404
|
||
tests/Package/PricingIntegrationTest.php
|
||
- ردیف package در price_snapshot_lines با مبلغ منفی
|
||
- جمع ردیفها = مبلغ نهایی (invariant تسک ۰۸ حفظ شود)
|
||
```
|
||
|
||
## ۱۰. مستندات
|
||
|
||
`docs/api/package.md` بساز. `docs/architecture/tenancy.md` را با دلیل تفاوت
|
||
«دفتر اعتبار (جفت tenant)» و «کیف پول (سراسری + انتساب)» بهروز کن —
|
||
این دو شبیهاند و اشتباه گرفتنشان نشتی مالی میسازد.
|