geek-cafe-saas-sdk 0.7.0__py3-none-any.whl → 0.7.2__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 +60 -136
  8. geek_cafe_saas_sdk/domains/files/services/file_system_service.py +43 -104
  9. geek_cafe_saas_sdk/domains/files/services/file_version_service.py +57 -131
  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.2.dist-info}/METADATA +1 -1
  77. {geek_cafe_saas_sdk-0.7.0.dist-info → geek_cafe_saas_sdk-0.7.2.dist-info}/RECORD +79 -20
  78. {geek_cafe_saas_sdk-0.7.0.dist-info → geek_cafe_saas_sdk-0.7.2.dist-info}/WHEEL +0 -0
  79. {geek_cafe_saas_sdk-0.7.0.dist-info → geek_cafe_saas_sdk-0.7.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,300 @@
1
+ """
2
+ UsageRecord model for metered addon usage tracking.
3
+
4
+ Geek Cafe, LLC
5
+ MIT License. See Project Root for the license information.
6
+ """
7
+
8
+ import datetime as dt
9
+ from typing import Dict, Any, Optional
10
+ from geek_cafe_saas_sdk.models.base_model import BaseModel
11
+ from boto3_assist.dynamodb.dynamodb_index import DynamoDBIndex, DynamoDBKey
12
+
13
+
14
+ class UsageRecord(BaseModel):
15
+ """
16
+ Usage record for metered billing.
17
+
18
+ Tracks usage events for metered addons (API calls, storage, etc.)
19
+ Aggregated for billing at end of period.
20
+
21
+ Key Features:
22
+ - Event-based tracking
23
+ - Aggregation support
24
+ - Idempotency keys
25
+ - Metadata for debugging
26
+
27
+ Examples:
28
+ - API Calls: 1000 calls made
29
+ - Storage: 50GB used
30
+ - SMS Sent: 150 messages
31
+ - Compute Hours: 25 hours
32
+ """
33
+
34
+ # Action constants
35
+ ACTION_INCREMENT = "increment" # Add to usage
36
+ ACTION_DECREMENT = "decrement" # Subtract from usage
37
+ ACTION_SET = "set" # Set absolute value
38
+
39
+ def __init__(self):
40
+ super().__init__()
41
+
42
+ # Identification
43
+ self._tenant_id: str = ""
44
+ self._subscription_id: str = ""
45
+ self._addon_code: str = ""
46
+
47
+ # Usage details
48
+ self._quantity: float = 0.0 # Amount of usage
49
+ self._action: str = self.ACTION_INCREMENT # increment|decrement|set
50
+ self._timestamp_utc_ts: float = 0.0 # When usage occurred
51
+
52
+ # Metering
53
+ self._meter_event_name: str = "" # e.g., "api_call", "storage_gb"
54
+ self._unit_name: Optional[str] = None # e.g., "call", "GB", "hour"
55
+
56
+ # Billing period
57
+ self._billing_period_start_utc_ts: Optional[float] = None
58
+ self._billing_period_end_utc_ts: Optional[float] = None
59
+
60
+ # Idempotency
61
+ self._idempotency_key: Optional[str] = None # Prevent duplicate recording
62
+
63
+ # Status
64
+ self._is_processed: bool = False # Whether included in invoice
65
+ self._processed_utc_ts: Optional[float] = None
66
+ self._invoice_id: Optional[str] = None
67
+
68
+ # Metadata
69
+ self._metadata: Dict[str, Any] = {} # Custom metadata
70
+ self._source: Optional[str] = None # Where usage came from
71
+ self._description: Optional[str] = None
72
+
73
+ # CRITICAL: Call _setup_indexes() as LAST line in __init__
74
+ self._setup_indexes()
75
+
76
+ def _setup_indexes(self):
77
+ """Setup DynamoDB indexes for usage record queries."""
78
+
79
+ # Primary index: Usage record by ID
80
+ primary: DynamoDBIndex = DynamoDBIndex()
81
+ primary.name = "primary"
82
+ primary.partition_key.attribute_name = "pk"
83
+ primary.partition_key.value = lambda: DynamoDBKey.build_key(("usage", self.id))
84
+ primary.sort_key.attribute_name = "sk"
85
+ primary.sort_key.value = lambda: "metadata"
86
+ self.indexes.add_primary(primary)
87
+
88
+ # GSI1: Usage by tenant and subscription
89
+ gsi: DynamoDBIndex = DynamoDBIndex()
90
+ gsi.name = "gsi1"
91
+ gsi.partition_key.attribute_name = f"{gsi.name}_pk"
92
+ gsi.partition_key.value = lambda: DynamoDBKey.build_key(("tenant", self.tenant_id), ("subscription", self.subscription_id))
93
+ gsi.sort_key.attribute_name = f"{gsi.name}_sk"
94
+ gsi.sort_key.value = lambda: DynamoDBKey.build_key(("timestamp", self.timestamp_utc_ts))
95
+ self.indexes.add_secondary(gsi)
96
+
97
+ # GSI2: Usage by addon and period (for aggregation)
98
+ gsi: DynamoDBIndex = DynamoDBIndex()
99
+ gsi.name = "gsi2"
100
+ gsi.partition_key.attribute_name = f"{gsi.name}_pk"
101
+ gsi.partition_key.value = lambda: DynamoDBKey.build_key(("tenant", self.tenant_id), ("addon", self.addon_code))
102
+ gsi.sort_key.attribute_name = f"{gsi.name}_sk"
103
+ gsi.sort_key.value = lambda: DynamoDBKey.build_key(("timestamp", self.timestamp_utc_ts))
104
+ self.indexes.add_secondary(gsi)
105
+
106
+ # Tenant ID
107
+ @property
108
+ def tenant_id(self) -> str:
109
+ """Tenant ID."""
110
+ return self._tenant_id
111
+
112
+ @tenant_id.setter
113
+ def tenant_id(self, value: str):
114
+ self._tenant_id = value
115
+
116
+ # Subscription ID
117
+ @property
118
+ def subscription_id(self) -> str:
119
+ """Subscription ID."""
120
+ return self._subscription_id
121
+
122
+ @subscription_id.setter
123
+ def subscription_id(self, value: str):
124
+ self._subscription_id = value
125
+
126
+ # Addon Code
127
+ @property
128
+ def addon_code(self) -> str:
129
+ """Addon code."""
130
+ return self._addon_code
131
+
132
+ @addon_code.setter
133
+ def addon_code(self, value: str):
134
+ self._addon_code = value
135
+
136
+ # Quantity
137
+ @property
138
+ def quantity(self) -> float:
139
+ """Usage quantity."""
140
+ return self._quantity
141
+
142
+ @quantity.setter
143
+ def quantity(self, value: float):
144
+ if value < 0:
145
+ raise ValueError("quantity cannot be negative")
146
+ self._quantity = value
147
+
148
+ # Action
149
+ @property
150
+ def action(self) -> str:
151
+ """Usage action."""
152
+ return self._action
153
+
154
+ @action.setter
155
+ def action(self, value: str):
156
+ valid_actions = [self.ACTION_INCREMENT, self.ACTION_DECREMENT, self.ACTION_SET]
157
+ if value not in valid_actions:
158
+ raise ValueError(f"Invalid action: {value}. Must be one of {valid_actions}")
159
+ self._action = value
160
+
161
+ # Timestamp
162
+ @property
163
+ def timestamp_utc_ts(self) -> float:
164
+ """Timestamp when usage occurred."""
165
+ return self._timestamp_utc_ts
166
+
167
+ @timestamp_utc_ts.setter
168
+ def timestamp_utc_ts(self, value: float):
169
+ self._timestamp_utc_ts = value
170
+
171
+ # Meter Event Name
172
+ @property
173
+ def meter_event_name(self) -> str:
174
+ """Meter event name."""
175
+ return self._meter_event_name
176
+
177
+ @meter_event_name.setter
178
+ def meter_event_name(self, value: str):
179
+ self._meter_event_name = value
180
+
181
+ # Unit Name
182
+ @property
183
+ def unit_name(self) -> Optional[str]:
184
+ """Unit name."""
185
+ return self._unit_name
186
+
187
+ @unit_name.setter
188
+ def unit_name(self, value: Optional[str]):
189
+ self._unit_name = value
190
+
191
+ # Billing Period Start
192
+ @property
193
+ def billing_period_start_utc_ts(self) -> Optional[float]:
194
+ """Billing period start timestamp."""
195
+ return self._billing_period_start_utc_ts
196
+
197
+ @billing_period_start_utc_ts.setter
198
+ def billing_period_start_utc_ts(self, value: Optional[float]):
199
+ self._billing_period_start_utc_ts = value
200
+
201
+ # Billing Period End
202
+ @property
203
+ def billing_period_end_utc_ts(self) -> Optional[float]:
204
+ """Billing period end timestamp."""
205
+ return self._billing_period_end_utc_ts
206
+
207
+ @billing_period_end_utc_ts.setter
208
+ def billing_period_end_utc_ts(self, value: Optional[float]):
209
+ self._billing_period_end_utc_ts = value
210
+
211
+ # Idempotency Key
212
+ @property
213
+ def idempotency_key(self) -> Optional[str]:
214
+ """Idempotency key."""
215
+ return self._idempotency_key
216
+
217
+ @idempotency_key.setter
218
+ def idempotency_key(self, value: Optional[str]):
219
+ self._idempotency_key = value
220
+
221
+ # Is Processed
222
+ @property
223
+ def is_processed(self) -> bool:
224
+ """Whether usage has been processed/billed."""
225
+ return self._is_processed
226
+
227
+ @is_processed.setter
228
+ def is_processed(self, value: bool):
229
+ self._is_processed = value
230
+
231
+ # Processed Timestamp
232
+ @property
233
+ def processed_utc_ts(self) -> Optional[float]:
234
+ """When usage was processed."""
235
+ return self._processed_utc_ts
236
+
237
+ @processed_utc_ts.setter
238
+ def processed_utc_ts(self, value: Optional[float]):
239
+ self._processed_utc_ts = value
240
+
241
+ # Invoice ID
242
+ @property
243
+ def invoice_id(self) -> Optional[str]:
244
+ """Invoice ID if processed."""
245
+ return self._invoice_id
246
+
247
+ @invoice_id.setter
248
+ def invoice_id(self, value: Optional[str]):
249
+ self._invoice_id = value
250
+
251
+ # Metadata
252
+ @property
253
+ def metadata(self) -> Dict[str, Any]:
254
+ """Custom metadata."""
255
+ return self._metadata
256
+
257
+ @metadata.setter
258
+ def metadata(self, value: Dict[str, Any]):
259
+ self._metadata = value if value else {}
260
+
261
+ # Source
262
+ @property
263
+ def source(self) -> Optional[str]:
264
+ """Usage source."""
265
+ return self._source
266
+
267
+ @source.setter
268
+ def source(self, value: Optional[str]):
269
+ self._source = value
270
+
271
+ # Description
272
+ @property
273
+ def description(self) -> Optional[str]:
274
+ """Usage description."""
275
+ return self._description
276
+
277
+ @description.setter
278
+ def description(self, value: Optional[str]):
279
+ self._description = value
280
+
281
+ # Helper Methods
282
+
283
+ def mark_processed(self, invoice_id: Optional[str] = None):
284
+ """Mark usage record as processed."""
285
+ self._is_processed = True
286
+ self._processed_utc_ts = dt.datetime.now(dt.UTC).timestamp()
287
+ if invoice_id:
288
+ self._invoice_id = invoice_id
289
+
290
+ def is_increment(self) -> bool:
291
+ """Check if action is increment."""
292
+ return self._action == self.ACTION_INCREMENT
293
+
294
+ def is_decrement(self) -> bool:
295
+ """Check if action is decrement."""
296
+ return self._action == self.ACTION_DECREMENT
297
+
298
+ def is_set(self) -> bool:
299
+ """Check if action is set."""
300
+ return self._action == self.ACTION_SET
@@ -0,0 +1,10 @@
1
+ """
2
+ Subscriptions Domain Services.
3
+
4
+ Geek Cafe, LLC
5
+ MIT License. See Project Root for the license information.
6
+ """
7
+
8
+ from .subscription_manager_service import SubscriptionManagerService
9
+
10
+ __all__ = ["SubscriptionManagerService"]