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.

Files changed (79) hide show
  1. geek_cafe_saas_sdk/__init__.py +1 -1
  2. geek_cafe_saas_sdk/domains/files/models/directory.py +42 -6
  3. geek_cafe_saas_sdk/domains/files/models/file.py +40 -4
  4. geek_cafe_saas_sdk/domains/files/models/file_share.py +33 -0
  5. geek_cafe_saas_sdk/domains/files/models/file_version.py +24 -0
  6. geek_cafe_saas_sdk/domains/files/services/directory_service.py +54 -135
  7. geek_cafe_saas_sdk/domains/files/services/file_share_service.py +37 -120
  8. geek_cafe_saas_sdk/domains/files/services/file_system_service.py +40 -102
  9. geek_cafe_saas_sdk/domains/files/services/file_version_service.py +44 -124
  10. geek_cafe_saas_sdk/domains/messaging/services/contact_thread_service.py +55 -7
  11. geek_cafe_saas_sdk/domains/notifications/__init__.py +18 -0
  12. geek_cafe_saas_sdk/domains/notifications/handlers/__init__.py +1 -0
  13. geek_cafe_saas_sdk/domains/notifications/handlers/create_webhook/app.py +73 -0
  14. geek_cafe_saas_sdk/domains/notifications/handlers/get/app.py +40 -0
  15. geek_cafe_saas_sdk/domains/notifications/handlers/get_preferences/app.py +34 -0
  16. geek_cafe_saas_sdk/domains/notifications/handlers/list/app.py +43 -0
  17. geek_cafe_saas_sdk/domains/notifications/handlers/list_webhooks/app.py +40 -0
  18. geek_cafe_saas_sdk/domains/notifications/handlers/mark_read/app.py +40 -0
  19. geek_cafe_saas_sdk/domains/notifications/handlers/send/app.py +83 -0
  20. geek_cafe_saas_sdk/domains/notifications/handlers/update_preferences/app.py +45 -0
  21. geek_cafe_saas_sdk/domains/notifications/models/__init__.py +16 -0
  22. geek_cafe_saas_sdk/domains/notifications/models/notification.py +717 -0
  23. geek_cafe_saas_sdk/domains/notifications/models/notification_preference.py +365 -0
  24. geek_cafe_saas_sdk/domains/notifications/models/webhook_subscription.py +339 -0
  25. geek_cafe_saas_sdk/domains/notifications/services/__init__.py +10 -0
  26. geek_cafe_saas_sdk/domains/notifications/services/notification_service.py +576 -0
  27. geek_cafe_saas_sdk/domains/payments/__init__.py +16 -0
  28. geek_cafe_saas_sdk/domains/payments/handlers/README.md +334 -0
  29. geek_cafe_saas_sdk/domains/payments/handlers/__init__.py +6 -0
  30. geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/create/app.py +105 -0
  31. geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/get/app.py +60 -0
  32. geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/update/app.py +97 -0
  33. geek_cafe_saas_sdk/domains/payments/handlers/payment_intents/create/app.py +97 -0
  34. geek_cafe_saas_sdk/domains/payments/handlers/payment_intents/get/app.py +60 -0
  35. geek_cafe_saas_sdk/domains/payments/handlers/payments/get/app.py +60 -0
  36. geek_cafe_saas_sdk/domains/payments/handlers/payments/list/app.py +68 -0
  37. geek_cafe_saas_sdk/domains/payments/handlers/payments/record/app.py +118 -0
  38. geek_cafe_saas_sdk/domains/payments/handlers/refunds/create/app.py +89 -0
  39. geek_cafe_saas_sdk/domains/payments/handlers/refunds/get/app.py +60 -0
  40. geek_cafe_saas_sdk/domains/payments/models/__init__.py +17 -0
  41. geek_cafe_saas_sdk/domains/payments/models/billing_account.py +521 -0
  42. geek_cafe_saas_sdk/domains/payments/models/payment.py +639 -0
  43. geek_cafe_saas_sdk/domains/payments/models/payment_intent_ref.py +539 -0
  44. geek_cafe_saas_sdk/domains/payments/models/refund.py +404 -0
  45. geek_cafe_saas_sdk/domains/payments/services/__init__.py +11 -0
  46. geek_cafe_saas_sdk/domains/payments/services/payment_service.py +405 -0
  47. geek_cafe_saas_sdk/domains/subscriptions/__init__.py +19 -0
  48. geek_cafe_saas_sdk/domains/subscriptions/handlers/README.md +408 -0
  49. geek_cafe_saas_sdk/domains/subscriptions/handlers/__init__.py +1 -0
  50. geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/create/app.py +81 -0
  51. geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/get/app.py +48 -0
  52. geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/list/app.py +54 -0
  53. geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/update/app.py +54 -0
  54. geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/create/app.py +83 -0
  55. geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/get/app.py +47 -0
  56. geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/validate/app.py +62 -0
  57. geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/create/app.py +82 -0
  58. geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/get/app.py +48 -0
  59. geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/list/app.py +66 -0
  60. geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/update/app.py +54 -0
  61. geek_cafe_saas_sdk/domains/subscriptions/handlers/usage/aggregate/app.py +72 -0
  62. geek_cafe_saas_sdk/domains/subscriptions/handlers/usage/record/app.py +89 -0
  63. geek_cafe_saas_sdk/domains/subscriptions/models/__init__.py +13 -0
  64. geek_cafe_saas_sdk/domains/subscriptions/models/addon.py +604 -0
  65. geek_cafe_saas_sdk/domains/subscriptions/models/discount.py +492 -0
  66. geek_cafe_saas_sdk/domains/subscriptions/models/plan.py +569 -0
  67. geek_cafe_saas_sdk/domains/subscriptions/models/usage_record.py +300 -0
  68. geek_cafe_saas_sdk/domains/subscriptions/services/__init__.py +10 -0
  69. geek_cafe_saas_sdk/domains/subscriptions/services/subscription_manager_service.py +694 -0
  70. geek_cafe_saas_sdk/domains/tenancy/models/subscription.py +123 -1
  71. geek_cafe_saas_sdk/domains/tenancy/services/subscription_service.py +213 -0
  72. geek_cafe_saas_sdk/lambda_handlers/_base/base_handler.py +7 -0
  73. geek_cafe_saas_sdk/services/database_service.py +10 -6
  74. geek_cafe_saas_sdk/utilities/environment_variables.py +16 -0
  75. geek_cafe_saas_sdk/utilities/logging_utility.py +77 -0
  76. {geek_cafe_saas_sdk-0.7.0.dist-info → geek_cafe_saas_sdk-0.7.1.dist-info}/METADATA +1 -1
  77. {geek_cafe_saas_sdk-0.7.0.dist-info → geek_cafe_saas_sdk-0.7.1.dist-info}/RECORD +79 -20
  78. {geek_cafe_saas_sdk-0.7.0.dist-info → geek_cafe_saas_sdk-0.7.1.dist-info}/WHEEL +0 -0
  79. {geek_cafe_saas_sdk-0.7.0.dist-info → geek_cafe_saas_sdk-0.7.1.dist-info}/licenses/LICENSE +0 -0
