"""Rules Engine: pure expansion of a BOQ item into activity + dependency drafts. Template-driven (NO hardcoded if/else on procurement_type or category). Reads activity_templates + dependency_templates via a repo; never writes to the DB. """ import re from app.rules.errors import NoTemplateError from app.rules.templates_repo import TemplatesRepo from app.rules.types import ActivityDraft, BOQItemInput, DependencyDraft, ExpandResult _PLACEHOLDER = re.compile(r"\{(\w+)\}") def _render_template(template: str, boq: BOQItemInput) -> str: return _PLACEHOLDER.sub(lambda m: str(getattr(boq, m.group(1), "") or ""), template) def _num(value) -> float | None: return None if value is None else float(value) def expand(boq: BOQItemInput, repo: TemplatesRepo) -> ExpandResult: templates = repo.find_activity_templates( category_id=boq.category_id, procurement_type=boq.procurement_type, active=True, ) if not templates: raise NoTemplateError(boq.category_id, boq.procurement_type) override_effort = boq.metadata.get("override_effort_days") override_lead = boq.metadata.get("override_lead_time_days") activities: list[ActivityDraft] = [ ActivityDraft( boq_item_id=boq.id, template_id=t["id"], kind=t["kind"], name=_render_template(t["name_template"], boq), effort_days=_num(override_effort) if override_effort is not None else _num(t["default_effort_days"]), lead_time_days=_num(override_lead) if override_lead is not None else _num(t["default_lead_time_days"]), clock=t["clock"], noisy=bool(t["noisy"]), responsibility=( boq.procurement_responsibility if t["kind"] == "PROCUREMENT" else "CONTRACTOR" ), _tag=f"{boq.id}:{t['kind']}", ) for t in templates ] dep_rows = repo.find_dependency_templates(category_id=boq.category_id, active=True) dependencies: list[DependencyDraft] = [] for d in dep_rows: pred = next((a for a in activities if a.kind == d["predecessor_kind"]), None) succ = next((a for a in activities if a.kind == d["successor_kind"]), None) if pred and succ: dependencies.append( DependencyDraft( predecessor_tag=pred._tag, successor_tag=succ._tag, dep_type=d["dep_type"], lag_days=float(d["lag_days"]), origin="TEMPLATE", ) ) return ExpandResult(activities=activities, dependencies=dependencies)