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
@@ -0,0 +1,405 @@
1
+ """PaymentService for payment operations with DynamoDB and PSP integration.
2
+
3
+ Geek Cafe, LLC
4
+ MIT License. See Project Root for the license information.
5
+ """
6
+
7
+ from typing import Dict, Any, Optional, List
8
+ import datetime as dt
9
+ from boto3.dynamodb.conditions import Key
10
+ from boto3_assist.dynamodb.dynamodb import DynamoDB
11
+ from geek_cafe_saas_sdk.services.database_service import DatabaseService
12
+ from geek_cafe_saas_sdk.core.service_result import ServiceResult
13
+ from geek_cafe_saas_sdk.core.service_errors import ValidationError, NotFoundError
14
+ from geek_cafe_saas_sdk.core.error_codes import ErrorCode
15
+ from geek_cafe_saas_sdk.domains.payments.models.billing_account import BillingAccount
16
+ from geek_cafe_saas_sdk.domains.payments.models.payment_intent_ref import PaymentIntentRef
17
+ from geek_cafe_saas_sdk.domains.payments.models.payment import Payment
18
+ from geek_cafe_saas_sdk.domains.payments.models.refund import Refund
19
+
20
+
21
+ class PaymentService(DatabaseService[Payment]):
22
+ """Service for managing payments, billing accounts, and refunds."""
23
+
24
+ def __init__(self, *, dynamodb: Optional[DynamoDB] = None, table_name: Optional[str] = None):
25
+ super().__init__(dynamodb=dynamodb, table_name=table_name)
26
+
27
+ # Abstract method implementations (delegating to specific methods)
28
+ def create(self, **kwargs) -> ServiceResult[Payment]:
29
+ """Create method - delegates to record_payment."""
30
+ return self.record_payment(**kwargs)
31
+
32
+ def get_by_id(self, resource_id: str, **kwargs) -> ServiceResult[Payment]:
33
+ """Get by ID method - delegates to get_payment."""
34
+ tenant_id = kwargs.get("tenant_id")
35
+ return self.get_payment(payment_id=resource_id, tenant_id=tenant_id)
36
+
37
+ def update(self, resource_id: str, updates: Dict[str, Any], **kwargs) -> ServiceResult[Payment]:
38
+ """Update method - not supported for immutable payments."""
39
+ return ServiceResult.error_result(
40
+ message="Direct payment updates not supported (payments are immutable)",
41
+ error_code="OPERATION_NOT_SUPPORTED"
42
+ )
43
+
44
+ def delete(self, resource_id: str, **kwargs) -> ServiceResult[bool]:
45
+ """Delete method - not supported for payments."""
46
+ return ServiceResult.error_result(
47
+ message="Payment deletion not supported (use refunds instead)",
48
+ error_code="OPERATION_NOT_SUPPORTED"
49
+ )
50
+
51
+ # ====================
52
+ # BILLING ACCOUNT METHODS
53
+ # ====================
54
+
55
+ def create_billing_account(
56
+ self,
57
+ tenant_id: str,
58
+ account_holder_id: str,
59
+ account_holder_type: str = "user",
60
+ currency_code: str = "USD",
61
+ billing_email: Optional[str] = None,
62
+ **kwargs
63
+ ) -> ServiceResult[BillingAccount]:
64
+ """Create a new billing account."""
65
+ try:
66
+ account = BillingAccount()
67
+ account.prep_for_save()
68
+ account.tenant_id = tenant_id
69
+ account.account_holder_id = account_holder_id
70
+ account.account_holder_type = account_holder_type
71
+ account.currency_code = currency_code
72
+ account.billing_email = billing_email
73
+
74
+ # Set optional fields
75
+ account = account.map(kwargs)
76
+
77
+ # Validate
78
+ is_valid, errors = account.validate()
79
+ if not is_valid:
80
+ raise ValidationError(f"Validation failed: {', '.join(errors)}", "billing_account")
81
+
82
+ # Save to DynamoDB
83
+ account.prep_for_save()
84
+ return self._save_model(account)
85
+ except Exception as e:
86
+ return ServiceResult.exception_result(e, ErrorCode.INTERNAL_ERROR, "create_billing_account")
87
+
88
+ def get_billing_account(self, account_id: str, tenant_id: str) -> ServiceResult[BillingAccount]:
89
+ """Get billing account by ID."""
90
+ try:
91
+ account = self._get_model_by_id_with_tenant_check(account_id, BillingAccount, tenant_id)
92
+
93
+ if not account:
94
+ raise NotFoundError(f"Billing account not found: {account_id}")
95
+
96
+ return ServiceResult.success_result(account)
97
+ except Exception as e:
98
+ return ServiceResult.exception_result(e, ErrorCode.INTERNAL_ERROR, "get_billing_account")
99
+
100
+ def update_billing_account(
101
+ self,
102
+ account_id: str,
103
+ tenant_id: str,
104
+ updates: Dict[str, Any]
105
+ ) -> ServiceResult[BillingAccount]:
106
+ """Update billing account."""
107
+ try:
108
+ # Get current account
109
+ result = self.get_billing_account(account_id, tenant_id)
110
+ if not result.success:
111
+ return result
112
+
113
+ account = result.data
114
+
115
+ # Apply updates
116
+ for key, value in updates.items():
117
+ if hasattr(account, key):
118
+ setattr(account, key, value)
119
+
120
+ account.updated_utc_ts = dt.datetime.now(dt.UTC).timestamp()
121
+
122
+ # Validate
123
+ is_valid, errors = account.validate()
124
+ if not is_valid:
125
+ raise ValidationError(f"Validation failed: {', '.join(errors)}", "billing_account")
126
+
127
+ # Save
128
+ account.prep_for_save()
129
+ return self._save_model(account)
130
+ except Exception as e:
131
+ return ServiceResult.exception_result(e, ErrorCode.INTERNAL_ERROR, "update_billing_account")
132
+
133
+ # ====================
134
+ # PAYMENT INTENT METHODS
135
+ # ====================
136
+
137
+ def create_payment_intent(
138
+ self,
139
+ tenant_id: str,
140
+ billing_account_id: str,
141
+ amount_cents: int,
142
+ currency_code: str = "USD",
143
+ psp_type: str = "stripe",
144
+ **kwargs
145
+ ) -> ServiceResult[PaymentIntentRef]:
146
+ """Create a payment intent reference."""
147
+ try:
148
+ intent = PaymentIntentRef()
149
+ intent.prep_for_save()
150
+ intent.tenant_id = tenant_id
151
+ intent.billing_account_id = billing_account_id
152
+ intent.amount_cents = amount_cents
153
+ intent.currency_code = currency_code
154
+ intent.psp_type = psp_type
155
+ intent.initiated_utc_ts = dt.datetime.now(dt.UTC).timestamp()
156
+
157
+ intent = intent.map(kwargs)
158
+
159
+ # Save
160
+ intent.prep_for_save()
161
+ return self._save_model(intent)
162
+ except Exception as e:
163
+ return ServiceResult.exception_result(e, ErrorCode.INTERNAL_ERROR, "create_payment_intent")
164
+
165
+ def get_payment_intent(self, intent_ref_id: str, tenant_id: str) -> ServiceResult[PaymentIntentRef]:
166
+ """Get payment intent by ID."""
167
+ try:
168
+ intent = self._get_model_by_id_with_tenant_check(intent_ref_id, PaymentIntentRef, tenant_id)
169
+
170
+ if not intent:
171
+ raise NotFoundError(f"Payment intent not found: {intent_ref_id}")
172
+
173
+ return ServiceResult.success_result(intent)
174
+ except Exception as e:
175
+ return ServiceResult.exception_result(e, ErrorCode.INTERNAL_ERROR, "get_payment_intent")
176
+
177
+ def update_payment_intent_status(
178
+ self,
179
+ intent_ref_id: str,
180
+ tenant_id: str,
181
+ status: str,
182
+ **kwargs
183
+ ) -> ServiceResult[PaymentIntentRef]:
184
+ """Update payment intent status."""
185
+ try:
186
+ result = self.get_payment_intent(intent_ref_id, tenant_id)
187
+ if not result.success:
188
+ return result
189
+
190
+ intent = result.data
191
+ intent = intent.map(kwargs)
192
+ intent.status = status
193
+ intent.updated_utc_ts = dt.datetime.now(dt.UTC).timestamp()
194
+
195
+
196
+
197
+ # Save
198
+ intent.prep_for_save()
199
+ return self._save_model(intent)
200
+ except Exception as e:
201
+ return ServiceResult.exception_result(e, ErrorCode.INTERNAL_ERROR, "update_payment_intent_status")
202
+
203
+ # ====================
204
+ # PAYMENT METHODS
205
+ # ====================
206
+
207
+ def record_payment(
208
+ self,
209
+ tenant_id: str,
210
+ billing_account_id: str,
211
+ payment_intent_ref_id: Optional[str],
212
+ gross_amount_cents: int,
213
+ fee_amount_cents: int,
214
+ currency_code: str = "USD",
215
+ psp_type: str = "stripe",
216
+ **kwargs
217
+ ) -> ServiceResult[Payment]:
218
+ """Record a settled payment."""
219
+ try:
220
+ payment = Payment()
221
+ # set optional fields
222
+ payment = payment.map(kwargs)
223
+ # set known fields
224
+ payment.tenant_id = tenant_id
225
+ payment.billing_account_id = billing_account_id
226
+ payment.payment_intent_ref_id = payment_intent_ref_id
227
+ payment.gross_amount_cents = gross_amount_cents
228
+ payment.fee_amount_cents = fee_amount_cents
229
+ payment.currency_code = currency_code
230
+ payment.psp_type = psp_type
231
+ payment.settled_utc_ts = dt.datetime.now(dt.UTC).timestamp()
232
+ payment.status = "succeeded"
233
+
234
+ # Save payment
235
+ payment.prep_for_save()
236
+ save_result = self._save_model(payment)
237
+
238
+ if not save_result.success:
239
+ return save_result
240
+
241
+ # Update the payment intent to link back to this payment
242
+ if payment_intent_ref_id:
243
+ intent = self._get_model_by_id_with_tenant_check(
244
+ payment_intent_ref_id, PaymentIntentRef, tenant_id
245
+ )
246
+ if intent:
247
+ intent.payment_id = payment.payment_id
248
+ intent.status = PaymentIntentRef.STATUS_SUCCEEDED
249
+ intent.prep_for_save()
250
+ self._save_model(intent)
251
+
252
+ return save_result
253
+ except Exception as e:
254
+ return ServiceResult.exception_result(e, ErrorCode.INTERNAL_ERROR, "record_payment")
255
+
256
+ def get_payment(self, payment_id: str, tenant_id: str) -> ServiceResult[Payment]:
257
+ """Get payment by ID."""
258
+ try:
259
+ payment = self._get_model_by_id_with_tenant_check(payment_id, Payment, tenant_id)
260
+
261
+ if not payment:
262
+ raise NotFoundError(f"Payment not found: {payment_id}")
263
+
264
+ return ServiceResult.success_result(payment)
265
+ except Exception as e:
266
+ return ServiceResult.exception_result(e, ErrorCode.INTERNAL_ERROR, "get_payment")
267
+
268
+ def list_payments(
269
+ self,
270
+ tenant_id: str,
271
+ billing_account_id: Optional[str] = None,
272
+ limit: int = 50
273
+ ) -> ServiceResult[List[Payment]]:
274
+ """List payments for a tenant or billing account."""
275
+ try:
276
+ # Create temp payment with appropriate fields for query
277
+ temp_payment = Payment()
278
+
279
+ if billing_account_id:
280
+ # Use GSI2 to query by billing account
281
+ temp_payment.billing_account_id = billing_account_id
282
+ gsi_name = "gsi2"
283
+ else:
284
+ # Use GSI1 to query by tenant
285
+ temp_payment.tenant_id = tenant_id
286
+ gsi_name = "gsi1"
287
+
288
+ # Query using helper method
289
+ query_result = self._query_by_index(temp_payment, gsi_name, limit=limit, ascending=False)
290
+
291
+ if not query_result.success:
292
+ return query_result
293
+
294
+ return ServiceResult.success_result(query_result.data)
295
+ except Exception as e:
296
+ return ServiceResult.exception_result(e, ErrorCode.INTERNAL_ERROR, "list_payments")
297
+
298
+ # ====================
299
+ # REFUND METHODS
300
+ # ====================
301
+
302
+ def create_refund(
303
+ self,
304
+ tenant_id: str,
305
+ payment_id: str,
306
+ amount_cents: int,
307
+ reason: Optional[str] = None,
308
+ initiated_by_id: Optional[str] = None,
309
+ **kwargs
310
+ ) -> ServiceResult[Refund]:
311
+ """Create a refund for a payment."""
312
+ try:
313
+ # Get the payment
314
+ payment_result = self.get_payment(payment_id, tenant_id)
315
+ if not payment_result.success:
316
+ return payment_result
317
+
318
+ payment = payment_result.data
319
+
320
+ # Validate refund amount
321
+ remaining = payment.get_remaining_amount_cents()
322
+ if amount_cents > remaining:
323
+ raise ValidationError(
324
+ f"Refund amount ({amount_cents}) exceeds remaining amount ({remaining})",
325
+ "amount_cents"
326
+ )
327
+
328
+ # Create refund
329
+ refund = Refund()
330
+ # set optional fields
331
+ refund = refund.map(kwargs)
332
+ # set known fields
333
+ refund.tenant_id = tenant_id
334
+ refund.payment_id = payment_id
335
+ refund.billing_account_id = payment.billing_account_id
336
+ refund.amount_cents = amount_cents
337
+ refund.currency_code = payment.currency_code
338
+ refund.psp_type = payment.psp_type
339
+ refund.reason = reason
340
+ refund.initiated_by_id = initiated_by_id
341
+ refund.initiated_utc_ts = dt.datetime.now(dt.UTC).timestamp()
342
+
343
+ # Validate
344
+ is_valid, errors = refund.validate()
345
+ if not is_valid:
346
+ raise ValidationError(f"Validation failed: {', '.join(errors)}", "refund")
347
+
348
+ # Save refund
349
+ refund.prep_for_save()
350
+ refund_result = self._save_model(refund)
351
+
352
+ if not refund_result.success:
353
+ return refund_result
354
+
355
+ # Update payment's refund tracking
356
+ payment.add_refund(refund_result.data.refund_id, amount_cents)
357
+ payment.prep_for_save()
358
+ self._save_model(payment)
359
+
360
+ return ServiceResult.success_result(refund_result.data)
361
+ except Exception as e:
362
+ return ServiceResult.exception_result(e, ErrorCode.INTERNAL_ERROR, "create_refund")
363
+
364
+ def get_refund(self, refund_id: str, tenant_id: str) -> ServiceResult[Refund]:
365
+ """Get refund by ID."""
366
+ try:
367
+ refund = self._get_model_by_id_with_tenant_check(refund_id, Refund, tenant_id)
368
+
369
+ if not refund:
370
+ raise NotFoundError(f"Refund not found: {refund_id}")
371
+
372
+ return ServiceResult.success_result(refund)
373
+ except Exception as e:
374
+ return ServiceResult.exception_result(e, ErrorCode.INTERNAL_ERROR, "get_refund")
375
+
376
+ def update_refund_status(
377
+ self,
378
+ refund_id: str,
379
+ tenant_id: str,
380
+ status: str,
381
+ **kwargs
382
+ ) -> ServiceResult[Refund]:
383
+ """Update refund status."""
384
+ try:
385
+ result = self.get_refund(refund_id, tenant_id)
386
+ if not result.success:
387
+ return result
388
+
389
+ refund = result.data
390
+ refund = refund.map(kwargs)
391
+ refund.status = status
392
+ refund.updated_utc_ts = dt.datetime.now(dt.UTC).timestamp()
393
+
394
+ if status == Refund.STATUS_SUCCEEDED:
395
+ refund.succeeded_utc_ts = dt.datetime.now(dt.UTC).timestamp()
396
+ elif status == Refund.STATUS_FAILED:
397
+ refund.failed_utc_ts = dt.datetime.now(dt.UTC).timestamp()
398
+
399
+
400
+
401
+ # Save
402
+ refund.prep_for_save()
403
+ return self._save_model(refund)
404
+ except Exception as e:
405
+ return ServiceResult.exception_result(e, ErrorCode.INTERNAL_ERROR, "update_refund_status")
@@ -0,0 +1,19 @@
1
+ """
2
+ Subscriptions Domain.
3
+
4
+ Platform-wide subscription management including plans, addons, usage tracking, and discounts.
5
+
6
+ Geek Cafe, LLC
7
+ MIT License. See Project Root for the license information.
8
+ """
9
+
10
+ from .models import Plan, Addon, UsageRecord, Discount
11
+ from .services import SubscriptionManagerService
12
+
13
+ __all__ = [
14
+ "Plan",
15
+ "Addon",
16
+ "UsageRecord",
17
+ "Discount",
18
+ "SubscriptionManagerService"
19
+ ]