geek-cafe-saas-sdk 0.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of geek-cafe-saas-sdk might be problematic. Click here for more details.

Files changed (194) hide show
  1. geek_cafe_saas_sdk/__init__.py +9 -0
  2. geek_cafe_saas_sdk/core/__init__.py +11 -0
  3. geek_cafe_saas_sdk/core/audit_mixin.py +33 -0
  4. geek_cafe_saas_sdk/core/error_codes.py +132 -0
  5. geek_cafe_saas_sdk/core/service_errors.py +19 -0
  6. geek_cafe_saas_sdk/core/service_result.py +121 -0
  7. geek_cafe_saas_sdk/decorators/__init__.py +64 -0
  8. geek_cafe_saas_sdk/decorators/auth.py +373 -0
  9. geek_cafe_saas_sdk/decorators/core.py +358 -0
  10. geek_cafe_saas_sdk/domains/__init__.py +0 -0
  11. geek_cafe_saas_sdk/domains/analytics/__init__.py +0 -0
  12. geek_cafe_saas_sdk/domains/analytics/handlers/__init__.py +0 -0
  13. geek_cafe_saas_sdk/domains/analytics/models/__init__.py +9 -0
  14. geek_cafe_saas_sdk/domains/analytics/models/website_analytics.py +219 -0
  15. geek_cafe_saas_sdk/domains/analytics/models/website_analytics_summary.py +220 -0
  16. geek_cafe_saas_sdk/domains/analytics/services/__init__.py +11 -0
  17. geek_cafe_saas_sdk/domains/analytics/services/website_analytics_service.py +232 -0
  18. geek_cafe_saas_sdk/domains/analytics/services/website_analytics_summary_service.py +212 -0
  19. geek_cafe_saas_sdk/domains/analytics/services/website_analytics_tally_service.py +610 -0
  20. geek_cafe_saas_sdk/domains/auth/__init__.py +0 -0
  21. geek_cafe_saas_sdk/domains/auth/handlers/__init__.py +0 -0
  22. geek_cafe_saas_sdk/domains/auth/handlers/users/create/app.py +41 -0
  23. geek_cafe_saas_sdk/domains/auth/handlers/users/delete/app.py +41 -0
  24. geek_cafe_saas_sdk/domains/auth/handlers/users/get/app.py +39 -0
  25. geek_cafe_saas_sdk/domains/auth/handlers/users/list/app.py +36 -0
  26. geek_cafe_saas_sdk/domains/auth/handlers/users/update/app.py +44 -0
  27. geek_cafe_saas_sdk/domains/auth/models/__init__.py +13 -0
  28. geek_cafe_saas_sdk/domains/auth/models/permission.py +134 -0
  29. geek_cafe_saas_sdk/domains/auth/models/resource_permission.py +245 -0
  30. geek_cafe_saas_sdk/domains/auth/models/role.py +213 -0
  31. geek_cafe_saas_sdk/domains/auth/models/user.py +285 -0
  32. geek_cafe_saas_sdk/domains/auth/services/__init__.py +16 -0
  33. geek_cafe_saas_sdk/domains/auth/services/authorization_service.py +376 -0
  34. geek_cafe_saas_sdk/domains/auth/services/permission_registry.py +464 -0
  35. geek_cafe_saas_sdk/domains/auth/services/resource_permission_service.py +408 -0
  36. geek_cafe_saas_sdk/domains/auth/services/user_service.py +274 -0
  37. geek_cafe_saas_sdk/domains/communities/__init__.py +0 -0
  38. geek_cafe_saas_sdk/domains/communities/handlers/__init__.py +0 -0
  39. geek_cafe_saas_sdk/domains/communities/handlers/communities/create/app.py +41 -0
  40. geek_cafe_saas_sdk/domains/communities/handlers/communities/delete/app.py +41 -0
  41. geek_cafe_saas_sdk/domains/communities/handlers/communities/get/app.py +39 -0
  42. geek_cafe_saas_sdk/domains/communities/handlers/communities/list/app.py +36 -0
  43. geek_cafe_saas_sdk/domains/communities/handlers/communities/update/app.py +44 -0
  44. geek_cafe_saas_sdk/domains/communities/models/__init__.py +6 -0
  45. geek_cafe_saas_sdk/domains/communities/models/community.py +326 -0
  46. geek_cafe_saas_sdk/domains/communities/models/community_member.py +227 -0
  47. geek_cafe_saas_sdk/domains/communities/services/__init__.py +6 -0
  48. geek_cafe_saas_sdk/domains/communities/services/community_member_service.py +412 -0
  49. geek_cafe_saas_sdk/domains/communities/services/community_service.py +479 -0
  50. geek_cafe_saas_sdk/domains/events/__init__.py +0 -0
  51. geek_cafe_saas_sdk/domains/events/handlers/__init__.py +0 -0
  52. geek_cafe_saas_sdk/domains/events/handlers/attendees/app.py +67 -0
  53. geek_cafe_saas_sdk/domains/events/handlers/cancel/app.py +66 -0
  54. geek_cafe_saas_sdk/domains/events/handlers/check_in/app.py +60 -0
  55. geek_cafe_saas_sdk/domains/events/handlers/create/app.py +93 -0
  56. geek_cafe_saas_sdk/domains/events/handlers/delete/app.py +42 -0
  57. geek_cafe_saas_sdk/domains/events/handlers/get/app.py +39 -0
  58. geek_cafe_saas_sdk/domains/events/handlers/invite/app.py +98 -0
  59. geek_cafe_saas_sdk/domains/events/handlers/list/app.py +125 -0
  60. geek_cafe_saas_sdk/domains/events/handlers/publish/app.py +49 -0
  61. geek_cafe_saas_sdk/domains/events/handlers/rsvp/app.py +83 -0
  62. geek_cafe_saas_sdk/domains/events/handlers/update/app.py +44 -0
  63. geek_cafe_saas_sdk/domains/events/models/__init__.py +3 -0
  64. geek_cafe_saas_sdk/domains/events/models/event.py +681 -0
  65. geek_cafe_saas_sdk/domains/events/models/event_attendee.py +324 -0
  66. geek_cafe_saas_sdk/domains/events/services/__init__.py +9 -0
  67. geek_cafe_saas_sdk/domains/events/services/event_attendee_service.py +571 -0
  68. geek_cafe_saas_sdk/domains/events/services/event_service.py +684 -0
  69. geek_cafe_saas_sdk/domains/files/__init__.py +0 -0
  70. geek_cafe_saas_sdk/domains/files/models/__init__.py +0 -0
  71. geek_cafe_saas_sdk/domains/files/models/directory.py +258 -0
  72. geek_cafe_saas_sdk/domains/files/models/file.py +312 -0
  73. geek_cafe_saas_sdk/domains/files/models/file_share.py +268 -0
  74. geek_cafe_saas_sdk/domains/files/models/file_version.py +216 -0
  75. geek_cafe_saas_sdk/domains/files/services/__init__.py +0 -0
  76. geek_cafe_saas_sdk/domains/files/services/directory_service.py +701 -0
  77. geek_cafe_saas_sdk/domains/files/services/file_share_service.py +663 -0
  78. geek_cafe_saas_sdk/domains/files/services/file_system_service.py +575 -0
  79. geek_cafe_saas_sdk/domains/files/services/file_version_service.py +739 -0
  80. geek_cafe_saas_sdk/domains/files/services/s3_file_service.py +501 -0
  81. geek_cafe_saas_sdk/domains/messaging/__init__.py +0 -0
  82. geek_cafe_saas_sdk/domains/messaging/handlers/__init__.py +0 -0
  83. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/create/app.py +86 -0
  84. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/delete/app.py +65 -0
  85. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/get/app.py +64 -0
  86. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/list/app.py +97 -0
  87. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/update/app.py +149 -0
  88. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/create/app.py +67 -0
  89. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/delete/app.py +65 -0
  90. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/get/app.py +64 -0
  91. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/list/app.py +102 -0
  92. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/update/app.py +127 -0
  93. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/create/app.py +94 -0
  94. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/delete/app.py +66 -0
  95. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/get/app.py +67 -0
  96. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/list/app.py +95 -0
  97. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/update/app.py +156 -0
  98. geek_cafe_saas_sdk/domains/messaging/models/__init__.py +13 -0
  99. geek_cafe_saas_sdk/domains/messaging/models/chat_channel.py +337 -0
  100. geek_cafe_saas_sdk/domains/messaging/models/chat_channel_member.py +180 -0
  101. geek_cafe_saas_sdk/domains/messaging/models/chat_message.py +426 -0
  102. geek_cafe_saas_sdk/domains/messaging/models/contact_thread.py +392 -0
  103. geek_cafe_saas_sdk/domains/messaging/services/__init__.py +11 -0
  104. geek_cafe_saas_sdk/domains/messaging/services/chat_channel_service.py +700 -0
  105. geek_cafe_saas_sdk/domains/messaging/services/chat_message_service.py +491 -0
  106. geek_cafe_saas_sdk/domains/messaging/services/contact_thread_service.py +497 -0
  107. geek_cafe_saas_sdk/domains/tenancy/__init__.py +0 -0
  108. geek_cafe_saas_sdk/domains/tenancy/handlers/__init__.py +0 -0
  109. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/activate/app.py +52 -0
  110. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/active/app.py +37 -0
  111. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/cancel/app.py +55 -0
  112. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/get/app.py +39 -0
  113. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/list/app.py +44 -0
  114. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/record_payment/app.py +56 -0
  115. geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/get/app.py +39 -0
  116. geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/me/app.py +37 -0
  117. geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/signup/app.py +61 -0
  118. geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/update/app.py +44 -0
  119. geek_cafe_saas_sdk/domains/tenancy/models/__init__.py +6 -0
  120. geek_cafe_saas_sdk/domains/tenancy/models/subscription.py +440 -0
  121. geek_cafe_saas_sdk/domains/tenancy/models/tenant.py +258 -0
  122. geek_cafe_saas_sdk/domains/tenancy/services/__init__.py +6 -0
  123. geek_cafe_saas_sdk/domains/tenancy/services/subscription_service.py +557 -0
  124. geek_cafe_saas_sdk/domains/tenancy/services/tenant_service.py +575 -0
  125. geek_cafe_saas_sdk/domains/voting/__init__.py +0 -0
  126. geek_cafe_saas_sdk/domains/voting/handlers/__init__.py +0 -0
  127. geek_cafe_saas_sdk/domains/voting/handlers/votes/create/app.py +128 -0
  128. geek_cafe_saas_sdk/domains/voting/handlers/votes/delete/app.py +41 -0
  129. geek_cafe_saas_sdk/domains/voting/handlers/votes/get/app.py +39 -0
  130. geek_cafe_saas_sdk/domains/voting/handlers/votes/list/app.py +38 -0
  131. geek_cafe_saas_sdk/domains/voting/handlers/votes/summerize/README.md +3 -0
  132. geek_cafe_saas_sdk/domains/voting/handlers/votes/update/app.py +44 -0
  133. geek_cafe_saas_sdk/domains/voting/models/__init__.py +9 -0
  134. geek_cafe_saas_sdk/domains/voting/models/vote.py +231 -0
  135. geek_cafe_saas_sdk/domains/voting/models/vote_summary.py +193 -0
  136. geek_cafe_saas_sdk/domains/voting/services/__init__.py +11 -0
  137. geek_cafe_saas_sdk/domains/voting/services/vote_service.py +264 -0
  138. geek_cafe_saas_sdk/domains/voting/services/vote_summary_service.py +198 -0
  139. geek_cafe_saas_sdk/domains/voting/services/vote_tally_service.py +533 -0
  140. geek_cafe_saas_sdk/lambda_handlers/README.md +404 -0
  141. geek_cafe_saas_sdk/lambda_handlers/__init__.py +67 -0
  142. geek_cafe_saas_sdk/lambda_handlers/_base/__init__.py +25 -0
  143. geek_cafe_saas_sdk/lambda_handlers/_base/api_key_handler.py +129 -0
  144. geek_cafe_saas_sdk/lambda_handlers/_base/authorized_secure_handler.py +218 -0
  145. geek_cafe_saas_sdk/lambda_handlers/_base/base_handler.py +185 -0
  146. geek_cafe_saas_sdk/lambda_handlers/_base/handler_factory.py +256 -0
  147. geek_cafe_saas_sdk/lambda_handlers/_base/public_handler.py +53 -0
  148. geek_cafe_saas_sdk/lambda_handlers/_base/secure_handler.py +89 -0
  149. geek_cafe_saas_sdk/lambda_handlers/_base/service_pool.py +94 -0
  150. geek_cafe_saas_sdk/lambda_handlers/directories/create/app.py +79 -0
  151. geek_cafe_saas_sdk/lambda_handlers/directories/delete/app.py +76 -0
  152. geek_cafe_saas_sdk/lambda_handlers/directories/get/app.py +74 -0
  153. geek_cafe_saas_sdk/lambda_handlers/directories/list/app.py +75 -0
  154. geek_cafe_saas_sdk/lambda_handlers/directories/move/app.py +79 -0
  155. geek_cafe_saas_sdk/lambda_handlers/files/delete/app.py +121 -0
  156. geek_cafe_saas_sdk/lambda_handlers/files/download/app.py +187 -0
  157. geek_cafe_saas_sdk/lambda_handlers/files/get/app.py +127 -0
  158. geek_cafe_saas_sdk/lambda_handlers/files/list/app.py +108 -0
  159. geek_cafe_saas_sdk/lambda_handlers/files/share/app.py +83 -0
  160. geek_cafe_saas_sdk/lambda_handlers/files/shares/list/app.py +84 -0
  161. geek_cafe_saas_sdk/lambda_handlers/files/shares/revoke/app.py +76 -0
  162. geek_cafe_saas_sdk/lambda_handlers/files/update/app.py +143 -0
  163. geek_cafe_saas_sdk/lambda_handlers/files/upload/app.py +151 -0
  164. geek_cafe_saas_sdk/middleware/__init__.py +36 -0
  165. geek_cafe_saas_sdk/middleware/auth.py +85 -0
  166. geek_cafe_saas_sdk/middleware/authorization.py +523 -0
  167. geek_cafe_saas_sdk/middleware/cors.py +63 -0
  168. geek_cafe_saas_sdk/middleware/error_handling.py +114 -0
  169. geek_cafe_saas_sdk/middleware/validation.py +80 -0
  170. geek_cafe_saas_sdk/models/__init__.py +20 -0
  171. geek_cafe_saas_sdk/models/base_model.py +233 -0
  172. geek_cafe_saas_sdk/services/__init__.py +18 -0
  173. geek_cafe_saas_sdk/services/database_service.py +441 -0
  174. geek_cafe_saas_sdk/utilities/__init__.py +88 -0
  175. geek_cafe_saas_sdk/utilities/cognito_utility.py +568 -0
  176. geek_cafe_saas_sdk/utilities/custom_exceptions.py +183 -0
  177. geek_cafe_saas_sdk/utilities/datetime_utility.py +410 -0
  178. geek_cafe_saas_sdk/utilities/dictionary_utility.py +78 -0
  179. geek_cafe_saas_sdk/utilities/dynamodb_utils.py +151 -0
  180. geek_cafe_saas_sdk/utilities/environment_loader.py +149 -0
  181. geek_cafe_saas_sdk/utilities/environment_variables.py +228 -0
  182. geek_cafe_saas_sdk/utilities/http_body_parameters.py +44 -0
  183. geek_cafe_saas_sdk/utilities/http_path_parameters.py +60 -0
  184. geek_cafe_saas_sdk/utilities/http_status_code.py +63 -0
  185. geek_cafe_saas_sdk/utilities/jwt_utility.py +234 -0
  186. geek_cafe_saas_sdk/utilities/lambda_event_utility.py +776 -0
  187. geek_cafe_saas_sdk/utilities/logging_utility.py +64 -0
  188. geek_cafe_saas_sdk/utilities/message_query_helper.py +340 -0
  189. geek_cafe_saas_sdk/utilities/response.py +209 -0
  190. geek_cafe_saas_sdk/utilities/string_functions.py +180 -0
  191. geek_cafe_saas_sdk-0.6.0.dist-info/METADATA +397 -0
  192. geek_cafe_saas_sdk-0.6.0.dist-info/RECORD +194 -0
  193. geek_cafe_saas_sdk-0.6.0.dist-info/WHEEL +4 -0
  194. geek_cafe_saas_sdk-0.6.0.dist-info/licenses/LICENSE +47 -0