@@ -45,11 +45,21 @@ class Subscription(BaseModel):
45
45
  # Subscription status
46
46
  self._status: str = "trial" # trial|active|past_due|canceled|expired
47
47
 
48
- # Plan details
48
+ # Plan details (NEW: References platform-wide Plan model)
49
+ self._plan_id: str | None = None # Reference to subscriptions.Plan.id
49
50
  self._plan_code: str | None = None # "free"|"basic"|"pro"|"enterprise"
50
51
  self._plan_name: str | None = None # Display name
51
52
  self._seat_count: int = 1 # Number of users/seats
52
53
 
54
+ # Addons (NEW: Support for add-on modules)
55
+ self._active_addons: list[str] = [] # List of active addon codes
56
+ self._addon_metadata: dict[str, dict] = {} # Per-addon settings
57
+
58
+ # Discounts (NEW: Promotional discounts)
59
+ self._discount_id: str | None = None # Reference to subscriptions.Discount.id
60
+ self._discount_code: str | None = None # Promo code used
61
+ self._discount_amount_cents: int = 0 # Discount applied per period
62
+
53
63
  # Pricing
54
64
  self._price_cents: int = 0 # Price in cents (e.g., 2999 = $29.99)
55
65
  self._currency: str = "USD"
@@ -166,6 +176,16 @@ class Subscription(BaseModel):
166
176
  raise ValueError(f"Invalid status: {value}. Must be one of {valid_statuses}")
