From cf69369231d587b5d15670cc75917262b4804c97 Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Fri, 17 Jul 2026 11:49:08 +0330 Subject: [PATCH] feat(discount): add DiscountEngine to evaluate rules for a session Evaluates all active owner rules against a session across the six types (patient tag, invoice amount, specific patient, occasion incl. birthday, service, visit count), honoring validity windows and capping each computed discount at the session's unpaid remainder. Returns priority-ordered suggestions with computed rials. Co-Authored-By: Claude Fable 5 --- src/Discount/Service/DiscountEngine.php | 150 ++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 src/Discount/Service/DiscountEngine.php diff --git a/src/Discount/Service/DiscountEngine.php b/src/Discount/Service/DiscountEngine.php new file mode 100644 index 00000000..dc36af2f --- /dev/null +++ b/src/Discount/Service/DiscountEngine.php @@ -0,0 +1,150 @@ + + * مرتب بر priority نزولی (از repository). فقط قوانینِ برقرار با discount_rials > 0. + */ + public function evaluate(PatientSession $session): array + { + $record = $session->getRecord(); + $rules = $this->ruleRepo->findActiveForOwner($record->getEntityType(), $record->getEntityId()); + + $final = $session->getFinalPriceRials(); + $remaining = max(0, $final - $session->getPaidTotalRials()); + $now = time(); + + $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); + if ($rials <= 0) { + continue; + } + $out[] = [ + 'rule_uuid' => $rule->getUuid(), + 'rule_name' => $rule->getName(), + 'type' => $rule->getType(), + 'discount_type' => $rule->getDiscountType(), + 'value' => $rule->getValue(), + 'discount_rials' => $rials, + 'combinable' => $rule->isCombinable(), + 'priority' => $rule->getPriority(), + ]; + } + return $out; + } + + private function withinValidity(DiscountRule $rule, int $now): bool + { + if ($rule->getValidFrom() !== null && $now < $rule->getValidFrom()) { + return false; + } + if ($rule->getValidTo() !== null && $now > $rule->getValidTo()) { + return false; + } + return true; + } + + /** + * مبنای محاسبه‌ی تخفیف اگر قانون برقرار باشد؛ null یعنی قانون اعمال نمی‌شود. + */ + private function applicableBase(DiscountRule $rule, PatientSession $session, int $final, int $now): ?int + { + $record = $session->getRecord(); + + return match ($rule->getType()) { + DiscountRule::TYPE_PATIENT_TAG => $this->hasTag($session, $rule->getTargetTagId()) ? $final : null, + + DiscountRule::TYPE_INVOICE_AMOUNT => ($rule->getMinAmountRials() !== null && $final >= $rule->getMinAmountRials()) ? $final : null, + + DiscountRule::TYPE_SPECIFIC_PATIENT => ($rule->getTargetRecordId() !== null && $record->getId() === $rule->getTargetRecordId()) ? $final : null, + + DiscountRule::TYPE_OCCASION => $this->occasionMatches($rule, $session, $now) ? $final : null, + + DiscountRule::TYPE_SERVICE => $this->serviceBase($session, $rule->getTargetServiceItemId()), + + DiscountRule::TYPE_VISIT_COUNT => ($rule->getMinVisitCount() !== null + && $this->sessionRepo->countByRecord($record) >= $rule->getMinVisitCount()) ? $final : null, + + default => null, + }; + } + + private function hasTag(PatientSession $session, ?int $tagId): bool + { + if ($tagId === null) { + return false; + } + foreach ($session->getRecord()->getTags() as $tag) { + if ($tag->getId() === $tagId) { + return true; + } + } + return false; + } + + /** مبنای تخفیف سرویس = جمع خطوطِ همان سرویس؛ null اگر سرویس در پرونده نباشد. */ + private function serviceBase(PatientSession $session, ?int $serviceItemId): ?int + { + if ($serviceItemId === null) { + return null; + } + $sum = 0; + foreach ($session->getServices() as $line) { + /** @var SessionService $line */ + if ($line->getServiceItem()->getId() === $serviceItemId) { + $sum += $line->getLineTotalRials(); + } + } + return $sum > 0 ? $sum : null; + } + + private function occasionMatches(DiscountRule $rule, PatientSession $session, int $now): bool + { + if ($rule->getOccasionKind() !== DiscountRule::OCCASION_BIRTHDAY) { + // مناسبت مبتنی بر بازه‌ی تاریخی؛ withinValidity قبلاً چک شده. + return true; + } + $profile = $this->profileRepo->findByUser($session->getRecord()->getUser()); + $dob = $profile?->getDateOfBirth(); + if ($dob === null) { + return false; + } + return date('m-d', $dob) === date('m-d', $now); + } + + private function computeRials(DiscountRule $rule, int $base): int + { + if ($rule->getDiscountType() === DiscountRule::DISCOUNT_PERCENT) { + return (int) round($base * min(100, $rule->getValue()) / 100); + } + return min($rule->getValue(), $base); + } +}