fix: mandate subscription provisioning, capacity errors, invitations API

Made-with: Cursor
This commit is contained in:
ValueOn AG 2026-04-02 23:53:36 +02:00
parent ecbdd1ea74
commit 50bf59879f
4 changed files with 39 additions and 7 deletions

View file

@ -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:
"""

View file

@ -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(

View file

@ -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)")

View file

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