167
177
  self._status = value
168
178
 
179
+ # Plan ID (NEW)
180
+ @property
181
+ def plan_id(self) -> str | None:
182
+ """Plan ID reference."""
183
+ return self._plan_id
184
+
185
+ @plan_id.setter
186
+ def plan_id(self, value: str | None):
187
+ self._plan_id = value
188
+
169
189
  # Plan Code
170
190
  @property
171
191
  def plan_code(self) -> str | None:
@@ -372,6 +392,58 @@ class Subscription(BaseModel):
372
392
  def notes(self, value: str | None):
373
393
  self._notes = value
374
394
 
395
+ # Active Addons (NEW)
396
+ @property
397
+ def active_addons(self) -> list[str]:
398
+ """List of active addon codes."""
399
+ return self._active_addons
400
+
401
+ @active_addons.setter
402
+ def active_addons(self, value: list[str]):
403
+ self._active_addons = value if value else []
404
+
405
+ # Addon Metadata (NEW)
406
+ @property
407
+ def addon_metadata(self) -> dict[str, dict]:
408
+ """Per-addon metadata."""
409
+ return self._addon_metadata
410
+
411
+ @addon_metadata.setter
412
+ def addon_metadata(self, value: dict[str, dict]):
413
+ self._addon_metadata = value if value else {}
414
+
415
+ # Discount ID (NEW)
416
+ @property
417
+ def discount_id(self) -> str | None:
418
+ """Discount ID reference."""
419
+ return self._discount_id
420
+
421
+ @discount_id.setter
422
+ def discount_id(self, value: str | None):
423
+ self._discount_id = value
424
+
425
+ # Discount Code (NEW)
426
+ @property
427
+ def discount_code(self) -> str | None:
428
+ """Promo code used."""
429
+ return self._discount_code
430
+
431
+ @discount_code.setter
432
+ def discount_code(self, value: str | None):
433
+ self._discount_code = value
434
+
435
+ # Discount Amount Cents (NEW)
436
+ @property
437
+ def discount_amount_cents(self) -> int:
438
+ """Discount amount per period in cents."""
439
+ return self._discount_amount_cents
440
+
441
+ @discount_amount_cents.setter
442
+ def discount_amount_cents(self, value: int):
443
+ if value < 0:
444
+ raise ValueError("discount_amount_cents cannot be negative")
445
+ self._discount_amount_cents = value
446
+
375
447
  # Helper Methods
376
448
 
377
449
  def is_active(self) -> bool:
@@ -438,3 +510,53 @@ class Subscription(BaseModel):
438
510
  """Get formatted price for display."""
439
511
  dollars = self._price_cents / 100
440
512
  return f"${dollars:.2f} {self._currency}"
