tenantEntityType(); $entityId = $address->tenantEntityId(); $list = $this->priceLists->findCovering($entityType, $entityId, $address, $at); $sources = []; $base = $this->priceFor($service, $address, $list, $at, $sources); $itemsTotal = 0; foreach ($items as $item) { $itemsTotal += $this->priceFor($item, $address, $list, $at, $sources); } $subtotal = $base + $itemsTotal; // ── پکیج ────────────────────────────────────────────────────────────── // پکیج **قیمت پایهٔ سرویس** را می‌پوشاند، نه آیتم‌های اضافه: «شش جلسه لیزر» // یعنی شش بار خودِ لیزر، نه هر چیزی که کنارش انتخاب شود. $usable = $patient === null ? null : $this->packages->firstUsable($patient, $service, $at); $covered = 0; if ($usable !== null) { $covered = min($base, $subtotal); $subtotal -= $covered; } // ── تخفیف ───────────────────────────────────────────────────────────── // قوانین دستهٔ «قیمت» کنار سیاست دستیِ درخواست می‌نشینند، نه به‌جایش: تخفیفی // که اپراتور دستی می‌دهد و تخفیفی که قانون می‌دهد هر دو واقعی‌اند. $policy = $this->mergePolicyDiscounts($service, $items, $address, $at, $subtotal, $policy, $sources); [$discount, $discounts] = $this->discountFor($subtotal, $policy); // تخفیف بیشتر از مبلغ، مبلغ را **صفر** می‌کند نه منفی: بدهی منفی یعنی کلینیک // به بیمار پول بدهکار شود، که هیچ‌جای این جریان معنا ندارد. $discount = min($discount, $subtotal); $afterDiscount = $subtotal - $discount; // ── بیمه ────────────────────────────────────────────────────────────── $insuranceBase = $this->percentOf($afterDiscount, $policy['insurance_base_percent'] ?? 0.0); $insuranceBase = min($insuranceBase, $afterDiscount); $remaining = $afterDiscount - $insuranceBase; $supplementary = min($this->percentOf($remaining, $policy['insurance_supplementary_percent'] ?? 0.0), $remaining); $patientShare = $remaining - $supplementary; // ── مالیات ──────────────────────────────────────────────────────────── // روی سهم بیمار حساب می‌شود، نه روی کل: بیمار مالیاتِ سهمی که بیمه می‌دهد را // نمی‌پردازد. $tax = $this->percentOf($patientShare, $policy['tax_percent'] ?? 0.0); $final = $patientShare + $tax; // ── بیعانه ──────────────────────────────────────────────────────────── $deposit = isset($policy['deposit_rials']) ? (int) $policy['deposit_rials'] : $this->percentOf($final, $policy['deposit_percent'] ?? 0.0); $deposit = max(0, min($deposit, $final)); if ($covered > 0) { $discounts[] = [ 'label' => sprintf('پوشش پکیج «%s»', $usable?->getPackage()->getName() ?? '—'), 'rials' => $covered, 'kind' => 'package', ]; } return new PriceQuote( baseRials: $base, itemsRials: $itemsTotal, discountRials: $discount, insuranceBaseRials: $insuranceBase, insuranceSupplementaryRials: $supplementary, taxRials: $tax, finalRials: $final, depositRials: $deposit, discounts: $discounts, sources: $sources, packageWillBeConsumed: $usable !== null, packageUuid: $usable?->getUuid(), ); } /** * اثر قوانین «قیمت» را به سیاست درخواست اضافه می‌کند. * * شناسه و **نسخهٔ** هر قانون در `sources` ثبت می‌شود تا فاکتور بتواند سه ماه بعد * بگوید کدام نسخه رویش اعمال شده بود. * * @param ServiceItem[] $items * @param array $policy * @param array $sources * @return array */ private function mergePolicyDiscounts( ServiceItem $service, array $items, DoctorAddress $address, int $at, int $subtotal, array $policy, array &$sources, ): array { $outcome = $this->policies->resolve( Policy::CATEGORY_PRICING, $address->tenantEntityType(), $address->tenantEntityId(), [ 'item_count' => count($items), 'subtotal_rials' => $subtotal, 'patient_tags' => $policy['patient_tags'] ?? [], 'visit_count' => $policy['visit_count'] ?? 0, ], $address, $service, $at, ); if ($outcome->appliedPolicies === []) { return $policy; } $sources['applied_policies'] = $outcome->appliedPolicies; $policy['discount_percent'] = (float) ($policy['discount_percent'] ?? 0) + (float) $outcome->effect(PolicySchema::EFFECT_DISCOUNT_PERCENT, 0); $policy['discount_rials'] = (int) ($policy['discount_rials'] ?? 0) + (int) $outcome->effect(PolicySchema::EFFECT_DISCOUNT_RIALS, 0); $policy['discount_label'] ??= $outcome->appliedPolicies[0]['name']; return $policy; } /** * @param array $sources */ private function priceFor( ServiceItem $service, DoctorAddress $address, ?\App\Pricing\Entity\PriceList $list, int $at, array &$sources, ): int { $override = $this->overrides->mapForAddress([(int) $service->getId()], $address)[(int) $service->getId()] ?? null; if ($override?->getPriceRials() !== null) { $sources[$service->getUuid()] = 'branch_override'; return $override->getPriceRials(); } if ($list !== null) { $price = $this->priceListItems->priceMap($list, [$service])[(int) $service->getId()] ?? null; if ($price !== null) { $sources[$service->getUuid()] = 'price_list'; return $price; } } $tariff = $this->tariffs->findForServiceYear((int) $service->getId(), $this->jalali->jalaliYear($at)); if ($tariff !== null) { $sources[$service->getUuid()] = 'tariff'; return (int) $tariff->getPriceRials(); } $sources[$service->getUuid()] = 'service_item'; return $service->getPriceRials(); } /** * @param array $policy * @return array{0: int, 1: list>} */ private function discountFor(int $subtotal, array $policy): array { $discounts = []; $total = 0; if (($policy['discount_percent'] ?? 0.0) > 0) { $amount = $this->percentOf($subtotal, (float) $policy['discount_percent']); $total += $amount; $discounts[] = [ 'label' => $policy['discount_label'] ?? 'تخفیف درصدی', 'percent' => $policy['discount_percent'], 'rials' => $amount, ]; } if (($policy['discount_rials'] ?? 0) > 0) { $amount = (int) $policy['discount_rials']; $total += $amount; $discounts[] = [ 'label' => $policy['discount_label'] ?? 'تخفیف مبلغی', 'rials' => $amount, ]; } // سقف جمع تخفیف‌ها per محیط: چند تخفیفِ جداگانه که هرکدام منطقی‌اند، با هم // می‌توانند مبلغ را بی‌معنا کنند. $cap = $policy['max_total_discount_percent'] ?? null; if ($cap !== null && $cap >= 0) { $maxAllowed = $this->percentOf($subtotal, (float) $cap); if ($total > $maxAllowed) { $discounts[] = [ 'label' => sprintf('سقف تخفیف %s٪ اعمال شد', $cap), 'rials' => $maxAllowed - $total, ]; $total = $maxAllowed; } } return [$total, $discounts]; } /** ریال واحد صحیح است؛ گرد کردن به پایین از اضافه‌گرفتن جلوگیری می‌کند. */ private function percentOf(int $amount, float $percent): int { if ($percent <= 0) { return 0; } return (int) floor($amount * $percent / 100); } }