Files
clinicpro/docs/new_feture/taskes/task-11-package-credit-ledger/implementation_notes.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

6.7 KiB
Raw Blame History

نکات پیاده‌سازی — تسک ۱۱

۱. 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)» و «کیف پول (سراسری + انتساب)» به‌روز کن — این دو شبیه‌اند و اشتباه گرفتنشان نشتی مالی می‌سازد.