513
+
514
+ # NEW Helper Methods for Addons and Discounts
515
+
516
+ def has_addon(self, addon_code: str) -> bool:
517
+ """Check if addon is active on subscription."""
518
+ return addon_code in self._active_addons
519
+
520
+ def add_addon(self, addon_code: str, metadata: dict | None = None):
521
+ """Add an addon to subscription."""
522
+ if addon_code not in self._active_addons:
523
+ self._active_addons.append(addon_code)
524
+
525
+ if metadata:
526
+ self._addon_metadata[addon_code] = metadata
527
+
528
+ def remove_addon(self, addon_code: str):
529
+ """Remove an addon from subscription."""
530
+ if addon_code in self._active_addons:
531
+ self._active_addons.remove(addon_code)
532
+
533
+ if addon_code in self._addon_metadata:
534
+ del self._addon_metadata[addon_code]
535
+
536
+ def get_addon_metadata(self, addon_code: str) -> dict | None:
537
+ """Get metadata for specific addon."""
538
+ return self._addon_metadata.get(addon_code)
539
+
540
+ def set_addon_metadata(self, addon_code: str, metadata: dict):
541
+ """Set metadata for specific addon."""
542
+ self._addon_metadata[addon_code] = metadata
543
+
544
+ def has_discount(self) -> bool:
545
+ """Check if subscription has an active discount."""
546
+ return self._discount_id is not None
547
+
548
+ def apply_discount(self, discount_id: str, discount_code: str, discount_amount_cents: int):
549
+ """Apply a discount to subscription."""
550
+ self._discount_id = discount_id
551
+ self._discount_code = discount_code
552
+ self._discount_amount_cents = discount_amount_cents
553
+
554
+ def remove_discount(self):
555
+ """Remove discount from subscription."""
556
+ self._discount_id = None
557
+ self._discount_code = None
558
+ self._discount_amount_cents = 0
559
+
560
+ def get_final_price_cents(self) -> int:
561
+ """Get final price after discount."""
562
+ return max(0, self._price_cents - self._discount_amount_cents)
@@ -555,3 +555,216 @@ class SubscriptionService(DatabaseService[Subscription]):
555
555
  except Exception as e:
556
556
  return self._handle_service_exception(e, 'delete_subscription',
557
557
  resource_id=resource_id, tenant_id=tenant_id)
558
+
559
+ # ========================================================================
560
+ # NEW: Plan and Addon Integration Methods
561
+ # ========================================================================
562
+
563
+ def create_from_plan(
564
+ self,
565
+ tenant_id: str,
566
+ user_id: str,
567
+ plan_id: str,
568
+ plan_code: str,
569
+ plan_name: str,
570
+ price_cents: int,
571
+ seat_count: int = 1,
572
+ billing_interval: str = "month",
573
+ **kwargs
574
+ ) -> ServiceResult[Subscription]:
575
+ """
576
+ Create a subscription from a Plan definition.
577
+
578
+ This links the tenant subscription to a platform-wide Plan,
579
+ copying pricing and configuration at subscription time.
580
+
581
+ Args:
582
+ tenant_id: Tenant ID
583
+ user_id: User creating subscription
584
+ plan_id: Reference to subscriptions.Plan.id
585
+ plan_code: Plan code
586
+ plan_name: Plan name
587
+ price_cents: Price in cents
588
+ seat_count: Number of seats
589
+ billing_interval: "month" or "year"
590
+ **kwargs: Additional fields
591
+
592
+ Returns:
593
+ ServiceResult with Subscription
594
+ """
595
+ payload = {
596
+ "plan_id": plan_id,
597
+ "plan_code": plan_code,
598
+ "plan_name": plan_name,
599
+ "price_cents": price_cents,
600
+ "seat_count": seat_count,
601
+ "billing_interval": billing_interval,
602
+ **kwargs
603
+ }
604
+
605
+ return self.activate_subscription(tenant_id, user_id, payload)
606
+
607
+ def add_addon_to_subscription(
608
+ self,
609
+ subscription_id: str,
610
+ tenant_id: str,
611
+ user_id: str,
612
+ addon_code: str,
613
+ addon_metadata: Optional[Dict[str, Any]] = None
614
+ ) -> ServiceResult[Subscription]:
615
+ """
616
+ Add an addon to an existing subscription.
617
+
618
+ Args:
619
+ subscription_id: Subscription ID
620
+ tenant_id: Tenant ID
621
+ user_id: User performing action
622
+ addon_code: Addon code to add
623
+ addon_metadata: Optional addon-specific settings
624
+
625
+ Returns:
626
+ ServiceResult with updated Subscription
627
+ """
628
+ try:
629
+ # Get subscription
630
+ result = self.get_by_id(subscription_id, tenant_id, user_id)
631
+ if not result.success:
632
+ return result
633
+
634
+ subscription = result.data
635
+
636
+ # Add addon
637
+ subscription.add_addon(addon_code, addon_metadata)
638
+
639
+ # Update
640
+ subscription.updated_by_id = user_id
641
+ subscription.updated_utc_ts = dt.datetime.now(dt.UTC).timestamp()
642
+ subscription.version += 1
643
+
644
+ # Save
645
+ return self._save_model(subscription)
646
+
647
+ except Exception as e:
648
+ return self._handle_service_exception(
649
+ e, 'add_addon_to_subscription',
650
+ subscription_id=subscription_id, tenant_id=tenant_id
651
+ )
652
+
653
+ def remove_addon_from_subscription(
654
+ self,
655
+ subscription_id: str,
656
+ tenant_id: str,
657
+ user_id: str,
658
+ addon_code: str
659
+ ) -> ServiceResult[Subscription]:
660
+ """
661
+ Remove an addon from a subscription.
662
+
663
+ Args:
664
+ subscription_id: Subscription ID
665
+ tenant_id: Tenant ID
666
+ user_id: User performing action
667
+ addon_code: Addon code to remove
668
+
669
+ Returns:
670
+ ServiceResult with updated Subscription
671
+ """
672
+ try:
673
+ result = self.get_by_id(subscription_id, tenant_id, user_id)
674
+ if not result.success:
675
+ return result
676
+
677
+ subscription = result.data
678
+ subscription.remove_addon(addon_code)
679
+
680
+ subscription.updated_by_id = user_id
681
+ subscription.updated_utc_ts = dt.datetime.now(dt.UTC).timestamp()
682
+ subscription.version += 1
683
+
684
+ return self._save_model(subscription)
685
+
686
+ except Exception as e:
687
+ return self._handle_service_exception(
688
+ e, 'remove_addon_from_subscription',
689
+ subscription_id=subscription_id, tenant_id=tenant_id
690
+ )
691
+
692
+ def apply_discount_to_subscription(
693
+ self,
694
+ subscription_id: str,
695
+ tenant_id: str,
696
+ user_id: str,
697
+ discount_id: str,
698
+ discount_code: str,
699
+ discount_amount_cents: int
700
+ ) -> ServiceResult[Subscription]:
701
+ """
702
+ Apply a discount to a subscription.
703
+
704
+ Args:
705
+ subscription_id: Subscription ID
706
+ tenant_id: Tenant ID
707
+ user_id: User performing action
708
+ discount_id: Reference to subscriptions.Discount.id
709
+ discount_code: Discount code
710
+ discount_amount_cents: Discount amount per period
711
+
712
+ Returns:
713
+ ServiceResult with updated Subscription
714
+ """
715
+ try:
716
+ result = self.get_by_id(subscription_id, tenant_id, user_id)
717
+ if not result.success:
718
+ return result
719
+
720
+ subscription = result.data
721
+ subscription.apply_discount(discount_id, discount_code, discount_amount_cents)
722
+
723
+ subscription.updated_by_id = user_id
724
+ subscription.updated_utc_ts = dt.datetime.now(dt.UTC).timestamp()
725
+ subscription.version += 1
726
+
727
+ return self._save_model(subscription)
728
+
729
+ except Exception as e:
730
+ return self._handle_service_exception(
731
+ e, 'apply_discount_to_subscription',
732
+ subscription_id=subscription_id, tenant_id=tenant_id
733
+ )
734
+
735
+ def remove_discount_from_subscription(
736
+ self,
737
+ subscription_id: str,
738
+ tenant_id: str,
739
+ user_id: str
740
+ ) -> ServiceResult[Subscription]:
741
+ """
742
+ Remove discount from a subscription.
743
+
744
+ Args:
745
+ subscription_id: Subscription ID
746
+ tenant_id: Tenant ID
747
+ user_id: User performing action
748
+
749
+ Returns:
750
+ ServiceResult with updated Subscription
751
+ """
752
+ try:
753
+ result = self.get_by_id(subscription_id, tenant_id, user_id)
754
+ if not result.success:
755
+ return result
756
+
757
+ subscription = result.data
758
+ subscription.remove_discount()
759
+
760
+ subscription.updated_by_id = user_id
761
+ subscription.updated_utc_ts = dt.datetime.now(dt.UTC).timestamp()
762
+ subscription.version += 1
763
+
764
+ return self._save_model(subscription)
765
+
766
+ except Exception as e:
767
+ return self._handle_service_exception(
768
+ e, 'remove_discount_from_subscription',
769
+ subscription_id=subscription_id, tenant_id=tenant_id
770
+ )
@@ -14,6 +14,8 @@ from geek_cafe_saas_sdk.utilities.response import (
14
14
  service_result_to_response,
15
15
  )
