""" Routing rule models — Smart routing for inbound calls. Rules are evaluated in priority order (lower first). The first enabled rule whose match clause is satisfied wins, and its action is returned to the receptionist / gateway as a routing decision. """ from datetime import datetime from enum import Enum from typing import Optional from pydantic import BaseModel, Field class RoutingActionType(str, Enum): RING_DEVICE = "ring_device" # Ring a single device RING_CHAIN = "ring_chain" # Ring a chain of devices (priority order) TAKE_MESSAGE = "take_message" # Skip the ring, go straight to voicemail REJECT = "reject" # Decline the call (busy / 603) DND = "dnd" # Do Not Disturb — also reject, but tagged for analytics class TimeRange(BaseModel): """A recurring time-of-day window.""" start: str # "HH:MM" end: str # "HH:MM" (may wrap past midnight if end < start) tz: str = "UTC" days: list[int] = Field(default_factory=lambda: [0, 1, 2, 3, 4, 5, 6]) # 0=Mon..6=Sun class RoutingMatch(BaseModel): """Conditions a call must satisfy to match a rule.""" caller_pattern: Optional[str] = None # glob, e.g. "+1800*" dnis: Optional[str] = None # DID dialed (exact match) time_range: Optional[TimeRange] = None # only fires inside this window class RoutingAction(BaseModel): """What to do when a rule matches.""" type: RoutingActionType device_id: Optional[str] = None # for RING_DEVICE device_ids: list[str] = Field(default_factory=list) # for RING_CHAIN ring_timeout: int = 25 # seconds per device message: Optional[str] = None # optional TTS string for REJECT/DND class RoutingRule(BaseModel): id: str name: str priority: int = 100 enabled: bool = True match: RoutingMatch = Field(default_factory=RoutingMatch) action: RoutingAction created_at: Optional[datetime] = None updated_at: Optional[datetime] = None class RoutingRuleCreate(BaseModel): name: str priority: int = 100 enabled: bool = True match: RoutingMatch = Field(default_factory=RoutingMatch) action: RoutingAction class RoutingRuleUpdate(BaseModel): name: Optional[str] = None priority: Optional[int] = None enabled: Optional[bool] = None match: Optional[RoutingMatch] = None action: Optional[RoutingAction] = None class RoutingDecision(BaseModel): """Result of evaluating the routing rules for an inbound call.""" matched_rule_id: Optional[str] = None matched_rule_name: Optional[str] = None action: RoutingAction reason: str = ""