| import json |
| from decimal import Decimal |
| from typing import Any |
|
|
| from redis import Redis |
| from sqlalchemy import and_, or_, select |
| from sqlalchemy.orm import Session |
|
|
| from app.core.config import get_settings |
| from app.models.rule import RuleMaster |
|
|
|
|
| def _serialize_rule(rule: RuleMaster) -> dict[str, Any]: |
| return { |
| "id": rule.id, |
| "tenant_id": rule.tenant_id, |
| "product_type": rule.product_type, |
| "dealer_id": rule.dealer_id, |
| "rule_code": rule.rule_code, |
| "rule_name": rule.rule_name, |
| "rule_type": rule.rule_type, |
| "field_name": rule.field_name, |
| "operator": rule.operator, |
| "field_value": rule.field_value, |
| "score_value": str(rule.score_value) if rule.score_value is not None else None, |
| "score_component": rule.score_component, |
| "decision_action": rule.decision_action, |
| "reason_code": rule.reason_code, |
| "priority": rule.priority, |
| "version": rule.version, |
| "is_active": rule.is_active, |
| "created_by": rule.created_by, |
| "created_at": rule.created_at.isoformat(), |
| "updated_at": rule.updated_at.isoformat(), |
| } |
|
|
|
|
| def _hydrate_rule(data: dict[str, Any]) -> RuleMaster: |
| rule = RuleMaster(**{k: v for k, v in data.items() if k not in {"created_at", "updated_at"}}) |
| if data.get("score_value") is not None: |
| rule.score_value = Decimal(data["score_value"]) |
| return rule |
|
|
|
|
| class RuleRepository: |
| def __init__(self) -> None: |
| self.settings = get_settings() |
| self.redis: Redis | None = None |
| if self.settings.redis_url: |
| try: |
| self.redis = Redis.from_url(self.settings.redis_url, decode_responses=True) |
| except Exception: |
| self.redis = None |
|
|
| def cache_key(self, tenant_id: str, product_type: str, dealer_id: str | None) -> str: |
| dealer = dealer_id or "ALL" |
| return f"rules:{tenant_id}:{product_type}:{dealer}" |
|
|
| def get_active_rules( |
| self, db: Session, tenant_id: str, product_type: str, dealer_id: str | None |
| ) -> list[RuleMaster]: |
| key = self.cache_key(tenant_id, product_type, dealer_id) |
| if self.redis: |
| try: |
| cached = self.redis.get(key) |
| if cached: |
| return [_hydrate_rule(item) for item in json.loads(cached)] |
| except Exception: |
| pass |
|
|
| stmt = ( |
| select(RuleMaster) |
| .where( |
| RuleMaster.is_active.is_(True), |
| RuleMaster.tenant_id == tenant_id, |
| RuleMaster.product_type == product_type, |
| or_(RuleMaster.dealer_id.is_(None), RuleMaster.dealer_id == dealer_id), |
| ) |
| .order_by(RuleMaster.priority.asc(), RuleMaster.id.asc()) |
| ) |
| rules = list(db.scalars(stmt).all()) |
| if self.redis: |
| try: |
| self.redis.setex(key, 300, json.dumps([_serialize_rule(rule) for rule in rules])) |
| except Exception: |
| pass |
| return rules |
|
|
| def invalidate(self, tenant_id: str, product_type: str, dealer_id: str | None = None) -> None: |
| if not self.redis: |
| return |
| keys = [ |
| self.cache_key(tenant_id, product_type, dealer_id), |
| self.cache_key(tenant_id, product_type, None), |
| ] |
| try: |
| self.redis.delete(*set(keys)) |
| except Exception: |
| pass |
|
|
| def list_rules( |
| self, |
| db: Session, |
| tenant_id: str | None = None, |
| product_type: str | None = None, |
| dealer_id: str | None = None, |
| rule_type: str | None = None, |
| is_active: bool | None = None, |
| ) -> list[RuleMaster]: |
| filters = [] |
| if tenant_id: |
| filters.append(RuleMaster.tenant_id == tenant_id) |
| if product_type: |
| filters.append(RuleMaster.product_type == product_type) |
| if dealer_id: |
| filters.append(RuleMaster.dealer_id == dealer_id) |
| if rule_type: |
| filters.append(RuleMaster.rule_type == rule_type) |
| if is_active is not None: |
| filters.append(RuleMaster.is_active.is_(is_active)) |
| stmt = select(RuleMaster).order_by(RuleMaster.priority.asc(), RuleMaster.id.asc()) |
| if filters: |
| stmt = stmt.where(and_(*filters)) |
| return list(db.scalars(stmt).all()) |
|
|
|
|