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
@@ -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
- pk = f"FILE#{tenant_id}#{file_id}"
137
- sk = f"VERSION#{version.version_id}"
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
- # GSI1: Versions by file (ordered by version number)
144
- item["gsi1_pk"] = f"FILE#{tenant_id}#{file_id}"
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
- if not file_id:
198
- raise ValidationError("file_id is required", "file_id")
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
- result = self.dynamodb.get(
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
- file_result = self._get_file(tenant_id, file_id, user_id)
218
- if not file_result.success:
219
- raise AccessDeniedError("You do not have access to this file version")
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
- pk = f"FILE#{tenant_id}#{file_id}"
284
- sk = f"VERSION#{resource_id}"
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
- pk = f"FILE#{tenant_id}#{file_id}"
352
- sk = f"VERSION#{resource_id}"
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
- self.dynamodb.save(
363
- item=item,
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
- # Query GSI1 for versions
412
- gsi1_pk = f"FILE#{tenant_id}#{file_id}"
369
+ # Use GSI1 to query versions by file
370
+ temp_version = FileVersion()
371
+ temp_version.file_id = file_id
413
372
 
414
- results = self.dynamodb.query(
415
- key=Key('gsi1_pk').eq(gsi1_pk) & Key('gsi1_sk').begins_with("VERSION#"),
416
- table_name=self.table_name,
417
- index_name="gsi1",
418
- limit=limit,
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 item in results.get('Items', []):
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
- pk = f"FILE#{tenant_id}#{file_id}"
483
- sk = f"VERSION#{version_id}"
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
- # Preserve GSI1 keys
490
- item["gsi1_pk"] = f"FILE#{tenant_id}#{file_id}"
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 = "METADATA"
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
- item = version.to_dictionary()
644
- item["pk"] = pk
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 = "METADATA"
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
- item = file.to_dictionary()
684
- item["pk"] = pk
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
- pk = f"FILE#{tenant_id}#{file_id}"
723
- sk = f"VERSION#{version.version_id}"
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) -> ServiceResult[List[ContactThread]]:
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
- # Filter by tenant and exclude deleted threads
152
- active_threads = [
153
- t for t in result.data
154
- if not t.is_deleted() and t.tenant_id == tenant_id
155
- ]
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