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,571 @@
1
+ # Event Attendee 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 geek_cafe_saas_sdk.core.service_result import ServiceResult
7
+ from geek_cafe_saas_sdk.core.service_errors import ValidationError, NotFoundError, AccessDeniedError
8
+ from geek_cafe_saas_sdk.domains.events.models import EventAttendee
9
+ from geek_cafe_saas_sdk.utilities.dynamodb_utils import build_projection_with_reserved_keywords
10
+ import datetime as dt
11
+
12
+
13
+ class EventAttendeeService(DatabaseService[EventAttendee]):
14
+ """Service for EventAttendee database operations (RSVP tracking, invitations, check-in)."""
15
+
16
+ def __init__(self, *, dynamodb: DynamoDB = None, table_name: str = None):
17
+ super().__init__(dynamodb=dynamodb, table_name=table_name)
18
+
19
+ # Required abstract methods from DatabaseService
20
+ def create(self, tenant_id: str, user_id: str, **kwargs) -> ServiceResult[EventAttendee]:
21
+ """Create method - delegates to invite() for EventAttendee."""
22
+ if 'event_id' not in kwargs:
23
+ return ServiceResult.error_result("event_id is required", "VALIDATION_ERROR")
24
+
25
+ event_id = kwargs.pop('event_id')
26
+ invited_by = kwargs.pop('invited_by_user_id', user_id)
27
+
28
+ return self.invite(
29
+ event_id=event_id,
30
+ user_id=kwargs.pop('user_id', user_id),
31
+ tenant_id=tenant_id,
32
+ invited_by_user_id=invited_by,
33
+ **kwargs
34
+ )
35
+
36
+ def get_by_id(self, resource_id: str, tenant_id: str, user_id: str) -> ServiceResult[EventAttendee]:
37
+ """Get method - resource_id should be in format 'event_id:user_id'."""
38
+ try:
39
+ if ':' in resource_id:
40
+ event_id, attendee_user_id = resource_id.split(':', 1)
41
+ else:
42
+ return ServiceResult.error_result("Invalid resource_id format. Use 'event_id:user_id'", "VALIDATION_ERROR")
43
+
44
+ return self.get_attendee(event_id, attendee_user_id, tenant_id)
45
+ except Exception as e:
46
+ return self._handle_service_exception(e, 'get_by_id', resource_id=resource_id)
47
+
48
+ def update(self, resource_id: str, tenant_id: str, user_id: str, updates: Dict[str, Any]) -> ServiceResult[EventAttendee]:
49
+ """Update method - updates attendee record."""
50
+ try:
51
+ if ':' in resource_id:
52
+ event_id, attendee_user_id = resource_id.split(':', 1)
53
+ else:
54
+ return ServiceResult.error_result("Invalid resource_id format. Use 'event_id:user_id'", "VALIDATION_ERROR")
55
+
56
+ # Get existing attendee
57
+ result = self.get_attendee(event_id, attendee_user_id, tenant_id)
58
+ if not result.success:
59
+ return result
60
+
61
+ attendee = result.data
62
+
63
+ # Update fields
64
+ for key, value in updates.items():
65
+ if hasattr(attendee, key):
66
+ setattr(attendee, key, value)
67
+
68
+ attendee.updated_by_id = user_id
69
+ attendee.prep_for_save()
70
+ return self._save_model(attendee)
71
+
72
+ except Exception as e:
73
+ return self._handle_service_exception(e, 'update', resource_id=resource_id)
74
+
75
+ def delete(self, resource_id: str, tenant_id: str, user_id: str) -> ServiceResult[bool]:
76
+ """Delete method - soft deletes attendee."""
77
+ try:
78
+ if ':' in resource_id:
79
+ event_id, attendee_user_id = resource_id.split(':', 1)
80
+ else:
81
+ return ServiceResult.error_result("Invalid resource_id format. Use 'event_id:user_id'", "VALIDATION_ERROR")
82
+
83
+ return self.remove_attendee(event_id, attendee_user_id, tenant_id, user_id)
84
+
85
+ except Exception as e:
86
+ return self._handle_service_exception(e, 'delete', resource_id=resource_id)
87
+
88
+ def invite(self, event_id: str, user_id: str, tenant_id: str,
89
+ invited_by_user_id: str, **kwargs) -> ServiceResult[EventAttendee]:
90
+ """
91
+ Invite a user to an event.
92
+
93
+ Args:
94
+ event_id: Event ID
95
+ user_id: User ID to invite
96
+ tenant_id: Tenant ID
97
+ invited_by_user_id: Who is inviting
98
+ **kwargs: Additional fields (role, registration_data, etc.)
99
+ """
100
+ try:
101
+ # Check if already invited
102
+ existing = self.get_attendee(event_id, user_id, tenant_id)
103
+ if existing.success:
104
+ return ServiceResult.error_result("User is already invited to this event", "ALREADY_INVITED")
105
+
106
+ # Create attendee record
107
+ attendee = EventAttendee()
108
+ attendee.event_id = event_id
109
+ attendee.user_id = user_id
110
+ attendee.tenant_id = tenant_id
111
+ attendee.rsvp_status = kwargs.get('rsvp_status', 'invited')
112
+ attendee.role = kwargs.get('role', 'attendee')
113
+ attendee.invited_at_utc_ts = dt.datetime.now(dt.UTC).timestamp()
114
+ attendee.invited_by_user_id = invited_by_user_id
115
+ attendee.created_by_id = invited_by_user_id
116
+
117
+ # Optional fields
118
+ if 'registration_data' in kwargs:
119
+ attendee.registration_data = kwargs['registration_data']
120
+ if 'registration_notes' in kwargs:
121
+ attendee.registration_notes = kwargs['registration_notes']
122
+ if 'notification_preferences' in kwargs:
123
+ attendee.notification_preferences = kwargs['notification_preferences']
124
+
125
+ attendee.prep_for_save()
126
+ return self._save_model(attendee)
127
+
128
+ except Exception as e:
129
+ return self._handle_service_exception(e, 'invite_attendee', event_id=event_id, user_id=user_id)
130
+
131
+ def update_rsvp(self, event_id: str, user_id: str, tenant_id: str,
132
+ rsvp_status: str, **kwargs) -> ServiceResult[EventAttendee]:
133
+ """
134
+ Update RSVP status for an attendee.
135
+
136
+ Args:
137
+ event_id: Event ID
138
+ user_id: User ID
139
+ tenant_id: Tenant ID
140
+ rsvp_status: New RSVP status (accepted, declined, tentative)
141
+ **kwargs: Additional fields (plus_one_count, etc.)
142
+ """
143
+ try:
144
+ # Validate status
145
+ if rsvp_status not in ['accepted', 'declined', 'tentative', 'waitlist']:
146
+ raise ValidationError(f"Invalid RSVP status: {rsvp_status}")
147
+
148
+ # Get existing attendee
149
+ result = self.get_attendee(event_id, user_id, tenant_id)
150
+ if not result.success:
151
+ raise NotFoundError(f"Attendee not found for event {event_id}")
152
+
153
+ attendee = result.data
154
+ old_status = attendee.rsvp_status
155
+
156
+ # Update status
157
+ attendee.rsvp_status = rsvp_status
158
+ attendee.responded_at_utc_ts = dt.datetime.now(dt.UTC).timestamp()
159
+ attendee.updated_by_id = user_id
160
+
161
+ # Update optional fields
162
+ if 'plus_one_count' in kwargs:
163
+ attendee.plus_one_count = kwargs['plus_one_count']
164
+ if 'plus_one_names' in kwargs:
165
+ attendee.plus_one_names = kwargs['plus_one_names']
166
+ if 'registration_data' in kwargs:
167
+ attendee.registration_data = kwargs['registration_data']
168
+ if 'registration_notes' in kwargs:
169
+ attendee.registration_notes = kwargs['registration_notes']
170
+
171
+ attendee.prep_for_save()
172
+ return self._save_model(attendee)
173
+
174
+ except Exception as e:
175
+ return self._handle_service_exception(e, 'update_rsvp', event_id=event_id, user_id=user_id)
176
+
177
+ def add_host(self, event_id: str, user_id: str, tenant_id: str,
178
+ added_by_user_id: str, role: str = 'co_host') -> ServiceResult[EventAttendee]:
179
+ """
180
+ Add a host/co-organizer to an event.
181
+
182
+ Args:
183
+ event_id: Event ID
184
+ user_id: User ID to make host
185
+ tenant_id: Tenant ID
186
+ added_by_user_id: Who is adding them
187
+ role: 'organizer' or 'co_host'
188
+ """
189
+ try:
190
+ if role not in ['organizer', 'co_host']:
191
+ raise ValidationError(f"Invalid host role: {role}")
192
+
193
+ # Check if already attendee
194
+ result = self.get_attendee(event_id, user_id, tenant_id)
195
+
196
+ if result.success:
197
+ # Update existing attendee to host role
198
+ attendee = result.data
199
+ attendee.role = role
200
+ attendee.rsvp_status = 'accepted' # Hosts are auto-accepted
201
+ attendee.updated_by_id = added_by_user_id
202
+ attendee.prep_for_save()
203
+ return self._save_model(attendee)
204
+ else:
205
+ # Create new host attendee
206
+ return self.invite(
207
+ event_id=event_id,
208
+ user_id=user_id,
209
+ tenant_id=tenant_id,
210
+ invited_by_user_id=added_by_user_id,
211
+ role=role,
212
+ rsvp_status='accepted'
213
+ )
214
+
215
+ except Exception as e:
216
+ return self._handle_service_exception(e, 'add_host', event_id=event_id, user_id=user_id)
217
+
218
+ def get_attendee(self, event_id: str, user_id: str, tenant_id: str,
219
+ include_deleted: bool = False) -> ServiceResult[EventAttendee]:
220
+ """Get a specific attendee record.
221
+
222
+ Args:
223
+ event_id: Event ID
224
+ user_id: User ID
225
+ tenant_id: Tenant ID
226
+ include_deleted: If True, return deleted attendees as well
227
+ """
228
+ try:
229
+ # Use GSI1 to query by event, then filter by user_id
230
+ # This is more efficient than scanning
231
+ temp = EventAttendee()
232
+ temp.event_id = event_id
233
+
234
+ # Query by event using GSI1
235
+ result = self._query_by_index(
236
+ model=temp,
237
+ index_name="gsi1",
238
+ limit=100 # Should be small number per event
239
+ )
240
+
241
+ if not result.success:
242
+ return result
243
+
244
+ # Find the specific user's attendee record
245
+ for attendee in result.data:
246
+ if attendee.user_id == user_id and attendee.tenant_id == tenant_id:
247
+ # Return even if deleted if include_deleted is True
248
+ if not include_deleted and attendee.is_deleted():
249
+ return ServiceResult.error_result(f"Attendee not found", "NOT_FOUND")
250
+ return ServiceResult.success_result(attendee)
251
+
252
+ return ServiceResult.error_result(f"Attendee not found", "NOT_FOUND")
253
+
254
+ except Exception as e:
255
+ return self._handle_service_exception(e, 'get_attendee', event_id=event_id, user_id=user_id)
256
+
257
+ def list_by_event(self, event_id: str, tenant_id: str,
258
+ rsvp_status: str = None, role: str = None,
259
+ limit: int = 100) -> ServiceResult[List[EventAttendee]]:
260
+ """
261
+ List all attendees for an event.
262
+
263
+ Args:
264
+ event_id: Event ID
265
+ tenant_id: Tenant ID
266
+ rsvp_status: Optional filter by RSVP status
267
+ role: Optional filter by role
268
+ limit: Max results
269
+ """
270
+ try:
271
+ temp = EventAttendee()
272
+ temp.event_id = event_id
273
+ # Leave role and rsvp_status as None to query across all values
274
+ # (model defaults are now None, so GSI keys won't include them)
275
+ if role:
276
+ temp.role = role
277
+ if rsvp_status:
278
+ temp.rsvp_status = rsvp_status
279
+
280
+ # Use helper method for query
281
+ result = self._query_by_index(
282
+ model=temp,
283
+ index_name="gsi1",
284
+ ascending=False,
285
+ limit=limit
286
+ )
287
+
288
+ if not result.success:
289
+ return result
290
+
291
+ # Post-query filtering
292
+ valid_attendees = []
293
+ for attendee in result.data:
294
+ # Tenant isolation and deleted check
295
+ if attendee.tenant_id != tenant_id or attendee.is_deleted():
296
+ continue
297
+
298
+ # Filter by RSVP status if specified (post-query for flexibility)
299
+ if rsvp_status and attendee.rsvp_status != rsvp_status:
300
+ continue
301
+
302
+ # Filter by role if specified (post-query for flexibility)
303
+ if role and attendee.role != role:
304
+ continue
305
+
306
+ valid_attendees.append(attendee)
307
+
308
+ return ServiceResult.success_result(valid_attendees)
309
+
310
+ except Exception as e:
311
+ return self._handle_service_exception(e, 'list_by_event', event_id=event_id)
312
+
313
+ def list_user_events(self, user_id: str, tenant_id: str,
314
+ rsvp_status: str = None, upcoming_only: bool = True,
315
+ limit: int = 50) -> ServiceResult[List[EventAttendee]]:
316
+ """
317
+ List events a user is attending/invited to.
318
+
319
+ Args:
320
+ user_id: User ID
321
+ tenant_id: Tenant ID
322
+ rsvp_status: Optional filter by RSVP status
323
+ upcoming_only: Only future events
324
+ limit: Max results
325
+ """
326
+ try:
327
+ temp = EventAttendee()
328
+ temp.user_id = user_id
329
+ # Leave rsvp_status as None to query across all values
330
+ if rsvp_status:
331
+ temp.rsvp_status = rsvp_status
332
+
333
+ # Use helper method for query
334
+ result = self._query_by_index(
335
+ model=temp,
336
+ index_name="gsi2",
337
+ ascending=False,
338
+ limit=limit
339
+ )
340
+
341
+ if not result.success:
342
+ return result
343
+
344
+ # Post-query filtering
345
+ valid_attendees = []
346
+ for attendee in result.data:
347
+ # Tenant isolation and deleted check
348
+ if attendee.tenant_id != tenant_id or attendee.is_deleted():
349
+ continue
350
+
351
+ # Filter by RSVP status if specified
352
+ if rsvp_status and attendee.rsvp_status != rsvp_status:
353
+ continue
354
+
355
+ valid_attendees.append(attendee)
356
+
357
+ return ServiceResult.success_result(valid_attendees)
358
+
359
+ except Exception as e:
360
+ return self._handle_service_exception(e, 'list_user_events', user_id=user_id)
361
+
362
+ def list_hosts(self, event_id: str, tenant_id: str, limit: int = 20) -> ServiceResult[List[EventAttendee]]:
363
+ """
364
+ List all hosts (organizers and co-hosts) for an event.
365
+
366
+ Args:
367
+ event_id: Event ID
368
+ tenant_id: Tenant ID
369
+ limit: Max results
370
+ """
371
+ try:
372
+ # Get organizers
373
+ organizers = self.list_by_event(event_id, tenant_id, role="organizer", limit=limit)
374
+
375
+ # Get co-hosts
376
+ co_hosts = self.list_by_event(event_id, tenant_id, role="co_host", limit=limit)
377
+
378
+ # Combine
379
+ all_hosts = []
380
+ if organizers.success:
381
+ all_hosts.extend(organizers.data)
382
+ if co_hosts.success:
383
+ all_hosts.extend(co_hosts.data)
384
+
385
+ return ServiceResult.success_result(all_hosts)
386
+
387
+ except Exception as e:
388
+ return self._handle_service_exception(e, 'list_hosts', event_id=event_id)
389
+
390
+ def get_attendee_count(self, event_id: str, tenant_id: str,
391
+ rsvp_status: str = 'accepted') -> ServiceResult[int]:
392
+ """
393
+ Get count of attendees by RSVP status.
394
+
395
+ Args:
396
+ event_id: Event ID
397
+ tenant_id: Tenant ID
398
+ rsvp_status: RSVP status to count
399
+ """
400
+ try:
401
+ result = self.list_by_event(event_id, tenant_id, rsvp_status=rsvp_status, limit=1000)
402
+ if not result.success:
403
+ return ServiceResult.error_result("Failed to count attendees", "COUNT_FAILED")
404
+
405
+ count = len(result.data)
406
+
407
+ # Add up +1 guests for accepted
408
+ if rsvp_status == 'accepted':
409
+ total_count = sum(a.total_attendee_count() for a in result.data)
410
+ return ServiceResult.success_result(total_count)
411
+
412
+ return ServiceResult.success_result(count)
413
+
414
+ except Exception as e:
415
+ return self._handle_service_exception(e, 'get_attendee_count', event_id=event_id)
416
+
417
+ def check_in(self, event_id: str, user_id: str, tenant_id: str,
418
+ checked_in_by_user_id: str) -> ServiceResult[EventAttendee]:
419
+ """
420
+ Check in an attendee at the event.
421
+
422
+ Args:
423
+ event_id: Event ID
424
+ user_id: Attendee user ID
425
+ tenant_id: Tenant ID
426
+ checked_in_by_user_id: Who is checking them in
427
+ """
428
+ try:
429
+ # Get attendee
430
+ result = self.get_attendee(event_id, user_id, tenant_id)
431
+ if not result.success:
432
+ raise NotFoundError(f"Attendee not found")
433
+
434
+ attendee = result.data
435
+
436
+ # Must be accepted to check in
437
+ if not attendee.has_accepted():
438
+ raise ValidationError("Only accepted attendees can check in")
439
+
440
+ # Already checked in?
441
+ if attendee.checked_in:
442
+ return ServiceResult.error_result("Attendee already checked in", "ALREADY_CHECKED_IN")
443
+
444
+ # Check in
445
+ attendee.checked_in = True
446
+ attendee.checked_in_at_utc_ts = dt.datetime.now(dt.UTC).timestamp()
447
+ attendee.checked_in_by_user_id = checked_in_by_user_id
448
+ attendee.updated_by_id = checked_in_by_user_id
449
+
450
+ attendee.prep_for_save()
451
+ return self._save_model(attendee)
452
+
453
+ except Exception as e:
454
+ return self._handle_service_exception(e, 'check_in', event_id=event_id, user_id=user_id)
455
+
456
+ def promote_from_waitlist(self, event_id: str, user_id: str, tenant_id: str,
457
+ promoted_by_user_id: str) -> ServiceResult[EventAttendee]:
458
+ """
459
+ Promote an attendee from waitlist to accepted.
460
+
461
+ Args:
462
+ event_id: Event ID
463
+ user_id: User ID on waitlist
464
+ tenant_id: Tenant ID
465
+ promoted_by_user_id: Who is promoting them
466
+ """
467
+ try:
468
+ # Get attendee
469
+ result = self.get_attendee(event_id, user_id, tenant_id)
470
+ if not result.success:
471
+ raise NotFoundError(f"Attendee not found")
472
+
473
+ attendee = result.data
474
+
475
+ # Must be on waitlist
476
+ if not attendee.is_on_waitlist():
477
+ raise ValidationError("Attendee is not on waitlist")
478
+
479
+ # Promote to accepted
480
+ attendee.rsvp_status = 'accepted'
481
+ attendee.responded_at_utc_ts = dt.datetime.now(dt.UTC).timestamp()
482
+ attendee.updated_by_id = promoted_by_user_id
483
+
484
+ attendee.prep_for_save()
485
+ return self._save_model(attendee)
486
+
487
+ except Exception as e:
488
+ return self._handle_service_exception(e, 'promote_from_waitlist', event_id=event_id, user_id=user_id)
489
+
490
+ def remove_attendee(self, event_id: str, user_id: str, tenant_id: str,
491
+ removed_by_user_id: str) -> ServiceResult[bool]:
492
+ """
493
+ Remove an attendee from an event (soft delete).
494
+
495
+ Args:
496
+ event_id: Event ID
497
+ user_id: User ID to remove
498
+ tenant_id: Tenant ID
499
+ removed_by_user_id: Who is removing them
500
+ """
501
+ try:
502
+ # Get attendee
503
+ result = self.get_attendee(event_id, user_id, tenant_id)
504
+ if not result.success:
505
+ raise NotFoundError(f"Attendee not found")
506
+
507
+ attendee = result.data
508
+
509
+ # Soft delete
510
+ attendee.deleted_utc_ts = dt.datetime.now(dt.UTC).timestamp()
511
+ attendee.deleted_by_id = removed_by_user_id
512
+ attendee.updated_by_id = removed_by_user_id
513
+
514
+ attendee.prep_for_save()
515
+ save_result = self._save_model(attendee)
516
+
517
+ return ServiceResult.success_result(save_result.success)
518
+
519
+ except Exception as e:
520
+ return self._handle_service_exception(e, 'remove_attendee', event_id=event_id, user_id=user_id)
521
+
522
+ def bulk_invite(self, event_id: str, user_ids: List[str], tenant_id: str,
523
+ invited_by_user_id: str, **kwargs) -> ServiceResult[Dict[str, Any]]:
524
+ """
525
+ Invite multiple users to an event.
526
+
527
+ Args:
528
+ event_id: Event ID
529
+ user_ids: List of user IDs to invite
530
+ tenant_id: Tenant ID
531
+ invited_by_user_id: Who is inviting
532
+ **kwargs: Additional fields applied to all invites
533
+
534
+ Returns:
535
+ Dict with 'invited_count', 'failed_count', 'successful', and 'failed' lists
536
+ """
537
+ try:
538
+ successful = []
539
+ failed = []
540
+
541
+ for user_id in user_ids:
542
+ result = self.invite(
543
+ event_id=event_id,
544
+ user_id=user_id,
545
+ tenant_id=tenant_id,
546
+ invited_by_user_id=invited_by_user_id,
547
+ **kwargs
548
+ )
549
+
550
+ if result.success:
551
+ successful.append({
552
+ 'user_id': user_id,
553
+ 'id': result.data.id
554
+ })
555
+ else:
556
+ failed.append({
557
+ 'user_id': user_id,
558
+ 'error': result.error_message
559
+ })
560
+
561
+ results = {
562
+ 'invited_count': len(successful),
563
+ 'failed_count': len(failed),
564
+ 'successful': successful,
565
+ 'failed': failed
566
+ }
567
+
568
+ return ServiceResult.success_result(results)
569
+
570
+ except Exception as e:
571
+ return self._handle_service_exception(e, 'bulk_invite', event_id=event_id)