16
16
  from geek_cafe_saas_sdk.utilities.lambda_event_utility import LambdaEventUtility
17
+ from geek_cafe_saas_sdk.utilities.logging_utility import LoggingUtility
18
+ from geek_cafe_saas_sdk.utilities.environment_variables import EnvironmentVariables
17
19
  from geek_cafe_saas_sdk.middleware.auth import extract_user_context
18
20
  from .service_pool import ServicePool
19
21
 
@@ -94,6 +96,11 @@ class BaseLambdaHandler:
94
96
  Lambda response dictionary
95
97
  """
96
98
  try:
99
+ # Log event payload if enabled (sanitized for security)
100
+ if EnvironmentVariables.should_log_lambda_events():
101
+ sanitized_event = LoggingUtility.sanitize_event_for_logging(event)
102
+ logger.info("Lambda event received", extra={"event": sanitized_event})
103
+
97
104
  # Unwrap message if needed (SQS, SNS, etc.)
98
105
  if self.unwrap_message and "message" in event:
99
106
  event = event["message"]
@@ -142,7 +142,7 @@ class DatabaseService(ABC, Generic[T]):
142
142
  return None
143
143
 
144
144
  def _get_model_by_id_with_tenant_check(
145
- self, resource_id: str, model_class, tenant_id: str
145
+ self, resource_id: str, model_class, tenant_id: str, include_deleted: bool = True
146
146
  ) -> Optional[T]:
147
147
  """
148
148
  Get model by ID with automatic tenant validation.
@@ -155,13 +155,16 @@ class DatabaseService(ABC, Generic[T]):
155
155
  resource_id: The resource ID to fetch
156
156
  model_class: The model class to instantiate
