""" HSEQ SaaS Multi-Tenant — Module Manager Beheer van module toggles per tenant (CompanyModule). Ondersteunt aan/uit zetten van modules, configuratie per module, en validatie tegen abonnementsniveau (plan). Bronnen: [1] Phase 2 Systeemarchitectuur — phase2_system_architecture_v1.0.md [2] Phase 2 Database Architectuur — phase2_database_architecture_v1.0.md """ from __future__ import annotations import uuid from datetime import datetime, timezone from typing import Optional from sqlalchemy.orm import Session from models_v1_0 import ( CompanyModule, PLAN_DEFAULT_MODULES, ) # --------------------------------------------------------------------------- # Module configuratie per plan # --------------------------------------------------------------------------- PLAN_MODULE_MAP: dict[str, set[str]] = { plan: set(modules) for plan, modules in PLAN_DEFAULT_MODULES.items() } def _validate_module_for_plan(module_key: str, plan: str) -> bool: """ Controleert of een module beschikbaar is voor het opgegeven abonnement. Args: module_key: Module identifier. plan: Abonnementstype (starter, professional, enterprise). Returns: True indien de module beschikbaar is voor het plan. """ allowed = PLAN_MODULE_MAP.get(plan, PLAN_MODULE_MAP["starter"]) return module_key in allowed # --------------------------------------------------------------------------- # Query operaties # --------------------------------------------------------------------------- def get_enabled_modules(session: Session, company_id: uuid.UUID) -> list[str]: """ Retourneert lijst van ingeschakelde module keys voor een tenant. Args: session: Actieve SQLAlchemy session. company_id: UUID van het bedrijf. Returns: Lijst van module_key strings (alleen ingeschakelde modules). """ modules = ( session.query(CompanyModule) .filter_by(company_id=company_id, enabled=True) .all() ) return [m.module_key for m in modules] def get_all_modules_status(session: Session, company_id: uuid.UUID) -> list[dict]: """ Retourneert status van alle modules voor een tenant (aan en uit). Args: session: Actieve SQLAlchemy session. company_id: UUID van het bedrijf. Returns: Lijst van dicts met module_key, enabled, en config. """ modules = ( session.query(CompanyModule) .filter_by(company_id=company_id) .all() ) return [ { "module_key": m.module_key, "enabled": m.enabled, "config": m.config or {}, "enabled_at": m.enabled_at.isoformat() if m.enabled_at else None, } for m in modules ] def is_module_enabled(session: Session, company_id: uuid.UUID, module_key: str) -> bool: """ Controleert of een specifieke module is ingeschakeld voor een tenant. Gebruikt door @require_module decorator voor runtime toegangscontrole [1]. Args: session: Actieve SQLAlchemy session. company_id: UUID van het bedrijf. module_key: Module identifier. Returns: True indien de module is ingeschakeld. """ module = ( session.query(CompanyModule) .filter_by(company_id=company_id, module_key=module_key) .first() ) return module is not None and module.enabled # --------------------------------------------------------------------------- # Mutatie operaties # --------------------------------------------------------------------------- def enable_module( session: Session, company_id: uuid.UUID, module_key: str, plan: str, config: Optional[dict] = None, ) -> Optional[CompanyModule]: """ Schakelt een module in voor een tenant. Valideert dat de module beschikbaar is voor het abonnement. Als de module al bestaat maar uitgeschakeld is, wordt deze ingeschakeld. Args: session: Actieve SQLAlchemy session. company_id: UUID van het bedrijf. module_key: Module identifier. plan: Huidig abonnementstype (voor validatie). config: Optionele module-specifieke configuratie. Returns: CompanyModule-object of None indien niet toegestaan voor dit plan. Raises: ValueError: Indien de module niet beschikbaar is voor het plan. """ if not _validate_module_for_plan(module_key, plan): raise ValueError( f"Module '{module_key}' is niet beschikbaar voor plan '{plan}'. " f"Upgrade naar een hoger abonnement voor toegang." ) existing = ( session.query(CompanyModule) .filter_by(company_id=company_id, module_key=module_key) .first() ) if existing is not None: existing.enabled = True existing.enabled_at = datetime.now(timezone.utc) if config is not None: existing.config = config return existing new_module = CompanyModule( company_id=company_id, module_key=module_key, enabled=True, config=config or {}, ) session.add(new_module) return new_module def disable_module( session: Session, company_id: uuid.UUID, module_key: str, ) -> Optional[CompanyModule]: """ Schakelt een module uit voor een tenant. Soft toggle — module-record blijft bestaan. Args: session: Actieve SQLAlchemy session. company_id: UUID van het bedrijf. module_key: Module identifier. Returns: CompanyModule-object of None indien niet gevonden. """ module = ( session.query(CompanyModule) .filter_by(company_id=company_id, module_key=module_key) .first() ) if module is None: return None module.enabled = False return module def update_module_config( session: Session, company_id: uuid.UUID, module_key: str, config: dict, ) -> Optional[CompanyModule]: """ Werkt de configuratie van een tenant-module bij. Gebruikt voor module-specifieke instellingen zoals branding, thresholds, of integratie-configuratie [1]. Args: session: Actieve SQLAlchemy session. company_id: UUID van het bedrijf. module_key: Module identifier. config: Nieuwe configuratie dict (merge met bestaand). Returns: CompanyModule-object of None indien niet gevonden. """ module = ( session.query(CompanyModule) .filter_by(company_id=company_id, module_key=module_key) .first() ) if module is None: return None existing_config = module.config or {} existing_config.update(config) module.config = existing_config return module def bulk_set_modules( session: Session, company_id: uuid.UUID, module_keys: list[str], plan: str, ) -> list[CompanyModule]: """ Stelt module-status in voor een tenant in bulk. Schakelt opgegeven modules in, alle andere uit. Gebruikt bij onboarding en plan-wijzigingen. Args: session: Actieve SQLAlchemy session. company_id: UUID van het bedrijf. module_keys: Lijst van in te schakelen module keys. plan: Abonnementstype (voor validatie). Returns: Lijst van CompanyModule-objecten. Raises: ValueError: Indien een module niet beschikbaar is voor het plan. """ allowed = PLAN_MODULE_MAP.get(plan, PLAN_MODULE_MAP["starter"]) # Valideer alle modules vooraf for key in module_keys: if key not in allowed: raise ValueError( f"Module '{key}' is niet beschikbaar voor plan '{plan}'." ) # Haal bestaande modules op existing_modules = ( session.query(CompanyModule) .filter_by(company_id=company_id) .all() ) existing_map = {m.module_key: m for m in existing_modules} result = [] now = datetime.now(timezone.utc) # Schakel gewenste modules in for key in module_keys: if key in existing_map: mod = existing_map[key] mod.enabled = True mod.enabled_at = now else: mod = CompanyModule( company_id=company_id, module_key=key, enabled=True, config={}, ) session.add(mod) result.append(mod) # Schakel ongewenste modules uit for key, mod in existing_map.items(): if key not in module_keys: mod.enabled = False return result