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]: