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,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)
|