diff --git a/docs/api/patient.md b/docs/api/patient.md index 1da259ee..50e50d7e 100644 --- a/docs/api/patient.md +++ b/docs/api/patient.md @@ -485,7 +485,8 @@ Updates mutable fields on a session. ``` - `payment_method: "wallet"` روی مراجعه‌ی تسویه‌نشده، کل مبلغ نهایی را از کیف پول بیمار کسر می‌کند (موجودی ناکافی → `422 ERR_WALLET_INSUFFICIENT`). -- **تخفیف تسویه:** `discount_type` = `percent` (۰..۱۰۰) یا `fixed` (ریال، حداکثر برابر مبلغ نهایی) یا `null` (حذف تخفیف). مبلغ محاسبه‌شده در `discount_rials` برمی‌گردد. تخفیف نمی‌تواند از «مبلغ نهایی منهای پرداخت‌های ثبت‌شده» بیشتر شود. تخفیفی که مانده را صفر کند مراجعه را تسویه‌شده می‌کند (`is_paid`, `paid_at`). +- **تخفیف تسویه (دستی):** `discount_type` = `percent` (۰..۱۰۰) یا `fixed` (ریال، حداکثر برابر مبلغ نهایی) یا `null` (حذف تخفیف). مبلغ محاسبه‌شده در `discount_rials` برمی‌گردد. تخفیف نمی‌تواند از «مبلغ نهایی منهای پرداخت‌های ثبت‌شده» بیشتر شود. تخفیفی که مانده را صفر کند مراجعه را تسویه‌شده می‌کند (`is_paid`, `paid_at`). +- **تخفیف بر اساس قانون:** `discount_rule_uuid` (رشته) → قانون تخفیف (owner-scoped) اعمال می‌شود؛ مقدار ریالی از خود قانون توسط موتور محاسبه می‌گردد (نوع/مبنا بر اساس قانون). `''`/`null` → حذف تخفیف. اولویت بر `discount_type` دستی. قانونِ نامعتبر → `404`. منبع اعمال‌شده در پاسخ به‌صورت `applied_discount_rule_id` و `applied_discount_rule_label` (audit) برمی‌گردد. قوانین قابل‌اعمال از `GET /api/v1/session/{uuid}/discount-suggestions` (نگاه کنید به `discount.md`). - `paid_at`: unix timestamp زمان تسویه. **Response 200:** diff --git a/src/Discount/Service/DiscountEngine.php b/src/Discount/Service/DiscountEngine.php index dc36af2f..62e01968 100644 --- a/src/Discount/Service/DiscountEngine.php +++ b/src/Discount/Service/DiscountEngine.php @@ -36,14 +36,7 @@ class DiscountEngine $out = []; foreach ($rules as $rule) { - if (!$this->withinValidity($rule, $now)) { - continue; - } - $base = $this->applicableBase($rule, $session, $final, $now); - if ($base === null) { - continue; - } - $rials = min($this->computeRials($rule, $base), $remaining); + $rials = $this->computeForRule($session, $rule, $now, $remaining); if ($rials <= 0) { continue; } @@ -61,6 +54,24 @@ class DiscountEngine return $out; } + /** + * مبلغ تخفیف ریالیِ یک قانون برای یک پرونده؛ 0 اگر برقرار نباشد. + * $remaining را ندهی، از خود session محاسبه می‌شود. + */ + public function computeForRule(PatientSession $session, DiscountRule $rule, ?int $now = null, ?int $remaining = null): int + { + $now ??= time(); + $remaining ??= max(0, $session->getFinalPriceRials() - $session->getPaidTotalRials()); + if (!$this->withinValidity($rule, $now)) { + return 0; + } + $base = $this->applicableBase($rule, $session, $session->getFinalPriceRials(), $now); + if ($base === null) { + return 0; + } + return min($this->computeRials($rule, $base), $remaining); + } + private function withinValidity(DiscountRule $rule, int $now): bool { if ($rule->getValidFrom() !== null && $now < $rule->getValidFrom()) { diff --git a/src/Patient/Controller/PatientController.php b/src/Patient/Controller/PatientController.php index 0a5d2f5c..b0a8d70f 100644 --- a/src/Patient/Controller/PatientController.php +++ b/src/Patient/Controller/PatientController.php @@ -60,6 +60,7 @@ class PatientController extends BaseController private readonly \App\Settlement\Repository\WalletTransactionRepository $walletRepo, private readonly \App\Settlement\Repository\SettlementRepository $settlementRepo, private readonly \App\Settlement\Service\WalletService $walletService, + private readonly \App\Discount\Repository\DiscountRuleRepository $discountRuleRepo, private readonly LoggerInterface $logger, ) {} @@ -1046,8 +1047,21 @@ class PatientController extends BaseController if (isset($data['notes'])) { $session->setNotes($data['notes']); } - // تخفیف تسویه: discount_type = percent|fixed|null (null = حذف تخفیف) - if (array_key_exists('discount_type', $data)) { + // تخفیف بر اساس قانون (discount_rule_uuid): مقدار از خود قانون، با ثبت منبع. + // '' یا null → حذف تخفیف. اولویت بر تخفیف دستی. + if (array_key_exists('discount_rule_uuid', $data)) { + $ruleUuid = $data['discount_rule_uuid']; + if ($ruleUuid === null || $ruleUuid === '') { + $this->patientService->applyDiscount($session, null, 0); + } else { + $rule = $this->discountRuleRepo->findByUuidForOwner((string) $ruleUuid, $entityType, $entityId); + if ($rule === null) { + return $this->error(ErrorCodes::ERR_VALIDATION_002, 'قانون تخفیف یافت نشد', 404, 'discount_rule_uuid'); + } + $this->patientService->applyDiscountRule($session, $rule); + } + } elseif (array_key_exists('discount_type', $data)) { + // تخفیف دستی: discount_type = percent|fixed|null (null = حذف تخفیف) $type = $data['discount_type'] !== null ? (string) $data['discount_type'] : null; $this->patientService->applyDiscount($session, $type, (int) ($data['discount_value'] ?? 0)); } diff --git a/src/Patient/Service/PatientService.php b/src/Patient/Service/PatientService.php index c0e41fba..a0c716df 100644 --- a/src/Patient/Service/PatientService.php +++ b/src/Patient/Service/PatientService.php @@ -50,6 +50,7 @@ class PatientService private readonly BillingCalculator $billingCalculator, private readonly WalletService $walletService, private readonly EntityInsurancePricingRepository $pricingRepo, + private readonly \App\Discount\Service\DiscountEngine $discountEngine, ) {} /** @@ -268,7 +269,7 @@ class PatientService * type=null → حذف تخفیف. percent باید 0..100 و fixed حداکثر برابر مبلغ نهایی باشد. * تخفیف نمی‌تواند از مانده‌ی قابل‌تخفیف (مبلغ نهایی منهای پرداخت‌های ثبت‌شده) بیشتر شود. */ - public function applyDiscount(PatientSession $session, ?string $type, int $value): PatientSession + public function applyDiscount(PatientSession $session, ?string $type, int $value, ?int $ruleId = null, ?string $ruleLabel = null): PatientSession { if ($type === null) { $session->setDiscount(null, 0, 0); @@ -298,15 +299,35 @@ class PatientService throw new AppException(ErrorCodes::ERR_SESSION_DISCOUNT_INVALID, null, 422, 'discount_value'); } - $session->setDiscount($type, $value, $rials); + $this->persistDiscount($session, $type, $value, $rials, $ruleId, $ruleLabel); + + return $session; + } + + /** + * اعمال یک قانون تخفیف روی مراجعه؛ مبلغ ریالی از موتور (با در نظر گرفتن نوع/مبنا) + * محاسبه و منبع قانون برای audit ثبت می‌شود. + */ + public function applyDiscountRule(PatientSession $session, \App\Discount\Entity\DiscountRule $rule): PatientSession + { + $rials = $this->discountEngine->computeForRule($session, $rule); + if ($rials <= 0) { + throw new AppException(ErrorCodes::ERR_SESSION_DISCOUNT_INVALID, null, 422, 'discount_rule_uuid'); + } + $this->persistDiscount($session, $rule->getDiscountType(), $rule->getValue(), $rials, $rule->getId(), $rule->getName()); + + return $session; + } + + private function persistDiscount(PatientSession $session, string $type, int $value, int $rials, ?int $ruleId, ?string $ruleLabel): void + { + $session->setDiscount($type, $value, $rials, $ruleId, $ruleLabel); if ($session->getRemainingRials() === 0 && $session->getPaymentMethod() === 'pending') { // تخفیف صددرصدی بدهی را صفر کرد — مراجعه تسویه‌شده تلقی می‌شود $session->setPaymentMethod('cash'); $session->setPaidAt(time()); } $this->sessionRepo->save($session); - - return $session; } /**