# Copyright (c) 2026 PowerOn AG # All rights reserved. """ Shared service exception classes. Centralises the three cross-layer exception types so that both the serviceCenter layer and the workflows/interfaces layers can import them from one place without creating circular dependencies. """ from typing import Dict, Any, Optional from modules.shared.i18nRegistry import t from modules.datamodels.datamodelSubscription import SubscriptionStatusEnum # ============================================================================ # Subscription action / reason constants # ============================================================================ SUBSCRIPTION_USER_ACTION_UPGRADE = "UPGRADE_SUBSCRIPTION" SUBSCRIPTION_USER_ACTION_REACTIVATE = "REACTIVATE_SUBSCRIPTION" SUBSCRIPTION_USER_ACTION_ADD_PAYMENT = "ADD_PAYMENT_METHOD" SUBSCRIPTION_USER_ACTION_CONTACT_ADMIN = "CONTACT_ADMIN" SUBSCRIPTION_REASONS = { "SUBSCRIPTION_INACTIVE", "SUBSCRIPTION_PAYMENT_REQUIRED", "SUBSCRIPTION_PAYMENT_PENDING", "SUBSCRIPTION_EXPIRED", } # ============================================================================ # Subscription helper functions # ============================================================================ def _subscriptionReasonForStatus(status: SubscriptionStatusEnum) -> str: if status == SubscriptionStatusEnum.PENDING: return "SUBSCRIPTION_PAYMENT_PENDING" if status == SubscriptionStatusEnum.PAST_DUE: return "SUBSCRIPTION_PAYMENT_REQUIRED" if status == SubscriptionStatusEnum.EXPIRED: return "SUBSCRIPTION_EXPIRED" return "SUBSCRIPTION_INACTIVE" def _subscriptionUserActionForStatus(status: SubscriptionStatusEnum) -> str: if status in (SubscriptionStatusEnum.PAST_DUE, SubscriptionStatusEnum.PENDING): return SUBSCRIPTION_USER_ACTION_ADD_PAYMENT return SUBSCRIPTION_USER_ACTION_UPGRADE def _subscriptionLimitsHint() -> str: return " " + t( "Details zu Ihrem Abonnement, den enthaltenen Limits und Upgrade-Optionen: " "Menü «Administration» → «Billing» → Registerkarte «Abonnement»." ) def _enterpriseLimitsHint() -> str: return " " + t( "Ihr Enterprise-Abonnement wird vom Plattform-Administrator verwaltet. " "Bitte kontaktieren Sie den Administrator für eine Anpassung der Limiten." ) # ============================================================================ # Exception classes # ============================================================================ class SubscriptionInactiveException(Exception): def __init__(self, status: SubscriptionStatusEnum, mandateId: str = "", message: Optional[str] = None): self.status = status self.mandateId = mandateId self.reason = _subscriptionReasonForStatus(status) self.userAction = _subscriptionUserActionForStatus(status) self.message = message or t( "Kein aktives Abonnement für diesen Mandanten. Bitte wählen Sie einen Plan unter Billing." ) super().__init__(self.message) def toClientDict(self) -> Dict[str, Any]: out: Dict[str, Any] = { "error": self.reason, "message": self.message, "userAction": self.userAction, "subscriptionUiPath": "/admin/billing?tab=subscription", } if self.mandateId: out["mandateId"] = self.mandateId return out class SubscriptionCapacityException(Exception): def __init__(self, resourceType: str, currentCount: int, maxAllowed: int, message: Optional[str] = None, isEnterprise: bool = False): self.resourceType = resourceType self.currentCount = currentCount self.maxAllowed = maxAllowed self.isEnterprise = isEnterprise hint = _enterpriseLimitsHint() if isEnterprise else _subscriptionLimitsHint() if message is not None: self.message = message elif resourceType == "users": self.message = t( "Mit dem aktuellen Abonnement sind für diesen Mandanten höchstens {maxAllowed} " "Benutzer zulässig (derzeit {currentCount}). " "Ohne Planwechsel können keine weiteren Benutzer hinzugefügt werden." ).format(maxAllowed=maxAllowed, currentCount=currentCount) + hint elif resourceType == "featureInstances": self.message = t( "Es sind höchstens {maxAllowed} aktive Module erlaubt (derzeit {currentCount}). " "Bitte Abonnement erweitern oder ein Modul entfernen." ).format(maxAllowed=maxAllowed, currentCount=currentCount) + hint elif resourceType == "dataVolumeMB": self.message = t( "Das im Abonnement enthaltene Datenvolumen ({maxAllowed} MB) reicht nicht " "(aktuell ca. {currentCount} MB). Bitte Speicher-Limit oder Plan anpassen." ).format(maxAllowed=maxAllowed, currentCount=currentCount) + hint else: self.message = t( "Abonnement-Limit überschritten (Ressource «{resourceType}»: " "aktuell {currentCount}, erlaubt {maxAllowed})." ).format(resourceType=resourceType, currentCount=currentCount, maxAllowed=maxAllowed) + hint super().__init__(self.message) def toClientDict(self) -> Dict[str, Any]: action = SUBSCRIPTION_USER_ACTION_CONTACT_ADMIN if self.isEnterprise else SUBSCRIPTION_USER_ACTION_UPGRADE return { "error": f"SUBSCRIPTION_{self.resourceType.upper()}_LIMIT", "currentCount": self.currentCount, "maxAllowed": self.maxAllowed, "message": self.message, "userAction": action, "subscriptionUiPath": "/admin/billing?tab=subscription", } class BillingContextError(Exception): """Raised when billing context is incomplete (missing mandateId, user, etc.). This is a FAIL-SAFE error: AI calls MUST NOT proceed without valid billing context. Acts like a 0 CHF credit card pre-authorization check - validates that billing CAN be recorded before any expensive AI operation starts. """ def __init__(self, message: str = None): self.message = message or "Billing context incomplete - AI call blocked" super().__init__(self.message) # ============================================================================ # Workflow execution pause exceptions # (Canonical location — formerly in automation2/executors/inputExecutor.py) # ============================================================================ class PauseForHumanTaskError(Exception): """Raised when execution must pause for a human task. Contains runId, taskId.""" def __init__(self, runId: str, taskId: str, nodeId: str): self.runId = runId self.taskId = taskId self.nodeId = nodeId super().__init__(f"Pause for human task {taskId} (run {runId}, node {nodeId})") class PauseForEmailWaitError(Exception): """Raised when execution must pause waiting for a new email. Background poller will resume.""" def __init__(self, runId: str, nodeId: str, waitConfig: Dict[str, Any]): self.runId = runId self.nodeId = nodeId self.waitConfig = waitConfig super().__init__(f"Pause for email wait (run {runId}, node {nodeId})")