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 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
namespace App\Discount\Service;
|
||||
|
||||
use App\Discount\Entity\DiscountRule;
|
||||
use App\Discount\Repository\DiscountRuleRepository;
|
||||
use App\Patient\Entity\PatientSession;
|
||||
use App\Patient\Entity\SessionService;
|
||||
use App\Patient\Repository\PatientSessionRepository;
|
||||
use App\UserProfile\Repository\UserProfileRepository;
|
||||
|
||||
/**
|
||||
* موتور تخفیف: برای یک پرونده، قوانین قابلاعمالِ owner را ارزیابی و مبلغ تخفیف
|
||||
* هرکدام را محاسبه میکند. اعمال نهایی در PatientService::applyDiscount انجام میشود.
|
||||
*/
|
||||
class DiscountEngine
|
||||
{
|
||||
public function __construct(
|
||||
private readonly DiscountRuleRepository $ruleRepo,
|
||||
private readonly PatientSessionRepository $sessionRepo,
|
||||
private readonly UserProfileRepository $profileRepo,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return array<int, array{rule_uuid: string, rule_name: string, type: string, discount_type: string, value: int, discount_rials: int, combinable: bool, priority: int}>
|
||||
* مرتب بر 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user