171 lines
7.2 KiB
Python
171 lines
7.2 KiB
Python
# 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})")
|