@@ -0,0 +1,193 @@
1
+
2
+ from boto3_assist.dynamodb.dynamodb_index import DynamoDBIndex, DynamoDBKey
3
+ from boto3_assist.utilities.string_utility import StringUtility
4
+ import datetime as dt
5
+ from typing import Dict, Any
6
+ from geek_cafe_saas_sdk.models.base_model import BaseModel
7
+
8
+ class VoteSummary(BaseModel):
9
+ def __init__(self):
10
+ super().__init__()
11
+ self._content: Dict[str, Any] = {}
12
+ self._target_id: str | None = None
13
+
14
+ # Enhanced summary fields
15
+ self._vote_type: str = "single_choice" # Type of voting used
16
+ self._choice_breakdown: Dict[str, int] = {} # {"A": 150, "B": 100, "C": 50}
17
+ self._choice_percentages: Dict[str, float] = {} # {"A": 50.0, "B": 33.3, "C": 16.7}
18
+ self._choice_averages: Dict[str, float] = {} # For rating votes: {"A": 4.5, "B": 3.2}
19
+ self._total_participants: int = 0 # Number of people who voted
20
+ self._total_selections: int = 0 # For multi-select: total selections made
21
+
22
+ # Legacy fields (for backward compatibility)
23
+ self._total_up_votes: int = 0
24
+ self._total_down_votes: int = 0
25
+ self._total_votes: int = 0
26
+
27
+ self._setup_indexes()
28
+
29
+ def _setup_indexes(self):
30
+ primary: DynamoDBIndex = DynamoDBIndex()
31
+ primary.name = "primary"
32
+ primary.partition_key.attribute_name = "pk"
33
+ primary.partition_key.value = lambda: DynamoDBKey.build_key(
34
+ ("vote-summary", self.id)
35
+ )
36
+
37
+ primary.sort_key.attribute_name = "sk"
38
+ primary.sort_key.value = lambda: DynamoDBKey.build_key(("vote-summary", self.id))
39
+ self.indexes.add_primary(primary)
40
+
41
+ ## GSI: 1
42
+ # GSI: all vote summaries
43
+ gsi: DynamoDBIndex = DynamoDBIndex()
44
+
45
+ gsi.name = "gsi1"
46
+ gsi.partition_key.attribute_name = f"{gsi.name}_pk"
47
+ gsi.partition_key.value = lambda: DynamoDBKey.build_key(("vote-summary", "all"))
48
+ gsi.sort_key.attribute_name = f"{gsi.name}_sk"
49
+ gsi.sort_key.value = lambda: DynamoDBKey.build_key(
50
+ ("ts", self.created_utc_ts)
51
+ )
52
+ self.indexes.add_secondary(gsi)
53
+
54
+ ## GSI: 2
55
+ # GSI: vote summary by target_id (for quick lookup by target)
56
+ gsi: DynamoDBIndex = DynamoDBIndex()
57
+
58
+ gsi.name = "gsi2"
59
+ gsi.partition_key.attribute_name = f"{gsi.name}_pk"
60
+ gsi.partition_key.value = lambda: DynamoDBKey.build_key(("target", self.target_id))
61
+ gsi.sort_key.attribute_name = f"{gsi.name}_sk"
62
+ gsi.sort_key.value = lambda: DynamoDBKey.build_key(
63
+ ("model", "vote-summary")
64
+ )
65
+ self.indexes.add_secondary(gsi)
66
+
67
+ ## GSI: 3
68
+ # GSI: vote summaries by tenant
69
+ gsi: DynamoDBIndex = DynamoDBIndex()
70
+
71
+ gsi.name = "gsi3"
72
+ gsi.partition_key.attribute_name = f"{gsi.name}_pk"
73
+ gsi.partition_key.value = lambda: DynamoDBKey.build_key(("tenant", self.tenant_id))
74
+ gsi.sort_key.attribute_name = f"{gsi.name}_sk"
75
+ gsi.sort_key.value = lambda: DynamoDBKey.build_key(
76
+ ("model", "vote-summary"),("ts", self.created_utc_ts)
77
+ )
78
+ self.indexes.add_secondary(gsi)
79
+
80
+
81
+
82
+ @property
83
+ def content(self) -> Dict[str, Any]:
84
+ """Get content (boto3-assist v0.30.0+ auto-converts Decimals to float)."""
85
+ return self._content
86
+
87
+ @content.setter
88
+ def content(self, value: Dict[str, Any]):
89
+ """Set content (store as-is for boto3-assist compatibility)."""
90
+ self._content = value if value is not None else {}
91
+
92
+
93
+ @property
94
+ def total_up_votes(self) -> int:
95
+ return self._total_up_votes
96
+
97
+ @total_up_votes.setter
98
+ def total_up_votes(self, value: int):
99
+ self._total_up_votes = value
100
+
101
+ @property
102
+ def total_down_votes(self) -> int:
103
+ return self._total_down_votes
104
+
105
+ @total_down_votes.setter
106
+ def total_down_votes(self, value: int):
107
+ self._total_down_votes = value
108
+
109
+ @property
110
+ def total_votes(self) -> int:
111
+ return self._total_votes
112
+
113
+ @total_votes.setter
114
+ def total_votes(self, value: int):
115
+ self._total_votes = value
116
+
117
+ @property
118
+ def target_id(self) -> str | None:
119
+ return self._target_id
120
+
121
+ @target_id.setter
122
+ def target_id(self, value: str | None):
123
+ self._target_id = value
124
+
125
+ @property
126
+ def vote_type(self) -> str:
127
+ return self._vote_type
128
+
129
+ @vote_type.setter
130
+ def vote_type(self, value: str):
131
+ self._vote_type = value
132
+
133
+ @property
134
+ def choice_breakdown(self) -> Dict[str, int]:
135
+ return self._choice_breakdown
136
+
137
+ @choice_breakdown.setter
138
+ def choice_breakdown(self, value: Dict[str, int]):
139
+ self._choice_breakdown = value
140
+
141
+ @property
142
+ def choice_percentages(self) -> Dict[str, float]:
143
+ return self._choice_percentages
144
+
145
+ @choice_percentages.setter
146
+ def choice_percentages(self, value: Dict[str, float]):
147
+ self._choice_percentages = value
148
+
149
+ @property
150
+ def choice_averages(self) -> Dict[str, float]:
151
+ """Average ratings for rating-type votes (boto3-assist v0.30.0+ auto-converts Decimals)."""
152
+ return self._choice_averages
153
+
154
+ @choice_averages.setter
155
+ def choice_averages(self, value: Dict[str, float]):
156
+ self._choice_averages = value
157
+
158
+ @property
159
+ def total_participants(self) -> int:
160
+ return self._total_participants
161
+
162
+ @total_participants.setter
163
+ def total_participants(self, value: int):
164
+ self._total_participants = value
165
+
166
+ @property
167
+ def total_selections(self) -> int:
168
+ return self._total_selections
169
+
170
+ @total_selections.setter
171
+ def total_selections(self, value: int):
172
+ self._total_selections = value
173
+
174
+ # Helper methods
175
+ def calculate_percentages(self):
176
+ """Calculate percentages from choice breakdown."""
177
+ if self.total_participants > 0:
178
+ self.choice_percentages = {
179
+ choice: (count / self.total_participants * 100)
180
+ for choice, count in self.choice_breakdown.items()
181
+ }
182
+ else:
183
+ self.choice_percentages = {}
184
+
185
+ def get_winning_choice(self) -> str | None:
186
+ """Get the choice with the most votes."""
187
+ if not self.choice_breakdown:
188
+ return None
189
+ return max(self.choice_breakdown.items(), key=lambda x: x[1])[0]
190
+
191
+ def get_choice_percentage(self, choice_id: str) -> float:
192
+ """Get percentage for a specific choice."""
193
+ return self.choice_percentages.get(choice_id, 0.0)
@@ -0,0 +1,11 @@
1
+ # Voting Domain Services
2
+
3
+ from .vote_service import VoteService
4
+ from .vote_summary_service import VoteSummaryService
5
+ from .vote_tally_service import VoteTallyService
6
+
7
+ __all__ = [
8
+ "VoteService",
9
+ "VoteSummaryService",
10
+ "VoteTallyService",
11
+ ]
@@ -0,0 +1,264 @@
1
+ # Vote Service
2
+
3
+ from typing import Dict, Any
4
+ from boto3_assist.dynamodb.dynamodb import DynamoDB
5
+ from geek_cafe_saas_sdk.services.database_service import DatabaseService
6
+ from geek_cafe_saas_sdk.core.service_result import ServiceResult
7
+ from geek_cafe_saas_sdk.core.service_errors import ValidationError, NotFoundError
8
+ from geek_cafe_saas_sdk.domains.voting.models import Vote
9
+
10
+
11
+ class VoteService(DatabaseService[Vote]):
12
+ """Service for Vote database operations."""
13
+
14
+ def __init__(self, *, dynamodb: DynamoDB = None, table_name: str = None):
15
+ super().__init__(dynamodb=dynamodb, table_name=table_name)
16
+
17
+ def create(self, tenant_id: str, user_id: str, **kwargs) -> ServiceResult[Vote]:
18
+ """Create or update (upsert) a vote for a target by a user."""
19
+ try:
20
+ # Validate required fields
21
+ required_fields = ['target_id']
22
+ self._validate_required_fields(kwargs, required_fields)
23
+
24
+ # First check if a vote already exists for this user+target
25
+ existing = self._get_by_user_and_target(user_id, kwargs.get('target_id'))
26
+ if existing:
27
+ # Update the existing vote with new data
28
+ return self._update_existing_vote(existing, tenant_id, user_id, **kwargs)
29
+
30
+ # Create new vote instance
31
+ vote = Vote()
32
+ vote.tenant_id = tenant_id
33
+ vote.user_id = user_id
34
+ vote.target_id = kwargs.get('target_id')
35
+ vote.created_by_id = user_id
36
+
37
+ # Set vote data based on type
38
+ self._set_vote_data(vote, **kwargs)
39
+
40
+ # Prepare for save (sets ID and timestamps)
41
+ vote.prep_for_save()
42
+
43
+ # Save to database
44
+ return self._save_model(vote)
45
+
46
+ except Exception as e:
47
+ return self._handle_service_exception(e, 'create_vote', tenant_id=tenant_id, user_id=user_id)
48
+
49
+ def _update_existing_vote(self, existing_vote: Vote, tenant_id: str, user_id: str, **kwargs) -> ServiceResult[Vote]:
50
+ """Update an existing vote with new data."""
51
+ # Set new vote data
52
+ self._set_vote_data(existing_vote, **kwargs)
53
+
54
+ # Update metadata
55
+ existing_vote.updated_by_id = user_id
56
+ existing_vote.prep_for_save() # Updates timestamp
57
+
58
+ # Save updated vote
59
+ return self._save_model(existing_vote)
60
+
61
+ def _set_vote_data(self, vote: Vote, **kwargs):
62
+ """Set vote data based on the voting pattern."""
63
+ # Check if this is a legacy binary vote (has up_vote or down_vote but no vote_type)
64
+ has_legacy_fields = 'up_vote' in kwargs or 'down_vote' in kwargs
65
+ has_vote_type = 'vote_type' in kwargs
66
+
67
+ if has_legacy_fields and not has_vote_type:
68
+ # This is a legacy binary vote
69
+ vote.vote_type = 'legacy'
70
+ vote.up_vote = int(kwargs.get('up_vote', 0) or 0)
71
+ vote.down_vote = int(kwargs.get('down_vote', 0) or 0)
72
+ vote.choices = kwargs.get('choices', {})
73
+ vote.content = kwargs.get('content', {})
74
+ return
75
+
76
+ # Enhanced voting patterns
77
+ vote_type = kwargs.get('vote_type', 'single_choice')
78
+ vote.vote_type = vote_type
79
+ vote.content = kwargs.get('content', {})
80
+
81
+ if vote_type == 'single_choice':
82
+ choice_id = kwargs.get('choice_id') or kwargs.get('selected_choice')
83
+ available_choices = kwargs.get('available_choices', [])
84
+ if choice_id:
85
+ vote.set_single_choice(choice_id, available_choices)
86
+
87
+ elif vote_type == 'multi_select':
88
+ selected_choices = kwargs.get('selected_choices', [])
89
+ available_choices = kwargs.get('available_choices', [])
90
+ max_selections = kwargs.get('max_selections')
91
+ vote.set_multi_select(selected_choices, available_choices, max_selections)
92
+
93
+ elif vote_type == 'ranking':
94
+ ranked_choices = kwargs.get('ranked_choices', [])
95
+ vote.set_ranking(ranked_choices)
96
+
97
+ elif vote_type == 'rating':
98
+ ratings = kwargs.get('ratings', {})
99
+ vote.set_rating(ratings)
100
+
101
+ elif vote_type == 'legacy':
102
+ # Explicit legacy support
103
+ vote.up_vote = int(kwargs.get('up_vote', 0) or 0)
104
+ vote.down_vote = int(kwargs.get('down_vote', 0) or 0)
105
+ vote.choices = kwargs.get('choices', {})
106
+
107
+ else:
108
+ # Default to single choice if unknown type
109
+ vote.up_vote = int(kwargs.get('up_vote', 0) or 0)
110
+ vote.down_vote = int(kwargs.get('down_vote', 0) or 0)
111
+ vote.choices = kwargs.get('choices', {})
112
+
113
+ # Enhanced creation methods for specific vote types
114
+ def create_single_choice_vote(self, tenant_id: str, user_id: str, target_id: str,
115
+ choice_id: str, available_choices: list[str] = None,
116
+ content: Dict[str, Any] = None) -> ServiceResult[Vote]:
117
+ """Create a single choice vote (A/B/C/D test)."""
118
+ return self.create(
119
+ tenant_id=tenant_id,
120
+ user_id=user_id,
121
+ target_id=target_id,
122
+ vote_type='single_choice',
123
+ choice_id=choice_id,
124
+ available_choices=available_choices or [],
125
+ content=content or {}
126
+ )
127
+
128
+ def create_multi_select_vote(self, tenant_id: str, user_id: str, target_id: str,
129
+ selected_choices: list[str], available_choices: list[str] = None,
130
+ max_selections: int = None, content: Dict[str, Any] = None) -> ServiceResult[Vote]:
131
+ """Create a multi-select vote."""
132
+ return self.create(
133
+ tenant_id=tenant_id,
134
+ user_id=user_id,
135
+ target_id=target_id,
136
+ vote_type='multi_select',
137
+ selected_choices=selected_choices,
138
+ available_choices=available_choices or [],
139
+ max_selections=max_selections,
140
+ content=content or {}
141
+ )
142
+
143
+ def create_ranking_vote(self, tenant_id: str, user_id: str, target_id: str,
144
+ ranked_choices: list[str], content: Dict[str, Any] = None) -> ServiceResult[Vote]:
145
+ """Create a ranking vote."""
146
+ return self.create(
147
+ tenant_id=tenant_id,
148
+ user_id=user_id,
149
+ target_id=target_id,
150
+ vote_type='ranking',
151
+ ranked_choices=ranked_choices,
152
+ content=content or {}
153
+ )
154
+
155
+ def create_rating_vote(self, tenant_id: str, user_id: str, target_id: str,
156
+ ratings: Dict[str, float], content: Dict[str, Any] = None) -> ServiceResult[Vote]:
157
+ """Create a rating vote."""
158
+ return self.create(
159
+ tenant_id=tenant_id,
160
+ user_id=user_id,
161
+ target_id=target_id,
162
+ vote_type='rating',
163
+ ratings=ratings,
164
+ content=content or {}
165
+ )
166
+
167
+ def _get_by_user_and_target(self, user_id: str, target_id: str) -> Vote | None:
168
+ """Helper: get a vote by user and target via GSI4."""
169
+ model = Vote()
170
+ model.user_id = user_id
171
+ model.target_id = target_id
172
+ result = self._query_by_index(model, "gsi4")
173
+ if result.success and result.data:
174
+ return result.data[0]
175
+ return None
176
+
177
+ def get_by_id(self, resource_id: str, tenant_id: str, user_id: str) -> ServiceResult[Vote]:
178
+ """Get vote by ID with access control."""
179
+ try:
180
+ vote = self._get_model_by_id(resource_id, Vote)
181
+
182
+ if not vote:
183
+ raise NotFoundError(f"Vote with ID {resource_id} not found")
184
+
185
+ # Validate tenant access
186
+ if hasattr(vote, 'tenant_id'):
187
+ self._validate_tenant_access(vote.tenant_id, tenant_id)
188
+
189
+ return ServiceResult.success_result(vote)
190
+
191
+ except Exception as e:
192
+ return self._handle_service_exception(e, 'get_vote', resource_id=resource_id, tenant_id=tenant_id)
193
+
194
+ def update(self, resource_id: str, tenant_id: str, user_id: str,
195
+ updates: Dict[str, Any]) -> ServiceResult[Vote]:
196
+ """Update vote with access control."""
197
+ try:
198
+ # Get existing vote
199
+ vote = self._get_model_by_id(resource_id, Vote)
200
+
201
+ if not vote:
202
+ raise NotFoundError(f"Vote with ID {resource_id} not found")
203
+
204
+ # Validate tenant access
205
+ if hasattr(vote, 'tenant_id'):
206
+ self._validate_tenant_access(vote.tenant_id, tenant_id)
207
+
208
+ # Apply updates
209
+ for field, value in updates.items():
210
+ if hasattr(vote, field) and field not in ['id', 'created_utc_ts', 'tenant_id']:
211
+ setattr(vote, field, value)
212
+
213
+ # Update metadata
214
+ vote.updated_by_id = user_id
215
+ vote.prep_for_save() # Updates timestamp
216
+
217
+ # Save updated vote
218
+ return self._save_model(vote)
219
+
220
+ except Exception as e:
221
+ return self._handle_service_exception(e, 'update_vote', resource_id=resource_id, tenant_id=tenant_id)
222
+
223
+ def delete(self, resource_id: str, tenant_id: str, user_id: str) -> ServiceResult[bool]:
224
+ """Delete vote with access control."""
225
+ try:
226
+ vote = self._get_model_by_id(resource_id, Vote)
227
+
228
+ if not vote:
229
+ raise NotFoundError(f"Vote with ID {resource_id} not found")
230
+
231
+ if hasattr(vote, 'tenant_id'):
232
+ self._validate_tenant_access(vote.tenant_id, tenant_id)
233
+
234
+ return self._delete_model(vote)
235
+
236
+ except Exception as e:
237
+ return self._handle_service_exception(e, 'delete_vote', resource_id=resource_id, tenant_id=tenant_id)
238
+
239
+ def list_by_user(self, user_id: str, ascending: bool = False) -> ServiceResult[list[Vote]]:
240
+ """List votes by user."""
241
+ try:
242
+ model = Vote()
243
+ model.user_id = user_id
244
+ return self._query_by_index(model, "gsi2", ascending=ascending)
245
+ except Exception as e:
246
+ return self._handle_service_exception(e, 'list_votes', user_id=user_id)
247
+
248
+ def list_by_tenant(self, tenant_id: str) -> ServiceResult[list[Vote]]:
249
+ """List votes by tenant."""
250
+ try:
251
+ model = Vote()
252
+ model.tenant_id = tenant_id
253
+ return self._query_by_index(model, "gsi3")
254
+ except Exception as e:
255
+ return self._handle_service_exception(e, 'list_votes', tenant_id=tenant_id)
256
+
257
+ def list_by_target(self, target_id: str, *, start_key: dict = None, limit: int = None) -> ServiceResult[list[Vote]]:
258
+ """List votes by target with optional pagination."""
259
+ try:
260
+ model = Vote()
261
+ model.target_id = target_id
262
+ return self._query_by_index(model, "gsi5", start_key=start_key, limit=limit)
263
+ except Exception as e:
264
+ return self._handle_service_exception(e, 'list_votes', target_id=target_id)
@@ -0,0 +1,198 @@
1
+ # Vote Summary Service
2
+
3
+ from typing import Dict, Any
4
+ from boto3_assist.dynamodb.dynamodb import DynamoDB
5
+ from geek_cafe_saas_sdk.services.database_service import DatabaseService
6
+ from geek_cafe_saas_sdk.core.service_result import ServiceResult
7
+ from geek_cafe_saas_sdk.core.service_errors import ValidationError, NotFoundError
8
+ from geek_cafe_saas_sdk.domains.voting.models import VoteSummary
9
+
10
+
11
+ class VoteSummaryService(DatabaseService[VoteSummary]):
12
+ """Service for VoteSummary database operations."""
13
+
14
+ def __init__(self, *, dynamodb: DynamoDB = None, table_name: str = None):
15
+ super().__init__(dynamodb=dynamodb, table_name=table_name)
16
+
17
+ def create(self, tenant_id: str, user_id: str, **kwargs) -> ServiceResult[VoteSummary]:
18
+ """Create or update (upsert) a vote summary for a target."""
19
+ try:
20
+ # Validate required fields
21
+ required_fields = ['target_id']
22
+ self._validate_required_fields(kwargs, required_fields)
23
+
24
+ # First check if a summary already exists for this target
25
+ existing = self._get_by_target_id(kwargs.get('target_id'))
26
+ if existing:
27
+ # Update the existing summary with all new data
28
+ return self._update_existing_summary(existing, tenant_id, user_id, **kwargs)
29
+
30
+ # Create new vote summary instance
31
+ summary = VoteSummary()
32
+ summary.tenant_id = tenant_id
33
+ summary.user_id = user_id
34
+ summary.target_id = kwargs.get('target_id')
35
+ summary.created_by_id = user_id
36
+
37
+ # Set enhanced fields
38
+ summary.vote_type = kwargs.get('vote_type', 'single_choice')
39
+ summary.choice_breakdown = kwargs.get('choice_breakdown', {})
40
+ summary.choice_percentages = kwargs.get('choice_percentages', {})
41
+ summary.choice_averages = kwargs.get('choice_averages', {}) # For rating votes
42
+ summary.total_participants = int(kwargs.get('total_participants', 0) or 0)
43
+ summary.total_selections = int(kwargs.get('total_selections', 0) or 0)
44
+
45
+ # Set legacy fields for backward compatibility
46
+ summary.total_up_votes = int(kwargs.get('total_up_votes', 0) or 0)
47
+ summary.total_down_votes = int(kwargs.get('total_down_votes', 0) or 0)
48
+ summary.total_votes = int(kwargs.get('total_votes', 0) or 0)
49
+
50
+ # Set content
51
+ summary.content = kwargs.get('content', {})
52
+
53
+ # Calculate percentages if not provided
54
+ if not summary.choice_percentages and summary.choice_breakdown:
55
+ summary.calculate_percentages()
56
+
57
+ # Prepare for save (sets ID and timestamps)
58
+ summary.prep_for_save()
59
+
60
+ # Save to database
61
+ return self._save_model(summary)
62
+
63
+ except Exception as e:
64
+ return self._handle_service_exception(e, 'create_vote_summary', tenant_id=tenant_id, user_id=user_id)
65
+
66
+ def _update_existing_summary(self, existing_summary: VoteSummary, tenant_id: str, user_id: str, **kwargs) -> ServiceResult[VoteSummary]:
67
+ """Update an existing summary with new data."""
68
+ # Update enhanced fields
69
+ existing_summary.vote_type = kwargs.get('vote_type', existing_summary.vote_type)
70
+ existing_summary.choice_breakdown = kwargs.get('choice_breakdown', existing_summary.choice_breakdown)
71
+ existing_summary.choice_percentages = kwargs.get('choice_percentages', existing_summary.choice_percentages)
72
+ existing_summary.choice_averages = kwargs.get('choice_averages', existing_summary.choice_averages)
73
+ existing_summary.total_participants = int(kwargs.get('total_participants', existing_summary.total_participants) or 0)
74
+ existing_summary.total_selections = int(kwargs.get('total_selections', existing_summary.total_selections) or 0)
75
+
76
+ # Update legacy fields
77
+ existing_summary.total_up_votes = int(kwargs.get('total_up_votes', existing_summary.total_up_votes) or 0)
78
+ existing_summary.total_down_votes = int(kwargs.get('total_down_votes', existing_summary.total_down_votes) or 0)
79
+ existing_summary.total_votes = int(kwargs.get('total_votes', existing_summary.total_votes) or 0)
80
+
81
+ # Update content
82
+ existing_summary.content = kwargs.get('content', existing_summary.content or {})
83
+
84
+ # Recalculate percentages if needed
85
+ if not existing_summary.choice_percentages and existing_summary.choice_breakdown:
86
+ existing_summary.calculate_percentages()
87
+
88
+ # Update metadata
89
+ existing_summary.updated_by_id = user_id
90
+ existing_summary.prep_for_save() # Updates timestamp
91
+
92
+ # Save updated summary
93
+ return self._save_model(existing_summary)
94
+
95
+ def _get_by_target_id(self, target_id: str) -> VoteSummary | None:
96
+ """Helper: get a vote summary by target_id via GSI2."""
97
+ model = VoteSummary()
98
+ model.target_id = target_id
99
+ result = self._query_by_index(model, "gsi2")
100
+ if result.success and result.data:
101
+ return result.data[0]
102
+ return None
103
+
104
+ def get_by_id(self, resource_id: str, tenant_id: str, user_id: str) -> ServiceResult[VoteSummary]:
105
+ """Get vote summary by ID with access control."""
106
+ try:
107
+ summary = self._get_model_by_id(resource_id, VoteSummary)
108
+
109
+ if not summary:
110
+ raise NotFoundError(f"VoteSummary with ID {resource_id} not found")
111
+
112
+ # Validate tenant access
113
+ if hasattr(summary, 'tenant_id'):
114
+ self._validate_tenant_access(summary.tenant_id, tenant_id)
115
+
116
+ return ServiceResult.success_result(summary)
117
+
118
+ except Exception as e:
119
+ return self._handle_service_exception(e, 'get_vote_summary', resource_id=resource_id, tenant_id=tenant_id)
120
+
121
+ def get_by_target_id(self, target_id: str, tenant_id: str, user_id: str) -> ServiceResult[VoteSummary]:
122
+ """Get vote summary by target_id with access control."""
123
+ try:
124
+ summary = self._get_by_target_id(target_id)
125
+
126
+ if not summary:
127
+ raise NotFoundError(f"VoteSummary for target {target_id} not found")
128
+
129
+ # Validate tenant access
130
+ if hasattr(summary, 'tenant_id'):
131
+ self._validate_tenant_access(summary.tenant_id, tenant_id)
132
+
133
+ return ServiceResult.success_result(summary)
134
+
135
+ except Exception as e:
136
+ return self._handle_service_exception(e, 'get_vote_summary_by_target', target_id=target_id, tenant_id=tenant_id)
137
+
138
+ def update(self, resource_id: str, tenant_id: str, user_id: str,
139
+ updates: Dict[str, Any]) -> ServiceResult[VoteSummary]:
140
+ """Update vote summary with access control."""
141
+ try:
142
+ # Get existing summary
143
+ summary = self._get_model_by_id(resource_id, VoteSummary)
144
+
145
+ if not summary:
146
+ raise NotFoundError(f"VoteSummary with ID {resource_id} not found")
147
+
148
+ # Validate tenant access
149
+ if hasattr(summary, 'tenant_id'):
150
+ self._validate_tenant_access(summary.tenant_id, tenant_id)
151
+
152
+ # Apply updates
153
+ for field, value in updates.items():
154
+ if hasattr(summary, field) and field not in ['id', 'created_utc_ts', 'tenant_id']:
155
+ setattr(summary, field, value)
156
+
157
+ # Update metadata
158
+ summary.updated_by_id = user_id
159
+ summary.prep_for_save() # Updates timestamp
160
+
161
+ # Save updated summary
162
+ return self._save_model(summary)
163
+
164
+ except Exception as e:
165
+ return self._handle_service_exception(e, 'update_vote_summary', resource_id=resource_id, tenant_id=tenant_id)
166
+
167
+ def delete(self, resource_id: str, tenant_id: str, user_id: str) -> ServiceResult[bool]:
168
+ """Delete vote summary with access control."""
169
+ try:
170
+ summary = self._get_model_by_id(resource_id, VoteSummary)
171
+
172
+ if not summary:
173
+ raise NotFoundError(f"VoteSummary with ID {resource_id} not found")
174
+
175
+ if hasattr(summary, 'tenant_id'):
176
+ self._validate_tenant_access(summary.tenant_id, tenant_id)
177
+
178
+ return self._delete_model(summary)
179
+
180
+ except Exception as e:
181
+ return self._handle_service_exception(e, 'delete_vote_summary', resource_id=resource_id, tenant_id=tenant_id)
182
+
183
+ def list_by_tenant(self, tenant_id: str) -> ServiceResult[list[VoteSummary]]:
184
+ """List vote summaries by tenant."""
185
+ try:
186
+ model = VoteSummary()
187
+ model.tenant_id = tenant_id
188
+ return self._query_by_index(model, "gsi3")
189
+ except Exception as e:
190
+ return self._handle_service_exception(e, 'list_vote_summaries', tenant_id=tenant_id)
191
+
192
+ def list_all(self) -> ServiceResult[list[VoteSummary]]:
193
+ """List all vote summaries."""
194
+ try:
195
+ model = VoteSummary()
196
+ return self._query_by_index(model, "gsi1")
197
+ except Exception as e:
198
+ return self._handle_service_exception(e, 'list_all_vote_summaries')