Files
clinicpro/docs/new_feture/taskes/task-08-pricing-snapshot/architecture.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

معماری — تسک ۰۸

ساختار فایل

src/Pricing/
├── Entity/
│   ├── PriceList.php
│   ├── PriceListItem.php
│   ├── PriceSnapshot.php
│   ├── PriceSnapshotLine.php
│   └── DepositPolicy.php
├── Service/
│   ├── PricingEngine.php          # ارکستراتور هفت‌مرحله‌ای
│   ├── PriceResolver.php          # قیمت پایه: لیست → تعرفه → سرویس
│   ├── SnapshotWriter.php
│   └── DepositCalculator.php
├── Dto/{PriceQuote, PriceLine}.php
├── Controller/{PriceListController, PricingController}.php
└── Repository/…

PricingEngine — هفت مرحلهٔ مستند

public function quote(QuoteRequest $req): PriceQuote
{
    $lines = [];

    // ۱ قیمت پایهٔ سرویس (از لیست قیمت معتبر در تاریخ رزرو)
    $lines[] = PriceLine::base($this->resolver->servicePrice($req->service, $req->branch, $req->at));

    // ۲ جمع قیمت آیتم‌های انتخابی
    foreach ($req->options as $option) {
        $lines[] = PriceLine::option($option, $this->resolver->optionPrice($option, $req->branch, $req->at));
    }

    // ۳ قوانین قیمت به ترتیب اولویت  ← DiscountEngine موجود، بعداً تسک ۰۹
    $lines = $this->discounts->apply($lines, $req);

    // ۴ کسر از اعتبار پکیج           ← قلاب تسک ۱۱ (فعلاً no-op)
    $lines = $this->packages->consume($lines, $req);

    // ۵ مالیات و سهم بیمه            ← AppointmentInsuranceService موجود
    $lines = $this->insurance->apply($lines, $req);
    $lines = $this->tax->apply($lines, $req);

    // ۶ بیعانه
    $deposit = $this->depositCalculator->forQuote($lines, $req);

    // ۷ خروجی تفکیک‌شده (ذخیره فقط در confirm انجام می‌شود)
    return new PriceQuote($lines, $deposit);
}

هر مرحله یک سرویس مستقل با اینترفیس خودش. مرحلهٔ ۳ و ۴ از روز اول در زنجیره هستند حتی وقتی خالی‌اند — همان دلیل تسک ۰۵: امضای عمومی بعداً عوض نشود.

PriceResolver — ترتیب اولویت

۱. ServiceBranchOverride.price_rials      (تسک ۰۴ — اختصاصی‌ترین)
۲. PriceListItem از PriceList فعالی که تاریخ رزرو را می‌پوشاند و شعبه‌اش مطابق است
۳. PriceListItem از PriceList فعال محیط (بدون شعبه)
۴. Tariff::findForServiceYear(سال شمسی تاریخ رزرو)      ← موجود، دست‌نخورده
۵. ServiceItem.price_rials                              ← آخرین fallback

هیچ‌وقت خطا یا صفر برنمی‌گرداند. سطر ۴ و ۵ تضمین می‌کنند همهٔ داده‌های موجود بدون هیچ لیست قیمتی درست کار کنند.

تاریخ مبنا: تاریخ رزرو (slot_start)، نه تاریخ ثبت. مستند بند ۱۲: «از لیست قیمت معتبر در تاریخ رزرو». اگر بیمار امروز برای سه ماه بعد نوبت بگیرد، قیمت آن روز اعمال می‌شود. این تصمیم را در docs/api/pricing.md صریح بنویس — دو تفسیر دارد و پشتیبانی از هر دو غیرممکن است.

PriceSnapshot — فاکتور منجمد

class PriceSnapshot
{
    use TenantOwnedTrait;
    private Appointment $appointment;
    private int $baseRials;
    private int $optionsRials;
    private int $discountRials;
    private int $insuranceBaseRials;
    private int $insuranceSupplementaryRials;
    private int $taxRials;
    private int $finalRials;
    private int $depositRials;
    private array $appliedPolicyIds = [];   // قانون‌های اعمال‌شده — قانون پنجم مستند
    private int $createdAt;
    private Collection $lines;              // PriceSnapshotLine
}

appliedPolicyIds از روز اول: مستند بند ۸ می‌گوید «هر نوبت فهرست قانون‌هایی که رویش اعمال شده را ذخیره می‌کند». تسک ۰۹ نسخهٔ قانون‌ها را هم اضافه می‌کند؛ فعلاً شناسهٔ DiscountRule ها ثبت می‌شود.

PriceSnapshotLine ردیف‌های تفکیک‌شده: نوع (base|option|discount|insurance|tax)، نام، مبلغ، ارجاع اختیاری به منبع (سرویس/آیتم/قانون).

رابطه با Invoice موجود

Invoice/InvoiceItem (دامنهٔ Billing) باقی می‌مانند و کارشان صورتحساب مراجعهٔ انجام‌شده است. PriceSnapshot کار متفاوتی می‌کند: قیمت لحظهٔ رزرو.

PriceSnapshot Invoice
کِی ساخته می‌شود confirm نوبت پایان مراجعه
چه چیزی را ثبت می‌کند آن‌چه قرار بود پرداخت شود آن‌چه واقعاً انجام و صورتحساب شد
تغییر می‌کند هرگز تا تسویه

اگر بیمار سر نوبت خدمت اضافه بگیرد، Invoice فرق می‌کند و PriceSnapshot نه — و همین تفاوت، منبع گزارش «اختلاف پیش‌بینی و واقعیت» است.

این جدول را در docs/architecture/insurance-billing-system.md اضافه کن، وگرنه اولین کسی که هر دو را می‌بیند یکی را حذف می‌کند.

سیاست بیعانه

class DepositPolicy
{
    use TenantOwnedTrait;
    private ?ServiceItem $service = null;   // null = پیش‌فرض محیط
    private string $mode;                   // none | fixed | percent
    private int $value = 0;
    private ?int $minRials = null;
    private ?int $maxRials = null;
}

DepositCalculator اختصاصی‌ترین سیاست را می‌گیرد (سرویس بر محیط) و مقدار را به Appointment.deposit_required/deposit_amount_rials موجود می‌نویسد — ستون‌های جدید لازم نیست.

پنل ادمین

  • PriceListsPage.tsx — لیست با بازهٔ شمسی و وضعیت (پیش‌نویس/فعال/منقضی)
  • PriceListFormPage.tsx — بازهٔ تاریخ با PersianDatePicker، شعبه با SearchableSelect، جدول سرویس‌ها با PriceInput
  • در AppointmentDetailPage.tsx یک کارت «فاکتور» با ردیف‌های snapshot
  • «کپی از لیست قیمت قبلی» — بدون آن، کلینیک با ۲۰۰ سرویس هرگز لیست جدید نمی‌سازد