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
@@ -0,0 +1,83 @@
1
+ """
2
+ Lambda handler for creating discounts.
3
+
4
+ Admin endpoint - requires authentication.
5
+ """
6
+
7
+ from typing import Dict, Any
8
+ from geek_cafe_saas_sdk.lambda_handlers import create_handler
9
+ from geek_cafe_saas_sdk.domains.subscriptions.services import SubscriptionManagerService
10
+
11
+
12
+ handler_wrapper = create_handler(
13
+ service_class=SubscriptionManagerService,
14
+ require_body=True,
15
+ convert_case=True
16
+ )
17
+
18
+
19
+ def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
20
+ """
21
+ Create a new discount/promo code.
22
+
23
+ Expected body:
24
+ {
25
+ "discountCode": "SUMMER25",
26
+ "discountName": "Summer Sale",
27
+ "discountType": "percentage",
28
+ "percentOff": 25.0,
29
+ "duration": "repeating",
30
+ "durationInMonths": 3,
31
+ "maxRedemptions": 100
32
+ }
33
+
34
+ Returns 201 with created discount
35
+ """
36
+ return handler_wrapper.execute(event, context, create_discount, injected_service)
37
+
38
+
39
+ def create_discount(
40
+ event: Dict[str, Any],
41
+ service: SubscriptionManagerService,
42
+ user_context: Dict[str, str]
43
+ ) -> Any:
44
+ """
45
+ Business logic for creating a discount.
46
+ """
47
+ payload = event["parsed_body"]
48
+
49
+ # Extract required fields
50
+ discount_code = payload.get("discount_code")
51
+ if not discount_code:
52
+ raise ValueError("discount_code is required")
53
+
54
+ discount_name = payload.get("discount_name")
55
+ if not discount_name:
56
+ raise ValueError("discount_name is required")
57
+
58
+ discount_type = payload.get("discount_type", "percentage")
59
+
60
+ # Build kwargs for optional fields
61
+ kwargs = {}
62
+ optional_fields = [
63
+ "description", "amount_off_cents", "percent_off", "trial_extension_days",
64
+ "currency", "duration", "duration_in_months",
65
+ "valid_from_utc_ts", "valid_until_utc_ts",
66
+ "max_redemptions", "max_redemptions_per_customer",
67
+ "status", "minimum_amount_cents",
68
+ "applies_to_plan_codes", "applies_to_addon_codes", "applies_to_intervals",
69
+ "first_time_transaction", "campaign_name", "source", "notes"
70
+ ]
71
+
72
+ for field in optional_fields:
73
+ if field in payload:
74
+ kwargs[field] = payload[field]
75
+
76
+ result = service.create_discount(
77
+ discount_code=discount_code,
78
+ discount_name=discount_name,
79
+ discount_type=discount_type,
80
+ **kwargs
81
+ )
82
+
83
+ return result
@@ -0,0 +1,47 @@
1
+ """
2
+ Lambda handler for getting a discount.
3
+
4
+ Admin endpoint - requires authentication.
5
+ """
6
+
7
+ from typing import Dict, Any
8
+ from geek_cafe_saas_sdk.lambda_handlers import create_handler
9
+ from geek_cafe_saas_sdk.domains.subscriptions.services import SubscriptionManagerService
10
+
11
+
12
+ handler_wrapper = create_handler(
13
+ service_class=SubscriptionManagerService,
14
+ require_body=False,
15
+ convert_case=True
16
+ )
17
+
18
+
19
+ def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
20
+ """
21
+ Get a discount by ID.
22
+
23
+ Path parameters:
24
+ - discountId: Discount ID
25
+
26
+ Returns 200 with discount details
27
+ """
28
+ return handler_wrapper.execute(event, context, get_discount, injected_service)
29
+
30
+
31
+ def get_discount(
32
+ event: Dict[str, Any],
33
+ service: SubscriptionManagerService,
34
+ user_context: Dict[str, str]
35
+ ) -> Any:
36
+ """
37
+ Business logic for getting a discount.
38
+ """
39
+ path_params = event.get("pathParameters") or {}
40
+ discount_id = path_params.get("discount_id")
41
+
42
+ if not discount_id:
43
+ raise ValueError("discount_id is required in path")
44
+
45
+ result = service.get_discount(discount_id=discount_id)
46
+
47
+ return result
@@ -0,0 +1,62 @@
1
+ """
2
+ Lambda handler for validating discount codes.
3
+
4
+ Public endpoint - no authentication required.
5
+ """
6
+
7
+ from typing import Dict, Any
8
+ from geek_cafe_saas_sdk.lambda_handlers import create_handler
9
+ from geek_cafe_saas_sdk.domains.subscriptions.services import SubscriptionManagerService
10
+
11
+
12
+ handler_wrapper = create_handler(
13
+ service_class=SubscriptionManagerService,
14
+ require_auth=False,
15
+ require_body=True,
16
+ convert_case=True
17
+ )
18
+
19
+
20
+ def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
21
+ """
22
+ Validate a discount code.
23
+
24
+ Expected body:
25
+ {
26
+ "discountCode": "SUMMER25",
27
+ "planCode": "pro", // optional
28
+ "amountCents": 2999, // optional
29
+ "isFirstPurchase": false // optional
30
+ }
31
+
32
+ Returns 200 with discount details if valid
33
+ """
34
+ return handler_wrapper.execute(event, context, validate_discount, injected_service)
35
+
36
+
37
+ def validate_discount(
38
+ event: Dict[str, Any],
39
+ service: SubscriptionManagerService,
40
+ user_context: Dict[str, str]
41
+ ) -> Any:
42
+ """
43
+ Business logic for validating a discount.
44
+ """
45
+ payload = event["parsed_body"]
46
+
47
+ discount_code = payload.get("discount_code")
48
+ if not discount_code:
49
+ raise ValueError("discount_code is required")
50
+
51
+ plan_code = payload.get("plan_code")
52
+ amount_cents = payload.get("amount_cents")
53
+ is_first_purchase = payload.get("is_first_purchase", False)
54
+
55
+ result = service.validate_discount(
56
+ discount_code=discount_code,
57
+ plan_code=plan_code,
58
+ amount_cents=amount_cents,
59
+ is_first_purchase=is_first_purchase
60
+ )
61
+
62
+ return result
@@ -0,0 +1,82 @@
1
+ """
2
+ Lambda handler for creating subscription plans.
3
+
4
+ Admin endpoint - requires authentication.
5
+ """
6
+
7
+ from typing import Dict, Any
8
+ from geek_cafe_saas_sdk.lambda_handlers import create_handler
9
+ from geek_cafe_saas_sdk.domains.subscriptions.services import SubscriptionManagerService
10
+
11
+
12
+ handler_wrapper = create_handler(
13
+ service_class=SubscriptionManagerService,
14
+ require_body=True,
15
+ convert_case=True
16
+ )
17
+
18
+
19
+ def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
20
+ """
21
+ Create a new subscription plan.
22
+
23
+ Expected body:
24
+ {
25
+ "planCode": "pro",
26
+ "planName": "Pro Plan",
27
+ "priceMonthlyC ents": 2999,
28
+ "description": "For growing teams",
29
+ "features": {"api_access": true, "sso": false},
30
+ "limits": {"max_projects": 100, "max_storage_gb": 500}
31
+ }
32
+
33
+ Returns 201 with created plan
34
+ """
35
+ return handler_wrapper.execute(event, context, create_plan, injected_service)
36
+
37
+
38
+ def create_plan(
39
+ event: Dict[str, Any],
40
+ service: SubscriptionManagerService,
41
+ user_context: Dict[str, str]
42
+ ) -> Any:
43
+ """
44
+ Business logic for creating a plan.
45
+ """
46
+ payload = event["parsed_body"]
47
+
48
+ # Extract required fields
49
+ plan_code = payload.get("plan_code")
50
+ if not plan_code:
51
+ raise ValueError("plan_code is required")
52
+
53
+ plan_name = payload.get("plan_name")
54
+ if not plan_name:
55
+ raise ValueError("plan_name is required")
56
+
57
+ price_monthly_cents = payload.get("price_monthly_cents", 0)
58
+
59
+ # Build kwargs for optional fields
60
+ kwargs = {}
61
+ optional_fields = [
62
+ "description", "tagline", "status", "is_public", "is_featured", "sort_order",
63
+ "price_annual_cents", "price_monthly_currency", "price_annual_currency",
64
+ "annual_discount_percentage", "trial_days", "trial_requires_payment_method",
65
+ "min_seats", "max_seats", "price_per_additional_seat_cents",
66
+ "features", "limits", "included_addon_ids", "compatible_addon_ids",
67
+ "feature_list", "cta_text", "recommended",
68
+ "allow_downgrades", "allow_upgrades"
69
+ ]
70
+
71
+ for field in optional_fields:
72
+ if field in payload:
73
+ kwargs[field] = payload[field]
74
+
75
+ result = service.create_plan(
76
+ plan_code=plan_code,
77
+ plan_name=plan_name,
78
+ price_monthly_cents=price_monthly_cents,
79
+ **kwargs
80
+ )
81
+
82
+ return result
@@ -0,0 +1,48 @@
1
+ """
2
+ Lambda handler for getting a subscription plan.
3
+
4
+ Public endpoint - no authentication required.
5
+ """
6
+
7
+ from typing import Dict, Any
8
+ from geek_cafe_saas_sdk.lambda_handlers import create_handler
9
+ from geek_cafe_saas_sdk.domains.subscriptions.services import SubscriptionManagerService
10
+
11
+
12
+ handler_wrapper = create_handler(
13
+ service_class=SubscriptionManagerService,
14
+ require_auth=False,
15
+ require_body=False,
16
+ convert_case=True
17
+ )
18
+
19
+
20
+ def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
21
+ """
22
+ Get a subscription plan by ID.
23
+
24
+ Path parameters:
25
+ - planId: Plan ID
26
+
27
+ Returns 200 with plan details
28
+ """
29
+ return handler_wrapper.execute(event, context, get_plan, injected_service)
30
+
31
+
32
+ def get_plan(
33
+ event: Dict[str, Any],
34
+ service: SubscriptionManagerService,
35
+ user_context: Dict[str, str]
36
+ ) -> Any:
37
+ """
38
+ Business logic for getting a plan.
39
+ """
40
+ path_params = event.get("pathParameters") or {}
41
+ plan_id = path_params.get("plan_id")
42
+
43
+ if not plan_id:
44
+ raise ValueError("plan_id is required in path")
45
+
46
+ result = service.get_plan(plan_id=plan_id)
47
+
48
+ return result
@@ -0,0 +1,66 @@
1
+ """
2
+ Lambda handler for listing subscription plans.
3
+
4
+ Public endpoint - no authentication required for public plans.
5
+ """
6
+
7
+ from typing import Dict, Any
8
+ from geek_cafe_saas_sdk.lambda_handlers import create_handler
9
+ from geek_cafe_saas_sdk.domains.subscriptions.services import SubscriptionManagerService
10
+
11
+
12
+ # Public endpoint - no auth required
13
+ handler_wrapper = create_handler(
14
+ service_class=SubscriptionManagerService,
15
+ require_auth=False,
16
+ require_body=False,
17
+ convert_case=True
18
+ )
19
+
20
+
21
+ def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
22
+ """
23
+ List public subscription plans.
24
+
25
+ Args:
26
+ event: Lambda event from API Gateway
27
+ context: Lambda context
28
+ injected_service: Optional SubscriptionManagerService for testing
29
+
30
+ Query parameters:
31
+ - status: Filter by status (default: "active")
32
+ - isPublic: Filter by public visibility (default: true)
33
+ - limit: Max results (default: 50)
34
+
35
+ Returns 200 with list of plans
36
+ """
37
+ return handler_wrapper.execute(event, context, list_plans, injected_service)
38
+
39
+
40
+ def list_plans(
41
+ event: Dict[str, Any],
42
+ service: SubscriptionManagerService,
43
+ user_context: Dict[str, str]
44
+ ) -> Any:
45
+ """
46
+ Business logic for listing plans.
47
+ """
48
+ params = event.get("queryStringParameters") or {}
49
+
50
+ status = params.get("status", "active")
51
+ is_public = params.get("is_public")
52
+ limit = int(params.get("limit", "50"))
53
+
54
+ # Convert string to bool if provided
55
+ if is_public is not None:
56
+ is_public = is_public.lower() in ["true", "1", "yes"]
57
+ else:
58
+ is_public = True # Default to public plans only
59
+
60
+ result = service.list_plans(
61
+ status=status,
62
+ is_public=is_public,
63
+ limit=limit
64
+ )
65
+
66
+ return result
@@ -0,0 +1,54 @@
1
+ """
2
+ Lambda handler for updating subscription plans.
3
+
4
+ Admin endpoint - requires authentication.
5
+ """
6
+
7
+ from typing import Dict, Any
8
+ from geek_cafe_saas_sdk.lambda_handlers import create_handler
9
+ from geek_cafe_saas_sdk.domains.subscriptions.services import SubscriptionManagerService
10
+
11
+
12
+ handler_wrapper = create_handler(
13
+ service_class=SubscriptionManagerService,
14
+ require_body=True,
15
+ convert_case=True
16
+ )
17
+
18
+
19
+ def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
20
+ """
21
+ Update a subscription plan.
22
+
23
+ Path parameters:
24
+ - planId: Plan ID
25
+
26
+ Body contains fields to update
27
+
28
+ Returns 200 with updated plan
29
+ """
30
+ return handler_wrapper.execute(event, context, update_plan, injected_service)
31
+
32
+
33
+ def update_plan(
34
+ event: Dict[str, Any],
35
+ service: SubscriptionManagerService,
36
+ user_context: Dict[str, str]
37
+ ) -> Any:
38
+ """
39
+ Business logic for updating a plan.
40
+ """
41
+ path_params = event.get("pathParameters") or {}
42
+ plan_id = path_params.get("plan_id")
43
+
44
+ if not plan_id:
45
+ raise ValueError("plan_id is required in path")
46
+
47
+ payload = event["parsed_body"]
48
+
49
+ result = service.update_plan(
50
+ plan_id=plan_id,
51
+ updates=payload
52
+ )
53
+
54
+ return result
@@ -0,0 +1,72 @@
1
+ """
2
+ Lambda handler for aggregating usage.
3
+
4
+ Requires authentication (secure mode).
5
+ """
6
+
7
+ from typing import Dict, Any
8
+ from geek_cafe_saas_sdk.lambda_handlers import create_handler
9
+ from geek_cafe_saas_sdk.domains.subscriptions.services import SubscriptionManagerService
10
+
11
+
12
+ handler_wrapper = create_handler(
13
+ service_class=SubscriptionManagerService,
14
+ require_body=False,
15
+ convert_case=True
16
+ )
17
+
18
+
19
+ def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
20
+ """
21
+ Get aggregated usage for a billing period.
22
+
23
+ Query parameters:
24
+ - subscriptionId: Subscription ID (required)
25
+ - addonCode: Addon code (required)
26
+ - periodStart: Period start timestamp (required)
27
+ - periodEnd: Period end timestamp (required)
28
+
29
+ Returns 200 with aggregated usage total
30
+ """
31
+ return handler_wrapper.execute(event, context, aggregate_usage, injected_service)
32
+
33
+
34
+ def aggregate_usage(
35
+ event: Dict[str, Any],
36
+ service: SubscriptionManagerService,
37
+ user_context: Dict[str, str]
38
+ ) -> Any:
39
+ """
40
+ Business logic for aggregating usage.
41
+ """
42
+ params = event.get("queryStringParameters") or {}
43
+
44
+ tenant_id = user_context.get("tenant_id")
45
+
46
+ subscription_id = params.get("subscription_id")
47
+ if not subscription_id:
48
+ raise ValueError("subscription_id is required")
49
+
50
+ addon_code = params.get("addon_code")
51
+ if not addon_code:
52
+ raise ValueError("addon_code is required")
53
+
54
+ period_start = params.get("period_start")
55
+ if not period_start:
56
+ raise ValueError("period_start is required")
57
+ period_start = float(period_start)
58
+
59
+ period_end = params.get("period_end")
60
+ if not period_end:
61
+ raise ValueError("period_end is required")
62
+ period_end = float(period_end)
63
+
64
+ result = service.aggregate_usage(
65
+ tenant_id=tenant_id,
66
+ subscription_id=subscription_id,
67
+ addon_code=addon_code,
68
+ period_start=period_start,
69
+ period_end=period_end
70
+ )
71
+
72
+ return result
@@ -0,0 +1,89 @@
1
+ """
2
+ Lambda handler for recording usage events.
3
+
4
+ Requires authentication (secure mode).
5
+ """
6
+
7
+ from typing import Dict, Any
8
+ from geek_cafe_saas_sdk.lambda_handlers import create_handler
9
+ from geek_cafe_saas_sdk.domains.subscriptions.services import SubscriptionManagerService
10
+
11
+
12
+ handler_wrapper = create_handler(
13
+ service_class=SubscriptionManagerService,
14
+ require_body=True,
15
+ convert_case=True
16
+ )
17
+
18
+
19
+ def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
20
+ """
21
+ Record a usage event for metered billing.
22
+
23
+ Expected body:
24
+ {
25
+ "subscriptionId": "sub-123",
26
+ "addonCode": "extra_storage",
27
+ "meterEventName": "storage_gb",
28
+ "quantity": 50.5,
29
+ "action": "increment", // optional: increment, decrement, set
30
+ "idempotencyKey": "unique-key", // optional
31
+ "metadata": {} // optional
32
+ }
33
+
34
+ Returns 201 with usage record
35
+ """
36
+ return handler_wrapper.execute(event, context, record_usage, injected_service)
37
+
38
+
39
+ def record_usage(
40
+ event: Dict[str, Any],
41
+ service: SubscriptionManagerService,
42
+ user_context: Dict[str, str]
43
+ ) -> Any:
44
+ """
45
+ Business logic for recording usage.
46
+ """
47
+ payload = event["parsed_body"]
48
+
49
+ tenant_id = user_context.get("tenant_id")
50
+
51
+ # Extract required fields
52
+ subscription_id = payload.get("subscription_id")
53
+ if not subscription_id:
54
+ raise ValueError("subscription_id is required")
55
+
56
+ addon_code = payload.get("addon_code")
57
+ if not addon_code:
58
+ raise ValueError("addon_code is required")
59
+
60
+ meter_event_name = payload.get("meter_event_name")
61
+ if not meter_event_name:
62
+ raise ValueError("meter_event_name is required")
63
+
64
+ quantity = payload.get("quantity")
65
+ if quantity is None:
66
+ raise ValueError("quantity is required")
67
+
68
+ # Build kwargs for optional fields
69
+ kwargs = {}
70
+ optional_fields = [
71
+ "action", "unit_name", "idempotency_key",
72
+ "billing_period_start_utc_ts", "billing_period_end_utc_ts",
73
+ "metadata", "source", "description"
74
+ ]
75
+
76
+ for field in optional_fields:
77
+ if field in payload:
78
+ kwargs[field] = payload[field]
79
+
80
+ result = service.record_usage(
81
+ tenant_id=tenant_id,
82
+ subscription_id=subscription_id,
83
+ addon_code=addon_code,
84
+ meter_event_name=meter_event_name,
85
+ quantity=quantity,
86
+ **kwargs
87
+ )
88
+
89
+ return result
@@ -0,0 +1,13 @@
1
+ """
2
+ Subscriptions Domain Models.
3
+
4
+ Geek Cafe, LLC
5
+ MIT License. See Project Root for the license information.
6
+ """
7
+
8
+ from .plan import Plan
9
+ from .addon import Addon
10
+ from .usage_record import UsageRecord
11
+ from .discount import Discount
12
+
13
+ __all__ = ["Plan", "Addon", "UsageRecord", "Discount"]