157
157
  tenant_id: The tenant ID from JWT (authenticated user's tenant)
158
+ include_deleted: If True, returns deleted items. If False, returns None for deleted items.
159
+ Default is True since get-by-id operations typically need to verify deletion,
160
+ perform restores, or show audit history.
158
161
 
159
162
  Returns:
160
163
  The model if found and belongs to tenant, None otherwise
161
164
 
162
165
  Security:
163
166
  - Returns None for resources in different tenants (prevents enumeration)
164
- - Returns None for deleted resources
167
+ - Optionally filters deleted resources based on include_deleted parameter
165
168
  - Single source of truth: tenant_id from JWT only
166
169
  """
167
170
  model = self._get_model_by_id(resource_id, model_class)
@@ -175,10 +178,11 @@ class DatabaseService(ABC, Generic[T]):
175
178
  # from users in other tenants (prevent enumeration attacks)
176
179
  return None
177
180
 
178
- # Hide deleted resources
179
- if hasattr(model, 'is_deleted') and callable(model.is_deleted):
180
- if model.is_deleted():
181
- return None
181
+ # Optionally hide deleted resources
182
+ if not include_deleted:
183
+ if hasattr(model, 'is_deleted') and callable(model.is_deleted):
184
+ if model.is_deleted():
185
+ return None
182
186
 
183
187
  return model
184
188
 
@@ -226,3 +226,19 @@ class EnvironmentVariables:
226
226
  """
227
227
  env = EnvironmentVariables.get_environment_setting()
228
228
  return env.lower().startswith("dev")
229
+
230
+ @staticmethod
231
+ def should_log_lambda_events() -> bool:
232
+ """
233
+ Determine if Lambda event payloads should be logged.
234
+
235
+ Set LOG_LAMBDA_EVENTS=true to enable event logging for debugging.
236
+ Useful for troubleshooting Lambda invocations.
237
+
238
+ Note: Event payloads are sanitized to remove sensitive fields before logging.
239
+
240
+ Returns:
241
+ True if event logging is enabled, False otherwise.
242
+ """
243
+ value = os.getenv("LOG_LAMBDA_EVENTS", "false").lower() == "true"
244
+ return value
@@ -1,3 +1,5 @@
1
+ import json
2
+ from typing import Any, Dict
1
3
  from aws_lambda_powertools import Logger
2
4
  from geek_cafe_saas_sdk.utilities.environment_variables import (
3
5
  EnvironmentVariables,
@@ -48,6 +50,81 @@ class LoggingUtility:
48
50
  "metric_filter": metric_filter,
49
51
  }
50
52
  return response
53
+
54
+ @staticmethod
55
+ def sanitize_event_for_logging(event: Dict[str, Any]) -> Dict[str, Any]:
56
+ """
57
+ Sanitize a Lambda event dictionary by masking sensitive fields.
58
+
59
+ This removes or masks sensitive information like:
60
+ - Authorization headers
61
+ - API keys
62
+ - Passwords
63
+ - Tokens
64
+ - SSNs, credit cards, etc.
65
+
66
+ Args:
67
+ event: The Lambda event dictionary to sanitize
68
+
69
+ Returns:
70
+ A sanitized copy of the event safe for logging
71
+ """
72
+ if not isinstance(event, dict):
73
+ return event
74
+
75
+ # Fields that should be completely removed
76
+ REMOVE_FIELDS = {
77
+ 'password', 'passwd', 'pwd',
78
+ 'secret', 'api_key', 'apikey',
79
+ 'token', 'access_token', 'refresh_token',
80
+ 'private_key', 'privatekey',
81
+ 'ssn', 'credit_card', 'creditcard',
82
+ 'cvv', 'pin'
83
+ }
84
+
85
+ # Fields that should be masked (show first few chars)
86
+ MASK_FIELDS = {
87
+ 'authorization', 'x_api_key',
88
+ 'cookie', 'session'
89
+ }
90
+
91
+ def sanitize_value(key: str, value: Any) -> Any:
92
+ """Recursively sanitize values."""
93
+ try:
94
+ key_lower = key.lower().replace('-', '_').replace(' ', '_')
95
+
96
+ # Remove sensitive fields entirely (exact match)
97
+ if key_lower in REMOVE_FIELDS:
98
+ return '[REDACTED]'
99
+
100
+ # Mask partially visible fields (exact match)
101
+ if key_lower in MASK_FIELDS:
102
+ if isinstance(value, str) and len(value) > 20:
103
+ return f"{value[:4]}...{value[-4:]}"
104
+ return '[MASKED]'
105
+
106
+ # Recursively handle nested structures
107
+ if isinstance(value, dict):
108
+ return {k: sanitize_value(k, v) for k, v in value.items()}
109
+ elif isinstance(value, list):
110
+ return [sanitize_value(key, item) if isinstance(item, dict) else item
111
+ for item in value]
112
+
113
+ return value
114
+ except Exception:
115
+ # If we can't safely process the value, redact it
116
+ return '[SANITIZATION_ERROR]'
117
+
118
+ # Create a deep copy and sanitize
119
+ try:
120
+ sanitized = {k: sanitize_value(k, v) for k, v in event.items()}
121
+ # Validate that the result is JSON-serializable
122
+ import json
123
+ json.dumps(sanitized)
124
+ return sanitized
125
+ except Exception as e:
126
+ # If sanitization fails, return safe fallback
127
+ return {"error": "Failed to sanitize event"}
51
128
 
52
129
 
53
130
  class LogLevels:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: geek_cafe_saas_sdk
3
- Version: 0.7.0
3
+ Version: 0.7.1
4
4
  Summary: Base Reusable Services for SaaS
5
5
  Project-URL: Homepage, https://github.com/geekcafe/geek-cafe-services
6
6
  Project-URL: Documentation, https://github.com/geekcafe/geek-cafe-services/blob/main/README.md