platform-core/modules/datamodels/serviceExceptions.py
ValueOn AG cf0233f193
Some checks failed
Deploy Plattform-Core (Int) / test (push) Failing after 13s
Deploy Plattform-Core (Int) / deploy (push) Has been skipped
refactor: architecture cleanup + fix scheduler Automation2Workflow error
Fix: add missing Automation2Workflow/Automation2WorkflowRun imports to interfaceFeatureGraphicalEditor.py (caused scheduler crash on boot)
Refactor: gdprDeletion via onUserDelete lifecycle hooks
Refactor: i18nBootSync accounting labels via app.py parameter injection
Refactor: serviceHub moved to serviceCenter/serviceHub.py
Split: teamsbot/service.py, realEstate/main, routeTrustee, routeBilling
Cleanup: remove obsolete methodTrustee, serviceExceptions shim
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-07 07:59:31 +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)