platform-core/modules/shared/serviceExceptions.py
ValueOn AG bc7c6fe27c
Some checks failed
Deploy Plattform-Core (Int) / test (push) Failing after 13s
Deploy Plattform-Core (Int) / deploy (push) Has been skipped
elimination of technical issues (imports)
2026-06-06 00:32:45 +02:00

146 lines
6.2 KiB
Python

# Copyright (c) 2025 Patrick Motsch
# 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)