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.
- geek_cafe_saas_sdk/__init__.py +2 -2
- geek_cafe_saas_sdk/domains/files/handlers/README.md +446 -0
- geek_cafe_saas_sdk/domains/files/handlers/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/files/handlers/files/create/app.py +121 -0
- geek_cafe_saas_sdk/domains/files/handlers/files/download/app.py +80 -0
- geek_cafe_saas_sdk/domains/files/handlers/files/get/app.py +62 -0
- geek_cafe_saas_sdk/domains/files/handlers/files/list/app.py +72 -0
- geek_cafe_saas_sdk/domains/files/handlers/lineage/create_derived/app.py +99 -0
- geek_cafe_saas_sdk/domains/files/handlers/lineage/create_main/app.py +104 -0
- geek_cafe_saas_sdk/domains/files/handlers/lineage/download_bundle/app.py +99 -0
- geek_cafe_saas_sdk/domains/files/handlers/lineage/get_lineage/app.py +68 -0
- geek_cafe_saas_sdk/domains/files/handlers/lineage/prepare_bundle/app.py +76 -0
- geek_cafe_saas_sdk/domains/files/models/__init__.py +17 -0
- geek_cafe_saas_sdk/domains/files/models/directory.py +42 -6
- geek_cafe_saas_sdk/domains/files/models/file.py +158 -16
- geek_cafe_saas_sdk/domains/files/models/file_share.py +33 -0
- geek_cafe_saas_sdk/domains/files/models/file_version.py +24 -0
- geek_cafe_saas_sdk/domains/files/services/__init__.py +21 -0
- geek_cafe_saas_sdk/domains/files/services/directory_service.py +54 -135
- geek_cafe_saas_sdk/domains/files/services/file_lineage_service.py +487 -0
- geek_cafe_saas_sdk/domains/files/services/file_share_service.py +37 -120
- geek_cafe_saas_sdk/domains/files/services/file_system_service.py +67 -103
- geek_cafe_saas_sdk/domains/files/services/file_version_service.py +44 -124
- geek_cafe_saas_sdk/domains/messaging/services/contact_thread_service.py +55 -7
- geek_cafe_saas_sdk/domains/notifications/__init__.py +18 -0
- geek_cafe_saas_sdk/domains/notifications/handlers/__init__.py +1 -0
- geek_cafe_saas_sdk/domains/notifications/handlers/create_webhook/app.py +73 -0
- geek_cafe_saas_sdk/domains/notifications/handlers/get/app.py +40 -0
- geek_cafe_saas_sdk/domains/notifications/handlers/get_preferences/app.py +34 -0
- geek_cafe_saas_sdk/domains/notifications/handlers/list/app.py +43 -0
- geek_cafe_saas_sdk/domains/notifications/handlers/list_webhooks/app.py +40 -0
- geek_cafe_saas_sdk/domains/notifications/handlers/mark_read/app.py +40 -0
- geek_cafe_saas_sdk/domains/notifications/handlers/send/app.py +83 -0
- geek_cafe_saas_sdk/domains/notifications/handlers/update_preferences/app.py +45 -0
- geek_cafe_saas_sdk/domains/notifications/models/__init__.py +16 -0
- geek_cafe_saas_sdk/domains/notifications/models/notification.py +717 -0
- geek_cafe_saas_sdk/domains/notifications/models/notification_preference.py +365 -0
- geek_cafe_saas_sdk/domains/notifications/models/webhook_subscription.py +339 -0
- geek_cafe_saas_sdk/domains/notifications/services/__init__.py +10 -0
- geek_cafe_saas_sdk/domains/notifications/services/notification_service.py +576 -0
- geek_cafe_saas_sdk/domains/payments/__init__.py +16 -0
- geek_cafe_saas_sdk/domains/payments/handlers/README.md +334 -0
- geek_cafe_saas_sdk/domains/payments/handlers/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/create/app.py +105 -0
- geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/get/app.py +60 -0
- geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/update/app.py +97 -0
- geek_cafe_saas_sdk/domains/payments/handlers/payment_intents/create/app.py +97 -0
- geek_cafe_saas_sdk/domains/payments/handlers/payment_intents/get/app.py +60 -0
- geek_cafe_saas_sdk/domains/payments/handlers/payments/get/app.py +60 -0
- geek_cafe_saas_sdk/domains/payments/handlers/payments/list/app.py +68 -0
- geek_cafe_saas_sdk/domains/payments/handlers/payments/record/app.py +118 -0
- geek_cafe_saas_sdk/domains/payments/handlers/refunds/create/app.py +89 -0
- geek_cafe_saas_sdk/domains/payments/handlers/refunds/get/app.py +60 -0
- geek_cafe_saas_sdk/domains/payments/models/__init__.py +17 -0
- geek_cafe_saas_sdk/domains/payments/models/billing_account.py +521 -0
- geek_cafe_saas_sdk/domains/payments/models/payment.py +639 -0
- geek_cafe_saas_sdk/domains/payments/models/payment_intent_ref.py +539 -0
- geek_cafe_saas_sdk/domains/payments/models/refund.py +404 -0
- geek_cafe_saas_sdk/domains/payments/services/__init__.py +11 -0
- geek_cafe_saas_sdk/domains/payments/services/payment_service.py +405 -0
- geek_cafe_saas_sdk/domains/subscriptions/__init__.py +19 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/README.md +408 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/__init__.py +1 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/create/app.py +81 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/get/app.py +48 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/list/app.py +54 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/update/app.py +54 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/create/app.py +83 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/get/app.py +47 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/validate/app.py +62 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/create/app.py +82 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/get/app.py +48 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/list/app.py +66 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/update/app.py +54 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/usage/aggregate/app.py +72 -0
- geek_cafe_saas_sdk/domains/subscriptions/handlers/usage/record/app.py +89 -0
- geek_cafe_saas_sdk/domains/subscriptions/models/__init__.py +13 -0
- geek_cafe_saas_sdk/domains/subscriptions/models/addon.py +604 -0
- geek_cafe_saas_sdk/domains/subscriptions/models/discount.py +492 -0
- geek_cafe_saas_sdk/domains/subscriptions/models/plan.py +569 -0
- geek_cafe_saas_sdk/domains/subscriptions/models/usage_record.py +300 -0
- geek_cafe_saas_sdk/domains/subscriptions/services/__init__.py +10 -0
- geek_cafe_saas_sdk/domains/subscriptions/services/subscription_manager_service.py +694 -0
- geek_cafe_saas_sdk/domains/tenancy/models/subscription.py +123 -1
- geek_cafe_saas_sdk/domains/tenancy/services/subscription_service.py +213 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/base_handler.py +7 -0
- geek_cafe_saas_sdk/services/database_service.py +10 -6
- geek_cafe_saas_sdk/utilities/cognito_utility.py +16 -26
- geek_cafe_saas_sdk/utilities/environment_variables.py +16 -0
- geek_cafe_saas_sdk/utilities/logging_utility.py +77 -0
- {geek_cafe_saas_sdk-0.6.0.dist-info → geek_cafe_saas_sdk-0.7.1.dist-info}/METADATA +11 -11
- {geek_cafe_saas_sdk-0.6.0.dist-info → geek_cafe_saas_sdk-0.7.1.dist-info}/RECORD +94 -23
- {geek_cafe_saas_sdk-0.6.0.dist-info → geek_cafe_saas_sdk-0.7.1.dist-info}/WHEEL +0 -0
- {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
|
+
]
|