geek-cafe-saas-sdk 0.6.0__py3-none-any.whl → 0.7.1__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 (94) hide show
  1. geek_cafe_saas_sdk/__init__.py +2 -2
  2. geek_cafe_saas_sdk/domains/files/handlers/README.md +446 -0
  3. geek_cafe_saas_sdk/domains/files/handlers/__init__.py +6 -0
  4. geek_cafe_saas_sdk/domains/files/handlers/files/create/app.py +121 -0
  5. geek_cafe_saas_sdk/domains/files/handlers/files/download/app.py +80 -0
  6. geek_cafe_saas_sdk/domains/files/handlers/files/get/app.py +62 -0
  7. geek_cafe_saas_sdk/domains/files/handlers/files/list/app.py +72 -0
  8. geek_cafe_saas_sdk/domains/files/handlers/lineage/create_derived/app.py +99 -0
  9. geek_cafe_saas_sdk/domains/files/handlers/lineage/create_main/app.py +104 -0
  10. geek_cafe_saas_sdk/domains/files/handlers/lineage/download_bundle/app.py +99 -0
  11. geek_cafe_saas_sdk/domains/files/handlers/lineage/get_lineage/app.py +68 -0
  12. geek_cafe_saas_sdk/domains/files/handlers/lineage/prepare_bundle/app.py +76 -0
  13. geek_cafe_saas_sdk/domains/files/models/__init__.py +17 -0
  14. geek_cafe_saas_sdk/domains/files/models/directory.py +42 -6
  15. geek_cafe_saas_sdk/domains/files/models/file.py +158 -16
  16. geek_cafe_saas_sdk/domains/files/models/file_share.py +33 -0
  17. geek_cafe_saas_sdk/domains/files/models/file_version.py +24 -0
  18. geek_cafe_saas_sdk/domains/files/services/__init__.py +21 -0
  19. geek_cafe_saas_sdk/domains/files/services/directory_service.py +54 -135
  20. geek_cafe_saas_sdk/domains/files/services/file_lineage_service.py +487 -0
  21. geek_cafe_saas_sdk/domains/files/services/file_share_service.py +37 -120
  22. geek_cafe_saas_sdk/domains/files/services/file_system_service.py +67 -103
  23. geek_cafe_saas_sdk/domains/files/services/file_version_service.py +44 -124
  24. geek_cafe_saas_sdk/domains/messaging/services/contact_thread_service.py +55 -7
  25. geek_cafe_saas_sdk/domains/notifications/__init__.py +18 -0
  26. geek_cafe_saas_sdk/domains/notifications/handlers/__init__.py +1 -0
  27. geek_cafe_saas_sdk/domains/notifications/handlers/create_webhook/app.py +73 -0
  28. geek_cafe_saas_sdk/domains/notifications/handlers/get/app.py +40 -0
  29. geek_cafe_saas_sdk/domains/notifications/handlers/get_preferences/app.py +34 -0
  30. geek_cafe_saas_sdk/domains/notifications/handlers/list/app.py +43 -0
  31. geek_cafe_saas_sdk/domains/notifications/handlers/list_webhooks/app.py +40 -0
  32. geek_cafe_saas_sdk/domains/notifications/handlers/mark_read/app.py +40 -0
  33. geek_cafe_saas_sdk/domains/notifications/handlers/send/app.py +83 -0
  34. geek_cafe_saas_sdk/domains/notifications/handlers/update_preferences/app.py +45 -0
  35. geek_cafe_saas_sdk/domains/notifications/models/__init__.py +16 -0
  36. geek_cafe_saas_sdk/domains/notifications/models/notification.py +717 -0
  37. geek_cafe_saas_sdk/domains/notifications/models/notification_preference.py +365 -0
  38. geek_cafe_saas_sdk/domains/notifications/models/webhook_subscription.py +339 -0
  39. geek_cafe_saas_sdk/domains/notifications/services/__init__.py +10 -0
  40. geek_cafe_saas_sdk/domains/notifications/services/notification_service.py +576 -0
  41. geek_cafe_saas_sdk/domains/payments/__init__.py +16 -0
  42. geek_cafe_saas_sdk/domains/payments/handlers/README.md +334 -0
  43. geek_cafe_saas_sdk/domains/payments/handlers/__init__.py +6 -0
  44. geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/create/app.py +105 -0
  45. geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/get/app.py +60 -0
  46. geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/update/app.py +97 -0
  47. geek_cafe_saas_sdk/domains/payments/handlers/payment_intents/create/app.py +97 -0
  48. geek_cafe_saas_sdk/domains/payments/handlers/payment_intents/get/app.py +60 -0
  49. geek_cafe_saas_sdk/domains/payments/handlers/payments/get/app.py +60 -0
  50. geek_cafe_saas_sdk/domains/payments/handlers/payments/list/app.py +68 -0
  51. geek_cafe_saas_sdk/domains/payments/handlers/payments/record/app.py +118 -0
  52. geek_cafe_saas_sdk/domains/payments/handlers/refunds/create/app.py +89 -0
  53. geek_cafe_saas_sdk/domains/payments/handlers/refunds/get/app.py +60 -0
  54. geek_cafe_saas_sdk/domains/payments/models/__init__.py +17 -0
  55. geek_cafe_saas_sdk/domains/payments/models/billing_account.py +521 -0
  56. geek_cafe_saas_sdk/domains/payments/models/payment.py +639 -0
  57. geek_cafe_saas_sdk/domains/payments/models/payment_intent_ref.py +539 -0
  58. geek_cafe_saas_sdk/domains/payments/models/refund.py +404 -0
  59. geek_cafe_saas_sdk/domains/payments/services/__init__.py +11 -0
  60. geek_cafe_saas_sdk/domains/payments/services/payment_service.py +405 -0
  61. geek_cafe_saas_sdk/domains/subscriptions/__init__.py +19 -0
  62. geek_cafe_saas_sdk/domains/subscriptions/handlers/README.md +408 -0
  63. geek_cafe_saas_sdk/domains/subscriptions/handlers/__init__.py +1 -0
  64. geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/create/app.py +81 -0
  65. geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/get/app.py +48 -0
  66. geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/list/app.py +54 -0
  67. geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/update/app.py +54 -0
  68. geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/create/app.py +83 -0
  69. geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/get/app.py +47 -0
  70. geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/validate/app.py +62 -0
  71. geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/create/app.py +82 -0
  72. geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/get/app.py +48 -0
  73. geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/list/app.py +66 -0
  74. geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/update/app.py +54 -0
  75. geek_cafe_saas_sdk/domains/subscriptions/handlers/usage/aggregate/app.py +72 -0
  76. geek_cafe_saas_sdk/domains/subscriptions/handlers/usage/record/app.py +89 -0
  77. geek_cafe_saas_sdk/domains/subscriptions/models/__init__.py +13 -0
  78. geek_cafe_saas_sdk/domains/subscriptions/models/addon.py +604 -0
  79. geek_cafe_saas_sdk/domains/subscriptions/models/discount.py +492 -0
  80. geek_cafe_saas_sdk/domains/subscriptions/models/plan.py +569 -0
  81. geek_cafe_saas_sdk/domains/subscriptions/models/usage_record.py +300 -0
  82. geek_cafe_saas_sdk/domains/subscriptions/services/__init__.py +10 -0
  83. geek_cafe_saas_sdk/domains/subscriptions/services/subscription_manager_service.py +694 -0
  84. geek_cafe_saas_sdk/domains/tenancy/models/subscription.py +123 -1
  85. geek_cafe_saas_sdk/domains/tenancy/services/subscription_service.py +213 -0
  86. geek_cafe_saas_sdk/lambda_handlers/_base/base_handler.py +7 -0
  87. geek_cafe_saas_sdk/services/database_service.py +10 -6
  88. geek_cafe_saas_sdk/utilities/cognito_utility.py +16 -26
  89. geek_cafe_saas_sdk/utilities/environment_variables.py +16 -0
  90. geek_cafe_saas_sdk/utilities/logging_utility.py +77 -0
  91. {geek_cafe_saas_sdk-0.6.0.dist-info → geek_cafe_saas_sdk-0.7.1.dist-info}/METADATA +11 -11
  92. {geek_cafe_saas_sdk-0.6.0.dist-info → geek_cafe_saas_sdk-0.7.1.dist-info}/RECORD +94 -23
  93. {geek_cafe_saas_sdk-0.6.0.dist-info → geek_cafe_saas_sdk-0.7.1.dist-info}/WHEEL +0 -0
  94. {geek_cafe_saas_sdk-0.6.0.dist-info → geek_cafe_saas_sdk-0.7.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,365 @@
1
+ """
2
+ NotificationPreference Model - User notification channel preferences.
3
+
4
+ Manages per-user settings for notification delivery including:
5
+ - Channel enablement (email, SMS, push, in-app)
6
+ - Quiet hours/Do Not Disturb
7
+ - Notification type preferences
8
+ - Frequency controls
9
+
10
+ Geek Cafe, LLC
11
+ MIT License. See Project Root for the license information.
12
+ """
13
+
14
+ from typing import Dict, List, Any
15
+ from geek_cafe_saas_sdk.models.base_model import BaseModel
16
+ from boto3_assist.dynamodb.dynamodb_index import DynamoDBIndex, DynamoDBKey
17
+
18
+
19
+ class NotificationPreference(BaseModel):
20
+ """
21
+ User notification preferences model.
22
+
23
+ Controls how and when a user receives notifications across all channels.
24
+ Supports per-channel, per-type preferences with quiet hours.
25
+ """
26
+
27
+ def __init__(self):
28
+ super().__init__()
29
+
30
+ # User identification
31
+ self._user_id: str = "" # Required
32
+
33
+ # Global preferences
34
+ self._enabled: bool = True # Master switch
35
+ self._do_not_disturb: bool = False # Temporary DND
36
+ self._quiet_hours_enabled: bool = False
37
+ self._quiet_hours_start: str | None = None # "22:00"
38
+ self._quiet_hours_end: str | None = None # "08:00"
39
+ self._timezone: str = "UTC"
40
+
41
+ # Channel preferences
42
+ self._email_enabled: bool = True
43
+ self._email_address: str | None = None # Override default
44
+ self._email_frequency: str = "immediate" # immediate, daily_digest, weekly_digest
45
+
46
+ self._sms_enabled: bool = False # Opt-in required
47
+ self._sms_phone: str | None = None
48
+ self._sms_frequency: str = "immediate"
49
+
50
+ self._push_enabled: bool = True
51
+ self._push_device_tokens: List[str] = [] # Multiple devices
52
+
53
+ self._in_app_enabled: bool = True
54
+
55
+ self._webhook_enabled: bool = False
56
+ self._webhook_url: str | None = None
57
+
58
+ # Type-specific preferences
59
+ # Key = notification_type, Value = {channel: enabled}
60
+ self._type_preferences: Dict[str, Dict[str, bool]] = {}
61
+
62
+ # Digest settings
63
+ self._digest_time: str = "09:00" # When to send daily digest
64
+ self._digest_day: str = "monday" # For weekly digest
65
+
66
+ # Metadata
67
+ self._language: str = "en" # Preferred language
68
+ self._metadata: Dict[str, Any] = {}
69
+
70
+ # CRITICAL: Call _setup_indexes() as LAST line in __init__
71
+ self._setup_indexes()
72
+
73
+ def _setup_indexes(self):
74
+ """Setup DynamoDB indexes for notification preference queries."""
75
+
76
+ # Primary index: Preference by user ID
77
+ primary: DynamoDBIndex = DynamoDBIndex()
78
+ primary.name = "primary"
79
+ primary.partition_key.attribute_name = "pk"
80
+ primary.partition_key.value = lambda: DynamoDBKey.build_key(("preferences", self.user_id))
81
+ primary.sort_key.attribute_name = "sk"
82
+ primary.sort_key.value = lambda: "metadata"
83
+ self.indexes.add_primary(primary)
84
+
85
+ # Core Properties
86
+ @property
87
+ def user_id(self) -> str:
88
+ return self._user_id
89
+
90
+ @user_id.setter
91
+ def user_id(self, value: str):
92
+ self._user_id = value
93
+
94
+ @property
95
+ def enabled(self) -> bool:
96
+ return self._enabled
97
+
98
+ @enabled.setter
99
+ def enabled(self, value: bool):
100
+ self._enabled = value
101
+
102
+ @property
103
+ def do_not_disturb(self) -> bool:
104
+ return self._do_not_disturb
105
+
106
+ @do_not_disturb.setter
107
+ def do_not_disturb(self, value: bool):
108
+ self._do_not_disturb = value
109
+
110
+ @property
111
+ def quiet_hours_enabled(self) -> bool:
112
+ return self._quiet_hours_enabled
113
+
114
+ @quiet_hours_enabled.setter
115
+ def quiet_hours_enabled(self, value: bool):
116
+ self._quiet_hours_enabled = value
117
+
118
+ @property
119
+ def quiet_hours_start(self) -> str | None:
120
+ return self._quiet_hours_start
121
+
122
+ @quiet_hours_start.setter
123
+ def quiet_hours_start(self, value: str | None):
124
+ self._quiet_hours_start = value
125
+
126
+ @property
127
+ def quiet_hours_end(self) -> str | None:
128
+ return self._quiet_hours_end
129
+
130
+ @quiet_hours_end.setter
131
+ def quiet_hours_end(self, value: str | None):
132
+ self._quiet_hours_end = value
133
+
134
+ @property
135
+ def timezone(self) -> str:
136
+ return self._timezone
137
+
138
+ @timezone.setter
139
+ def timezone(self, value: str):
140
+ self._timezone = value
141
+
142
+ @property
143
+ def email_enabled(self) -> bool:
144
+ return self._email_enabled
145
+
146
+ @email_enabled.setter
147
+ def email_enabled(self, value: bool):
148
+ self._email_enabled = value
149
+
150
+ @property
151
+ def email_address(self) -> str | None:
152
+ return self._email_address
153
+
154
+ @email_address.setter
155
+ def email_address(self, value: str | None):
156
+ self._email_address = value
157
+
158
+ @property
159
+ def email_frequency(self) -> str:
160
+ return self._email_frequency
161
+
162
+ @email_frequency.setter
163
+ def email_frequency(self, value: str):
164
+ self._email_frequency = value
165
+
166
+ @property
167
+ def sms_enabled(self) -> bool:
168
+ return self._sms_enabled
169
+
170
+ @sms_enabled.setter
171
+ def sms_enabled(self, value: bool):
172
+ self._sms_enabled = value
173
+
174
+ @property
175
+ def sms_phone(self) -> str | None:
176
+ return self._sms_phone
177
+
178
+ @sms_phone.setter
179
+ def sms_phone(self, value: str | None):
180
+ self._sms_phone = value
181
+
182
+ @property
183
+ def sms_frequency(self) -> str:
184
+ return self._sms_frequency
185
+
186
+ @sms_frequency.setter
187
+ def sms_frequency(self, value: str):
188
+ self._sms_frequency = value
189
+
190
+ @property
191
+ def push_enabled(self) -> bool:
192
+ return self._push_enabled
193
+
194
+ @push_enabled.setter
195
+ def push_enabled(self, value: bool):
196
+ self._push_enabled = value
197
+
198
+ @property
199
+ def push_device_tokens(self) -> List[str]:
200
+ return self._push_device_tokens
201
+
202
+ @push_device_tokens.setter
203
+ def push_device_tokens(self, value: List[str]):
204
+ self._push_device_tokens = value if value else []
205
+
206
+ @property
207
+ def in_app_enabled(self) -> bool:
208
+ return self._in_app_enabled
209
+
210
+ @in_app_enabled.setter
211
+ def in_app_enabled(self, value: bool):
212
+ self._in_app_enabled = value
213
+
214
+ @property
215
+ def webhook_enabled(self) -> bool:
216
+ return self._webhook_enabled
217
+
218
+ @webhook_enabled.setter
219
+ def webhook_enabled(self, value: bool):
220
+ self._webhook_enabled = value
221
+
222
+ @property
223
+ def webhook_url(self) -> str | None:
224
+ return self._webhook_url
225
+
226
+ @webhook_url.setter
227
+ def webhook_url(self, value: str | None):
228
+ self._webhook_url = value
229
+
230
+ @property
231
+ def type_preferences(self) -> Dict[str, Dict[str, bool]]:
232
+ return self._type_preferences
233
+
234
+ @type_preferences.setter
235
+ def type_preferences(self, value: Dict[str, Dict[str, bool]]):
236
+ self._type_preferences = value if value else {}
237
+
238
+ @property
239
+ def digest_time(self) -> str:
240
+ return self._digest_time
241
+
242
+ @digest_time.setter
243
+ def digest_time(self, value: str):
244
+ self._digest_time = value
245
+
246
+ @property
247
+ def digest_day(self) -> str:
248
+ return self._digest_day
249
+
250
+ @digest_day.setter
251
+ def digest_day(self, value: str):
252
+ self._digest_day = value
253
+
254
+ @property
255
+ def language(self) -> str:
256
+ return self._language
257
+
258
+ @language.setter
259
+ def language(self, value: str):
260
+ self._language = value
261
+
262
+ @property
263
+ def metadata(self) -> Dict[str, Any]:
264
+ return self._metadata
265
+
266
+ @metadata.setter
267
+ def metadata(self, value: Dict[str, Any]):
268
+ self._metadata = value if value else {}
269
+
270
+ # Helper Methods
271
+ def is_channel_enabled(self, channel: str) -> bool:
272
+ """Check if a channel is globally enabled."""
273
+ if not self._enabled or self._do_not_disturb:
274
+ return False
275
+
276
+ channel_map = {
277
+ "email": self._email_enabled,
278
+ "sms": self._sms_enabled,
279
+ "push": self._push_enabled,
280
+ "in_app": self._in_app_enabled,
281
+ "webhook": self._webhook_enabled
282
+ }
283
+
284
+ return channel_map.get(channel, False)
285
+
286
+ def is_type_enabled(self, notification_type: str, channel: str) -> bool:
287
+ """Check if a specific notification type is enabled for a channel."""
288
+ if not self.is_channel_enabled(channel):
289
+ return False
290
+
291
+ if notification_type in self._type_preferences:
292
+ type_prefs = self._type_preferences[notification_type]
293
+ return type_prefs.get(channel, True)
294
+
295
+ return True
296
+
297
+ def set_type_preference(self, notification_type: str, channel: str, enabled: bool):
298
+ """Set preference for a notification type on a specific channel."""
299
+ if notification_type not in self._type_preferences:
300
+ self._type_preferences[notification_type] = {}
301
+
302
+ self._type_preferences[notification_type][channel] = enabled
303
+
304
+ def is_in_quiet_hours(self, current_time: str) -> bool:
305
+ """Check if current time is within quiet hours (HH:MM format)."""
306
+ if not self._quiet_hours_enabled:
307
+ return False
308
+
309
+ if not self._quiet_hours_start or not self._quiet_hours_end:
310
+ return False
311
+
312
+ try:
313
+ current = self._time_to_minutes(current_time)
314
+ start = self._time_to_minutes(self._quiet_hours_start)
315
+ end = self._time_to_minutes(self._quiet_hours_end)
316
+
317
+ if start > end: # Overnight hours
318
+ return current >= start or current <= end
319
+ else:
320
+ return start <= current <= end
321
+ except:
322
+ return False
323
+
324
+ def _time_to_minutes(self, time_str: str) -> int:
325
+ """Convert HH:MM to minutes since midnight."""
326
+ hours, minutes = map(int, time_str.split(":"))
327
+ return hours * 60 + minutes
328
+
329
+ def add_device_token(self, token: str):
330
+ """Add a push device token."""
331
+ if token and token not in self._push_device_tokens:
332
+ self._push_device_tokens.append(token)
333
+
334
+ def remove_device_token(self, token: str):
335
+ """Remove a push device token."""
336
+ if token in self._push_device_tokens:
337
+ self._push_device_tokens.remove(token)
338
+
339
+ def validate(self) -> tuple[bool, list[str]]:
340
+ """Validate preference data."""
341
+ errors = []
342
+
343
+ if not self._user_id:
344
+ errors.append("user_id is required")
345
+
346
+ # Validate quiet hours format
347
+ if self._quiet_hours_enabled:
348
+ if not self._quiet_hours_start or not self._quiet_hours_end:
349
+ errors.append("quiet_hours_start and quiet_hours_end required when enabled")
350
+ else:
351
+ try:
352
+ self._time_to_minutes(self._quiet_hours_start)
353
+ self._time_to_minutes(self._quiet_hours_end)
354
+ except:
355
+ errors.append("Invalid quiet hours format (use HH:MM)")
356
+
357
+ # Validate SMS phone if enabled
358
+ if self._sms_enabled and not self._sms_phone:
359
+ errors.append("sms_phone required when SMS is enabled")
360
+
361
+ # Validate webhook URL if enabled
362
+ if self._webhook_enabled and not self._webhook_url:
363
+ errors.append("webhook_url required when webhook is enabled")
364
+
365
+ return (len(errors) == 0, errors)
@@ -0,0 +1,339 @@
1
+ """
2
+ WebhookSubscription Model - Outbound webhook event subscriptions.
3
+
4
+ Allows tenants to subscribe to platform events via webhooks for integrations.
5
+
6
+ Geek Cafe, LLC
7
+ MIT License. See Project Root for the license information.
8
+ """
9
+
10
+ from typing import Dict, List, Any
11
+ from geek_cafe_saas_sdk.models.base_model import BaseModel
12
+ from boto3_assist.dynamodb.dynamodb_index import DynamoDBIndex, DynamoDBKey
13
+
14
+
15
+ class WebhookSubscription(BaseModel):
16
+ """
17
+ Webhook subscription for outbound event notifications.
18
+
19
+ Tenants can subscribe to platform events (payment completed, user created, etc.)
20
+ and receive HTTP POSTs to their endpoint.
21
+ """
22
+
23
+ # Subscription status
24
+ STATUS_ACTIVE = "active"
25
+ STATUS_PAUSED = "paused"
26
+ STATUS_DISABLED = "disabled"
27
+
28
+ def __init__(self):
29
+ super().__init__()
30
+
31
+ # Core fields
32
+ self._tenant_id: str = "" # Required
33
+ self._subscription_name: str = "" # Display name
34
+ self._url: str = "" # Webhook endpoint
35
+ self._status: str = self.STATUS_ACTIVE
36
+
37
+ # Event subscription
38
+ self._event_types: List[str] = [] # Events to subscribe to
39
+ self._event_filters: Dict[str, Any] = {} # Optional filters
40
+
41
+ # Security
42
+ self._secret: str | None = None # For HMAC signature
43
+ self._api_key: str | None = None # Optional API key header
44
+ self._custom_headers: Dict[str, str] = {} # Additional headers
45
+
46
+ # Configuration
47
+ self._http_method: str = "POST" # POST, PUT
48
+ self._content_type: str = "application/json"
49
+ self._timeout_seconds: int = 30
50
+ self._retry_enabled: bool = True
51
+ self._max_retries: int = 3
52
+ self._retry_delay_seconds: int = 60
53
+
54
+ # Statistics
55
+ self._total_deliveries: int = 0
56
+ self._successful_deliveries: int = 0
57
+ self._failed_deliveries: int = 0
58
+ self._last_delivery_utc_ts: float | None = None
59
+ self._last_success_utc_ts: float | None = None
60
+ self._last_failure_utc_ts: float | None = None
61
+ self._last_error: str | None = None
62
+
63
+ # Metadata
64
+ self._description: str | None = None
65
+ self._metadata: Dict[str, Any] = {}
66
+
67
+ # CRITICAL: Call _setup_indexes() as LAST line in __init__
68
+ self._setup_indexes()
69
+
70
+ def _setup_indexes(self):
71
+ """Setup DynamoDB indexes for webhook subscription queries."""
72
+
73
+ # Primary index: Webhook subscription by ID
74
+ primary: DynamoDBIndex = DynamoDBIndex()
75
+ primary.name = "primary"
76
+ primary.partition_key.attribute_name = "pk"
77
+ primary.partition_key.value = lambda: DynamoDBKey.build_key(("webhook", self.id))
78
+ primary.sort_key.attribute_name = "sk"
79
+ primary.sort_key.value = lambda: "metadata"
80
+ self.indexes.add_primary(primary)
81
+
82
+ # GSI1: Webhooks by tenant (for listing)
83
+ gsi: DynamoDBIndex = DynamoDBIndex()
84
+ gsi.name = "gsi1"
85
+ gsi.partition_key.attribute_name = f"{gsi.name}_pk"
86
+ gsi.partition_key.value = lambda: DynamoDBKey.build_key(("tenant", self.tenant_id))
87
+ gsi.sort_key.attribute_name = f"{gsi.name}_sk"
88
+ gsi.sort_key.value = lambda: DynamoDBKey.build_key(("webhook", self.created_utc_ts))
89
+ self.indexes.add_secondary(gsi)
90
+
91
+ # Properties
92
+ @property
93
+ def tenant_id(self) -> str:
94
+ return self._tenant_id
95
+
96
+ @tenant_id.setter
97
+ def tenant_id(self, value: str):
98
+ self._tenant_id = value
99
+
100
+ @property
101
+ def subscription_name(self) -> str:
102
+ return self._subscription_name
103
+
104
+ @subscription_name.setter
105
+ def subscription_name(self, value: str):
106
+ self._subscription_name = value
107
+
108
+ @property
109
+ def url(self) -> str:
110
+ return self._url
111
+
112
+ @url.setter
113
+ def url(self, value: str):
114
+ self._url = value
115
+
116
+ @property
117
+ def status(self) -> str:
118
+ return self._status
119
+
120
+ @status.setter
121
+ def status(self, value: str):
122
+ self._status = value
123
+
124
+ @property
125
+ def event_types(self) -> List[str]:
126
+ return self._event_types
127
+
128
+ @event_types.setter
129
+ def event_types(self, value: List[str]):
130
+ self._event_types = value if value else []
131
+
132
+ @property
133
+ def event_filters(self) -> Dict[str, Any]:
134
+ return self._event_filters
135
+
136
+ @event_filters.setter
137
+ def event_filters(self, value: Dict[str, Any]):
138
+ self._event_filters = value if value else {}
139
+
140
+ @property
141
+ def secret(self) -> str | None:
142
+ return self._secret
143
+
144
+ @secret.setter
145
+ def secret(self, value: str | None):
146
+ self._secret = value
147
+
148
+ @property
149
+ def api_key(self) -> str | None:
150
+ return self._api_key
151
+
152
+ @api_key.setter
153
+ def api_key(self, value: str | None):
154
+ self._api_key = value
155
+
156
+ @property
157
+ def custom_headers(self) -> Dict[str, str]:
158
+ return self._custom_headers
159
+
160
+ @custom_headers.setter
161
+ def custom_headers(self, value: Dict[str, str]):
162
+ self._custom_headers = value if value else {}
163
+
164
+ @property
165
+ def http_method(self) -> str:
166
+ return self._http_method
167
+
168
+ @http_method.setter
169
+ def http_method(self, value: str):
170
+ self._http_method = value
171
+
172
+ @property
173
+ def content_type(self) -> str:
174
+ return self._content_type
175
+
176
+ @content_type.setter
177
+ def content_type(self, value: str):
178
+ self._content_type = value
179
+
180
+ @property
181
+ def timeout_seconds(self) -> int:
182
+ return self._timeout_seconds
183
+
184
+ @timeout_seconds.setter
185
+ def timeout_seconds(self, value: int):
186
+ self._timeout_seconds = value
187
+
188
+ @property
189
+ def retry_enabled(self) -> bool:
190
+ return self._retry_enabled
191
+
192
+ @retry_enabled.setter
193
+ def retry_enabled(self, value: bool):
194
+ self._retry_enabled = value
195
+
196
+ @property
197
+ def max_retries(self) -> int:
198
+ return self._max_retries
199
+
200
+ @max_retries.setter
201
+ def max_retries(self, value: int):
202
+ self._max_retries = value
203
+
204
+ @property
205
+ def retry_delay_seconds(self) -> int:
206
+ return self._retry_delay_seconds
207
+
208
+ @retry_delay_seconds.setter
209
+ def retry_delay_seconds(self, value: int):
210
+ self._retry_delay_seconds = value
211
+
212
+ @property
213
+ def total_deliveries(self) -> int:
214
+ return self._total_deliveries
215
+
216
+ @total_deliveries.setter
217
+ def total_deliveries(self, value: int):
218
+ self._total_deliveries = value
219
+
220
+ @property
221
+ def successful_deliveries(self) -> int:
222
+ return self._successful_deliveries
223
+
224
+ @successful_deliveries.setter
225
+ def successful_deliveries(self, value: int):
226
+ self._successful_deliveries = value
227
+
228
+ @property
229
+ def failed_deliveries(self) -> int:
230
+ return self._failed_deliveries
231
+
232
+ @failed_deliveries.setter
233
+ def failed_deliveries(self, value: int):
234
+ self._failed_deliveries = value
235
+
236
+ @property
237
+ def last_delivery_utc_ts(self) -> float | None:
238
+ return self._last_delivery_utc_ts
239
+
240
+ @last_delivery_utc_ts.setter
241
+ def last_delivery_utc_ts(self, value: float | None):
242
+ self._last_delivery_utc_ts = value
243
+
244
+ @property
245
+ def last_success_utc_ts(self) -> float | None:
246
+ return self._last_success_utc_ts
247
+
248
+ @last_success_utc_ts.setter
249
+ def last_success_utc_ts(self, value: float | None):
250
+ self._last_success_utc_ts = value
251
+
252
+ @property
253
+ def last_failure_utc_ts(self) -> float | None:
254
+ return self._last_failure_utc_ts
255
+
256
+ @last_failure_utc_ts.setter
257
+ def last_failure_utc_ts(self, value: float | None):
258
+ self._last_failure_utc_ts = value
259
+
260
+ @property
261
+ def last_error(self) -> str | None:
262
+ return self._last_error
263
+
264
+ @last_error.setter
265
+ def last_error(self, value: str | None):
266
+ self._last_error = value
267
+
268
+ @property
269
+ def description(self) -> str | None:
270
+ return self._description
271
+
272
+ @description.setter
273
+ def description(self, value: str | None):
274
+ self._description = value
275
+
276
+ @property
277
+ def metadata(self) -> Dict[str, Any]:
278
+ return self._metadata
279
+
280
+ @metadata.setter
281
+ def metadata(self, value: Dict[str, Any]):
282
+ self._metadata = value if value else {}
283
+
284
+ # Helper Methods
285
+ def is_active(self) -> bool:
286
+ """Check if subscription is active."""
287
+ return self._status == self.STATUS_ACTIVE
288
+
289
+ def subscribes_to(self, event_type: str) -> bool:
290
+ """Check if subscription includes event type."""
291
+ return event_type in self._event_types or "*" in self._event_types
292
+
293
+ def record_success(self, timestamp: float):
294
+ """Record successful delivery."""
295
+ self._total_deliveries += 1
296
+ self._successful_deliveries += 1
297
+ self._last_delivery_utc_ts = timestamp
298
+ self._last_success_utc_ts = timestamp
299
+ self._last_error = None
300
+
301
+ def record_failure(self, timestamp: float, error: str):
302
+ """Record failed delivery."""
303
+ self._total_deliveries += 1
304
+ self._failed_deliveries += 1
305
+ self._last_delivery_utc_ts = timestamp
306
+ self._last_failure_utc_ts = timestamp
307
+ self._last_error = error
308
+
309
+ def get_success_rate(self) -> float:
310
+ """Calculate success rate percentage."""
311
+ if self._total_deliveries == 0:
312
+ return 0.0
313
+ return (self._successful_deliveries / self._total_deliveries) * 100
314
+
315
+ def validate(self) -> tuple[bool, list[str]]:
316
+ """Validate subscription data."""
317
+ errors = []
318
+
319
+ if not self._tenant_id:
320
+ errors.append("tenant_id is required")
321
+
322
+ if not self._subscription_name:
323
+ errors.append("subscription_name is required")
324
+
325
+ if not self._url:
326
+ errors.append("url is required")
327
+ elif not self._url.startswith(("http://", "https://")):
328
+ errors.append("url must start with http:// or https://")
329
+
330
+ if not self._event_types:
331
+ errors.append("event_types cannot be empty")
332
+
333
+ if self._http_method not in ["POST", "PUT", "PATCH"]:
334
+ errors.append("http_method must be POST, PUT, or PATCH")
335
+
336
+ if self._timeout_seconds < 1 or self._timeout_seconds > 300:
337
+ errors.append("timeout_seconds must be between 1 and 300")
338
+
339
+ return (len(errors) == 0, errors)
@@ -0,0 +1,10 @@
1
+ """
2
+ Notifications Domain Services.
3
+
4
+ Geek Cafe, LLC
5
+ MIT License. See Project Root for the license information.
6
+ """
7
+
8
+ from .notification_service import NotificationService
9
+
10
+ __all__ = ["NotificationService"]