geek-cafe-saas-sdk 0.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of geek-cafe-saas-sdk might be problematic. Click here for more details.

Files changed (194) hide show
  1. geek_cafe_saas_sdk/__init__.py +9 -0
  2. geek_cafe_saas_sdk/core/__init__.py +11 -0
  3. geek_cafe_saas_sdk/core/audit_mixin.py +33 -0
  4. geek_cafe_saas_sdk/core/error_codes.py +132 -0
  5. geek_cafe_saas_sdk/core/service_errors.py +19 -0
  6. geek_cafe_saas_sdk/core/service_result.py +121 -0
  7. geek_cafe_saas_sdk/decorators/__init__.py +64 -0
  8. geek_cafe_saas_sdk/decorators/auth.py +373 -0
  9. geek_cafe_saas_sdk/decorators/core.py +358 -0
  10. geek_cafe_saas_sdk/domains/__init__.py +0 -0
  11. geek_cafe_saas_sdk/domains/analytics/__init__.py +0 -0
  12. geek_cafe_saas_sdk/domains/analytics/handlers/__init__.py +0 -0
  13. geek_cafe_saas_sdk/domains/analytics/models/__init__.py +9 -0
  14. geek_cafe_saas_sdk/domains/analytics/models/website_analytics.py +219 -0
  15. geek_cafe_saas_sdk/domains/analytics/models/website_analytics_summary.py +220 -0
  16. geek_cafe_saas_sdk/domains/analytics/services/__init__.py +11 -0
  17. geek_cafe_saas_sdk/domains/analytics/services/website_analytics_service.py +232 -0
  18. geek_cafe_saas_sdk/domains/analytics/services/website_analytics_summary_service.py +212 -0
  19. geek_cafe_saas_sdk/domains/analytics/services/website_analytics_tally_service.py +610 -0
  20. geek_cafe_saas_sdk/domains/auth/__init__.py +0 -0
  21. geek_cafe_saas_sdk/domains/auth/handlers/__init__.py +0 -0
  22. geek_cafe_saas_sdk/domains/auth/handlers/users/create/app.py +41 -0
  23. geek_cafe_saas_sdk/domains/auth/handlers/users/delete/app.py +41 -0
  24. geek_cafe_saas_sdk/domains/auth/handlers/users/get/app.py +39 -0
  25. geek_cafe_saas_sdk/domains/auth/handlers/users/list/app.py +36 -0
  26. geek_cafe_saas_sdk/domains/auth/handlers/users/update/app.py +44 -0
  27. geek_cafe_saas_sdk/domains/auth/models/__init__.py +13 -0
  28. geek_cafe_saas_sdk/domains/auth/models/permission.py +134 -0
  29. geek_cafe_saas_sdk/domains/auth/models/resource_permission.py +245 -0
  30. geek_cafe_saas_sdk/domains/auth/models/role.py +213 -0
  31. geek_cafe_saas_sdk/domains/auth/models/user.py +285 -0
  32. geek_cafe_saas_sdk/domains/auth/services/__init__.py +16 -0
  33. geek_cafe_saas_sdk/domains/auth/services/authorization_service.py +376 -0
  34. geek_cafe_saas_sdk/domains/auth/services/permission_registry.py +464 -0
  35. geek_cafe_saas_sdk/domains/auth/services/resource_permission_service.py +408 -0
  36. geek_cafe_saas_sdk/domains/auth/services/user_service.py +274 -0
  37. geek_cafe_saas_sdk/domains/communities/__init__.py +0 -0
  38. geek_cafe_saas_sdk/domains/communities/handlers/__init__.py +0 -0
  39. geek_cafe_saas_sdk/domains/communities/handlers/communities/create/app.py +41 -0
  40. geek_cafe_saas_sdk/domains/communities/handlers/communities/delete/app.py +41 -0
  41. geek_cafe_saas_sdk/domains/communities/handlers/communities/get/app.py +39 -0
  42. geek_cafe_saas_sdk/domains/communities/handlers/communities/list/app.py +36 -0
  43. geek_cafe_saas_sdk/domains/communities/handlers/communities/update/app.py +44 -0
  44. geek_cafe_saas_sdk/domains/communities/models/__init__.py +6 -0
  45. geek_cafe_saas_sdk/domains/communities/models/community.py +326 -0
  46. geek_cafe_saas_sdk/domains/communities/models/community_member.py +227 -0
  47. geek_cafe_saas_sdk/domains/communities/services/__init__.py +6 -0
  48. geek_cafe_saas_sdk/domains/communities/services/community_member_service.py +412 -0
  49. geek_cafe_saas_sdk/domains/communities/services/community_service.py +479 -0
  50. geek_cafe_saas_sdk/domains/events/__init__.py +0 -0
  51. geek_cafe_saas_sdk/domains/events/handlers/__init__.py +0 -0
  52. geek_cafe_saas_sdk/domains/events/handlers/attendees/app.py +67 -0
  53. geek_cafe_saas_sdk/domains/events/handlers/cancel/app.py +66 -0
  54. geek_cafe_saas_sdk/domains/events/handlers/check_in/app.py +60 -0
  55. geek_cafe_saas_sdk/domains/events/handlers/create/app.py +93 -0
  56. geek_cafe_saas_sdk/domains/events/handlers/delete/app.py +42 -0
  57. geek_cafe_saas_sdk/domains/events/handlers/get/app.py +39 -0
  58. geek_cafe_saas_sdk/domains/events/handlers/invite/app.py +98 -0
  59. geek_cafe_saas_sdk/domains/events/handlers/list/app.py +125 -0
  60. geek_cafe_saas_sdk/domains/events/handlers/publish/app.py +49 -0
  61. geek_cafe_saas_sdk/domains/events/handlers/rsvp/app.py +83 -0
  62. geek_cafe_saas_sdk/domains/events/handlers/update/app.py +44 -0
  63. geek_cafe_saas_sdk/domains/events/models/__init__.py +3 -0
  64. geek_cafe_saas_sdk/domains/events/models/event.py +681 -0
  65. geek_cafe_saas_sdk/domains/events/models/event_attendee.py +324 -0
  66. geek_cafe_saas_sdk/domains/events/services/__init__.py +9 -0
  67. geek_cafe_saas_sdk/domains/events/services/event_attendee_service.py +571 -0
  68. geek_cafe_saas_sdk/domains/events/services/event_service.py +684 -0
  69. geek_cafe_saas_sdk/domains/files/__init__.py +0 -0
  70. geek_cafe_saas_sdk/domains/files/models/__init__.py +0 -0
  71. geek_cafe_saas_sdk/domains/files/models/directory.py +258 -0
  72. geek_cafe_saas_sdk/domains/files/models/file.py +312 -0
  73. geek_cafe_saas_sdk/domains/files/models/file_share.py +268 -0
  74. geek_cafe_saas_sdk/domains/files/models/file_version.py +216 -0
  75. geek_cafe_saas_sdk/domains/files/services/__init__.py +0 -0
  76. geek_cafe_saas_sdk/domains/files/services/directory_service.py +701 -0
  77. geek_cafe_saas_sdk/domains/files/services/file_share_service.py +663 -0
  78. geek_cafe_saas_sdk/domains/files/services/file_system_service.py +575 -0
  79. geek_cafe_saas_sdk/domains/files/services/file_version_service.py +739 -0
  80. geek_cafe_saas_sdk/domains/files/services/s3_file_service.py +501 -0
  81. geek_cafe_saas_sdk/domains/messaging/__init__.py +0 -0
  82. geek_cafe_saas_sdk/domains/messaging/handlers/__init__.py +0 -0
  83. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/create/app.py +86 -0
  84. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/delete/app.py +65 -0
  85. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/get/app.py +64 -0
  86. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/list/app.py +97 -0
  87. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/update/app.py +149 -0
  88. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/create/app.py +67 -0
  89. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/delete/app.py +65 -0
  90. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/get/app.py +64 -0
  91. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/list/app.py +102 -0
  92. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/update/app.py +127 -0
  93. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/create/app.py +94 -0
  94. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/delete/app.py +66 -0
  95. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/get/app.py +67 -0
  96. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/list/app.py +95 -0
  97. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/update/app.py +156 -0
  98. geek_cafe_saas_sdk/domains/messaging/models/__init__.py +13 -0
  99. geek_cafe_saas_sdk/domains/messaging/models/chat_channel.py +337 -0
  100. geek_cafe_saas_sdk/domains/messaging/models/chat_channel_member.py +180 -0
  101. geek_cafe_saas_sdk/domains/messaging/models/chat_message.py +426 -0
  102. geek_cafe_saas_sdk/domains/messaging/models/contact_thread.py +392 -0
  103. geek_cafe_saas_sdk/domains/messaging/services/__init__.py +11 -0
  104. geek_cafe_saas_sdk/domains/messaging/services/chat_channel_service.py +700 -0
  105. geek_cafe_saas_sdk/domains/messaging/services/chat_message_service.py +491 -0
  106. geek_cafe_saas_sdk/domains/messaging/services/contact_thread_service.py +497 -0
  107. geek_cafe_saas_sdk/domains/tenancy/__init__.py +0 -0
  108. geek_cafe_saas_sdk/domains/tenancy/handlers/__init__.py +0 -0
  109. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/activate/app.py +52 -0
  110. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/active/app.py +37 -0
  111. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/cancel/app.py +55 -0
  112. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/get/app.py +39 -0
  113. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/list/app.py +44 -0
  114. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/record_payment/app.py +56 -0
  115. geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/get/app.py +39 -0
  116. geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/me/app.py +37 -0
  117. geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/signup/app.py +61 -0
  118. geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/update/app.py +44 -0
  119. geek_cafe_saas_sdk/domains/tenancy/models/__init__.py +6 -0
  120. geek_cafe_saas_sdk/domains/tenancy/models/subscription.py +440 -0
  121. geek_cafe_saas_sdk/domains/tenancy/models/tenant.py +258 -0
  122. geek_cafe_saas_sdk/domains/tenancy/services/__init__.py +6 -0
  123. geek_cafe_saas_sdk/domains/tenancy/services/subscription_service.py +557 -0
  124. geek_cafe_saas_sdk/domains/tenancy/services/tenant_service.py +575 -0
  125. geek_cafe_saas_sdk/domains/voting/__init__.py +0 -0
  126. geek_cafe_saas_sdk/domains/voting/handlers/__init__.py +0 -0
  127. geek_cafe_saas_sdk/domains/voting/handlers/votes/create/app.py +128 -0
  128. geek_cafe_saas_sdk/domains/voting/handlers/votes/delete/app.py +41 -0
  129. geek_cafe_saas_sdk/domains/voting/handlers/votes/get/app.py +39 -0
  130. geek_cafe_saas_sdk/domains/voting/handlers/votes/list/app.py +38 -0
  131. geek_cafe_saas_sdk/domains/voting/handlers/votes/summerize/README.md +3 -0
  132. geek_cafe_saas_sdk/domains/voting/handlers/votes/update/app.py +44 -0
  133. geek_cafe_saas_sdk/domains/voting/models/__init__.py +9 -0
  134. geek_cafe_saas_sdk/domains/voting/models/vote.py +231 -0
  135. geek_cafe_saas_sdk/domains/voting/models/vote_summary.py +193 -0
  136. geek_cafe_saas_sdk/domains/voting/services/__init__.py +11 -0
  137. geek_cafe_saas_sdk/domains/voting/services/vote_service.py +264 -0
  138. geek_cafe_saas_sdk/domains/voting/services/vote_summary_service.py +198 -0
  139. geek_cafe_saas_sdk/domains/voting/services/vote_tally_service.py +533 -0
  140. geek_cafe_saas_sdk/lambda_handlers/README.md +404 -0
  141. geek_cafe_saas_sdk/lambda_handlers/__init__.py +67 -0
  142. geek_cafe_saas_sdk/lambda_handlers/_base/__init__.py +25 -0
  143. geek_cafe_saas_sdk/lambda_handlers/_base/api_key_handler.py +129 -0
  144. geek_cafe_saas_sdk/lambda_handlers/_base/authorized_secure_handler.py +218 -0
  145. geek_cafe_saas_sdk/lambda_handlers/_base/base_handler.py +185 -0
  146. geek_cafe_saas_sdk/lambda_handlers/_base/handler_factory.py +256 -0
  147. geek_cafe_saas_sdk/lambda_handlers/_base/public_handler.py +53 -0
  148. geek_cafe_saas_sdk/lambda_handlers/_base/secure_handler.py +89 -0
  149. geek_cafe_saas_sdk/lambda_handlers/_base/service_pool.py +94 -0
  150. geek_cafe_saas_sdk/lambda_handlers/directories/create/app.py +79 -0
  151. geek_cafe_saas_sdk/lambda_handlers/directories/delete/app.py +76 -0
  152. geek_cafe_saas_sdk/lambda_handlers/directories/get/app.py +74 -0
  153. geek_cafe_saas_sdk/lambda_handlers/directories/list/app.py +75 -0
  154. geek_cafe_saas_sdk/lambda_handlers/directories/move/app.py +79 -0
  155. geek_cafe_saas_sdk/lambda_handlers/files/delete/app.py +121 -0
  156. geek_cafe_saas_sdk/lambda_handlers/files/download/app.py +187 -0
  157. geek_cafe_saas_sdk/lambda_handlers/files/get/app.py +127 -0
  158. geek_cafe_saas_sdk/lambda_handlers/files/list/app.py +108 -0
  159. geek_cafe_saas_sdk/lambda_handlers/files/share/app.py +83 -0
  160. geek_cafe_saas_sdk/lambda_handlers/files/shares/list/app.py +84 -0
  161. geek_cafe_saas_sdk/lambda_handlers/files/shares/revoke/app.py +76 -0
  162. geek_cafe_saas_sdk/lambda_handlers/files/update/app.py +143 -0
  163. geek_cafe_saas_sdk/lambda_handlers/files/upload/app.py +151 -0
  164. geek_cafe_saas_sdk/middleware/__init__.py +36 -0
  165. geek_cafe_saas_sdk/middleware/auth.py +85 -0
  166. geek_cafe_saas_sdk/middleware/authorization.py +523 -0
  167. geek_cafe_saas_sdk/middleware/cors.py +63 -0
  168. geek_cafe_saas_sdk/middleware/error_handling.py +114 -0
  169. geek_cafe_saas_sdk/middleware/validation.py +80 -0
  170. geek_cafe_saas_sdk/models/__init__.py +20 -0
  171. geek_cafe_saas_sdk/models/base_model.py +233 -0
  172. geek_cafe_saas_sdk/services/__init__.py +18 -0
  173. geek_cafe_saas_sdk/services/database_service.py +441 -0
  174. geek_cafe_saas_sdk/utilities/__init__.py +88 -0
  175. geek_cafe_saas_sdk/utilities/cognito_utility.py +568 -0
  176. geek_cafe_saas_sdk/utilities/custom_exceptions.py +183 -0
  177. geek_cafe_saas_sdk/utilities/datetime_utility.py +410 -0
  178. geek_cafe_saas_sdk/utilities/dictionary_utility.py +78 -0
  179. geek_cafe_saas_sdk/utilities/dynamodb_utils.py +151 -0
  180. geek_cafe_saas_sdk/utilities/environment_loader.py +149 -0
  181. geek_cafe_saas_sdk/utilities/environment_variables.py +228 -0
  182. geek_cafe_saas_sdk/utilities/http_body_parameters.py +44 -0
  183. geek_cafe_saas_sdk/utilities/http_path_parameters.py +60 -0
  184. geek_cafe_saas_sdk/utilities/http_status_code.py +63 -0
  185. geek_cafe_saas_sdk/utilities/jwt_utility.py +234 -0
  186. geek_cafe_saas_sdk/utilities/lambda_event_utility.py +776 -0
  187. geek_cafe_saas_sdk/utilities/logging_utility.py +64 -0
  188. geek_cafe_saas_sdk/utilities/message_query_helper.py +340 -0
  189. geek_cafe_saas_sdk/utilities/response.py +209 -0
  190. geek_cafe_saas_sdk/utilities/string_functions.py +180 -0
  191. geek_cafe_saas_sdk-0.6.0.dist-info/METADATA +397 -0
  192. geek_cafe_saas_sdk-0.6.0.dist-info/RECORD +194 -0
  193. geek_cafe_saas_sdk-0.6.0.dist-info/WHEEL +4 -0
  194. geek_cafe_saas_sdk-0.6.0.dist-info/licenses/LICENSE +47 -0
