import uuid from fastapi import HTTPException from sqlalchemy.ext.asyncio import AsyncSession from org_settings.models import ExcludeCompany, ExcludeUniversity, OrgSettings from org_settings.serializers import ( serialize_exclude_company, serialize_exclude_university, serialize_org_setting, ) VALID_CATEGORIES = ( "general", "notifications", "email_templates", "career_portal", "branding", "security", # REQ-ANL-08: holds `analytics.tth_baseline` ({"days": N, "source": "..."}), # surfaced by /analytics/kpis/fetch as tth_baseline_* fields. "analytics", ) def _as_uuid(value): if value in (None, ""): return None try: return uuid.UUID(str(value)) except (TypeError, ValueError): return None def _user_id(current_user): if not current_user or not current_user.get("id"): raise HTTPException(status_code=401, detail="Not authenticated") uid = _as_uuid(current_user["id"]) if uid is None: raise HTTPException(status_code=401, detail="Invalid user id") return uid class OrgSetting: def __init__(self, session: AsyncSession): self.session = session async def get_settings(self, category=None): if category and category not in VALID_CATEGORIES: raise HTTPException( status_code=422, detail=f"category must be one of {', '.join(VALID_CATEGORIES)}" ) rows, total = await OrgSettings.fetch_settings(self.session, category=category) return [serialize_org_setting(r) for r in rows], total async def update_settings(self, payload, current_user): items = payload.get("settings") or [] if not items: raise HTTPException(status_code=400, detail="settings is required") cleaned = [] for item in items: key = (item.get("key") or "").strip() category = (item.get("category") or "").strip() if not key: raise HTTPException(status_code=422, detail="key is required") if category not in VALID_CATEGORIES: raise HTTPException( status_code=422, detail=f"category must be one of {', '.join(VALID_CATEGORIES)}" ) cleaned.append({ "setting_key": key, "setting_value": item.get("value"), "category": category, }) rows = await OrgSettings.upsert_settings(self.session, cleaned, _user_id(current_user)) return [serialize_org_setting(r) for r in rows] MAX_NAME_LEN = 200 MAX_URL_LEN = 500 def _clean_name(value, *, label="name"): name = (value or "").strip() if not name: raise HTTPException(status_code=422, detail=f"{label} is required") if len(name) > MAX_NAME_LEN: raise HTTPException(status_code=422, detail=f"{label} must be {MAX_NAME_LEN} characters or fewer") return name def _clean_linkedin_url(value): url = (value or "").strip() or None if url is None: return None if len(url) > MAX_URL_LEN: raise HTTPException(status_code=422, detail=f"linkedin_url must be {MAX_URL_LEN} characters or fewer") lowered = url.lower() if not (lowered.startswith("http://") or lowered.startswith("https://")): raise HTTPException(status_code=422, detail="linkedin_url must be an http(s) URL") return url class Exclusion: def __init__(self, session: AsyncSession): self.session = session async def get_universities(self): rows, total = await ExcludeUniversity.fetch_rows(self.session) return [serialize_exclude_university(r) for r in rows], total async def create_university(self, payload, current_user): name = _clean_name(payload.get("name"), label="name") if await ExcludeUniversity.get_by_name(self.session, name): raise HTTPException(status_code=409, detail="That university is already excluded") row = await ExcludeUniversity.insert_row(self.session, { "name": name, "created_by": _user_id(current_user), }) return serialize_exclude_university(row) async def update_university(self, record_id, payload, current_user): _user_id(current_user) fields = {} if "name" in payload: name = _clean_name(payload.get("name"), label="name") existing = await ExcludeUniversity.get_by_name(self.session, name, exclude_id=record_id) if existing: raise HTTPException(status_code=409, detail="That university is already excluded") fields["name"] = name if not fields: raise HTTPException(status_code=400, detail="No fields to update") row = await ExcludeUniversity.update_row(self.session, record_id, fields) if not row: raise HTTPException(status_code=404, detail="Excluded university not found") return serialize_exclude_university(row) async def delete_university(self, record_id, current_user): _user_id(current_user) row = await ExcludeUniversity.soft_delete_row(self.session, record_id) if not row: raise HTTPException(status_code=404, detail="Excluded university not found") return {"id": str(row.id), "deleted": True} async def get_companies(self): rows, total = await ExcludeCompany.fetch_rows(self.session) return [serialize_exclude_company(r) for r in rows], total async def create_company(self, payload, current_user): name = _clean_name(payload.get("name"), label="name") linkedin_url = _clean_linkedin_url(payload.get("linkedin_url")) if await ExcludeCompany.get_by_name(self.session, name): raise HTTPException(status_code=409, detail="That company is already excluded") row = await ExcludeCompany.insert_row(self.session, { "name": name, "linkedin_url": linkedin_url, "created_by": _user_id(current_user), }) return serialize_exclude_company(row) async def update_company(self, record_id, payload, current_user): _user_id(current_user) fields = {} if "name" in payload: name = _clean_name(payload.get("name"), label="name") existing = await ExcludeCompany.get_by_name(self.session, name, exclude_id=record_id) if existing: raise HTTPException(status_code=409, detail="That company is already excluded") fields["name"] = name if "linkedin_url" in payload: fields["linkedin_url"] = _clean_linkedin_url(payload.get("linkedin_url")) if not fields: raise HTTPException(status_code=400, detail="No fields to update") row = await ExcludeCompany.update_row(self.session, record_id, fields) if not row: raise HTTPException(status_code=404, detail="Excluded company not found") return serialize_exclude_company(row) async def delete_company(self, record_id, current_user): _user_id(current_user) row = await ExcludeCompany.soft_delete_row(self.session, record_id) if not row: raise HTTPException(status_code=404, detail="Excluded company not found") return {"id": str(row.id), "deleted": True}