geek-cafe-saas-sdk 0.7.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.
- geek_cafe_saas_sdk/__init__.py +1 -1
- geek_cafe_saas_sdk/domains/files/models/directory.py +42 -6
- geek_cafe_saas_sdk/domains/files/models/file.py +40 -4
- geek_cafe_saas_sdk/domains/files/models/file_share.py +33 -0
- geek_cafe_saas_sdk/domains/files/models/file_version.py +24 -0
- geek_cafe_saas_sdk/domains/files/services/directory_service.py +54 -135
- geek_cafe_saas_sdk/domains/files/services/file_share_service.py +37 -120
- geek_cafe_saas_sdk/domains/files/services/file_system_service.py +40 -102
- geek_cafe_saas_sdk/domains/files/services/file_version_service.py +44 -124
- geek_cafe_saas_sdk/domains/messaging/services/contact_thread_service.py +55 -7
- geek_cafe_saas_sdk/domains/notifications/__init__.py +18 -0
- geek_cafe_saas_sdk/domains/notifications/handlers/__init__.py +1 -0
- geek_cafe_saas_sdk/domains/notifications/handlers/create_webhook/app.py +73 -0
- geek_cafe_saas_sdk/domains/notifications/handlers/get/app.py +40 -0
- geek_cafe_saas_sdk/domains/notifications/handlers/get_preferences/app.py +34 -0
- geek_cafe_saas_sdk/domains/notifications/handlers/list/app.py +43 -0
- geek_cafe_saas_sdk/domains/notifications/handlers/list_webhooks/app.py +40 -0
- geek_cafe_saas_sdk/domains/notifications/handlers/mark_read/app.py +40 -0
- geek_cafe_saas_sdk/domains/notifications/handlers/send/app.py +83 -0
- geek_cafe_saas_sdk/domains/notifications/handlers/update_preferences/app.py +45 -0
- geek_cafe_saas_sdk/domains/notifications/models/__init__.py +16 -0
- geek_cafe_saas_sdk/domains/notifications/models/notification.py +717 -0
- geek_cafe_saas_sdk/domains/notifications/models/notification_preference.py +365 -0
- geek_cafe_saas_sdk/domains/notifications/models/webhook_subscription.py +339 -0
- geek_cafe_saas_sdk/domains/notifications/services/__init__.py +10 -0
- geek_cafe_saas_sdk/domains/notifications/services/notification_service.py +576 -0
- geek_cafe_saas_sdk/domains/payments/__init__.py +16 -0
- geek_cafe_saas_sdk/domains/payments/handlers/README.md +334 -0
- geek_cafe_saas_sdk/domains/payments/handlers/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/create/app.py +105 -0
- geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/get/app.py +60 -0
- geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/update/app.py +97 -0
- geek_cafe_saas_sdk/domains/payments/handlers/payment_intents/create/app.py +97 -0
- geek_cafe_saas_sdk/domains/payments/handlers/payment_intents/get/app.py +60 -0
- geek_cafe_saas_sdk/domains/payments/handlers/payments/get/app.py +60 -0
- geek_cafe_saas_sdk/domains/payments/handlers/payments/list/app.py +68 -0
- geek_cafe_saas_sdk/domains/payments/handlers/payments/record/app.py +118 -0
- geek_cafe_saas_sdk/domains/payments/handlers/refunds/create/app.py +89 -0
- geek_cafe_saas_sdk/domains/payments/handlers/refunds/get/app.py +60 -0
- geek_cafe_saas_sdk/domains/payments/models/__init__.py +17 -0
- geek_cafe_saas_sdk/domains/payments/models/billing_account.py +521 -0
- geek_cafe_saas_sdk/domains/payments/models/payment.py +639 -0
- geek_cafe_saas_sdk/domains/payments/models/payment_intent_ref.py +539 -0
- geek_cafe_saas_sdk/domains/payments/models/refund.py +404 -0
- geek_cafe_saas_sdk/domains/payments/services/__init__.py +11 -0
- geek_cafe_saas_sdk/domains/payments/services/payment_service.py +405 -0
- geek_cafe_saas_sdk/domains/subscriptions/__init__.py +19 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/README.md +408 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/__init__.py +1 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/create/app.py +81 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/get/app.py +48 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/list/app.py +54 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/update/app.py +54 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/create/app.py +83 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/get/app.py +47 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/validate/app.py +62 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/create/app.py +82 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/get/app.py +48 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/list/app.py +66 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/update/app.py +54 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/usage/aggregate/app.py +72 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/usage/record/app.py +89 -0
- geek_cafe_saas_sdk/domains/subscriptions/models/__init__.py +13 -0
- geek_cafe_saas_sdk/domains/subscriptions/models/addon.py +604 -0
- geek_cafe_saas_sdk/domains/subscriptions/models/discount.py +492 -0
- geek_cafe_saas_sdk/domains/subscriptions/models/plan.py +569 -0
- geek_cafe_saas_sdk/domains/subscriptions/models/usage_record.py +300 -0
- geek_cafe_saas_sdk/domains/subscriptions/services/__init__.py +10 -0
- geek_cafe_saas_sdk/domains/subscriptions/services/subscription_manager_service.py +694 -0
- geek_cafe_saas_sdk/domains/tenancy/models/subscription.py +123 -1
- geek_cafe_saas_sdk/domains/tenancy/services/subscription_service.py +213 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/base_handler.py +7 -0
- geek_cafe_saas_sdk/services/database_service.py +10 -6
- geek_cafe_saas_sdk/utilities/environment_variables.py +16 -0
- geek_cafe_saas_sdk/utilities/logging_utility.py +77 -0
- {geek_cafe_saas_sdk-0.7.0.dist-info → geek_cafe_saas_sdk-0.7.1.dist-info}/METADATA +1 -1
- {geek_cafe_saas_sdk-0.7.0.dist-info → geek_cafe_saas_sdk-0.7.1.dist-info}/RECORD +79 -20
- {geek_cafe_saas_sdk-0.7.0.dist-info → geek_cafe_saas_sdk-0.7.1.dist-info}/WHEEL +0 -0
- {geek_cafe_saas_sdk-0.7.0.dist-info → geek_cafe_saas_sdk-0.7.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -133,21 +133,11 @@ class FileVersionService(DatabaseService[FileVersion]):
|
|
|
133
133
|
self._mark_version_as_not_current(tenant_id, file_id, file.current_version_id)
|
|
134
134
|
|
|
135
135
|
# Save version metadata to DynamoDB
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
item = version.to_dictionary()
|
|
140
|
-
item["pk"] = pk
|
|
141
|
-
item["sk"] = sk
|
|
136
|
+
version.prep_for_save()
|
|
137
|
+
save_result = self._save_model(version)
|
|
142
138
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
item["gsi1_sk"] = f"VERSION#{new_version_num:010d}" # Pad for sorting
|
|
146
|
-
|
|
147
|
-
self.dynamodb.save(
|
|
148
|
-
item=item,
|
|
149
|
-
table_name=self.table_name
|
|
150
|
-
)
|
|
139
|
+
if not save_result.success:
|
|
140
|
+
return save_result
|
|
151
141
|
|
|
152
142
|
# Update file record with new current version
|
|
153
143
|
self._update_file_current_version(
|
|
@@ -194,29 +184,22 @@ class FileVersionService(DatabaseService[FileVersion]):
|
|
|
194
184
|
ServiceResult with FileVersion model
|
|
195
185
|
"""
|
|
196
186
|
try:
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
pk = f"FILE#{tenant_id}#{file_id}"
|
|
201
|
-
sk = f"VERSION#{resource_id}"
|
|
187
|
+
# Use helper method with tenant check
|
|
188
|
+
version = self._get_model_by_id_with_tenant_check(resource_id, FileVersion, tenant_id)
|
|
202
189
|
|
|
203
|
-
|
|
204
|
-
table_name=self.table_name,
|
|
205
|
-
key={"pk": pk, "sk": sk}
|
|
206
|
-
)
|
|
207
|
-
|
|
208
|
-
# Check if version exists
|
|
209
|
-
if not result or 'Item' not in result:
|
|
190
|
+
if not version:
|
|
210
191
|
raise NotFoundError(f"Version not found: {resource_id}")
|
|
211
192
|
|
|
212
|
-
# Convert to FileVersion model
|
|
213
|
-
version = FileVersion()
|
|
214
|
-
version.map(result['Item'])
|
|
215
|
-
|
|
216
193
|
# Access control: Check file ownership
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
194
|
+
if file_id:
|
|
195
|
+
file_result = self._get_file(tenant_id, file_id, user_id)
|
|
196
|
+
if not file_result.success:
|
|
197
|
+
raise AccessDeniedError("You do not have access to this file version")
|
|
198
|
+
else:
|
|
199
|
+
# If no file_id provided, check using version's file_id
|
|
200
|
+
file_result = self._get_file(tenant_id, version.file_id, user_id)
|
|
201
|
+
if not file_result.success:
|
|
202
|
+
raise AccessDeniedError("You do not have access to this file version")
|
|
220
203
|
|
|
221
204
|
return ServiceResult.success_result(version)
|
|
222
205
|
|
|
@@ -280,23 +263,8 @@ class FileVersionService(DatabaseService[FileVersion]):
|
|
|
280
263
|
version.updated_utc_ts = dt.datetime.now(dt.UTC).timestamp()
|
|
281
264
|
|
|
282
265
|
# Save to DynamoDB
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
item = version.to_dictionary()
|
|
287
|
-
item["pk"] = pk
|
|
288
|
-
item["sk"] = sk
|
|
289
|
-
|
|
290
|
-
# Preserve GSI1 keys
|
|
291
|
-
item["gsi1_pk"] = f"FILE#{tenant_id}#{file_id}"
|
|
292
|
-
item["gsi1_sk"] = f"VERSION#{version.version_number:010d}"
|
|
293
|
-
|
|
294
|
-
self.dynamodb.save(
|
|
295
|
-
item=item,
|
|
296
|
-
table_name=self.table_name
|
|
297
|
-
)
|
|
298
|
-
|
|
299
|
-
return ServiceResult.success_result(version)
|
|
266
|
+
version.prep_for_save()
|
|
267
|
+
return self._save_model(version)
|
|
300
268
|
|
|
301
269
|
except (ValidationError, AccessDeniedError) as e:
|
|
302
270
|
return ServiceResult.error_result(
|
|
@@ -348,21 +316,11 @@ class FileVersionService(DatabaseService[FileVersion]):
|
|
|
348
316
|
version.status = "archived"
|
|
349
317
|
version.deleted_utc_ts = dt.datetime.now(dt.UTC).timestamp()
|
|
350
318
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
item = version.to_dictionary()
|
|
355
|
-
item["pk"] = pk
|
|
356
|
-
item["sk"] = sk
|
|
357
|
-
|
|
358
|
-
# Preserve GSI1 keys
|
|
359
|
-
item["gsi1_pk"] = f"FILE#{tenant_id}#{file_id}"
|
|
360
|
-
item["gsi1_sk"] = f"VERSION#{version.version_number:010d}"
|
|
319
|
+
version.prep_for_save()
|
|
320
|
+
save_result = self._save_model(version)
|
|
361
321
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
table_name=self.table_name
|
|
365
|
-
)
|
|
322
|
+
if not save_result.success:
|
|
323
|
+
return save_result
|
|
366
324
|
|
|
367
325
|
# Note: S3 file is kept for potential recovery
|
|
368
326
|
|
|
@@ -408,22 +366,19 @@ class FileVersionService(DatabaseService[FileVersion]):
|
|
|
408
366
|
error_code=ErrorCode.ACCESS_DENIED
|
|
409
367
|
)
|
|
410
368
|
|
|
411
|
-
#
|
|
412
|
-
|
|
369
|
+
# Use GSI1 to query versions by file
|
|
370
|
+
temp_version = FileVersion()
|
|
371
|
+
temp_version.file_id = file_id
|
|
413
372
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
ascending=False # Newest first
|
|
420
|
-
)
|
|
373
|
+
# Query using helper method (descending to get newest first)
|
|
374
|
+
query_result = self._query_by_index(temp_version, "gsi1", limit=limit, ascending=False)
|
|
375
|
+
|
|
376
|
+
if not query_result.success:
|
|
377
|
+
return query_result
|
|
421
378
|
|
|
379
|
+
# Filter results
|
|
422
380
|
versions = []
|
|
423
|
-
for
|
|
424
|
-
version = FileVersion()
|
|
425
|
-
version.map(item)
|
|
426
|
-
|
|
381
|
+
for version in query_result.data:
|
|
427
382
|
# Filter out archived versions (which includes deleted ones)
|
|
428
383
|
if version.status == "active":
|
|
429
384
|
versions.append(version)
|
|
@@ -479,21 +434,11 @@ class FileVersionService(DatabaseService[FileVersion]):
|
|
|
479
434
|
version_to_restore.is_current = True
|
|
480
435
|
version_to_restore.updated_utc_ts = dt.datetime.now(dt.UTC).timestamp()
|
|
481
436
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
item = version_to_restore.to_dictionary()
|
|
486
|
-
item["pk"] = pk
|
|
487
|
-
item["sk"] = sk
|
|
437
|
+
version_to_restore.prep_for_save()
|
|
438
|
+
save_result = self._save_model(version_to_restore)
|
|
488
439
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
item["gsi1_sk"] = f"VERSION#{version_to_restore.version_number:010d}"
|
|
492
|
-
|
|
493
|
-
self.dynamodb.save(
|
|
494
|
-
item=item,
|
|
495
|
-
table_name=self.table_name
|
|
496
|
-
)
|
|
440
|
+
if not save_result.success:
|
|
441
|
+
return save_result
|
|
497
442
|
|
|
498
443
|
# Update file record
|
|
499
444
|
self._update_file_current_version(
|
|
@@ -571,7 +516,7 @@ class FileVersionService(DatabaseService[FileVersion]):
|
|
|
571
516
|
"""Get file with access control."""
|
|
572
517
|
try:
|
|
573
518
|
pk = f"FILE#{tenant_id}#{file_id}"
|
|
574
|
-
sk = "
|
|
519
|
+
sk = "metadata"
|
|
575
520
|
|
|
576
521
|
result = self.dynamodb.get(
|
|
577
522
|
table_name=self.table_name,
|
|
@@ -640,18 +585,8 @@ class FileVersionService(DatabaseService[FileVersion]):
|
|
|
640
585
|
version.map(result['Item'])
|
|
641
586
|
version.is_current = False
|
|
642
587
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
item["sk"] = sk
|
|
646
|
-
|
|
647
|
-
# Preserve GSI1 keys
|
|
648
|
-
item["gsi1_pk"] = result['Item'].get('gsi1_pk')
|
|
649
|
-
item["gsi1_sk"] = result['Item'].get('gsi1_sk')
|
|
650
|
-
|
|
651
|
-
self.dynamodb.save(
|
|
652
|
-
item=item,
|
|
653
|
-
table_name=self.table_name
|
|
654
|
-
)
|
|
588
|
+
version.prep_for_save()
|
|
589
|
+
self._save_model(version)
|
|
655
590
|
except Exception:
|
|
656
591
|
pass # Best effort
|
|
657
592
|
|
|
@@ -665,7 +600,7 @@ class FileVersionService(DatabaseService[FileVersion]):
|
|
|
665
600
|
"""Update file record with current version info."""
|
|
666
601
|
try:
|
|
667
602
|
pk = f"FILE#{tenant_id}#{file_id}"
|
|
668
|
-
sk = "
|
|
603
|
+
sk = "metadata"
|
|
669
604
|
|
|
670
605
|
result = self.dynamodb.get(
|
|
671
606
|
table_name=self.table_name,
|
|
@@ -680,14 +615,8 @@ class FileVersionService(DatabaseService[FileVersion]):
|
|
|
680
615
|
file.version_count = version_number
|
|
681
616
|
file.updated_utc_ts = dt.datetime.now(dt.UTC).timestamp()
|
|
682
617
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
item["sk"] = sk
|
|
686
|
-
|
|
687
|
-
self.dynamodb.save(
|
|
688
|
-
item=item,
|
|
689
|
-
table_name=self.table_name
|
|
690
|
-
)
|
|
618
|
+
file.prep_for_save()
|
|
619
|
+
self._save_model(file)
|
|
691
620
|
except Exception:
|
|
692
621
|
pass # Best effort
|
|
693
622
|
|
|
@@ -719,17 +648,8 @@ class FileVersionService(DatabaseService[FileVersion]):
|
|
|
719
648
|
version.status = "archived"
|
|
720
649
|
version.updated_utc_ts = dt.datetime.now(dt.UTC).timestamp()
|
|
721
650
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
item_dict = version.to_dictionary()
|
|
726
|
-
item_dict["pk"] = pk
|
|
727
|
-
item_dict["sk"] = sk
|
|
728
|
-
|
|
729
|
-
self.dynamodb.save(
|
|
730
|
-
item=item_dict,
|
|
731
|
-
table_name=self.table_name
|
|
732
|
-
)
|
|
651
|
+
version.prep_for_save()
|
|
652
|
+
self._save_model(version)
|
|
733
653
|
except Exception:
|
|
734
654
|
pass # Best effort
|
|
735
655
|
|
|
@@ -20,6 +20,40 @@ class ContactThreadService(DatabaseService[ContactThread]):
|
|
|
20
20
|
def __init__(self, *, dynamodb: DynamoDB = None, table_name: str = None):
|
|
21
21
|
super().__init__(dynamodb=dynamodb, table_name=table_name)
|
|
22
22
|
|
|
23
|
+
def _user_has_cross_tenant_access(self, user_context: Optional[Dict[str, str]]) -> bool:
|
|
24
|
+
"""
|
|
25
|
+
Determine if user has cross-tenant access to contact threads.
|
|
26
|
+
|
|
27
|
+
Users with cross-tenant access can see threads from any tenant:
|
|
28
|
+
- Platform admins (roles contains "platform_admin")
|
|
29
|
+
- Support staff (roles contains "support_staff" or "support_admin")
|
|
30
|
+
- General admins (roles contains "admin")
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
user_context: User context from JWT containing roles
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
True if user has cross-tenant access, False otherwise
|
|
37
|
+
"""
|
|
38
|
+
if not user_context:
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
roles = user_context.get("roles", "")
|
|
42
|
+
|
|
43
|
+
cross_tenant_roles = [
|
|
44
|
+
"platform_admin",
|
|
45
|
+
"support_admin",
|
|
46
|
+
"support_staff",
|
|
47
|
+
"admin"
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
# Check if user has any cross-tenant role
|
|
51
|
+
for role in cross_tenant_roles:
|
|
52
|
+
if role in roles:
|
|
53
|
+
return True
|
|
54
|
+
|
|
55
|
+
return False
|
|
56
|
+
|
|
23
57
|
def create(self, tenant_id: str, user_id: str, payload: Dict[str, Any]) -> ServiceResult[ContactThread]:
|
|
24
58
|
"""
|
|
25
59
|
Create a new contact thread from a payload.
|
|
@@ -115,16 +149,22 @@ class ContactThreadService(DatabaseService[ContactThread]):
|
|
|
115
149
|
return self._handle_service_exception(e, 'get_contact_thread', resource_id=resource_id, tenant_id=tenant_id)
|
|
116
150
|
|
|
117
151
|
def list_by_inbox_and_status(self, inbox_id: str, status: str, tenant_id: str,
|
|
118
|
-
priority: str = None, limit: int = 50
|
|
152
|
+
priority: str = None, limit: int = 50,
|
|
153
|
+
user_context: Optional[Dict[str, str]] = None) -> ServiceResult[List[ContactThread]]:
|
|
119
154
|
"""
|
|
120
155
|
List contact threads by inbox and status using GSI1.
|
|
121
156
|
|
|
157
|
+
Supports role-based access control:
|
|
158
|
+
- Admins/support staff see ALL threads in the inbox (cross-tenant access)
|
|
159
|
+
- Regular users only see threads from their own tenant
|
|
160
|
+
|
|
122
161
|
Args:
|
|
123
162
|
inbox_id: Inbox ID (support, sales, etc.)
|
|
124
163
|
status: Status filter (open, in_progress, resolved, closed)
|
|
125
|
-
tenant_id: Tenant ID for filtering
|
|
164
|
+
tenant_id: Tenant ID for filtering (ignored for admins/support)
|
|
126
165
|
priority: Optional priority filter
|
|
127
166
|
limit: Maximum number of results
|
|
167
|
+
user_context: Optional user context with roles for access control
|
|
128
168
|
|
|
129
169
|
Returns:
|
|
130
170
|
ServiceResult with list of ContactThreads
|
|
@@ -148,11 +188,19 @@ class ContactThreadService(DatabaseService[ContactThread]):
|
|
|
148
188
|
if not result.success:
|
|
149
189
|
return result
|
|
150
190
|
|
|
151
|
-
#
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
191
|
+
# Apply role-based filtering
|
|
192
|
+
if self._user_has_cross_tenant_access(user_context):
|
|
193
|
+
# Admins/support: See all threads in the inbox, regardless of tenant
|
|
194
|
+
active_threads = [
|
|
195
|
+
t for t in result.data
|
|
196
|
+
if not t.is_deleted()
|
|
197
|
+
]
|
|
198
|
+
else:
|
|
199
|
+
# Regular users: Only see threads from their own tenant
|
|
200
|
+
active_threads = [
|
|
201
|
+
t for t in result.data
|
|
202
|
+
if not t.is_deleted() and t.tenant_id == tenant_id
|
|
203
|
+
]
|
|
156
204
|
|
|
157
205
|
return ServiceResult.success_result(active_threads)
|
|
158
206
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Notifications Domain.
|
|
3
|
+
|
|
4
|
+
Multi-channel notification delivery with user preferences and webhook support.
|
|
5
|
+
|
|
6
|
+
Geek Cafe, LLC
|
|
7
|
+
MIT License. See Project Root for the license information.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .models import Notification, NotificationPreference, WebhookSubscription
|
|
11
|
+
from .services import NotificationService
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"Notification",
|
|
15
|
+
"NotificationPreference",
|
|
16
|
+
"WebhookSubscription",
|
|
17
|
+
"NotificationService"
|
|
18
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Notifications Domain Handlers
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Lambda handler for creating webhook subscriptions."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Any
|
|
4
|
+
from geek_cafe_saas_sdk.lambda_handlers import create_handler
|
|
5
|
+
from geek_cafe_saas_sdk.domains.notifications.services import NotificationService
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
handler_wrapper = create_handler(
|
|
9
|
+
service_class=NotificationService,
|
|
10
|
+
require_body=True,
|
|
11
|
+
convert_case=True
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
|
|
16
|
+
"""
|
|
17
|
+
Create webhook subscription.
|
|
18
|
+
|
|
19
|
+
POST /webhooks
|
|
20
|
+
|
|
21
|
+
Body:
|
|
22
|
+
{
|
|
23
|
+
"subscriptionName": "Payment Events",
|
|
24
|
+
"url": "https://example.com/webhooks/payments",
|
|
25
|
+
"eventTypes": ["payment.completed", "payment.failed"],
|
|
26
|
+
"secret": "webhook_secret_key"
|
|
27
|
+
}
|
|
28
|
+
"""
|
|
29
|
+
return handler_wrapper.execute(event, context, create_webhook, injected_service)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def create_webhook(
|
|
33
|
+
event: Dict[str, Any],
|
|
34
|
+
service: NotificationService,
|
|
35
|
+
user_context: Dict[str, str]
|
|
36
|
+
) -> Any:
|
|
37
|
+
"""Business logic for creating webhook."""
|
|
38
|
+
tenant_id = user_context.get("tenant_id")
|
|
39
|
+
payload = event["parsed_body"]
|
|
40
|
+
|
|
41
|
+
subscription_name = payload.get("subscription_name")
|
|
42
|
+
if not subscription_name:
|
|
43
|
+
raise ValueError("subscription_name is required")
|
|
44
|
+
|
|
45
|
+
url = payload.get("url")
|
|
46
|
+
if not url:
|
|
47
|
+
raise ValueError("url is required")
|
|
48
|
+
|
|
49
|
+
event_types = payload.get("event_types")
|
|
50
|
+
if not event_types:
|
|
51
|
+
raise ValueError("event_types is required")
|
|
52
|
+
|
|
53
|
+
# Extract optional fields
|
|
54
|
+
kwargs = {}
|
|
55
|
+
optional_fields = [
|
|
56
|
+
"secret", "api_key", "custom_headers", "http_method", "content_type",
|
|
57
|
+
"timeout_seconds", "retry_enabled", "max_retries", "retry_delay_seconds",
|
|
58
|
+
"event_filters", "description", "metadata"
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
for field in optional_fields:
|
|
62
|
+
if field in payload:
|
|
63
|
+
kwargs[field] = payload[field]
|
|
64
|
+
|
|
65
|
+
result = service.create_webhook_subscription(
|
|
66
|
+
tenant_id=tenant_id,
|
|
67
|
+
subscription_name=subscription_name,
|
|
68
|
+
url=url,
|
|
69
|
+
event_types=event_types,
|
|
70
|
+
**kwargs
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
return result
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Lambda handler for getting a notification."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Any
|
|
4
|
+
from geek_cafe_saas_sdk.lambda_handlers import create_handler
|
|
5
|
+
from geek_cafe_saas_sdk.domains.notifications.services import NotificationService
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
handler_wrapper = create_handler(
|
|
9
|
+
service_class=NotificationService,
|
|
10
|
+
require_body=False,
|
|
11
|
+
convert_case=True
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
|
|
16
|
+
"""
|
|
17
|
+
Get a notification by ID.
|
|
18
|
+
|
|
19
|
+
GET /notifications/{notificationId}
|
|
20
|
+
"""
|
|
21
|
+
return handler_wrapper.execute(event, context, get_notification, injected_service)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_notification(
|
|
25
|
+
event: Dict[str, Any],
|
|
26
|
+
service: NotificationService,
|
|
27
|
+
user_context: Dict[str, str]
|
|
28
|
+
) -> Any:
|
|
29
|
+
"""Business logic for getting notification."""
|
|
30
|
+
tenant_id = user_context.get("tenant_id")
|
|
31
|
+
|
|
32
|
+
path_params = event.get("pathParameters") or {}
|
|
33
|
+
notification_id = path_params.get("notification_id")
|
|
34
|
+
|
|
35
|
+
if not notification_id:
|
|
36
|
+
raise ValueError("notification_id is required in path")
|
|
37
|
+
|
|
38
|
+
result = service.get_notification(tenant_id, notification_id)
|
|
39
|
+
|
|
40
|
+
return result
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Lambda handler for getting user notification preferences."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Any
|
|
4
|
+
from geek_cafe_saas_sdk.lambda_handlers import create_handler
|
|
5
|
+
from geek_cafe_saas_sdk.domains.notifications.services import NotificationService
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
handler_wrapper = create_handler(
|
|
9
|
+
service_class=NotificationService,
|
|
10
|
+
require_body=False,
|
|
11
|
+
convert_case=True
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
|
|
16
|
+
"""
|
|
17
|
+
Get user notification preferences.
|
|
18
|
+
|
|
19
|
+
GET /notifications/preferences
|
|
20
|
+
"""
|
|
21
|
+
return handler_wrapper.execute(event, context, get_preferences, injected_service)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_preferences(
|
|
25
|
+
event: Dict[str, Any],
|
|
26
|
+
service: NotificationService,
|
|
27
|
+
user_context: Dict[str, str]
|
|
28
|
+
) -> Any:
|
|
29
|
+
"""Business logic for getting preferences."""
|
|
30
|
+
user_id = user_context.get("user_id")
|
|
31
|
+
|
|
32
|
+
result = service.get_user_preferences(user_id)
|
|
33
|
+
|
|
34
|
+
return result
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Lambda handler for listing notifications."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Any
|
|
4
|
+
from geek_cafe_saas_sdk.lambda_handlers import create_handler
|
|
5
|
+
from geek_cafe_saas_sdk.domains.notifications.services import NotificationService
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
handler_wrapper = create_handler(
|
|
9
|
+
service_class=NotificationService,
|
|
10
|
+
require_body=False,
|
|
11
|
+
convert_case=True
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
|
|
16
|
+
"""
|
|
17
|
+
List notifications for current user.
|
|
18
|
+
|
|
19
|
+
GET /notifications?unreadOnly=true&limit=50
|
|
20
|
+
"""
|
|
21
|
+
return handler_wrapper.execute(event, context, list_notifications, injected_service)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def list_notifications(
|
|
25
|
+
event: Dict[str, Any],
|
|
26
|
+
service: NotificationService,
|
|
27
|
+
user_context: Dict[str, str]
|
|
28
|
+
) -> Any:
|
|
29
|
+
"""Business logic for listing notifications."""
|
|
30
|
+
user_id = user_context.get("user_id")
|
|
31
|
+
|
|
32
|
+
params = event.get("queryStringParameters") or {}
|
|
33
|
+
|
|
34
|
+
limit = int(params.get("limit", "50"))
|
|
35
|
+
unread_only = params.get("unread_only", "false").lower() == "true"
|
|
36
|
+
|
|
37
|
+
result = service.list_notifications(
|
|
38
|
+
recipient_id=user_id,
|
|
39
|
+
limit=limit,
|
|
40
|
+
unread_only=unread_only
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
return result
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Lambda handler for listing webhook subscriptions."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Any
|
|
4
|
+
from geek_cafe_saas_sdk.lambda_handlers import create_handler
|
|
5
|
+
from geek_cafe_saas_sdk.domains.notifications.services import NotificationService
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
handler_wrapper = create_handler(
|
|
9
|
+
service_class=NotificationService,
|
|
10
|
+
require_body=False,
|
|
11
|
+
convert_case=True
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
|
|
16
|
+
"""
|
|
17
|
+
List webhook subscriptions.
|
|
18
|
+
|
|
19
|
+
GET /webhooks?activeOnly=true
|
|
20
|
+
"""
|
|
21
|
+
return handler_wrapper.execute(event, context, list_webhooks, injected_service)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def list_webhooks(
|
|
25
|
+
event: Dict[str, Any],
|
|
26
|
+
service: NotificationService,
|
|
27
|
+
user_context: Dict[str, str]
|
|
28
|
+
) -> Any:
|
|
29
|
+
"""Business logic for listing webhooks."""
|
|
30
|
+
tenant_id = user_context.get("tenant_id")
|
|
31
|
+
|
|
32
|
+
params = event.get("queryStringParameters") or {}
|
|
33
|
+
active_only = params.get("active_only", "true").lower() == "true"
|
|
34
|
+
|
|
35
|
+
result = service.list_webhook_subscriptions(
|
|
36
|
+
tenant_id=tenant_id,
|
|
37
|
+
active_only=active_only
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
return result
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Lambda handler for marking notification as read."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Any
|
|
4
|
+
from geek_cafe_saas_sdk.lambda_handlers import create_handler
|
|
5
|
+
from geek_cafe_saas_sdk.domains.notifications.services import NotificationService
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
handler_wrapper = create_handler(
|
|
9
|
+
service_class=NotificationService,
|
|
10
|
+
require_body=False,
|
|
11
|
+
convert_case=True
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
|
|
16
|
+
"""
|
|
17
|
+
Mark notification as read.
|
|
18
|
+
|
|
19
|
+
PATCH /notifications/{notificationId}/read
|
|
20
|
+
"""
|
|
21
|
+
return handler_wrapper.execute(event, context, mark_read, injected_service)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def mark_read(
|
|
25
|
+
event: Dict[str, Any],
|
|
26
|
+
service: NotificationService,
|
|
27
|
+
user_context: Dict[str, str]
|
|
28
|
+
) -> Any:
|
|
29
|
+
"""Business logic for marking as read."""
|
|
30
|
+
tenant_id = user_context.get("tenant_id")
|
|
31
|
+
|
|
32
|
+
path_params = event.get("pathParameters") or {}
|
|
33
|
+
notification_id = path_params.get("notification_id")
|
|
34
|
+
|
|
35
|
+
if not notification_id:
|
|
36
|
+
raise ValueError("notification_id is required in path")
|
|
37
|
+
|
|
38
|
+
result = service.mark_as_read(tenant_id, notification_id)
|
|
39
|
+
|
|
40
|
+
return result
|