@@ -0,0 +1,575 @@
1
+ """
2
+ Geek Cafe, LLC
3
+ MIT License. See Project Root for the license information.
4
+
5
+ TenantService for managing tenant organizations.
6
+ """
7
+
8
+ from typing import Dict, Any, Optional, List
9
+ from boto3_assist.dynamodb.dynamodb import DynamoDB
10
+ from geek_cafe_saas_sdk.services.database_service import DatabaseService
11
+ from geek_cafe_saas_sdk.domains.auth.services import UserService
12
+ from geek_cafe_saas_sdk.core.service_result import ServiceResult
13
+ from geek_cafe_saas_sdk.core.service_errors import ValidationError, NotFoundError, AccessDeniedError
14
+ from geek_cafe_saas_sdk.domains.tenancy.models import Tenant
15
+ from geek_cafe_saas_sdk.domains.auth.models import User
16
+ from geek_cafe_saas_sdk.domains.tenancy.models import Subscription
17
+ from geek_cafe_saas_sdk.utilities.cognito_utility import CognitoUtility
18
+ from geek_cafe_saas_sdk.utilities.environment_variables import EnvironmentVariables
19
+ import datetime as dt
20
+
21
+
22
+ class TenantService(DatabaseService[Tenant]):
23
+ """
24
+ Service for tenant management operations.
25
+
26
+ Handles CRUD operations for tenants, including creating tenants with
27
+ primary users, managing tenant status, and coordinating with subscriptions.
28
+ """
29
+
30
+ def __init__(self, *, dynamodb: DynamoDB = None, table_name: str = None,
31
+ user_service: UserService = None, cognito_utility: CognitoUtility = None,
32
+ user_pool_id: str = None, enable_cognito: bool = True):
33
+ super().__init__(dynamodb=dynamodb, table_name=table_name)
34
+ # User service for creating primary users
35
+ self.user_service = user_service or UserService(
36
+ dynamodb=dynamodb, table_name=table_name
37
+ )
38
+ # Cognito integration (optional for testing)
39
+ self.cognito_utility = cognito_utility
40
+ self.user_pool_id = user_pool_id or EnvironmentVariables.get_cognito_user_pool()
41
+ self.enable_cognito = enable_cognito
42
+
43
+ def create(self, tenant_id: str, user_id: str, payload: Dict[str, Any]) -> ServiceResult[Tenant]:
44
+ """
45
+ Create a new tenant.
46
+
47
+ Args:
48
+ tenant_id: Tenant ID (for multi-tenant context, can be same as new tenant)
49
+ user_id: User ID creating the tenant
50
+ payload: Tenant data (name, status, plan_tier, etc.)
51
+
52
+ Returns:
53
+ ServiceResult with Tenant
54
+ """
55
+ try:
56
+ # Validate required fields
57
+ required_fields = ['name']
58
+ self._validate_required_fields(payload, required_fields)
59
+
60
+ # Create tenant instance
61
+ tenant = Tenant().map(payload)
62
+ tenant.tenant_id = tenant.id or tenant_id # Self-referential
63
+ tenant.user_id = user_id
64
+ tenant.created_by_id = user_id
65
+
66
+ # Set defaults
67
+ if not tenant.status:
68
+ tenant.status = "active"
69
+ if not tenant.plan_tier:
70
+ tenant.plan_tier = "free"
71
+
72
+ # Prepare for save
73
+ tenant.prep_for_save()
74
+
75
+ # Save to database
76
+ return self._save_model(tenant)
77
+
78
+ except Exception as e:
79
+ return self._handle_service_exception(e, 'create_tenant',
80
+ tenant_id=tenant_id, user_id=user_id)
81
+
82
+ def create_with_user(self, user_payload: Dict[str, Any],
83
+ tenant_payload: Optional[Dict[str, Any]] = None,
84
+ temp_password: Optional[str] = None,
85
+ send_invitation: bool = False) -> ServiceResult[Dict[str, Any]]:
86
+ """
87
+ Create tenant with primary user atomically (signup flow).
88
+
89
+ This is the main signup flow - creates a new tenant and primary admin user
90
+ together. The user becomes the tenant's primary contact.
91
+
92
+ If Cognito is enabled, also creates the Cognito user with custom attributes.
93
+
94
+ Args:
95
+ user_payload: User data (email, first_name, last_name, etc.)
96
+ tenant_payload: Optional tenant data (name, etc.). If not provided,
97
+ tenant name is derived from user info.
98
+ temp_password: Optional temporary password for Cognito user
99
+ send_invitation: If True, sends Cognito invitation email
100
+
101
+ Returns:
102
+ ServiceResult with dict containing:
103
+ {
104
+ "tenant": Tenant,
105
+ "user": User,
106
+ "cognito_user": dict (if Cognito enabled),
107
+ "temp_password": str (if auto-generated and Cognito enabled)
108
+ }
109
+ """
110
+ try:
111
+ # Validate user fields
112
+ required_user_fields = ['email', 'first_name', 'last_name']
113
+ self._validate_required_fields(user_payload, required_user_fields)
114
+
115
+ # Prepare tenant data
116
+ if tenant_payload is None:
117
+ tenant_payload = {}
118
+
119
+ # Generate tenant name from user if not provided
120
+ if 'name' not in tenant_payload:
121
+ first_name = user_payload.get('first_name', 'User')
122
+ last_name = user_payload.get('last_name', 'Organization')
123
+ tenant_payload['name'] = f"{first_name} {last_name}'s Organization"
124
+
125
+ # Create tenant first
126
+ tenant = Tenant().map(tenant_payload)
127
+ tenant.prep_for_save()
128
+
129
+ # Set self-referential tenant_id
130
+ tenant.tenant_id = tenant.id
131
+
132
+ # Set defaults
133
+ if not tenant.status:
134
+ tenant.status = "active"
135
+ if not tenant.plan_tier:
136
+ tenant.plan_tier = "free"
137
+
138
+ # Create default features for free tier
139
+ if not tenant.features:
140
+ tenant.features = {
141
+ "chat": True,
142
+ "events": True,
143
+ "groups": True,
144
+ "analytics": False,
145
+ "api_access": False
146
+ }
147
+
148
+ # Create primary user
149
+ user = User().map(user_payload)
150
+ user.tenant_id = tenant.id
151
+ user.prep_for_save()
152
+
153
+ # Set user as creator of themselves
154
+ user.user_id = user.id
155
+ user.created_by_id = user.id
156
+
157
+ # Grant tenant admin role
158
+ if 'tenant_admin' not in user.roles:
159
+ user.roles = ['tenant_admin']
160
+
161
+ # Set user status as active (they're signing up, not invited)
162
+ user.status = "active"
163
+ user.activated_utc_ts = dt.datetime.now(dt.UTC).timestamp()
164
+
165
+ # Link tenant to primary user
166
+ tenant.primary_contact_user_id = user.id
167
+ tenant.created_by_id = user.id
168
+ tenant.user_id = user.id
169
+
170
+ # Save both (in future, could use TransactWrite for atomicity)
171
+ tenant_result = self._save_model(tenant)
172
+ if not tenant_result.success:
173
+ return ServiceResult(
174
+ success=False,
175
+ message=f"Failed to create tenant: {tenant_result.message}",
176
+ error_code="TENANT_CREATION_FAILED"
177
+ )
178
+
179
+ # Save user
180
+ user_result = self.user_service._save_model(user)
181
+ if not user_result.success:
182
+ # TODO: Rollback tenant creation (or use TransactWrite)
183
+ return ServiceResult(
184
+ success=False,
185
+ message=f"Failed to create user: {user_result.message}",
186
+ error_code="USER_CREATION_FAILED"
187
+ )
188
+
189
+ # Create Cognito user if enabled
190
+ cognito_response = None
191
+ returned_temp_password = None
192
+
193
+ if self.enable_cognito and self.user_pool_id:
194
+ try:
195
+ # Initialize Cognito utility if not provided
196
+ cognito = self.cognito_utility or CognitoUtility()
197
+
198
+ # Store password for return (before it's used)
199
+ if not send_invitation:
200
+ returned_temp_password = temp_password
201
+
202
+ # Create Cognito user with custom attributes
203
+ cognito_response = cognito.admin_create_user(
204
+ user_pool_id=self.user_pool_id,
205
+ temp_password=temp_password,
206
+ user=user,
207
+ send_invitation=send_invitation
208
+ )
209
+
210
+ # Extract cognito username (sub) from response
211
+ if cognito_response and 'User' in cognito_response:
212
+ cognito_user = cognito_response['User']
213
+ user.cognito_user_name = cognito_user.get('Username')
214
+
215
+ # Update user in DynamoDB with Cognito username
216
+ self.user_service._save_model(user)
217
+
218
+ except Exception as cognito_error:
219
+ # Cognito creation failed - should we rollback?
220
+ # For now, log error and return partial success
221
+ return ServiceResult(
222
+ success=False,
223
+ message=f"User created in DynamoDB but Cognito creation failed: {str(cognito_error)}",
224
+ error_code="COGNITO_CREATION_FAILED",
225
+ data={
226
+ "tenant": tenant,
227
+ "user": user,
228
+ "cognito_error": str(cognito_error)
229
+ }
230
+ )
231
+
232
+ # Return successful result
233
+ result_data = {
234
+ "tenant": tenant,
235
+ "user": user
236
+ }
237
+
238
+ if cognito_response:
239
+ result_data["cognito_user"] = cognito_response
240
+
241
+ if returned_temp_password:
242
+ result_data["temp_password"] = returned_temp_password
243
+
244
+ return ServiceResult(
245
+ success=True,
246
+ data=result_data
247
+ )
248
+
249
+ except Exception as e:
250
+ return self._handle_service_exception(e, 'create_tenant_with_user')
251
+
252
+ def get_by_id(self, resource_id: str, tenant_id: str, user_id: str) -> ServiceResult[Tenant]:
253
+ """
254
+ Get tenant by ID with access control.
255
+
256
+ Args:
257
+ resource_id: Tenant ID to retrieve
258
+ tenant_id: Requesting tenant ID (for access control)
259
+ user_id: Requesting user ID (for access control)
260
+
261
+ Returns:
262
+ ServiceResult with Tenant
263
+ """
264
+ try:
265
+ tenant = self._get_model_by_id(resource_id, Tenant)
266
+
267
+ if not tenant:
268
+ raise NotFoundError(f"Tenant with ID {resource_id} not found")
269
+
270
+ # Check if deleted
271
+ if tenant.is_deleted():
272
+ raise NotFoundError(f"Tenant with ID {resource_id} not found")
273
+
274
+ # Validate tenant access (only same tenant can view, or admin)
275
+ # For now, allow if requesting tenant matches
276
+ if tenant.id != tenant_id:
277
+ # TODO: Add admin check here
278
+ raise AccessDeniedError("You don't have access to this tenant")
279
+
280
+ return ServiceResult.success_result(tenant)
281
+
282
+ except Exception as e:
283
+ return self._handle_service_exception(e, 'get_tenant',
284
+ resource_id=resource_id, tenant_id=tenant_id)
285
+
286
+ def update(self, resource_id: str, tenant_id: str, user_id: str,
287
+ payload: Dict[str, Any]) -> ServiceResult[Tenant]:
288
+ """
289
+ Update tenant information.
290
+
291
+ Args:
292
+ resource_id: Tenant ID to update
293
+ tenant_id: Requesting tenant ID (for access control)
294
+ user_id: User ID making the update
295
+ payload: Fields to update
296
+
297
+ Returns:
298
+ ServiceResult with updated Tenant
299
+ """
300
+ try:
301
+ # Get existing tenant
302
+ get_result = self.get_by_id(resource_id, tenant_id, user_id)
303
+ if not get_result.success:
304
+ return get_result
305
+
306
+ tenant = get_result.data
307
+
308
+ # Update fields from payload
309
+ tenant.map(payload)
310
+ tenant.updated_by_id = user_id
311
+ tenant.updated_utc_ts = dt.datetime.now(dt.UTC).timestamp()
312
+ tenant.version += 1
313
+
314
+ # Save updated tenant
315
+ return self._save_model(tenant)
316
+
317
+ except Exception as e:
318
+ return self._handle_service_exception(e, 'update_tenant',
319
+ resource_id=resource_id, tenant_id=tenant_id)
320
+
321
+ def list_by_status(self, status: str, tenant_id: str, user_id: str,
322
+ limit: int = 50) -> ServiceResult[List[Tenant]]:
323
+ """
324
+ List tenants by status (for admin queries).
325
+
326
+ Args:
327
+ status: Tenant status (active|inactive|archived)
328
+ tenant_id: Requesting tenant ID
329
+ user_id: Requesting user ID
330
+ limit: Maximum number of results
331
+
332
+ Returns:
333
+ ServiceResult with list of Tenants
334
+ """
335
+ try:
336
+ # TODO: Add admin check - only admins should list all tenants
337
+
338
+ # Create temp tenant for GSI1 query
339
+ temp_tenant = Tenant()
340
+ temp_tenant.status = status
341
+
342
+ result = self._query_by_index(
343
+ temp_tenant,
344
+ "gsi1",
345
+ ascending=False,
346
+ limit=limit
347
+ )
348
+
349
+ if not result.success:
350
+ return result
351
+
352
+ # Filter out deleted tenants
353
+ active_tenants = [t for t in result.data if not t.is_deleted()]
354
+
355
+ return ServiceResult.success_result(active_tenants)
356
+
357
+ except Exception as e:
358
+ return self._handle_service_exception(e, 'list_tenants_by_status',
359
+ status=status, tenant_id=tenant_id)
360
+
361
+ def list_all(self, tenant_id: str, user_id: str, limit: int = 50) -> ServiceResult[List[Tenant]]:
362
+ """
363
+ List all tenants sorted by name (for admin queries).
364
+
365
+ Args:
366
+ tenant_id: Requesting tenant ID
367
+ user_id: Requesting user ID
368
+ limit: Maximum number of results
369
+
370
+ Returns:
371
+ ServiceResult with list of Tenants
372
+ """
373
+ try:
374
+ # TODO: Add admin check - only admins should list all tenants
375
+
376
+ # Create temp tenant for GSI2 query
377
+ temp_tenant = Tenant()
378
+ temp_tenant.name = "" # This will be overridden by GSI2 PK
379
+
380
+ # Need to manually set GSI2 PK for "all tenants" query
381
+ # This is a workaround for the query pattern
382
+
383
+ # For now, query by active status as a proxy
384
+ return self.list_by_status("active", tenant_id, user_id, limit)
385
+
386
+ except Exception as e:
387
+ return self._handle_service_exception(e, 'list_all_tenants',
388
+ tenant_id=tenant_id)
389
+
390
+ def deactivate(self, resource_id: str, tenant_id: str, user_id: str) -> ServiceResult[Tenant]:
391
+ """
392
+ Deactivate a tenant (soft disable).
393
+
394
+ Sets tenant status to 'inactive'. Optionally can cascade to disable users.
395
+
396
+ Args:
397
+ resource_id: Tenant ID to deactivate
398
+ tenant_id: Requesting tenant ID
399
+ user_id: User ID performing action
400
+
401
+ Returns:
402
+ ServiceResult with updated Tenant
403
+ """
404
+ try:
405
+ # Get tenant
406
+ get_result = self.get_by_id(resource_id, tenant_id, user_id)
407
+ if not get_result.success:
408
+ return get_result
409
+
410
+ tenant = get_result.data
411
+
412
+ # Deactivate
413
+ tenant.deactivate()
414
+ tenant.updated_by_id = user_id
415
+ tenant.updated_utc_ts = dt.datetime.now(dt.UTC).timestamp()
416
+ tenant.version += 1
417
+
418
+ # Save
419
+ save_result = self._save_model(tenant)
420
+
421
+ # TODO: Cascade to users - disable all users in tenant
422
+ # This would be done in a separate method or background job
423
+
424
+ return save_result
425
+
426
+ except Exception as e:
427
+ return self._handle_service_exception(e, 'deactivate_tenant',
428
+ resource_id=resource_id, tenant_id=tenant_id)
429
+
430
+ def activate(self, resource_id: str, tenant_id: str, user_id: str) -> ServiceResult[Tenant]:
431
+ """
432
+ Activate a tenant.
433
+
434
+ Sets tenant status to 'active'.
435
+
436
+ Args:
437
+ resource_id: Tenant ID to activate
438
+ tenant_id: Requesting tenant ID
439
+ user_id: User ID performing action
440
+
441
+ Returns:
442
+ ServiceResult with updated Tenant
443
+ """
444
+ try:
445
+ # Get tenant
446
+ get_result = self.get_by_id(resource_id, tenant_id, user_id)
447
+ if not get_result.success:
448
+ return get_result
449
+
450
+ tenant = get_result.data
451
+
452
+ # Activate
453
+ tenant.activate()
454
+ tenant.updated_by_id = user_id
455
+ tenant.updated_utc_ts = dt.datetime.now(dt.UTC).timestamp()
456
+ tenant.version += 1
457
+
458
+ # Save
459
+ return self._save_model(tenant)
460
+
461
+ except Exception as e:
462
+ return self._handle_service_exception(e, 'activate_tenant',
463
+ resource_id=resource_id, tenant_id=tenant_id)
464
+
465
+ def get_user_count(self, tenant_id: str, user_id: str) -> ServiceResult[int]:
466
+ """
467
+ Get count of users in tenant.
468
+
469
+ Args:
470
+ tenant_id: Tenant ID
471
+ user_id: Requesting user ID
472
+
473
+ Returns:
474
+ ServiceResult with user count
475
+ """
476
+ try:
477
+ # Query users by tenant
478
+ users_result = self.user_service.get_users_by_tenant(
479
+ tenant_id, user_id, limit=1000 # TODO: Handle pagination for large counts
480
+ )
481
+
482
+ if not users_result.success:
483
+ return ServiceResult(
484
+ success=False,
485
+ error="Failed to count users",
486
+ error_code="USER_COUNT_FAILED"
487
+ )
488
+
489
+ # Count active users
490
+ active_users = [u for u in users_result.data if not u.is_deleted() and u.is_active()]
491
+ count = len(active_users)
492
+
493
+ return ServiceResult.success_result(count)
494
+
495
+ except Exception as e:
496
+ return self._handle_service_exception(e, 'get_user_count',
497
+ tenant_id=tenant_id)
498
+
499
+ def can_add_user(self, tenant_id: str, user_id: str) -> ServiceResult[bool]:
500
+ """
501
+ Check if tenant can add another user (based on subscription limits).
502
+
503
+ Args:
504
+ tenant_id: Tenant ID
505
+ user_id: Requesting user ID
506
+
507
+ Returns:
508
+ ServiceResult with boolean (True if can add, False if at limit)
509
+ """
510
+ try:
511
+ # Get tenant
512
+ tenant_result = self.get_by_id(tenant_id, tenant_id, user_id)
513
+ if not tenant_result.success:
514
+ return tenant_result
515
+
516
+ tenant = tenant_result.data
517
+
518
+ # If no user limit, can always add
519
+ if tenant.max_users is None:
520
+ return ServiceResult.success_result(True)
521
+
522
+ # Get current user count
523
+ count_result = self.get_user_count(tenant_id, user_id)
524
+ if not count_result.success:
525
+ return count_result
526
+
527
+ current_count = count_result.data
528
+
529
+ # Check if at limit
530
+ can_add = current_count < tenant.max_users
531
+
532
+ return ServiceResult.success_result(can_add)
533
+
534
+ except Exception as e:
535
+ return self._handle_service_exception(e, 'can_add_user',
536
+ tenant_id=tenant_id)
537
+
538
+ def delete(self, resource_id: str, tenant_id: str, user_id: str) -> ServiceResult[bool]:
539
+ """
540
+ Soft delete tenant.
541
+
542
+ Args:
543
+ resource_id: Tenant ID to delete
544
+ tenant_id: Requesting tenant ID
545
+ user_id: User ID performing delete
546
+
547
+ Returns:
548
+ ServiceResult with boolean (True if deleted)
549
+ """
550
+ try:
551
+ # Get tenant
552
+ get_result = self.get_by_id(resource_id, tenant_id, user_id)
553
+ if not get_result.success:
554
+ return ServiceResult(success=False, message=get_result.message,
555
+ error_code=get_result.error_code)
556
+
557
+ tenant = get_result.data
558
+
559
+ # Soft delete
560
+ tenant.deleted_utc_ts = dt.datetime.now(dt.UTC).timestamp()
561
+ tenant.deleted_by_id = user_id
562
+ tenant.updated_by_id = user_id
563
+ tenant.updated_utc_ts = dt.datetime.now(dt.UTC).timestamp()
564
+
565
+ # Save
566
+ save_result = self._save_model(tenant)
567
+ if not save_result.success:
568
+ return ServiceResult(success=False, message=save_result.message,
569
+ error_code=save_result.error_code)
570
+
571
+ return ServiceResult.success_result(True)
572
+
573
+ except Exception as e:
574
+ return self._handle_service_exception(e, 'delete_tenant',
575
+ resource_id=resource_id, tenant_id=tenant_id)
File without changes
File without changes