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.
- geek_cafe_saas_sdk/__init__.py +9 -0
- geek_cafe_saas_sdk/core/__init__.py +11 -0
- geek_cafe_saas_sdk/core/audit_mixin.py +33 -0
- geek_cafe_saas_sdk/core/error_codes.py +132 -0
- geek_cafe_saas_sdk/core/service_errors.py +19 -0
- geek_cafe_saas_sdk/core/service_result.py +121 -0
- geek_cafe_saas_sdk/decorators/__init__.py +64 -0
- geek_cafe_saas_sdk/decorators/auth.py +373 -0
- geek_cafe_saas_sdk/decorators/core.py +358 -0
- geek_cafe_saas_sdk/domains/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/analytics/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/analytics/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/analytics/models/__init__.py +9 -0
- geek_cafe_saas_sdk/domains/analytics/models/website_analytics.py +219 -0
- geek_cafe_saas_sdk/domains/analytics/models/website_analytics_summary.py +220 -0
- geek_cafe_saas_sdk/domains/analytics/services/__init__.py +11 -0
- geek_cafe_saas_sdk/domains/analytics/services/website_analytics_service.py +232 -0
- geek_cafe_saas_sdk/domains/analytics/services/website_analytics_summary_service.py +212 -0
- geek_cafe_saas_sdk/domains/analytics/services/website_analytics_tally_service.py +610 -0
- geek_cafe_saas_sdk/domains/auth/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/auth/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/create/app.py +41 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/delete/app.py +41 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/list/app.py +36 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/auth/models/__init__.py +13 -0
- geek_cafe_saas_sdk/domains/auth/models/permission.py +134 -0
- geek_cafe_saas_sdk/domains/auth/models/resource_permission.py +245 -0
- geek_cafe_saas_sdk/domains/auth/models/role.py +213 -0
- geek_cafe_saas_sdk/domains/auth/models/user.py +285 -0
- geek_cafe_saas_sdk/domains/auth/services/__init__.py +16 -0
- geek_cafe_saas_sdk/domains/auth/services/authorization_service.py +376 -0
- geek_cafe_saas_sdk/domains/auth/services/permission_registry.py +464 -0
- geek_cafe_saas_sdk/domains/auth/services/resource_permission_service.py +408 -0
- geek_cafe_saas_sdk/domains/auth/services/user_service.py +274 -0
- geek_cafe_saas_sdk/domains/communities/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/communities/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/create/app.py +41 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/delete/app.py +41 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/list/app.py +36 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/communities/models/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/communities/models/community.py +326 -0
- geek_cafe_saas_sdk/domains/communities/models/community_member.py +227 -0
- geek_cafe_saas_sdk/domains/communities/services/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/communities/services/community_member_service.py +412 -0
- geek_cafe_saas_sdk/domains/communities/services/community_service.py +479 -0
- geek_cafe_saas_sdk/domains/events/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/events/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/events/handlers/attendees/app.py +67 -0
- geek_cafe_saas_sdk/domains/events/handlers/cancel/app.py +66 -0
- geek_cafe_saas_sdk/domains/events/handlers/check_in/app.py +60 -0
- geek_cafe_saas_sdk/domains/events/handlers/create/app.py +93 -0
- geek_cafe_saas_sdk/domains/events/handlers/delete/app.py +42 -0
- geek_cafe_saas_sdk/domains/events/handlers/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/events/handlers/invite/app.py +98 -0
- geek_cafe_saas_sdk/domains/events/handlers/list/app.py +125 -0
- geek_cafe_saas_sdk/domains/events/handlers/publish/app.py +49 -0
- geek_cafe_saas_sdk/domains/events/handlers/rsvp/app.py +83 -0
- geek_cafe_saas_sdk/domains/events/handlers/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/events/models/__init__.py +3 -0
- geek_cafe_saas_sdk/domains/events/models/event.py +681 -0
- geek_cafe_saas_sdk/domains/events/models/event_attendee.py +324 -0
- geek_cafe_saas_sdk/domains/events/services/__init__.py +9 -0
- geek_cafe_saas_sdk/domains/events/services/event_attendee_service.py +571 -0
- geek_cafe_saas_sdk/domains/events/services/event_service.py +684 -0
- geek_cafe_saas_sdk/domains/files/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/files/models/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/files/models/directory.py +258 -0
- geek_cafe_saas_sdk/domains/files/models/file.py +312 -0
- geek_cafe_saas_sdk/domains/files/models/file_share.py +268 -0
- geek_cafe_saas_sdk/domains/files/models/file_version.py +216 -0
- geek_cafe_saas_sdk/domains/files/services/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/files/services/directory_service.py +701 -0
- geek_cafe_saas_sdk/domains/files/services/file_share_service.py +663 -0
- geek_cafe_saas_sdk/domains/files/services/file_system_service.py +575 -0
- geek_cafe_saas_sdk/domains/files/services/file_version_service.py +739 -0
- geek_cafe_saas_sdk/domains/files/services/s3_file_service.py +501 -0
- geek_cafe_saas_sdk/domains/messaging/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/create/app.py +86 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/delete/app.py +65 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/get/app.py +64 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/list/app.py +97 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/update/app.py +149 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/create/app.py +67 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/delete/app.py +65 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/get/app.py +64 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/list/app.py +102 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/update/app.py +127 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/create/app.py +94 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/delete/app.py +66 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/get/app.py +67 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/list/app.py +95 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/update/app.py +156 -0
- geek_cafe_saas_sdk/domains/messaging/models/__init__.py +13 -0
- geek_cafe_saas_sdk/domains/messaging/models/chat_channel.py +337 -0
- geek_cafe_saas_sdk/domains/messaging/models/chat_channel_member.py +180 -0
- geek_cafe_saas_sdk/domains/messaging/models/chat_message.py +426 -0
- geek_cafe_saas_sdk/domains/messaging/models/contact_thread.py +392 -0
- geek_cafe_saas_sdk/domains/messaging/services/__init__.py +11 -0
- geek_cafe_saas_sdk/domains/messaging/services/chat_channel_service.py +700 -0
- geek_cafe_saas_sdk/domains/messaging/services/chat_message_service.py +491 -0
- geek_cafe_saas_sdk/domains/messaging/services/contact_thread_service.py +497 -0
- geek_cafe_saas_sdk/domains/tenancy/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/activate/app.py +52 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/active/app.py +37 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/cancel/app.py +55 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/list/app.py +44 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/record_payment/app.py +56 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/me/app.py +37 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/signup/app.py +61 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/tenancy/models/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/tenancy/models/subscription.py +440 -0
- geek_cafe_saas_sdk/domains/tenancy/models/tenant.py +258 -0
- geek_cafe_saas_sdk/domains/tenancy/services/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/tenancy/services/subscription_service.py +557 -0
- geek_cafe_saas_sdk/domains/tenancy/services/tenant_service.py +575 -0
- geek_cafe_saas_sdk/domains/voting/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/voting/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/create/app.py +128 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/delete/app.py +41 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/list/app.py +38 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/summerize/README.md +3 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/voting/models/__init__.py +9 -0
- geek_cafe_saas_sdk/domains/voting/models/vote.py +231 -0
- geek_cafe_saas_sdk/domains/voting/models/vote_summary.py +193 -0
- geek_cafe_saas_sdk/domains/voting/services/__init__.py +11 -0
- geek_cafe_saas_sdk/domains/voting/services/vote_service.py +264 -0
- geek_cafe_saas_sdk/domains/voting/services/vote_summary_service.py +198 -0
- geek_cafe_saas_sdk/domains/voting/services/vote_tally_service.py +533 -0
- geek_cafe_saas_sdk/lambda_handlers/README.md +404 -0
- geek_cafe_saas_sdk/lambda_handlers/__init__.py +67 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/__init__.py +25 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/api_key_handler.py +129 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/authorized_secure_handler.py +218 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/base_handler.py +185 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/handler_factory.py +256 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/public_handler.py +53 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/secure_handler.py +89 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/service_pool.py +94 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/create/app.py +79 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/delete/app.py +76 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/get/app.py +74 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/list/app.py +75 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/move/app.py +79 -0
- geek_cafe_saas_sdk/lambda_handlers/files/delete/app.py +121 -0
- geek_cafe_saas_sdk/lambda_handlers/files/download/app.py +187 -0
- geek_cafe_saas_sdk/lambda_handlers/files/get/app.py +127 -0
- geek_cafe_saas_sdk/lambda_handlers/files/list/app.py +108 -0
- geek_cafe_saas_sdk/lambda_handlers/files/share/app.py +83 -0
- geek_cafe_saas_sdk/lambda_handlers/files/shares/list/app.py +84 -0
- geek_cafe_saas_sdk/lambda_handlers/files/shares/revoke/app.py +76 -0
- geek_cafe_saas_sdk/lambda_handlers/files/update/app.py +143 -0
- geek_cafe_saas_sdk/lambda_handlers/files/upload/app.py +151 -0
- geek_cafe_saas_sdk/middleware/__init__.py +36 -0
- geek_cafe_saas_sdk/middleware/auth.py +85 -0
- geek_cafe_saas_sdk/middleware/authorization.py +523 -0
- geek_cafe_saas_sdk/middleware/cors.py +63 -0
- geek_cafe_saas_sdk/middleware/error_handling.py +114 -0
- geek_cafe_saas_sdk/middleware/validation.py +80 -0
- geek_cafe_saas_sdk/models/__init__.py +20 -0
- geek_cafe_saas_sdk/models/base_model.py +233 -0
- geek_cafe_saas_sdk/services/__init__.py +18 -0
- geek_cafe_saas_sdk/services/database_service.py +441 -0
- geek_cafe_saas_sdk/utilities/__init__.py +88 -0
- geek_cafe_saas_sdk/utilities/cognito_utility.py +568 -0
- geek_cafe_saas_sdk/utilities/custom_exceptions.py +183 -0
- geek_cafe_saas_sdk/utilities/datetime_utility.py +410 -0
- geek_cafe_saas_sdk/utilities/dictionary_utility.py +78 -0
- geek_cafe_saas_sdk/utilities/dynamodb_utils.py +151 -0
- geek_cafe_saas_sdk/utilities/environment_loader.py +149 -0
- geek_cafe_saas_sdk/utilities/environment_variables.py +228 -0
- geek_cafe_saas_sdk/utilities/http_body_parameters.py +44 -0
- geek_cafe_saas_sdk/utilities/http_path_parameters.py +60 -0
- geek_cafe_saas_sdk/utilities/http_status_code.py +63 -0
- geek_cafe_saas_sdk/utilities/jwt_utility.py +234 -0
- geek_cafe_saas_sdk/utilities/lambda_event_utility.py +776 -0
- geek_cafe_saas_sdk/utilities/logging_utility.py +64 -0
- geek_cafe_saas_sdk/utilities/message_query_helper.py +340 -0
- geek_cafe_saas_sdk/utilities/response.py +209 -0
- geek_cafe_saas_sdk/utilities/string_functions.py +180 -0
- geek_cafe_saas_sdk-0.6.0.dist-info/METADATA +397 -0
- geek_cafe_saas_sdk-0.6.0.dist-info/RECORD +194 -0
- geek_cafe_saas_sdk-0.6.0.dist-info/WHEEL +4 -0
- 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)
|