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,392 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Geek Cafe, LLC
|
|
3
|
+
MIT License. See Project Root for the license information.
|
|
4
|
+
|
|
5
|
+
ContactThread model for guest-initiated contact and support tickets.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from boto3_assist.dynamodb.dynamodb_index import DynamoDBIndex, DynamoDBKey
|
|
9
|
+
from boto3_assist.utilities.string_utility import StringUtility
|
|
10
|
+
import datetime as dt
|
|
11
|
+
from typing import List, Optional, Dict, Any
|
|
12
|
+
from geek_cafe_saas_sdk.models.base_model import BaseModel
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ContactThread(BaseModel):
|
|
16
|
+
"""
|
|
17
|
+
ContactThread model for contact forms, support tickets, and guest communications.
|
|
18
|
+
|
|
19
|
+
Optimized for low-volume conversations with asymmetric access (guest sender vs staff responders).
|
|
20
|
+
Supports status workflow, assignment, and notification tracking.
|
|
21
|
+
|
|
22
|
+
Features:
|
|
23
|
+
- Guest-initiated contact (no auth required to create)
|
|
24
|
+
- Staff response and assignment tracking
|
|
25
|
+
- Status workflow (open, in_progress, resolved, closed)
|
|
26
|
+
- Priority levels
|
|
27
|
+
- Email notification support
|
|
28
|
+
- Inbox-based routing (support, sales, billing, etc.)
|
|
29
|
+
- Embedded messages (suitable for ~100 messages max)
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self):
|
|
33
|
+
super().__init__()
|
|
34
|
+
self._subject: str | None = None
|
|
35
|
+
self._status: str = "open" # open, in_progress, resolved, closed
|
|
36
|
+
self._priority: str = "medium" # low, medium, high, urgent
|
|
37
|
+
|
|
38
|
+
# Sender information (guest or authenticated user)
|
|
39
|
+
self._sender: Dict[str, Any] = {} # {id, name, email, session_id}
|
|
40
|
+
|
|
41
|
+
# Assignment and routing
|
|
42
|
+
self._assigned_to: str | None = None # Staff user ID
|
|
43
|
+
self._inbox_id: str = "support" # support, sales, billing, etc.
|
|
44
|
+
|
|
45
|
+
# Messages embedded in thread (suitable for low volume)
|
|
46
|
+
self._messages: List[Dict[str, Any]] = []
|
|
47
|
+
|
|
48
|
+
# Timestamps for workflow tracking
|
|
49
|
+
self._first_response_at: float | None = None
|
|
50
|
+
self._resolved_at: float | None = None
|
|
51
|
+
self._last_message_at: float | None = None
|
|
52
|
+
|
|
53
|
+
# Notification tracking
|
|
54
|
+
self._guest_notified: bool = False
|
|
55
|
+
self._notification_email: str | None = None
|
|
56
|
+
|
|
57
|
+
# Metadata and tagging
|
|
58
|
+
self._tags: List[str] = []
|
|
59
|
+
self._source: str = "web" # web, mobile, api, email
|
|
60
|
+
self._is_archived: bool = False
|
|
61
|
+
|
|
62
|
+
self._setup_indexes()
|
|
63
|
+
|
|
64
|
+
def _setup_indexes(self):
|
|
65
|
+
"""Setup DynamoDB indexes for efficient querying."""
|
|
66
|
+
|
|
67
|
+
# Primary index: threads by ID
|
|
68
|
+
primary: DynamoDBIndex = DynamoDBIndex()
|
|
69
|
+
primary.name = "primary"
|
|
70
|
+
primary.partition_key.attribute_name = "pk"
|
|
71
|
+
primary.partition_key.value = lambda: DynamoDBKey.build_key(("contact", self.id))
|
|
72
|
+
primary.sort_key.attribute_name = "sk"
|
|
73
|
+
primary.sort_key.value = lambda: DynamoDBKey.build_key(("contact", self.id))
|
|
74
|
+
self.indexes.add_primary(primary)
|
|
75
|
+
|
|
76
|
+
# GSI1: Query by inbox and status (most common query)
|
|
77
|
+
# Allows: "Show me all open tickets in support inbox"
|
|
78
|
+
gsi: DynamoDBIndex = DynamoDBIndex()
|
|
79
|
+
gsi.name = "gsi1"
|
|
80
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
81
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(
|
|
82
|
+
("inbox", self.inbox_id),
|
|
83
|
+
("status", self.status)
|
|
84
|
+
)
|
|
85
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
86
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(
|
|
87
|
+
("priority", self.priority),
|
|
88
|
+
("ts", self.created_utc_ts)
|
|
89
|
+
)
|
|
90
|
+
self.indexes.add_secondary(gsi)
|
|
91
|
+
|
|
92
|
+
# GSI2: Query by tenant and status
|
|
93
|
+
# Allows: "Show me all open tickets for this tenant"
|
|
94
|
+
gsi: DynamoDBIndex = DynamoDBIndex()
|
|
95
|
+
gsi.name = "gsi2"
|
|
96
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
97
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(
|
|
98
|
+
("tenant", self.tenant_id),
|
|
99
|
+
("status", self.status)
|
|
100
|
+
)
|
|
101
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
102
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(
|
|
103
|
+
("ts", self.last_message_at or self.created_utc_ts)
|
|
104
|
+
)
|
|
105
|
+
self.indexes.add_secondary(gsi)
|
|
106
|
+
|
|
107
|
+
# GSI3: Query by assigned staff member
|
|
108
|
+
# Allows: "Show me all tickets assigned to me"
|
|
109
|
+
gsi: DynamoDBIndex = DynamoDBIndex()
|
|
110
|
+
gsi.name = "gsi3"
|
|
111
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
112
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(
|
|
113
|
+
("assigned", self.assigned_to)
|
|
114
|
+
)
|
|
115
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
116
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(
|
|
117
|
+
("status", self.status),
|
|
118
|
+
("ts", self.last_message_at or self.created_utc_ts)
|
|
119
|
+
)
|
|
120
|
+
self.indexes.add_secondary(gsi)
|
|
121
|
+
|
|
122
|
+
# GSI4: All threads by tenant (for admin views)
|
|
123
|
+
gsi: DynamoDBIndex = DynamoDBIndex()
|
|
124
|
+
gsi.name = "gsi4"
|
|
125
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
126
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(
|
|
127
|
+
("tenant", self.tenant_id)
|
|
128
|
+
)
|
|
129
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
130
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(
|
|
131
|
+
("model", "contact"),
|
|
132
|
+
("ts", self.created_utc_ts)
|
|
133
|
+
)
|
|
134
|
+
self.indexes.add_secondary(gsi)
|
|
135
|
+
|
|
136
|
+
# GSI5: Query by sender email (find all contacts from same guest)
|
|
137
|
+
gsi: DynamoDBIndex = DynamoDBIndex()
|
|
138
|
+
gsi.name = "gsi5"
|
|
139
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
140
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(
|
|
141
|
+
("sender", self.sender.get("email") if self.sender else None)
|
|
142
|
+
)
|
|
143
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
144
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(
|
|
145
|
+
("ts", self.created_utc_ts)
|
|
146
|
+
)
|
|
147
|
+
self.indexes.add_secondary(gsi)
|
|
148
|
+
|
|
149
|
+
# Subject
|
|
150
|
+
@property
|
|
151
|
+
def subject(self) -> str | None:
|
|
152
|
+
return self._subject
|
|
153
|
+
|
|
154
|
+
@subject.setter
|
|
155
|
+
def subject(self, value: str | None):
|
|
156
|
+
self._subject = value
|
|
157
|
+
|
|
158
|
+
# Status
|
|
159
|
+
@property
|
|
160
|
+
def status(self) -> str:
|
|
161
|
+
return self._status
|
|
162
|
+
|
|
163
|
+
@status.setter
|
|
164
|
+
def status(self, value: str | None):
|
|
165
|
+
valid_statuses = ["open", "in_progress", "resolved", "closed"]
|
|
166
|
+
if value in valid_statuses:
|
|
167
|
+
self._status = value
|
|
168
|
+
# Auto-set resolved_at when status changes to resolved
|
|
169
|
+
if value == "resolved" and not self._resolved_at:
|
|
170
|
+
self._resolved_at = dt.datetime.now(dt.UTC).timestamp()
|
|
171
|
+
else:
|
|
172
|
+
self._status = "open" # default
|
|
173
|
+
|
|
174
|
+
# Priority
|
|
175
|
+
@property
|
|
176
|
+
def priority(self) -> str:
|
|
177
|
+
return self._priority
|
|
178
|
+
|
|
179
|
+
@priority.setter
|
|
180
|
+
def priority(self, value: str | None):
|
|
181
|
+
valid_priorities = ["low", "medium", "high", "urgent"]
|
|
182
|
+
if value in valid_priorities:
|
|
183
|
+
self._priority = value
|
|
184
|
+
else:
|
|
185
|
+
self._priority = "medium" # default
|
|
186
|
+
|
|
187
|
+
# Sender
|
|
188
|
+
@property
|
|
189
|
+
def sender(self) -> Dict[str, Any]:
|
|
190
|
+
return self._sender
|
|
191
|
+
|
|
192
|
+
@sender.setter
|
|
193
|
+
def sender(self, value: Dict[str, Any] | None):
|
|
194
|
+
self._sender = value if value else {}
|
|
195
|
+
|
|
196
|
+
# Assigned To
|
|
197
|
+
@property
|
|
198
|
+
def assigned_to(self) -> str | None:
|
|
199
|
+
return self._assigned_to
|
|
200
|
+
|
|
201
|
+
@assigned_to.setter
|
|
202
|
+
def assigned_to(self, value: str | None):
|
|
203
|
+
self._assigned_to = value
|
|
204
|
+
|
|
205
|
+
# Inbox ID
|
|
206
|
+
@property
|
|
207
|
+
def inbox_id(self) -> str:
|
|
208
|
+
return self._inbox_id
|
|
209
|
+
|
|
210
|
+
@inbox_id.setter
|
|
211
|
+
def inbox_id(self, value: str | None):
|
|
212
|
+
self._inbox_id = value if value else "support"
|
|
213
|
+
|
|
214
|
+
# Messages
|
|
215
|
+
@property
|
|
216
|
+
def messages(self) -> List[Dict[str, Any]]:
|
|
217
|
+
return self._messages
|
|
218
|
+
|
|
219
|
+
@messages.setter
|
|
220
|
+
def messages(self, value: List[Dict[str, Any]] | None):
|
|
221
|
+
if value is None:
|
|
222
|
+
self._messages = []
|
|
223
|
+
elif isinstance(value, list):
|
|
224
|
+
self._messages = value
|
|
225
|
+
else:
|
|
226
|
+
self._messages = []
|
|
227
|
+
|
|
228
|
+
# First Response At
|
|
229
|
+
@property
|
|
230
|
+
def first_response_at(self) -> float | None:
|
|
231
|
+
return self._first_response_at
|
|
232
|
+
|
|
233
|
+
@first_response_at.setter
|
|
234
|
+
def first_response_at(self, value: float | None):
|
|
235
|
+
self._first_response_at = value
|
|
236
|
+
|
|
237
|
+
# Resolved At
|
|
238
|
+
@property
|
|
239
|
+
def resolved_at(self) -> float | None:
|
|
240
|
+
return self._resolved_at
|
|
241
|
+
|
|
242
|
+
@resolved_at.setter
|
|
243
|
+
def resolved_at(self, value: float | None):
|
|
244
|
+
self._resolved_at = value
|
|
245
|
+
|
|
246
|
+
# Last Message At
|
|
247
|
+
@property
|
|
248
|
+
def last_message_at(self) -> float | None:
|
|
249
|
+
return self._last_message_at
|
|
250
|
+
|
|
251
|
+
@last_message_at.setter
|
|
252
|
+
def last_message_at(self, value: float | None):
|
|
253
|
+
self._last_message_at = value
|
|
254
|
+
|
|
255
|
+
# Guest Notified
|
|
256
|
+
@property
|
|
257
|
+
def guest_notified(self) -> bool:
|
|
258
|
+
return self._guest_notified
|
|
259
|
+
|
|
260
|
+
@guest_notified.setter
|
|
261
|
+
def guest_notified(self, value: bool | None):
|
|
262
|
+
self._guest_notified = value if value is not None else False
|
|
263
|
+
|
|
264
|
+
# Notification Email
|
|
265
|
+
@property
|
|
266
|
+
def notification_email(self) -> str | None:
|
|
267
|
+
return self._notification_email
|
|
268
|
+
|
|
269
|
+
@notification_email.setter
|
|
270
|
+
def notification_email(self, value: str | None):
|
|
271
|
+
self._notification_email = value
|
|
272
|
+
|
|
273
|
+
# Tags
|
|
274
|
+
@property
|
|
275
|
+
def tags(self) -> List[str]:
|
|
276
|
+
return self._tags
|
|
277
|
+
|
|
278
|
+
@tags.setter
|
|
279
|
+
def tags(self, value: List[str] | None):
|
|
280
|
+
if value is None:
|
|
281
|
+
self._tags = []
|
|
282
|
+
elif isinstance(value, list):
|
|
283
|
+
self._tags = value
|
|
284
|
+
else:
|
|
285
|
+
self._tags = []
|
|
286
|
+
|
|
287
|
+
# Source
|
|
288
|
+
@property
|
|
289
|
+
def source(self) -> str:
|
|
290
|
+
return self._source
|
|
291
|
+
|
|
292
|
+
@source.setter
|
|
293
|
+
def source(self, value: str | None):
|
|
294
|
+
self._source = value if value else "web"
|
|
295
|
+
|
|
296
|
+
# Is Archived
|
|
297
|
+
@property
|
|
298
|
+
def is_archived(self) -> bool:
|
|
299
|
+
return self._is_archived
|
|
300
|
+
|
|
301
|
+
@is_archived.setter
|
|
302
|
+
def is_archived(self, value: bool | None):
|
|
303
|
+
self._is_archived = value if value is not None else False
|
|
304
|
+
|
|
305
|
+
# Helper Methods
|
|
306
|
+
|
|
307
|
+
def add_message(self, message: Dict[str, Any]):
|
|
308
|
+
"""
|
|
309
|
+
Add a message to the contact thread.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
message: Message dict with fields: content, sender_id, sender_name, is_staff_reply, etc.
|
|
313
|
+
"""
|
|
314
|
+
if message not in self._messages:
|
|
315
|
+
# Ensure message has timestamp
|
|
316
|
+
if "created_at" not in message:
|
|
317
|
+
message["created_at"] = dt.datetime.now(dt.UTC).timestamp()
|
|
318
|
+
|
|
319
|
+
self._messages.append(message)
|
|
320
|
+
self._last_message_at = message["created_at"]
|
|
321
|
+
|
|
322
|
+
# Track first staff response
|
|
323
|
+
if message.get("is_staff_reply") and not self._first_response_at:
|
|
324
|
+
self._first_response_at = message["created_at"]
|
|
325
|
+
|
|
326
|
+
def get_message_count(self) -> int:
|
|
327
|
+
"""Get total number of messages in thread."""
|
|
328
|
+
return len(self._messages)
|
|
329
|
+
|
|
330
|
+
def get_recent_messages(self, limit: int = 5) -> List[Dict[str, Any]]:
|
|
331
|
+
"""
|
|
332
|
+
Get recent messages from the thread.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
limit: Number of messages to return
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
List of most recent messages, newest first
|
|
339
|
+
"""
|
|
340
|
+
sorted_messages = sorted(
|
|
341
|
+
self._messages,
|
|
342
|
+
key=lambda m: m.get("created_at", 0),
|
|
343
|
+
reverse=True
|
|
344
|
+
)
|
|
345
|
+
return sorted_messages[:limit]
|
|
346
|
+
|
|
347
|
+
def assign(self, staff_user_id: str):
|
|
348
|
+
"""
|
|
349
|
+
Assign this thread to a staff member.
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
staff_user_id: ID of the staff member to assign to
|
|
353
|
+
"""
|
|
354
|
+
self.assigned_to = staff_user_id
|
|
355
|
+
# Auto-change status to in_progress if currently open
|
|
356
|
+
if self.status == "open":
|
|
357
|
+
self.status = "in_progress"
|
|
358
|
+
|
|
359
|
+
def resolve(self):
|
|
360
|
+
"""Mark this thread as resolved."""
|
|
361
|
+
self.status = "resolved"
|
|
362
|
+
self.resolved_at = dt.datetime.now(dt.UTC).timestamp()
|
|
363
|
+
|
|
364
|
+
def reopen(self):
|
|
365
|
+
"""Reopen a resolved or closed thread."""
|
|
366
|
+
self.status = "open"
|
|
367
|
+
self.resolved_at = None
|
|
368
|
+
|
|
369
|
+
def can_user_access(self, user_id: str, user_inboxes: List[str] = None) -> bool:
|
|
370
|
+
"""
|
|
371
|
+
Check if a user can access this contact thread.
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
user_id: User ID to check
|
|
375
|
+
user_inboxes: List of inbox IDs the user has access to (e.g., ["support-inbox", "sales-inbox"])
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
True if user can access, False otherwise
|
|
379
|
+
"""
|
|
380
|
+
# Check if user is the sender
|
|
381
|
+
if self.sender and self.sender.get("id") == user_id:
|
|
382
|
+
return True
|
|
383
|
+
|
|
384
|
+
# Check if user is assigned to this thread
|
|
385
|
+
if self.assigned_to == user_id:
|
|
386
|
+
return True
|
|
387
|
+
|
|
388
|
+
# Check if user has access to the inbox this thread belongs to
|
|
389
|
+
if user_inboxes and self.inbox_id in user_inboxes:
|
|
390
|
+
return True
|
|
391
|
+
|
|
392
|
+
return False
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Messaging Domain Services
|
|
2
|
+
|
|
3
|
+
from .chat_channel_service import ChatChannelService
|
|
4
|
+
from .chat_message_service import ChatMessageService
|
|
5
|
+
from .contact_thread_service import ContactThreadService
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"ChatChannelService",
|
|
9
|
+
"ChatMessageService",
|
|
10
|
+
"ContactThreadService",
|
|
11
|
+
]
|