- 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.7 KiB
نکات پیادهسازی — تسک ۱۱
۱. quote نمایش میدهد، confirm مصرف میکند
بدترین باگ ممکن در این تسک:
// ❌ بیمار صفحه را سه بار رفرش میکند، سه جلسه از دست میدهد
public function quote(QuoteRequest $req): PriceQuote {
$this->packages->consume(…);
}
// ✅
public function quote(…): PriceQuote {
$pkg = $this->finder->firstUsable(…);
return $quote->withPackagePreview($pkg); // فقط نمایش
}
// و در BookingService::confirm() داخل تراکنش:
$this->packages->consume($pkg, $appointment);
تست اجباری: ده بار quote → مانده بدون تغییر.
۲. مانده صفر خطا نیست
if ($this->ledger->balance($pkg) <= 0) {
return false; // ✅ مبلغ کامل محاسبه میشود
// نه: throw new AppException(...)
}
بیمار با پکیج تمامشده باید بتواند نقدی نوبت بگیرد. 422 یعنی بنبست بیدلیل.
UI پیام بدهد: «اعتبار پکیج شما تمام شده؛ این نوبت نقدی محاسبه میشود.»
۳. uniq_scl_consume و idempotency
confirm تسک ۰۷ idempotent است. اگر دوبار صدا زده شود:
try {
$this->ledger->record($pkg, KIND_CONSUME, -1, $meta);
} catch (UniqueConstraintViolationException) {
// قبلاً مصرف شده — همان رفتار idempotent، نه خطا
}
با کلید یکتای (appointment_id, kind) این تضمین از دیتابیس میآید. همان الگوی تسک ۰۷.
۴. لغو = ردیف refund، نه حذف consume
// ❌ تاریخ را پاک میکند
$this->em->remove($consumeRow);
// ✅
$this->ledger->record($pkg, KIND_REFUND, +1, LedgerMeta::forCancellation($appt));
دفتر append-only است. بعد از سه ماه، سؤال «چند بار این بیمار نوبتش را لغو کرد؟» فقط از دفتر جواب دارد.
⚠️ بازگشت اعتبار مشروط به سیاست لغو است (تسک ۱۳). تا آن تسک نیامده، همیشه برگردان و
یک TODO با ارجاع به تسک ۱۳ بگذار — نه یک پرچم نیمکاره.
۵. FIFO و انقضا
->orderBy('pp.purchasedAt', 'ASC')
قدیمیترین اول. اگر LIFO باشد، پکیج قدیمی منقضی میشود و بیمار پولش را از دست میدهد — و شکایتش درست است.
valid_to هنگام خرید محاسبه و ذخیره میشود (purchased_at + validity_days * 86400)،
نه در زمان اجرا: تغییر validity_days تعریف پکیج نباید اعتبار خریدهای قبلی را عوض کند.
۶. قفل بدبینانه اینجا درست است
برخلاف تسک ۰۷ که قفل را رد کردیم:
$locked = $this->em->find(PatientPackage::class, $id, LockMode::PESSIMISTIC_WRITE);
نرخ رقابت اینجا ناچیز است (یک بیمار، یک پکیج) و یک ردیف قفل میشود، نه دهها سطل.
جدول مقایسه در architecture.md را در docs/api/package.md هم بنویس، وگرنه کسی روزی
«برای یکدستی» یکی را به دیگری تبدیل میکند.
۷. adjustment فقط با نقش مدیر و با دلیل
#[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)» و «کیف پول (سراسری + انتساب)» بهروز کن —
این دو شبیهاند و اشتباه گرفتنشان نشتی مالی میسازد.