$facts */ public function resolve( string $category, string $entityType, int $entityId, array $facts, ?DoctorAddress $address = null, ?ServiceItem $service = null, ?int $at = null, ): PolicyOutcome { $at = $at ?? time(); $candidates = $this->policies->findForCategory($entityType, $entityId, $category); $matched = []; foreach ($candidates as $policy) { if (!$policy->appliesAt($at) || !$this->inScope($policy, $address, $service)) { continue; } if ($this->evaluator->matches($policy, $facts)) { $matched[] = $policy; } } if ($matched === []) { return new PolicyOutcome(); } usort($matched, $this->comparator(...)); return $this->combine($matched); } /** * ارزیابی **یک** قانون، بدون رقابت و بدون ترکیب با بقیه. * * سؤال آزمایشگاه این است که «این قانون چه می‌کند»، نه «نتیجهٔ نهایی با همهٔ قوانین * چه می‌شود». دومی مفید است ولی چیزی نیست که کاربرِ در حال نوشتن قانون می‌پرسد. * * دامنه و اعتبار زمانی هم عمداً نادیده گرفته می‌شوند: کاربر دارد قانونِ **پیش‌نویس** * را روی نمونهٔ گذشته می‌آزماید؛ رد کردنش به‌خاطر اینکه هنوز فعال نیست بی‌معناست. * * @param array $facts */ public function evaluateOne(Policy $policy, array $facts): PolicyOutcome { if (!$this->evaluator->matches($policy, $facts)) { return new PolicyOutcome(); } return $this->combine([$policy]); } /** * قانونی که دامنه‌اش با این درخواست نمی‌خواند اصلاً کاندید نیست. * * دامنهٔ تهی یعنی «همه» — قانون سطح محیط روی همه‌چیز اعمال می‌شود. */ private function inScope(Policy $policy, ?DoctorAddress $address, ?ServiceItem $service): bool { if ($policy->getAddress() !== null && $policy->getAddress()->getId() !== $address?->getId()) { return false; } if ($policy->getServiceItem() !== null && $policy->getServiceItem()->getId() !== $service?->getId()) { return false; } if ($policy->getCatalogCategory() !== null && $policy->getCatalogCategory()->getId() !== $service?->getCatalogCategory()?->getId() ) { return false; } return true; } private function comparator(Policy $a, Policy $b): int { return [$b->getPriority(), $b->specificity(), $a->getCreatedAt()] <=> [$a->getPriority(), $a->specificity(), $b->getCreatedAt()]; } /** @param Policy[] $policies به ترتیب برنده‌ترین */ private function combine(array $policies): PolicyOutcome { $effects = []; $applied = []; $forbids = []; foreach ($policies as $policy) { $contributed = false; foreach ($policy->getEffects() as $effect) { $type = $effect['type'] ?? null; if (!is_string($type)) { continue; } $contributed = true; $mode = PolicySchema::COMBINATION[$type] ?? 'max'; $value = $effect['value'] ?? true; if ($mode === 'veto') { // متن دلخواه با کلید `reason` می‌آید؛ نبودنش خطا نیست چون نام // خودِ قانون همیشه یک توضیح قابل‌فهم است. $reason = $effect['reason'] ?? null; $forbids[] = is_string($reason) && trim($reason) !== '' ? $reason : sprintf('قانون «%s» این عملیات را مجاز نمی‌داند', $policy->getName()); continue; } $effects[$type] = match ($mode) { 'sum' => (float) ($effects[$type] ?? 0) + (float) $value, 'union' => array_values(array_unique([...($effects[$type] ?? []), ...(array) $value])), default => max($effects[$type] ?? $value, $value), // max }; } if ($contributed) { $applied[] = [ 'uuid' => $policy->getUuid(), 'name' => $policy->getName(), 'version' => $policy->getVersion(), ]; } } // جمع‌ها به عدد صحیح برمی‌گردند: دقیقه و ریال هر دو صحیح‌اند. foreach ($effects as $type => $value) { if (is_float($value)) { $effects[$type] = $value == (int) $value ? (int) $value : $value; } } return new PolicyOutcome($effects, $applied, $forbids); } }