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,497 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Geek Cafe, LLC
|
|
3
|
+
MIT License. See Project Root for the license information.
|
|
4
|
+
|
|
5
|
+
ContactThreadService for managing contact threads and support tickets.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Dict, Any, Optional, List
|
|
9
|
+
from boto3_assist.dynamodb.dynamodb import DynamoDB
|
|
10
|
+
from geek_cafe_saas_sdk.services.database_service import DatabaseService
|
|
11
|
+
from geek_cafe_saas_sdk.core.service_result import ServiceResult
|
|
12
|
+
from geek_cafe_saas_sdk.core.service_errors import ValidationError, NotFoundError, AccessDeniedError
|
|
13
|
+
from geek_cafe_saas_sdk.domains.messaging.models import ContactThread
|
|
14
|
+
import datetime as dt
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ContactThreadService(DatabaseService[ContactThread]):
|
|
18
|
+
"""Service for ContactThread database operations."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, *, dynamodb: DynamoDB = None, table_name: str = None):
|
|
21
|
+
super().__init__(dynamodb=dynamodb, table_name=table_name)
|
|
22
|
+
|
|
23
|
+
def create(self, tenant_id: str, user_id: str, payload: Dict[str, Any]) -> ServiceResult[ContactThread]:
|
|
24
|
+
"""
|
|
25
|
+
Create a new contact thread from a payload.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
tenant_id: Tenant ID
|
|
29
|
+
user_id: User ID creating the thread (can be guest session ID)
|
|
30
|
+
payload: Contact thread data including subject, sender, initial_message
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
ServiceResult with ContactThread
|
|
34
|
+
"""
|
|
35
|
+
try:
|
|
36
|
+
# Validate required fields
|
|
37
|
+
required_fields = ['subject', 'sender']
|
|
38
|
+
self._validate_required_fields(payload, required_fields)
|
|
39
|
+
|
|
40
|
+
# Validate sender has required fields
|
|
41
|
+
sender = payload.get('sender', {})
|
|
42
|
+
if not sender.get('id'):
|
|
43
|
+
raise ValidationError("Sender must have an 'id' field")
|
|
44
|
+
|
|
45
|
+
# Create and map thread instance from the payload
|
|
46
|
+
thread = ContactThread().map(payload)
|
|
47
|
+
thread.tenant_id = tenant_id
|
|
48
|
+
thread.user_id = user_id
|
|
49
|
+
thread.created_by_id = user_id
|
|
50
|
+
|
|
51
|
+
# Set defaults
|
|
52
|
+
if not thread.status:
|
|
53
|
+
thread.status = "open"
|
|
54
|
+
if not thread.priority:
|
|
55
|
+
thread.priority = "medium"
|
|
56
|
+
if not thread.inbox_id:
|
|
57
|
+
thread.inbox_id = "support"
|
|
58
|
+
|
|
59
|
+
# Add initial message if provided
|
|
60
|
+
if 'initial_message' in payload and payload['initial_message']:
|
|
61
|
+
initial_msg = {
|
|
62
|
+
"id": f"msg_{dt.datetime.now(dt.UTC).timestamp()}",
|
|
63
|
+
"content": payload['initial_message'],
|
|
64
|
+
"sender_id": sender.get('id'),
|
|
65
|
+
"sender_name": sender.get('name', 'Guest'),
|
|
66
|
+
"is_staff_reply": False,
|
|
67
|
+
"created_at": dt.datetime.now(dt.UTC).timestamp()
|
|
68
|
+
}
|
|
69
|
+
thread.add_message(initial_msg)
|
|
70
|
+
|
|
71
|
+
# Prepare for save (sets ID and timestamps)
|
|
72
|
+
thread.prep_for_save()
|
|
73
|
+
|
|
74
|
+
# Save to database
|
|
75
|
+
return self._save_model(thread)
|
|
76
|
+
|
|
77
|
+
except Exception as e:
|
|
78
|
+
return self._handle_service_exception(e, 'create_contact_thread', tenant_id=tenant_id, user_id=user_id)
|
|
79
|
+
|
|
80
|
+
def get_by_id(self, resource_id: str, tenant_id: str, user_id: str,
|
|
81
|
+
user_inboxes: List[str] = None) -> ServiceResult[ContactThread]:
|
|
82
|
+
"""
|
|
83
|
+
Get contact thread by ID with access control.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
resource_id: Thread ID
|
|
87
|
+
tenant_id: Tenant ID
|
|
88
|
+
user_id: User ID requesting access
|
|
89
|
+
user_inboxes: List of inbox IDs the user has access to
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
ServiceResult with ContactThread
|
|
93
|
+
"""
|
|
94
|
+
try:
|
|
95
|
+
thread = self._get_model_by_id(resource_id, ContactThread)
|
|
96
|
+
|
|
97
|
+
if not thread:
|
|
98
|
+
raise NotFoundError(f"Contact thread with ID {resource_id} not found")
|
|
99
|
+
|
|
100
|
+
# Check if deleted
|
|
101
|
+
if thread.is_deleted():
|
|
102
|
+
raise NotFoundError(f"Contact thread with ID {resource_id} not found")
|
|
103
|
+
|
|
104
|
+
# Validate tenant access
|
|
105
|
+
if hasattr(thread, 'tenant_id'):
|
|
106
|
+
self._validate_tenant_access(thread.tenant_id, tenant_id)
|
|
107
|
+
|
|
108
|
+
# Check if user can access this thread
|
|
109
|
+
if not thread.can_user_access(user_id, user_inboxes or []):
|
|
110
|
+
raise AccessDeniedError("Access denied to this contact thread")
|
|
111
|
+
|
|
112
|
+
return ServiceResult.success_result(thread)
|
|
113
|
+
|
|
114
|
+
except Exception as e:
|
|
115
|
+
return self._handle_service_exception(e, 'get_contact_thread', resource_id=resource_id, tenant_id=tenant_id)
|
|
116
|
+
|
|
117
|
+
def list_by_inbox_and_status(self, inbox_id: str, status: str, tenant_id: str,
|
|
118
|
+
priority: str = None, limit: int = 50) -> ServiceResult[List[ContactThread]]:
|
|
119
|
+
"""
|
|
120
|
+
List contact threads by inbox and status using GSI1.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
inbox_id: Inbox ID (support, sales, etc.)
|
|
124
|
+
status: Status filter (open, in_progress, resolved, closed)
|
|
125
|
+
tenant_id: Tenant ID for filtering
|
|
126
|
+
priority: Optional priority filter
|
|
127
|
+
limit: Maximum number of results
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
ServiceResult with list of ContactThreads
|
|
131
|
+
"""
|
|
132
|
+
try:
|
|
133
|
+
# Create a temporary thread instance to get the GSI key
|
|
134
|
+
temp_thread = ContactThread()
|
|
135
|
+
temp_thread.inbox_id = inbox_id
|
|
136
|
+
temp_thread.status = status
|
|
137
|
+
if priority:
|
|
138
|
+
temp_thread.priority = priority
|
|
139
|
+
|
|
140
|
+
# Query by GSI1 (inbox + status), sorted by priority and timestamp
|
|
141
|
+
result = self._query_by_index(
|
|
142
|
+
temp_thread,
|
|
143
|
+
"gsi1",
|
|
144
|
+
ascending=False, # Most recent first
|
|
145
|
+
limit=limit
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
if not result.success:
|
|
149
|
+
return result
|
|
150
|
+
|
|
151
|
+
# Filter by tenant and exclude deleted threads
|
|
152
|
+
active_threads = [
|
|
153
|
+
t for t in result.data
|
|
154
|
+
if not t.is_deleted() and t.tenant_id == tenant_id
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
return ServiceResult.success_result(active_threads)
|
|
158
|
+
|
|
159
|
+
except Exception as e:
|
|
160
|
+
return self._handle_service_exception(e, 'list_by_inbox_and_status',
|
|
161
|
+
inbox_id=inbox_id, status=status, tenant_id=tenant_id)
|
|
162
|
+
|
|
163
|
+
def list_by_tenant_and_status(self, tenant_id: str, status: str,
|
|
164
|
+
limit: int = 50) -> ServiceResult[List[ContactThread]]:
|
|
165
|
+
"""
|
|
166
|
+
List contact threads by tenant and status using GSI2.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
tenant_id: Tenant ID
|
|
170
|
+
status: Status filter
|
|
171
|
+
limit: Maximum number of results
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
ServiceResult with list of ContactThreads
|
|
175
|
+
"""
|
|
176
|
+
try:
|
|
177
|
+
temp_thread = ContactThread()
|
|
178
|
+
temp_thread.tenant_id = tenant_id
|
|
179
|
+
temp_thread.status = status
|
|
180
|
+
|
|
181
|
+
result = self._query_by_index(
|
|
182
|
+
temp_thread,
|
|
183
|
+
"gsi2",
|
|
184
|
+
ascending=False,
|
|
185
|
+
limit=limit
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
if not result.success:
|
|
189
|
+
return result
|
|
190
|
+
|
|
191
|
+
# Filter out deleted threads
|
|
192
|
+
active_threads = [t for t in result.data if not t.is_deleted()]
|
|
193
|
+
return ServiceResult.success_result(active_threads)
|
|
194
|
+
|
|
195
|
+
except Exception as e:
|
|
196
|
+
return self._handle_service_exception(e, 'list_by_tenant_and_status',
|
|
197
|
+
tenant_id=tenant_id, status=status)
|
|
198
|
+
|
|
199
|
+
def list_by_assigned_user(self, assigned_to: str, tenant_id: str,
|
|
200
|
+
status: str = None, limit: int = 50) -> ServiceResult[List[ContactThread]]:
|
|
201
|
+
"""
|
|
202
|
+
List contact threads assigned to a specific user using GSI3.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
assigned_to: Staff user ID
|
|
206
|
+
tenant_id: Tenant ID for filtering
|
|
207
|
+
status: Optional status filter
|
|
208
|
+
limit: Maximum number of results
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
ServiceResult with list of ContactThreads
|
|
212
|
+
"""
|
|
213
|
+
try:
|
|
214
|
+
temp_thread = ContactThread()
|
|
215
|
+
temp_thread.assigned_to = assigned_to
|
|
216
|
+
if status:
|
|
217
|
+
temp_thread.status = status
|
|
218
|
+
|
|
219
|
+
result = self._query_by_index(
|
|
220
|
+
temp_thread,
|
|
221
|
+
"gsi3",
|
|
222
|
+
ascending=False,
|
|
223
|
+
limit=limit
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
if not result.success:
|
|
227
|
+
return result
|
|
228
|
+
|
|
229
|
+
# Filter by tenant and exclude deleted threads
|
|
230
|
+
active_threads = [
|
|
231
|
+
t for t in result.data
|
|
232
|
+
if not t.is_deleted() and t.tenant_id == tenant_id
|
|
233
|
+
]
|
|
234
|
+
|
|
235
|
+
return ServiceResult.success_result(active_threads)
|
|
236
|
+
|
|
237
|
+
except Exception as e:
|
|
238
|
+
return self._handle_service_exception(e, 'list_by_assigned_user',
|
|
239
|
+
assigned_to=assigned_to, tenant_id=tenant_id)
|
|
240
|
+
|
|
241
|
+
def list_by_sender_email(self, sender_email: str, tenant_id: str,
|
|
242
|
+
limit: int = 50) -> ServiceResult[List[ContactThread]]:
|
|
243
|
+
"""
|
|
244
|
+
List all contact threads from a specific sender email using GSI5.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
sender_email: Sender's email address
|
|
248
|
+
tenant_id: Tenant ID for filtering
|
|
249
|
+
limit: Maximum number of results
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
ServiceResult with list of ContactThreads
|
|
253
|
+
"""
|
|
254
|
+
try:
|
|
255
|
+
temp_thread = ContactThread()
|
|
256
|
+
temp_thread.sender = {"email": sender_email}
|
|
257
|
+
|
|
258
|
+
result = self._query_by_index(
|
|
259
|
+
temp_thread,
|
|
260
|
+
"gsi5",
|
|
261
|
+
ascending=False,
|
|
262
|
+
limit=limit
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
if not result.success:
|
|
266
|
+
return result
|
|
267
|
+
|
|
268
|
+
# Filter by tenant and exclude deleted threads
|
|
269
|
+
active_threads = [
|
|
270
|
+
t for t in result.data
|
|
271
|
+
if not t.is_deleted() and t.tenant_id == tenant_id
|
|
272
|
+
]
|
|
273
|
+
|
|
274
|
+
return ServiceResult.success_result(active_threads)
|
|
275
|
+
|
|
276
|
+
except Exception as e:
|
|
277
|
+
return self._handle_service_exception(e, 'list_by_sender_email',
|
|
278
|
+
sender_email=sender_email, tenant_id=tenant_id)
|
|
279
|
+
|
|
280
|
+
def add_message(self, thread_id: str, tenant_id: str, user_id: str,
|
|
281
|
+
message_data: Dict[str, Any], user_inboxes: List[str] = None) -> ServiceResult[ContactThread]:
|
|
282
|
+
"""
|
|
283
|
+
Add a message to an existing contact thread.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
thread_id: Thread ID
|
|
287
|
+
tenant_id: Tenant ID
|
|
288
|
+
user_id: User ID posting the message
|
|
289
|
+
message_data: Message data including content, sender_name, is_staff_reply
|
|
290
|
+
user_inboxes: List of inbox IDs the user has access to
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
ServiceResult with updated ContactThread
|
|
294
|
+
"""
|
|
295
|
+
try:
|
|
296
|
+
# Get the thread
|
|
297
|
+
thread_result = self.get_by_id(thread_id, tenant_id, user_id, user_inboxes)
|
|
298
|
+
if not thread_result.success:
|
|
299
|
+
return thread_result
|
|
300
|
+
|
|
301
|
+
thread = thread_result.data
|
|
302
|
+
|
|
303
|
+
# Create the message
|
|
304
|
+
message = {
|
|
305
|
+
"id": message_data.get("id", f"msg_{dt.datetime.now(dt.UTC).timestamp()}"),
|
|
306
|
+
"content": message_data.get("content", ""),
|
|
307
|
+
"sender_id": user_id,
|
|
308
|
+
"sender_name": message_data.get("sender_name", ""),
|
|
309
|
+
"is_staff_reply": message_data.get("is_staff_reply", False),
|
|
310
|
+
"created_at": dt.datetime.now(dt.UTC).timestamp()
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
thread.add_message(message)
|
|
314
|
+
|
|
315
|
+
# Update metadata
|
|
316
|
+
thread.updated_by_id = user_id
|
|
317
|
+
thread.prep_for_save()
|
|
318
|
+
|
|
319
|
+
# Save the updated thread
|
|
320
|
+
return self._save_model(thread)
|
|
321
|
+
|
|
322
|
+
except Exception as e:
|
|
323
|
+
return self._handle_service_exception(e, 'add_message_to_contact_thread',
|
|
324
|
+
thread_id=thread_id, tenant_id=tenant_id)
|
|
325
|
+
|
|
326
|
+
def assign_thread(self, thread_id: str, tenant_id: str, user_id: str,
|
|
327
|
+
assigned_to: str, user_inboxes: List[str] = None) -> ServiceResult[ContactThread]:
|
|
328
|
+
"""
|
|
329
|
+
Assign a contact thread to a staff member.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
thread_id: Thread ID
|
|
333
|
+
tenant_id: Tenant ID
|
|
334
|
+
user_id: User ID performing the assignment
|
|
335
|
+
assigned_to: Staff user ID to assign to
|
|
336
|
+
user_inboxes: List of inbox IDs the user has access to
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
ServiceResult with updated ContactThread
|
|
340
|
+
"""
|
|
341
|
+
try:
|
|
342
|
+
thread_result = self.get_by_id(thread_id, tenant_id, user_id, user_inboxes)
|
|
343
|
+
if not thread_result.success:
|
|
344
|
+
return thread_result
|
|
345
|
+
|
|
346
|
+
thread = thread_result.data
|
|
347
|
+
thread.assign(assigned_to)
|
|
348
|
+
thread.updated_by_id = user_id
|
|
349
|
+
thread.prep_for_save()
|
|
350
|
+
|
|
351
|
+
return self._save_model(thread)
|
|
352
|
+
|
|
353
|
+
except Exception as e:
|
|
354
|
+
return self._handle_service_exception(e, 'assign_contact_thread',
|
|
355
|
+
thread_id=thread_id, assigned_to=assigned_to)
|
|
356
|
+
|
|
357
|
+
def update_status(self, thread_id: str, tenant_id: str, user_id: str,
|
|
358
|
+
status: str, user_inboxes: List[str] = None) -> ServiceResult[ContactThread]:
|
|
359
|
+
"""
|
|
360
|
+
Update the status of a contact thread.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
thread_id: Thread ID
|
|
364
|
+
tenant_id: Tenant ID
|
|
365
|
+
user_id: User ID performing the update
|
|
366
|
+
status: New status (open, in_progress, resolved, closed)
|
|
367
|
+
user_inboxes: List of inbox IDs the user has access to
|
|
368
|
+
|
|
369
|
+
Returns:
|
|
370
|
+
ServiceResult with updated ContactThread
|
|
371
|
+
"""
|
|
372
|
+
try:
|
|
373
|
+
thread_result = self.get_by_id(thread_id, tenant_id, user_id, user_inboxes)
|
|
374
|
+
if not thread_result.success:
|
|
375
|
+
return thread_result
|
|
376
|
+
|
|
377
|
+
thread = thread_result.data
|
|
378
|
+
thread.status = status
|
|
379
|
+
thread.updated_by_id = user_id
|
|
380
|
+
thread.prep_for_save()
|
|
381
|
+
|
|
382
|
+
return self._save_model(thread)
|
|
383
|
+
|
|
384
|
+
except Exception as e:
|
|
385
|
+
return self._handle_service_exception(e, 'update_contact_thread_status',
|
|
386
|
+
thread_id=thread_id, status=status)
|
|
387
|
+
|
|
388
|
+
def update(self, resource_id: str, tenant_id: str, user_id: str,
|
|
389
|
+
updates: Dict[str, Any], user_inboxes: List[str] = None) -> ServiceResult[ContactThread]:
|
|
390
|
+
"""
|
|
391
|
+
Update contact thread with access control.
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
resource_id: Thread ID
|
|
395
|
+
tenant_id: Tenant ID
|
|
396
|
+
user_id: User ID performing the update
|
|
397
|
+
updates: Dictionary of fields to update
|
|
398
|
+
user_inboxes: List of inbox IDs the user has access to
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
ServiceResult with updated ContactThread
|
|
402
|
+
"""
|
|
403
|
+
try:
|
|
404
|
+
# Get existing thread
|
|
405
|
+
thread = self._get_model_by_id(resource_id, ContactThread)
|
|
406
|
+
|
|
407
|
+
if not thread:
|
|
408
|
+
raise NotFoundError(f"Contact thread with ID {resource_id} not found")
|
|
409
|
+
|
|
410
|
+
# Validate tenant access
|
|
411
|
+
if hasattr(thread, 'tenant_id'):
|
|
412
|
+
self._validate_tenant_access(thread.tenant_id, tenant_id)
|
|
413
|
+
|
|
414
|
+
# Check permissions
|
|
415
|
+
if not thread.can_user_access(user_id, user_inboxes or []):
|
|
416
|
+
raise AccessDeniedError("Access denied: insufficient permissions")
|
|
417
|
+
|
|
418
|
+
# Apply updates (limited fields)
|
|
419
|
+
allowed_fields = ['subject', 'status', 'priority', 'assigned_to', 'tags', 'inbox_id']
|
|
420
|
+
for field, value in updates.items():
|
|
421
|
+
if field in allowed_fields and hasattr(thread, field):
|
|
422
|
+
setattr(thread, field, value)
|
|
423
|
+
|
|
424
|
+
# Update metadata
|
|
425
|
+
thread.updated_by_id = user_id
|
|
426
|
+
thread.prep_for_save()
|
|
427
|
+
|
|
428
|
+
# Save updated thread
|
|
429
|
+
return self._save_model(thread)
|
|
430
|
+
|
|
431
|
+
except Exception as e:
|
|
432
|
+
return self._handle_service_exception(e, 'update_contact_thread', resource_id=resource_id, tenant_id=tenant_id)
|
|
433
|
+
|
|
434
|
+
def delete(self, resource_id: str, tenant_id: str, user_id: str,
|
|
435
|
+
user_inboxes: List[str] = None) -> ServiceResult[bool]:
|
|
436
|
+
"""
|
|
437
|
+
Soft delete contact thread with access control.
|
|
438
|
+
|
|
439
|
+
Args:
|
|
440
|
+
resource_id: Thread ID
|
|
441
|
+
tenant_id: Tenant ID
|
|
442
|
+
user_id: User ID performing the deletion
|
|
443
|
+
user_inboxes: List of inbox IDs the user has access to
|
|
444
|
+
|
|
445
|
+
Returns:
|
|
446
|
+
ServiceResult with boolean success
|
|
447
|
+
"""
|
|
448
|
+
try:
|
|
449
|
+
# Get existing thread
|
|
450
|
+
thread = self._get_model_by_id(resource_id, ContactThread)
|
|
451
|
+
|
|
452
|
+
if not thread:
|
|
453
|
+
raise NotFoundError(f"Contact thread with ID {resource_id} not found")
|
|
454
|
+
|
|
455
|
+
# Check if already deleted
|
|
456
|
+
if thread.is_deleted():
|
|
457
|
+
return ServiceResult.success_result(True)
|
|
458
|
+
|
|
459
|
+
# Validate tenant access
|
|
460
|
+
if hasattr(thread, 'tenant_id'):
|
|
461
|
+
self._validate_tenant_access(thread.tenant_id, tenant_id)
|
|
462
|
+
|
|
463
|
+
# Check permissions
|
|
464
|
+
if not thread.can_user_access(user_id, user_inboxes or []):
|
|
465
|
+
raise AccessDeniedError("Access denied: insufficient permissions")
|
|
466
|
+
|
|
467
|
+
# Soft delete: set deleted timestamp and metadata
|
|
468
|
+
thread.deleted_utc_ts = dt.datetime.now(dt.UTC).timestamp()
|
|
469
|
+
thread.deleted_by_id = user_id
|
|
470
|
+
thread.prep_for_save()
|
|
471
|
+
|
|
472
|
+
# Save the updated thread
|
|
473
|
+
save_result = self._save_model(thread)
|
|
474
|
+
if save_result.success:
|
|
475
|
+
return ServiceResult.success_result(True)
|
|
476
|
+
else:
|
|
477
|
+
return save_result
|
|
478
|
+
|
|
479
|
+
except Exception as e:
|
|
480
|
+
return self._handle_service_exception(e, 'delete_contact_thread', resource_id=resource_id, tenant_id=tenant_id)
|
|
481
|
+
|
|
482
|
+
def _handle_service_exception(self, exception: Exception, operation: str, **context) -> ServiceResult:
|
|
483
|
+
"""
|
|
484
|
+
Handle service exceptions with consistent error responses.
|
|
485
|
+
|
|
486
|
+
Delegates to parent class for proper error code mapping.
|
|
487
|
+
|
|
488
|
+
Args:
|
|
489
|
+
exception: The exception that occurred
|
|
490
|
+
operation: Name of the operation that failed
|
|
491
|
+
**context: Additional context for debugging
|
|
492
|
+
|
|
493
|
+
Returns:
|
|
494
|
+
ServiceResult with error details
|
|
495
|
+
"""
|
|
496
|
+
# Use parent's exception handler for proper error code mapping
|
|
497
|
+
return super()._handle_service_exception(exception, operation, **context)
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# src/geek_cafe_saas_sdk/lambda_handlers/subscriptions/activate/app.py
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Dict, Any
|
|
5
|
+
|
|
6
|
+
from geek_cafe_saas_sdk.services import SubscriptionService
|
|
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
|
+
subscription_service_pool = ServicePool(SubscriptionService)
|
|
12
|
+
|
|
13
|
+
def handler(event: Dict[str, Any], context: object, injected_service=None) -> Dict[str, Any]:
|
|
14
|
+
"""
|
|
15
|
+
Lambda handler for activating a subscription (upgrade/downgrade).
|
|
16
|
+
|
|
17
|
+
This creates a new subscription and sets it as the tenant's active subscription.
|
|
18
|
+
Updates the tenant's plan_tier automatically.
|
|
19
|
+
|
|
20
|
+
Expected body:
|
|
21
|
+
{
|
|
22
|
+
"plan_code": "pro_monthly",
|
|
23
|
+
"plan_name": "Pro Plan",
|
|
24
|
+
"price_cents": 2999,
|
|
25
|
+
"seat_count": 10,
|
|
26
|
+
"current_period_start_utc_ts": 1729123200.0,
|
|
27
|
+
"current_period_end_utc_ts": 1731801600.0
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
event: API Gateway event
|
|
32
|
+
context: Lambda context
|
|
33
|
+
injected_service: Optional SubscriptionService for testing
|
|
34
|
+
"""
|
|
35
|
+
try:
|
|
36
|
+
subscription_service = injected_service if injected_service else subscription_service_pool.get()
|
|
37
|
+
body = LambdaEventUtility.get_body_from_event(event)
|
|
38
|
+
user_id = LambdaEventUtility.get_authenticated_user_id(event)
|
|
39
|
+
tenant_id = LambdaEventUtility.get_authenticated_user_tenant_id(event)
|
|
40
|
+
|
|
41
|
+
result = subscription_service.activate_subscription(
|
|
42
|
+
tenant_id=tenant_id,
|
|
43
|
+
user_id=user_id,
|
|
44
|
+
payload=body
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
return service_result_to_response(result, success_status=201)
|
|
48
|
+
|
|
49
|
+
except json.JSONDecodeError:
|
|
50
|
+
return error_response("Invalid JSON in request body.", "VALIDATION_ERROR", 400)
|
|
51
|
+
except Exception as e:
|
|
52
|
+
return error_response(f"An unexpected error occurred: {str(e)}", "INTERNAL_ERROR", 500)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# src/geek_cafe_saas_sdk/lambda_handlers/subscriptions/active/app.py
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Any
|
|
4
|
+
|
|
5
|
+
from geek_cafe_saas_sdk.services import SubscriptionService
|
|
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
|
+
subscription_service_pool = ServicePool(SubscriptionService)
|
|
11
|
+
|
|
12
|
+
def handler(event: Dict[str, Any], context: object, injected_service=None) -> Dict[str, Any]:
|
|
13
|
+
"""
|
|
14
|
+
Lambda handler for retrieving the tenant's active subscription.
|
|
15
|
+
|
|
16
|
+
This is a convenience endpoint for /subscriptions/active to get the current subscription.
|
|
17
|
+
Uses the active subscription pointer for O(1) lookup.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
event: API Gateway event
|
|
21
|
+
context: Lambda context
|
|
22
|
+
injected_service: Optional SubscriptionService for testing
|
|
23
|
+
"""
|
|
24
|
+
try:
|
|
25
|
+
subscription_service = injected_service if injected_service else subscription_service_pool.get()
|
|
26
|
+
user_id = LambdaEventUtility.get_authenticated_user_id(event)
|
|
27
|
+
tenant_id = LambdaEventUtility.get_authenticated_user_tenant_id(event)
|
|
28
|
+
|
|
29
|
+
result = subscription_service.get_active_subscription(
|
|
30
|
+
tenant_id=tenant_id,
|
|
31
|
+
user_id=user_id
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
return service_result_to_response(result)
|
|
35
|
+
|
|
36
|
+
except Exception as e:
|
|
37
|
+
return error_response(f"An unexpected error occurred: {str(e)}", "INTERNAL_ERROR", 500)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# src/geek_cafe_saas_sdk/lambda_handlers/subscriptions/cancel/app.py
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Dict, Any
|
|
5
|
+
|
|
6
|
+
from geek_cafe_saas_sdk.services import SubscriptionService
|
|
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
|
+
subscription_service_pool = ServicePool(SubscriptionService)
|
|
12
|
+
|
|
13
|
+
def handler(event: Dict[str, Any], context: object, injected_service=None) -> Dict[str, Any]:
|
|
14
|
+
"""
|
|
15
|
+
Lambda handler for canceling a subscription.
|
|
16
|
+
|
|
17
|
+
Expected body (optional):
|
|
18
|
+
{
|
|
19
|
+
"reason": "User requested cancellation",
|
|
20
|
+
"immediate": false // If true, cancel immediately. If false, cancel at period end.
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
event: API Gateway event
|
|
25
|
+
context: Lambda context
|
|
26
|
+
injected_service: Optional SubscriptionService for testing
|
|
27
|
+
"""
|
|
28
|
+
try:
|
|
29
|
+
subscription_service = injected_service if injected_service else subscription_service_pool.get()
|
|
30
|
+
body = LambdaEventUtility.get_body_from_event(event) or {}
|
|
31
|
+
user_id = LambdaEventUtility.get_authenticated_user_id(event)
|
|
32
|
+
tenant_id = LambdaEventUtility.get_authenticated_user_tenant_id(event)
|
|
33
|
+
resource_id = LambdaEventUtility.get_value_from_path_parameters(event, 'id')
|
|
34
|
+
|
|
35
|
+
if not resource_id:
|
|
36
|
+
return error_response("Subscription ID is required in the path.", "VALIDATION_ERROR", 400)
|
|
37
|
+
|
|
38
|
+
# Get optional cancellation parameters
|
|
39
|
+
reason = body.get('reason', 'User requested cancellation')
|
|
40
|
+
immediate = body.get('immediate', False)
|
|
41
|
+
|
|
42
|
+
result = subscription_service.cancel_subscription(
|
|
43
|
+
subscription_id=resource_id,
|
|
44
|
+
tenant_id=tenant_id,
|
|
45
|
+
user_id=user_id,
|
|
46
|
+
reason=reason,
|
|
47
|
+
immediate=immediate
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
return service_result_to_response(result)
|
|
51
|
+
|
|
52
|
+
except json.JSONDecodeError:
|
|
53
|
+
return error_response("Invalid JSON in request body.", "VALIDATION_ERROR", 400)
|
|
54
|
+
except Exception as e:
|
|
55
|
+
return error_response(f"An unexpected error occurred: {str(e)}", "INTERNAL_ERROR", 500)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# src/geek_cafe_saas_sdk/lambda_handlers/subscriptions/get/app.py
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Any
|
|
4
|
+
|
|
5
|
+
from geek_cafe_saas_sdk.services import SubscriptionService
|
|
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
|
+
subscription_service_pool = ServicePool(SubscriptionService)
|
|
11
|
+
|
|
12
|
+
def handler(event: Dict[str, Any], context: object, injected_service=None) -> Dict[str, Any]:
|
|
13
|
+
"""
|
|
14
|
+
Lambda handler for retrieving a subscription by ID.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
event: API Gateway event
|
|
18
|
+
context: Lambda context
|
|
19
|
+
injected_service: Optional SubscriptionService for testing
|
|
20
|
+
"""
|
|
21
|
+
try:
|
|
22
|
+
subscription_service = injected_service if injected_service else subscription_service_pool.get()
|
|
23
|
+
user_id = LambdaEventUtility.get_authenticated_user_id(event)
|
|
24
|
+
tenant_id = LambdaEventUtility.get_authenticated_user_tenant_id(event)
|
|
25
|
+
resource_id = LambdaEventUtility.get_value_from_path_parameters(event, 'id')
|
|
26
|
+
|
|
27
|
+
if not resource_id:
|
|
28
|
+
return error_response("Subscription ID is required in the path.", "VALIDATION_ERROR", 400)
|
|
29
|
+
|
|
30
|
+
result = subscription_service.get_by_id(
|
|
31
|
+
resource_id=resource_id,
|
|
32
|
+
tenant_id=tenant_id,
|
|
33
|
+
user_id=user_id
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
return service_result_to_response(result)
|
|
37
|
+
|
|
38
|
+
except Exception as e:
|
|
39
|
+
return error_response(f"An unexpected error occurred: {str(e)}", "INTERNAL_ERROR", 500)
|