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,479 @@
1
+ # Community Service
2
+
3
+ from typing import Dict, Any, Optional, List
4
+ from boto3_assist.dynamodb.dynamodb import DynamoDB
5
+ from geek_cafe_saas_sdk.services.database_service import DatabaseService
6
+ from .community_member_service import CommunityMemberService
7
+ from geek_cafe_saas_sdk.core.service_result import ServiceResult
8
+ from geek_cafe_saas_sdk.core.service_errors import ValidationError, NotFoundError, AccessDeniedError
9
+ from geek_cafe_saas_sdk.domains.communities.models import Community
10
+ import datetime as dt
11
+
12
+
13
+ class CommunityService(DatabaseService[Community]):
14
+ """Service for Community database operations."""
15
+
16
+ def __init__(self, *, dynamodb: DynamoDB = None, table_name: str = None):
17
+ super().__init__(dynamodb=dynamodb, table_name=table_name)
18
+ # Initialize member service with same DB connection
19
+ self.member_service = CommunityMemberService(dynamodb=dynamodb, table_name=table_name)
20
+
21
+ def create(self, tenant_id: str, user_id: str, **kwargs) -> ServiceResult[Community]:
22
+ """Create a new community."""
23
+ try:
24
+ # Validate required fields
25
+ required_fields = ['name', 'description', 'category']
26
+ self._validate_required_fields(kwargs, required_fields)
27
+
28
+ # Validate community name uniqueness per user
29
+ if self._community_name_exists_for_user(kwargs['name'], user_id, tenant_id):
30
+ raise ValidationError("Community name already exists for this user")
31
+
32
+ # Validate category
33
+ if not self._is_valid_category(kwargs['category']):
34
+ raise ValidationError("Invalid category")
35
+
36
+ # Create community instance using map() approach
37
+ community = Community().map(kwargs)
38
+ community.owner_id = user_id # Creator is the owner
39
+ community.member_count = 1 # Owner is first member
40
+ community.tenant_id = tenant_id
41
+ community.user_id = user_id
42
+ community.created_by_id = user_id
43
+
44
+ # Prepare for save (sets ID and timestamps)
45
+ community.prep_for_save()
46
+
47
+ # Save to database
48
+ save_result = self._save_model(community)
49
+
50
+ if save_result.success:
51
+ # Add owner as first member (adjacent record)
52
+ member_result = self.member_service.add_member(
53
+ community_id=community.id,
54
+ user_id=user_id,
55
+ status="active"
56
+ )
57
+
58
+ if not member_result.success:
59
+ # Log warning but don't fail - community is created
60
+ pass
61
+
62
+ return save_result
63
+
64
+ except Exception as e:
65
+ return self._handle_service_exception(e, 'create_community', tenant_id=tenant_id, user_id=user_id)
66
+
67
+ def get_by_id(self, resource_id: str, tenant_id: str, user_id: str) -> ServiceResult[Community]:
68
+ """Get community by ID with access control."""
69
+ try:
70
+ community = self._get_model_by_id(resource_id, Community)
71
+
72
+ if not community:
73
+ raise NotFoundError(f"Community with ID {resource_id} not found")
74
+
75
+ # Check if deleted
76
+ if community.is_deleted():
77
+ raise NotFoundError(f"Community with ID {resource_id} not found")
78
+
79
+ # Validate tenant access
80
+ if hasattr(community, 'tenant_id'):
81
+ self._validate_tenant_access(community.tenant_id, tenant_id)
82
+
83
+ return ServiceResult.success_result(community)
84
+
85
+ except Exception as e:
86
+ return self._handle_service_exception(e, 'get_community', resource_id=resource_id, tenant_id=tenant_id)
87
+
88
+ def get_communities_by_owner(self, owner_id: str, tenant_id: str, user_id: str,
89
+ limit: int = 50) -> ServiceResult[List[Community]]:
90
+ """Get communities owned by a specific user using GSI1."""
91
+ try:
92
+ # Create a temporary community instance to get the GSI key
93
+ temp_community = Community()
94
+ temp_community.owner_id = owner_id
95
+
96
+ # Query by GSI1 (communities by owner), most recent first
97
+ result = self._query_by_index(
98
+ temp_community,
99
+ "gsi1",
100
+ ascending=False, # Most recent first
101
+ limit=limit
102
+ )
103
+
104
+ if not result.success:
105
+ return result
106
+
107
+ # Filter out deleted communities and validate tenant access
108
+ active_communities = []
109
+ for community in result.data:
110
+ if not community.is_deleted() and community.tenant_id == tenant_id:
111
+ active_communities.append(community)
112
+
113
+ return ServiceResult.success_result(active_communities)
114
+
115
+ except Exception as e:
116
+ return self._handle_service_exception(e, 'get_communities_by_owner',
117
+ owner_id=owner_id, tenant_id=tenant_id)
118
+
119
+ def get_communities_by_member(self, member_id: str, tenant_id: str, user_id: str,
120
+ limit: int = 50) -> ServiceResult[List[Community]]:
121
+ """Get communities where a user is a member."""
122
+ try:
123
+ # This is more complex - we'd need a GSI that indexes membership
124
+ # For now, we'll query all communities and filter client-side
125
+ # In production, we'd want a GSI for user->communities membership
126
+
127
+ all_communities_result = self.get_all_communities(tenant_id, user_id, limit=limit*2)
128
+
129
+ if not all_communities_result.success:
130
+ return all_communities_result
131
+
132
+ member_communities = [
133
+ community for community in all_communities_result.data
134
+ if community.is_user_member(member_id)
135
+ ][:limit]
136
+
137
+ return ServiceResult.success_result(member_communities)
138
+
139
+ except Exception as e:
140
+ return self._handle_service_exception(e, 'get_communities_by_member',
141
+ member_id=member_id, tenant_id=tenant_id)
142
+
143
+ def get_communities_by_privacy(self, privacy: str, tenant_id: str, user_id: str,
144
+ limit: int = 50) -> ServiceResult[List[Community]]:
145
+ """Get communities by privacy level using GSI2."""
146
+ try:
147
+ # Create a temporary community instance to get the GSI key
148
+ temp_community = Community()
149
+ temp_community.privacy = privacy
150
+
151
+ # Query by GSI2 (communities by privacy), most recent first
152
+ result = self._query_by_index(
153
+ temp_community,
154
+ "gsi2",
155
+ ascending=False, # Most recent first
156
+ limit=limit
157
+ )
158
+
159
+ if not result.success:
160
+ return result
161
+
162
+ # Filter out deleted communities and validate tenant access
163
+ active_communities = []
164
+ for community in result.data:
165
+ if not community.is_deleted() and community.tenant_id == tenant_id:
166
+ active_communities.append(community)
167
+
168
+ return ServiceResult.success_result(active_communities)
169
+
170
+ except Exception as e:
171
+ return self._handle_service_exception(e, 'get_communities_by_privacy',
172
+ privacy=privacy, tenant_id=tenant_id)
173
+
174
+ def get_all_communities(self, tenant_id: str, user_id: str, limit: int = 50) -> ServiceResult[List[Community]]:
175
+ """Get all communities for a tenant using GSI4."""
176
+ try:
177
+ # Create a temporary community instance to get the GSI key
178
+ temp_community = Community()
179
+ temp_community.tenant_id = tenant_id
180
+
181
+ # Query by GSI4 (communities by tenant), most recent first
182
+ result = self._query_by_index(
183
+ temp_community,
184
+ "gsi4",
185
+ ascending=False, # Most recent first
186
+ limit=limit
187
+ )
188
+
189
+ if not result.success:
190
+ return result
191
+
192
+ # Filter out deleted communities
193
+ active_communities = []
194
+ for community in result.data:
195
+ if not community.is_deleted():
196
+ active_communities.append(community)
197
+
198
+ return ServiceResult.success_result(active_communities)
199
+
200
+ except Exception as e:
201
+ return self._handle_service_exception(e, 'get_all_communities', tenant_id=tenant_id)
202
+
203
+ def get_communities_by_category(self, category: str, tenant_id: str, user_id: str,
204
+ limit: int = 50) -> ServiceResult[List[Community]]:
205
+ """Get communities by category using GSI3."""
206
+ try:
207
+ # Create a temporary community instance to get the GSI key
208
+ temp_community = Community()
209
+ temp_community.category = category
210
+
211
+ # Query by GSI3 (communities by category), most recent first
212
+ result = self._query_by_index(
213
+ temp_community,
214
+ "gsi3",
215
+ ascending=False, # Most recent first
216
+ limit=limit
217
+ )
218
+
219
+ if not result.success:
220
+ return result
221
+
222
+ # Filter out deleted communities and validate tenant access
223
+ active_communities = []
224
+ for community in result.data:
225
+ if not community.is_deleted() and community.tenant_id == tenant_id:
226
+ active_communities.append(community)
227
+
228
+ return ServiceResult.success_result(active_communities)
229
+
230
+ except Exception as e:
231
+ return self._handle_service_exception(e, 'get_communities_by_category',
232
+ category=category, tenant_id=tenant_id)
233
+
234
+ def update(self, resource_id: str, tenant_id: str, user_id: str,
235
+ updates: Dict[str, Any]) -> ServiceResult[Community]:
236
+ """Update community with access control."""
237
+ try:
238
+ # Get existing community
239
+ community = self._get_model_by_id(resource_id, Community)
240
+
241
+ if not community:
242
+ raise NotFoundError(f"Community with ID {resource_id} not found")
243
+
244
+ # Validate tenant access
245
+ if hasattr(community, 'tenant_id'):
246
+ self._validate_tenant_access(community.tenant_id, tenant_id)
247
+
248
+ # Check permissions (organizers only)
249
+ if not community.can_user_manage(user_id):
250
+ raise AccessDeniedError("Access denied: insufficient permissions")
251
+
252
+ # Cannot change owner
253
+ if 'owner_id' in updates:
254
+ raise ValidationError("Cannot change community owner")
255
+
256
+ # Validate category if being updated
257
+ if 'category' in updates and not self._is_valid_category(updates['category']):
258
+ raise ValidationError("Invalid category")
259
+
260
+ # Validate community name uniqueness if being updated
261
+ if 'name' in updates:
262
+ existing_community = self._get_community_by_name_and_owner(updates['name'], community.owner_id, tenant_id)
263
+ if existing_community and existing_community.id != resource_id:
264
+ raise ValidationError("Community name already exists for this user")
265
+
266
+ # Apply updates
267
+ for field, value in updates.items():
268
+ if hasattr(community, field) and field not in ['id', 'created_utc_ts', 'tenant_id', 'owner_id']:
269
+ if field == 'name':
270
+ community.name = value
271
+ elif field == 'description':
272
+ community.description = value
273
+ elif field == 'category':
274
+ community.category = value
275
+ elif field == 'privacy':
276
+ community.privacy = value
277
+ elif field == 'tags':
278
+ community.tags = value
279
+ elif field == 'joinApproval':
280
+ community.join_approval = value
281
+ elif field == 'requiresDues':
282
+ community.requires_dues = value
283
+ elif field == 'duesMonthly':
284
+ community.dues_monthly = value
285
+ elif field == 'duesYearly':
286
+ community.dues_yearly = value
287
+ elif field == 'co_owners':
288
+ community.co_owners = value
289
+ elif field == 'moderators':
290
+ community.moderators = value
291
+ elif field == 'members':
292
+ community.members = value
293
+
294
+ # Update metadata
295
+ community.updated_by_id = user_id
296
+ community.prep_for_save() # Updates timestamp
297
+
298
+ # Save updated community
299
+ return self._save_model(community)
300
+
301
+ except Exception as e:
302
+ return self._handle_service_exception(e, 'update_community', resource_id=resource_id, tenant_id=tenant_id)
303
+
304
+ def delete(self, resource_id: str, tenant_id: str, user_id: str) -> ServiceResult[bool]:
305
+ """Soft delete community with access control."""
306
+ try:
307
+ # Get existing community
308
+ community = self._get_model_by_id(resource_id, Community)
309
+
310
+ if not community:
311
+ raise NotFoundError(f"Community with ID {resource_id} not found")
312
+
313
+ # Check if already deleted
314
+ if community.is_deleted():
315
+ return ServiceResult.success_result(True)
316
+
317
+ # Validate tenant access
318
+ if hasattr(community, 'tenant_id'):
319
+ self._validate_tenant_access(community.tenant_id, tenant_id)
320
+
321
+ # Check permissions (owner only)
322
+ if community.owner_id != user_id:
323
+ raise AccessDeniedError("Access denied: only community owner can delete")
324
+
325
+ # Soft delete: set deleted timestamp and metadata
326
+ community.deleted_utc_ts = dt.datetime.now(dt.UTC).timestamp()
327
+ community.deleted_by_id = user_id
328
+ community.prep_for_save() # Updates timestamp
329
+
330
+ # Save the updated community
331
+ save_result = self._save_model(community)
332
+ if save_result.success:
333
+ return ServiceResult.success_result(True)
334
+ else:
335
+ return save_result
336
+
337
+ except Exception as e:
338
+ return self._handle_service_exception(e, 'delete_community', resource_id=resource_id, tenant_id=tenant_id)
339
+
340
+ # Convenience method for backwards compatibility / clearer naming
341
+ def list_by_tenant(self, tenant_id: str, user_id: str, limit: int = 50) -> ServiceResult[List[Community]]:
342
+ """Alias for get_all_communities."""
343
+ return self.get_all_communities(tenant_id, user_id, limit)
344
+
345
+ def _community_name_exists_for_user(self, name: str, user_id: str, tenant_id: str) -> bool:
346
+ """Check if community name already exists for this user."""
347
+ try:
348
+ community = self._get_community_by_name_and_owner(name, user_id, tenant_id)
349
+ return community is not None
350
+ except:
351
+ return False
352
+
353
+ def _get_community_by_name_and_owner(self, name: str, owner_id: str, tenant_id: str) -> Optional[Community]:
354
+ """Get community by name and owner (helper method)."""
355
+ # This would require a GSI for name+owner
356
+ # For now, we'll query owner's communities and check names
357
+ owner_communities_result = self.get_communities_by_owner(owner_id, tenant_id, owner_id, limit=100)
358
+ if owner_communities_result.success:
359
+ for community in owner_communities_result.data:
360
+ if community.name == name:
361
+ return community
362
+ return None
363
+
364
+ def _is_valid_category(self, category: str) -> bool:
365
+ """Validate community category."""
366
+ # This should come from a predefined list
367
+ valid_categories = [
368
+ "sports", "hobby", "professional", "educational",
369
+ "social", "religious", "political", "charity",
370
+ "entertainment", "other"
371
+ ]
372
+ return category.lower() in valid_categories
373
+
374
+ # Member Management Methods (delegates to CommunityMemberService)
375
+
376
+ def add_member(self, community_id: str, user_id: str, invited_by_id: str = None,
377
+ status: str = "active", tenant_id: str = None) -> ServiceResult:
378
+ """
379
+ Add a member to the community.
380
+
381
+ Args:
382
+ community_id: Community ID
383
+ user_id: User ID to add
384
+ invited_by_id: Optional ID of user who invited
385
+ status: Member status (default: active)
386
+ tenant_id: Tenant ID for access control
387
+ """
388
+ try:
389
+ # Get community to update count
390
+ if tenant_id:
391
+ community_result = self.get_by_id(community_id, tenant_id, invited_by_id or user_id)
392
+ if not community_result.success:
393
+ return community_result
394
+ community = community_result.data
395
+
396
+ # Add member via member service
397
+ result = self.member_service.add_member(community_id, user_id, invited_by_id, status)
398
+
399
+ # Update cached member count if successful and we have community
400
+ if result.success and tenant_id and community:
401
+ community.increment_member_count()
402
+ self._save_model(community)
403
+
404
+ return result
405
+
406
+ except Exception as e:
407
+ return self._handle_service_exception(e, 'add_member', community_id=community_id, user_id=user_id)
408
+
409
+ def remove_member(self, community_id: str, user_id: str, tenant_id: str = None) -> ServiceResult:
410
+ """
411
+ Remove a member from the community.
412
+
413
+ Args:
414
+ community_id: Community ID
415
+ user_id: User ID to remove
416
+ tenant_id: Optional tenant ID for updating cached count
417
+ """
418
+ try:
419
+ # Remove member via member service
420
+ result = self.member_service.remove_member(community_id, user_id)
421
+
422
+ # Update cached member count if we have tenant context
423
+ if result.success and tenant_id:
424
+ try:
425
+ community_result = self.get_by_id(community_id, tenant_id, user_id)
426
+ if community_result.success:
427
+ community = community_result.data
428
+ community.decrement_member_count()
429
+ self._save_model(community)
430
+ except:
431
+ pass # Don't fail removal if count update fails
432
+
433
+ return result
434
+
435
+ except Exception as e:
436
+ return self._handle_service_exception(e, 'remove_member', community_id=community_id, user_id=user_id)
437
+
438
+ def is_member(self, community_id: str, user_id: str, active_only: bool = True) -> bool:
439
+ """
440
+ Check if user is a member of the community.
441
+
442
+ Args:
443
+ community_id: Community ID
444
+ user_id: User ID
445
+ active_only: Only check active members (default: True)
446
+ """
447
+ return self.member_service.is_member(community_id, user_id, active_only)
448
+
449
+ def get_members(self, community_id: str, status: str = None, limit: int = 50) -> ServiceResult:
450
+ """
451
+ Get members of a community.
452
+
453
+ Args:
454
+ community_id: Community ID
455
+ status: Optional status filter
456
+ limit: Max results
457
+ """
458
+ return self.member_service.list_members(community_id, status, limit)
459
+
460
+ def get_member_count_realtime(self, community_id: str, status: str = "active") -> ServiceResult:
461
+ """
462
+ Get real-time member count from database.
463
+
464
+ Args:
465
+ community_id: Community ID
466
+ status: Status filter (default: active)
467
+ """
468
+ return self.member_service.get_member_count(community_id, status)
469
+
470
+ def get_user_communities(self, user_id: str, status: str = "active", limit: int = 50) -> ServiceResult:
471
+ """
472
+ Get communities a user is a member of.
473
+
474
+ Args:
475
+ user_id: User ID
476
+ status: Member status filter
477
+ limit: Max results
478
+ """
479
+ return self.member_service.list_user_communities(user_id, status, limit)
File without changes
File without changes
@@ -0,0 +1,67 @@
1
+ # src/geek_cafe_saas_sdk/lambda_handlers/events/attendees/app.py
2
+
3
+ from typing import Dict, Any
4
+
5
+ from geek_cafe_saas_sdk.domains.events.services.event_attendee_service import EventAttendeeService
6
+ from geek_cafe_saas_sdk.lambda_handlers import ServicePool
7
+ from geek_cafe_saas_sdk.utilities.response import service_result_to_response, error_response
8
+ from geek_cafe_saas_sdk.utilities.lambda_event_utility import LambdaEventUtility
9
+
10
+ attendee_service_pool = ServicePool(EventAttendeeService)
11
+
12
+ def handler(event: Dict[str, Any], context: object, injected_service=None) -> Dict[str, Any]:
13
+ """
14
+ Lambda handler for listing event attendees.
15
+
16
+ Supports filtering by RSVP status and role.
17
+
18
+ Args:
19
+ event: API Gateway event
20
+ context: Lambda context
21
+ injected_service: Optional EventAttendeeService for testing
22
+
23
+ Query Parameters:
24
+ event_id: Event ID (required)
25
+ rsvp_status: Filter by status (accepted, declined, tentative, invited, waitlist)
26
+ role: Filter by role (organizer, co_host, attendee, speaker, volunteer)
27
+ limit: Max results (default 100)
28
+
29
+ Examples:
30
+ /events/attendees?event_id=evt_123
31
+ /events/attendees?event_id=evt_123&rsvp_status=accepted
32
+ /events/attendees?event_id=evt_123&role=organizer
33
+ /events/attendees?event_id=evt_123&rsvp_status=accepted&role=speaker
34
+
35
+ Returns 200 with list of attendees
36
+ """
37
+ try:
38
+ attendee_service = injected_service if injected_service else attendee_service_pool.get()
39
+ user_id = LambdaEventUtility.get_authenticated_user_id(event)
40
+ tenant_id = LambdaEventUtility.get_authenticated_user_tenant_id(event)
41
+ query_params = event.get('queryStringParameters', {}) or {}
42
+
43
+ # Validate required param
44
+ event_id = query_params.get('event_id')
45
+ if not event_id:
46
+ return error_response("event_id query parameter is required", "VALIDATION_ERROR", 400)
47
+
48
+ # Extract optional filters
49
+ rsvp_status = query_params.get('rsvp_status')
50
+ role = query_params.get('role')
51
+ limit = int(query_params.get('limit', 100))
52
+
53
+ # Get attendees
54
+ result = attendee_service.list_by_event(
55
+ event_id=event_id,
56
+ tenant_id=tenant_id,
57
+ rsvp_status=rsvp_status,
58
+ role=role,
59
+ limit=limit
60
+ )
61
+
62
+ return service_result_to_response(result)
63
+
64
+ except ValueError as e:
65
+ return error_response(f"Invalid parameter value: {str(e)}", "VALIDATION_ERROR", 400)
66
+ except Exception as e:
67
+ return error_response(f"An unexpected error occurred: {str(e)}", "INTERNAL_ERROR", 500)
@@ -0,0 +1,66 @@
1
+ # src/geek_cafe_saas_sdk/lambda_handlers/events/cancel/app.py
2
+
3
+ import json
4
+ from typing import Dict, Any
5
+
6
+ from geek_cafe_saas_sdk.domains.events.services.event_service import EventService
7
+ from geek_cafe_saas_sdk.lambda_handlers import ServicePool
8
+ from geek_cafe_saas_sdk.utilities.response import service_result_to_response, error_response
9
+ from geek_cafe_saas_sdk.utilities.lambda_event_utility import LambdaEventUtility
10
+
11
+ event_service_pool = ServicePool(EventService)
12
+
13
+ def handler(event: Dict[str, Any], context: object, injected_service=None) -> Dict[str, Any]:
14
+ """
15
+ Lambda handler for cancelling an event.
16
+
17
+ Changes status to 'cancelled' with optional reason.
18
+
19
+ Args:
20
+ event: API Gateway event
21
+ context: Lambda context
22
+ injected_service: Optional EventService for testing
23
+
24
+ Path Parameters:
25
+ id: Event ID
26
+
27
+ Expected body (optional):
28
+ {
29
+ "cancellation_reason": "Weather concerns"
30
+ }
31
+
32
+ Example:
33
+ POST /events/{id}/cancel
34
+
35
+ Returns 200 with cancelled event
36
+ """
37
+ try:
38
+ event_service = injected_service if injected_service else event_service_pool.get()
39
+ user_id = LambdaEventUtility.get_authenticated_user_id(event)
40
+ tenant_id = LambdaEventUtility.get_authenticated_user_tenant_id(event)
41
+ resource_id = LambdaEventUtility.get_value_from_path_parameters(event, 'id')
42
+
43
+ if not resource_id:
44
+ return error_response("Event ID is required in the path.", "VALIDATION_ERROR", 400)
45
+
46
+ # Get optional cancellation reason
47
+ cancellation_reason = None
48
+ try:
49
+ body = LambdaEventUtility.get_body_from_event(event)
50
+ cancellation_reason = body.get('cancellation_reason')
51
+ except:
52
+ pass # Body is optional
53
+
54
+ result = event_service.cancel(
55
+ resource_id=resource_id,
56
+ tenant_id=tenant_id,
57
+ user_id=user_id,
58
+ cancellation_reason=cancellation_reason
59
+ )
60
+
61
+ return service_result_to_response(result)
62
+
63
+ except json.JSONDecodeError:
64
+ return error_response("Invalid JSON format in request body.", "VALIDATION_ERROR", 400)
65
+ except Exception as e:
66
+ return error_response(f"An unexpected error occurred: {str(e)}", "INTERNAL_ERROR", 500)
@@ -0,0 +1,60 @@
1
+ # src/geek_cafe_saas_sdk/lambda_handlers/events/check_in/app.py
2
+
3
+ import json
4
+ from typing import Dict, Any
5
+
6
+ from geek_cafe_saas_sdk.domains.events.services.event_attendee_service import EventAttendeeService
7
+ from geek_cafe_saas_sdk.lambda_handlers import ServicePool
8
+ from geek_cafe_saas_sdk.utilities.response import service_result_to_response, error_response
9
+ from geek_cafe_saas_sdk.utilities.lambda_event_utility import LambdaEventUtility
10
+
11
+ attendee_service_pool = ServicePool(EventAttendeeService)
12
+
13
+ def handler(event: Dict[str, Any], context: object, injected_service=None) -> Dict[str, Any]:
14
+ """
15
+ Lambda handler for checking in attendees.
16
+
17
+ Allows hosts/organizers to check in attendees at the event.
18
+
19
+ Args:
20
+ event: API Gateway event
21
+ context: Lambda context
22
+ injected_service: Optional EventAttendeeService for testing
23
+
24
+ Expected body:
25
+ {
26
+ "event_id": "evt_123",
27
+ "attendee_user_id": "user_456" // User to check in
28
+ }
29
+
30
+ Returns 200 with updated attendee record (check_in = true)
31
+ """
32
+ try:
33
+ attendee_service = injected_service if injected_service else attendee_service_pool.get()
34
+ body = LambdaEventUtility.get_body_from_event(event)
35
+ user_id = LambdaEventUtility.get_authenticated_user_id(event) # Who is checking them in
36
+ tenant_id = LambdaEventUtility.get_authenticated_user_tenant_id(event)
37
+
38
+ # Validate required fields
39
+ event_id = body.get('event_id')
40
+ attendee_user_id = body.get('attendee_user_id')
41
+
42
+ if not event_id:
43
+ return error_response("event_id is required", "VALIDATION_ERROR", 400)
44
+ if not attendee_user_id:
45
+ return error_response("attendee_user_id is required", "VALIDATION_ERROR", 400)
46
+
47
+ # Check in the attendee
48
+ result = attendee_service.check_in(
49
+ event_id=event_id,
50
+ user_id=attendee_user_id,
51
+ tenant_id=tenant_id,
52
+ checked_in_by_user_id=user_id
53
+ )
54
+
55
+ return service_result_to_response(result)
56
+
57
+ except json.JSONDecodeError:
58
+ return error_response("Invalid JSON format in request body.", "VALIDATION_ERROR", 400)
59
+ except Exception as e:
60
+ return error_response(f"An unexpected error occurred: {str(e)}", "INTERNAL_ERROR", 500)