From 50bf59879fded9d7ee7b7bbad6e75f63e7892802 Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Thu, 2 Apr 2026 23:53:36 +0200
Subject: [PATCH] fix: mandate subscription provisioning, capacity errors,
invitations API
Made-with: Cursor
---
modules/interfaces/interfaceDbApp.py | 4 ++-
modules/routes/routeDataMandates.py | 6 ++++
modules/routes/routeInvitations.py | 3 +-
.../mainServiceSubscription.py | 33 ++++++++++++++++---
4 files changed, 39 insertions(+), 7 deletions(-)
diff --git a/modules/interfaces/interfaceDbApp.py b/modules/interfaces/interfaceDbApp.py
index 5e346a86..d52c23d6 100644
--- a/modules/interfaces/interfaceDbApp.py
+++ b/modules/interfaces/interfaceDbApp.py
@@ -1990,8 +1990,10 @@ class AppObjects:
cleanedRecord = dict(createdRecord)
return UserMandate(**cleanedRecord)
except Exception as e:
+ if e.__class__.__name__ == "SubscriptionCapacityException":
+ raise
logger.error(f"Error creating UserMandate: {e}")
- raise ValueError(f"Failed to create UserMandate: {e}")
+ raise ValueError(f"Failed to create UserMandate: {e}") from e
def _ensureUserBillingAccount(self, userId: str, mandateId: str) -> None:
"""
diff --git a/modules/routes/routeDataMandates.py b/modules/routes/routeDataMandates.py
index 1615a03a..cb6a3efc 100644
--- a/modules/routes/routeDataMandates.py
+++ b/modules/routes/routeDataMandates.py
@@ -31,6 +31,7 @@ from modules.datamodels.datamodelMembership import UserMandate, UserMandateRole
from modules.datamodels.datamodelRbac import Role
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata, normalize_pagination_dict
from modules.routes.routeNotifications import create_access_change_notification
+from modules.serviceCenter.services.serviceSubscription.mainServiceSubscription import SubscriptionCapacityException
# =============================================================================
@@ -795,6 +796,11 @@ def add_user_to_mandate(
except HTTPException:
raise
+ except SubscriptionCapacityException as cap:
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail=cap.message,
+ )
except Exception as e:
logger.error(f"Error adding user to mandate: {e}")
raise HTTPException(
diff --git a/modules/routes/routeInvitations.py b/modules/routes/routeInvitations.py
index 8e3be0ba..6e34eb88 100644
--- a/modules/routes/routeInvitations.py
+++ b/modules/routes/routeInvitations.py
@@ -41,11 +41,10 @@ class InvitationCreate(BaseModel):
- Mandate-level: featureInstanceId omitted, roleIds are mandate-level roles (user, viewer, admin)
- Feature-instance-level: featureInstanceId required, roleIds are instance-level roles
- Email is required for new users; targetUsername is optional.
At least one of email or targetUsername must be provided.
"""
targetUsername: Optional[str] = Field(None, description="Username of the user to invite (must match on acceptance)")
- email: Optional[str] = Field(None, description="Email address to send invitation link (required for new users)")
+ email: Optional[str] = Field(None, description="Email address to send invitation link (optional if targetUsername is set)")
featureInstanceId: Optional[str] = Field(None, description="Feature instance to grant access to (optional for mandate-level invitations)")
roleIds: List[str] = Field(..., description="Role IDs: mandate-level (user, viewer, admin) or instance-level")
frontendUrl: str = Field(..., description="Frontend URL for building the invite link (provided by frontend)")
diff --git a/modules/serviceCenter/services/serviceSubscription/mainServiceSubscription.py b/modules/serviceCenter/services/serviceSubscription/mainServiceSubscription.py
index 9535a2da..89e20112 100644
--- a/modules/serviceCenter/services/serviceSubscription/mainServiceSubscription.py
+++ b/modules/serviceCenter/services/serviceSubscription/mainServiceSubscription.py
@@ -786,15 +786,40 @@ class SubscriptionInactiveException(Exception):
return out
+_SUBSCRIPTION_LIMITS_UI_HINT_DE = (
+ " Details zu Ihrem Abonnement, den enthaltenen Limits und Upgrade-Optionen: "
+ "Menü «Administration» → «Billing» → Registerkarte «Abonnement»."
+)
+
+
class SubscriptionCapacityException(Exception):
def __init__(self, resourceType: str, currentCount: int, maxAllowed: int, message: Optional[str] = None):
self.resourceType = resourceType
self.currentCount = currentCount
self.maxAllowed = maxAllowed
- self.message = message or (
- f"Ihr Plan erlaubt maximal {maxAllowed} {'Benutzer' if resourceType == 'users' else 'Feature-Instanzen'} "
- f"(aktuell {currentCount}). Bitte wechseln Sie zu einem grösseren Plan."
- )
+ if message is not None:
+ self.message = message
+ elif resourceType == "users":
+ self.message = (
+ f"Mit dem aktuellen Abonnement sind für diesen Mandanten höchstens {maxAllowed} "
+ f"Benutzer zulässig (derzeit {currentCount}). "
+ f"Ohne Planwechsel können keine weiteren Benutzer hinzugefügt werden."
+ ) + _SUBSCRIPTION_LIMITS_UI_HINT_DE
+ elif resourceType == "featureInstances":
+ self.message = (
+ f"Es sind höchstens {maxAllowed} aktive Feature-Instanzen erlaubt (derzeit {currentCount}). "
+ f"Bitte Abonnement erweitern oder eine Instanz entfernen."
+ ) + _SUBSCRIPTION_LIMITS_UI_HINT_DE
+ elif resourceType == "dataVolumeMB":
+ self.message = (
+ f"Das im Abonnement enthaltene Datenvolumen ({maxAllowed} MB) reicht nicht "
+ f"(aktuell ca. {currentCount} MB). Bitte Speicher-Limit oder Plan anpassen."
+ ) + _SUBSCRIPTION_LIMITS_UI_HINT_DE
+ else:
+ self.message = (
+ f"Abonnement-Limit überschritten (Ressource «{resourceType}»: "
+ f"aktuell {currentCount}, erlaubt {maxAllowed})."
+ ) + _SUBSCRIPTION_LIMITS_UI_HINT_DE
super().__init__(self.message)
def toClientDict(self) -> Dict[str, Any]: