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.
- geek_cafe_saas_sdk/__init__.py +1 -1
- geek_cafe_saas_sdk/domains/files/models/directory.py +42 -6
- geek_cafe_saas_sdk/domains/files/models/file.py +40 -4
- 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/directory_service.py +54 -135
- geek_cafe_saas_sdk/domains/files/services/file_share_service.py +60 -136
- geek_cafe_saas_sdk/domains/files/services/file_system_service.py +43 -104
- geek_cafe_saas_sdk/domains/files/services/file_version_service.py +57 -131
- 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/environment_variables.py +16 -0
- geek_cafe_saas_sdk/utilities/logging_utility.py +77 -0
- {geek_cafe_saas_sdk-0.7.0.dist-info → geek_cafe_saas_sdk-0.7.2.dist-info}/METADATA +1 -1
- {geek_cafe_saas_sdk-0.7.0.dist-info → geek_cafe_saas_sdk-0.7.2.dist-info}/RECORD +79 -20
- {geek_cafe_saas_sdk-0.7.0.dist-info → geek_cafe_saas_sdk-0.7.2.dist-info}/WHEEL +0 -0
- {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,404 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Refund model for payment system.
|
|
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 Optional, Dict, Any
|
|
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 Refund(BaseModel):
|
|
15
|
+
"""
|
|
16
|
+
Refund - Reversal metadata for payment refunds.
|
|
17
|
+
|
|
18
|
+
Represents a refund transaction that reverses all or part of a payment.
|
|
19
|
+
Contains details about the refund amount, reason, and status.
|
|
20
|
+
Links back to the original payment.
|
|
21
|
+
|
|
22
|
+
Multi-Tenancy:
|
|
23
|
+
- tenant_id: Organization/company issuing the refund
|
|
24
|
+
|
|
25
|
+
Access Patterns (DynamoDB Keys):
|
|
26
|
+
- pk: REFUND#{tenant_id}#{refund_id}
|
|
27
|
+
- sk: metadata
|
|
28
|
+
- gsi1_pk: tenant#{tenant_id}
|
|
29
|
+
- gsi1_sk: REFUND#{created_utc_ts}
|
|
30
|
+
- gsi2_pk: PAYMENT#{payment_id}
|
|
31
|
+
- gsi2_sk: REFUND#{created_utc_ts}
|
|
32
|
+
- gsi3_pk: PSP_REFUND#{psp_type}#{psp_refund_id}
|
|
33
|
+
- gsi3_sk: metadata
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self):
|
|
37
|
+
super().__init__()
|
|
38
|
+
|
|
39
|
+
# Identity (inherited from BaseModel: id, tenant_id)
|
|
40
|
+
self._payment_id: str | None = None # Original payment being refunded
|
|
41
|
+
self._billing_account_id: str | None = None # Associated billing account
|
|
42
|
+
|
|
43
|
+
# PSP Information
|
|
44
|
+
self._psp_type: str = "stripe" # "stripe", "paypal", "square", etc.
|
|
45
|
+
self._psp_refund_id: str | None = None # PSP's refund identifier
|
|
46
|
+
self._psp_balance_transaction_id: str | None = None # PSP balance transaction
|
|
47
|
+
|
|
48
|
+
# Refund Amount (in cents to avoid float issues)
|
|
49
|
+
self._amount_cents: int = 0 # Amount being refunded
|
|
50
|
+
self._currency_code: str = "USD" # ISO 4217 currency code
|
|
51
|
+
|
|
52
|
+
# Refund Details
|
|
53
|
+
self._reason: str | None = None # "duplicate", "fraudulent", "requested_by_customer"
|
|
54
|
+
self._description: str | None = None # Detailed reason/notes
|
|
55
|
+
|
|
56
|
+
# Status
|
|
57
|
+
self._status: str = "pending" # "pending", "succeeded", "failed", "canceled"
|
|
58
|
+
self._failure_reason: str | None = None # Reason if failed
|
|
59
|
+
|
|
60
|
+
# Processing Details
|
|
61
|
+
self._initiated_utc_ts: float | None = None # When refund was initiated
|
|
62
|
+
self._succeeded_utc_ts: float | None = None # When refund succeeded
|
|
63
|
+
self._failed_utc_ts: float | None = None # When refund failed
|
|
64
|
+
|
|
65
|
+
# Receipt
|
|
66
|
+
self._receipt_number: str | None = None # Refund receipt number
|
|
67
|
+
|
|
68
|
+
# Metadata
|
|
69
|
+
self._initiated_by_id: str | None = None # User who initiated refund
|
|
70
|
+
self._notes: str | None = None # Internal notes
|
|
71
|
+
|
|
72
|
+
# Related Records
|
|
73
|
+
self._dispute_id: str | None = None # Related dispute (if applicable)
|
|
74
|
+
|
|
75
|
+
# Additional PSP Data
|
|
76
|
+
self._psp_metadata: Dict[str, Any] | None = None # Raw PSP data
|
|
77
|
+
|
|
78
|
+
# CRITICAL: Call _setup_indexes() as LAST line in __init__
|
|
79
|
+
self._setup_indexes()
|
|
80
|
+
|
|
81
|
+
def _setup_indexes(self):
|
|
82
|
+
"""Setup DynamoDB indexes for refund queries."""
|
|
83
|
+
|
|
84
|
+
# Primary index: Refund by ID
|
|
85
|
+
primary = DynamoDBIndex()
|
|
86
|
+
primary.name = "primary"
|
|
87
|
+
primary.partition_key.attribute_name = "pk"
|
|
88
|
+
primary.partition_key.value = lambda: DynamoDBKey.build_key(("refund", self.id))
|
|
89
|
+
primary.sort_key.attribute_name = "sk"
|
|
90
|
+
primary.sort_key.value = lambda: "metadata"
|
|
91
|
+
self.indexes.add_primary(primary)
|
|
92
|
+
|
|
93
|
+
# GSI1: Refunds by tenant
|
|
94
|
+
gsi = DynamoDBIndex()
|
|
95
|
+
gsi.name = "gsi1"
|
|
96
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
97
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(("tenant", self.tenant_id))
|
|
98
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
99
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(("refund", self.created_utc_ts))
|
|
100
|
+
self.indexes.add_secondary(gsi)
|
|
101
|
+
|
|
102
|
+
# GSI2: Refunds by payment
|
|
103
|
+
gsi = DynamoDBIndex()
|
|
104
|
+
gsi.name = "gsi2"
|
|
105
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
106
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(("payment", self.payment_id))
|
|
107
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
108
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(("refund", self.created_utc_ts))
|
|
109
|
+
self.indexes.add_secondary(gsi)
|
|
110
|
+
|
|
111
|
+
# Refund Reason Constants
|
|
112
|
+
REASON_DUPLICATE = "duplicate"
|
|
113
|
+
REASON_FRAUDULENT = "fraudulent"
|
|
114
|
+
REASON_REQUESTED_BY_CUSTOMER = "requested_by_customer"
|
|
115
|
+
REASON_EXPIRED_UNCAPTURED_CHARGE = "expired_uncaptured_charge"
|
|
116
|
+
|
|
117
|
+
# Status Constants
|
|
118
|
+
STATUS_PENDING = "pending"
|
|
119
|
+
STATUS_SUCCEEDED = "succeeded"
|
|
120
|
+
STATUS_FAILED = "failed"
|
|
121
|
+
STATUS_CANCELED = "canceled"
|
|
122
|
+
|
|
123
|
+
# Properties - Identity
|
|
124
|
+
@property
|
|
125
|
+
def refund_id(self) -> str | None:
|
|
126
|
+
"""Unique refund ID (alias for id)."""
|
|
127
|
+
return self.id
|
|
128
|
+
|
|
129
|
+
@refund_id.setter
|
|
130
|
+
def refund_id(self, value: str | None):
|
|
131
|
+
self.id = value
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def payment_id(self) -> str | None:
|
|
135
|
+
"""Original payment being refunded."""
|
|
136
|
+
return self._payment_id
|
|
137
|
+
|
|
138
|
+
@payment_id.setter
|
|
139
|
+
def payment_id(self, value: str | None):
|
|
140
|
+
self._payment_id = value
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def billing_account_id(self) -> str | None:
|
|
144
|
+
"""Associated billing account ID."""
|
|
145
|
+
return self._billing_account_id
|
|
146
|
+
|
|
147
|
+
@billing_account_id.setter
|
|
148
|
+
def billing_account_id(self, value: str | None):
|
|
149
|
+
self._billing_account_id = value
|
|
150
|
+
|
|
151
|
+
# Properties - PSP Information
|
|
152
|
+
@property
|
|
153
|
+
def psp_type(self) -> str:
|
|
154
|
+
"""Payment service provider type."""
|
|
155
|
+
return self._psp_type
|
|
156
|
+
|
|
157
|
+
@psp_type.setter
|
|
158
|
+
def psp_type(self, value: str):
|
|
159
|
+
valid_types = ["stripe", "paypal", "square", "braintree"]
|
|
160
|
+
if value not in valid_types:
|
|
161
|
+
raise ValueError(f"Invalid psp_type: {value}. Must be one of {valid_types}")
|
|
162
|
+
self._psp_type = value
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def psp_refund_id(self) -> str | None:
|
|
166
|
+
"""PSP refund identifier."""
|
|
167
|
+
return self._psp_refund_id
|
|
168
|
+
|
|
169
|
+
@psp_refund_id.setter
|
|
170
|
+
def psp_refund_id(self, value: str | None):
|
|
171
|
+
self._psp_refund_id = value
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def psp_balance_transaction_id(self) -> str | None:
|
|
175
|
+
"""PSP balance transaction ID."""
|
|
176
|
+
return self._psp_balance_transaction_id
|
|
177
|
+
|
|
178
|
+
@psp_balance_transaction_id.setter
|
|
179
|
+
def psp_balance_transaction_id(self, value: str | None):
|
|
180
|
+
self._psp_balance_transaction_id = value
|
|
181
|
+
|
|
182
|
+
# Properties - Amount
|
|
183
|
+
@property
|
|
184
|
+
def amount_cents(self) -> int:
|
|
185
|
+
"""Refund amount in cents."""
|
|
186
|
+
return self._amount_cents
|
|
187
|
+
|
|
188
|
+
@amount_cents.setter
|
|
189
|
+
def amount_cents(self, value: int):
|
|
190
|
+
if value < 0:
|
|
191
|
+
raise ValueError("amount_cents must be non-negative")
|
|
192
|
+
self._amount_cents = value
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def currency_code(self) -> str:
|
|
196
|
+
"""ISO 4217 currency code."""
|
|
197
|
+
return self._currency_code
|
|
198
|
+
|
|
199
|
+
@currency_code.setter
|
|
200
|
+
def currency_code(self, value: str):
|
|
201
|
+
if not value or len(value) != 3:
|
|
202
|
+
raise ValueError("currency_code must be a 3-letter ISO 4217 code")
|
|
203
|
+
self._currency_code = value.upper()
|
|
204
|
+
|
|
205
|
+
# Properties - Refund Details
|
|
206
|
+
@property
|
|
207
|
+
def reason(self) -> str | None:
|
|
208
|
+
"""Refund reason."""
|
|
209
|
+
return self._reason
|
|
210
|
+
|
|
211
|
+
@reason.setter
|
|
212
|
+
def reason(self, value: str | None):
|
|
213
|
+
valid_reasons = [
|
|
214
|
+
self.REASON_DUPLICATE,
|
|
215
|
+
self.REASON_FRAUDULENT,
|
|
216
|
+
self.REASON_REQUESTED_BY_CUSTOMER,
|
|
217
|
+
self.REASON_EXPIRED_UNCAPTURED_CHARGE,
|
|
218
|
+
]
|
|
219
|
+
if value and value not in valid_reasons:
|
|
220
|
+
# Allow custom reasons, but warn
|
|
221
|
+
pass
|
|
222
|
+
self._reason = value
|
|
223
|
+
|
|
224
|
+
@property
|
|
225
|
+
def description(self) -> str | None:
|
|
226
|
+
"""Detailed refund description."""
|
|
227
|
+
return self._description
|
|
228
|
+
|
|
229
|
+
@description.setter
|
|
230
|
+
def description(self, value: str | None):
|
|
231
|
+
self._description = value
|
|
232
|
+
|
|
233
|
+
# Properties - Status
|
|
234
|
+
@property
|
|
235
|
+
def status(self) -> str:
|
|
236
|
+
"""Refund status."""
|
|
237
|
+
return self._status
|
|
238
|
+
|
|
239
|
+
@status.setter
|
|
240
|
+
def status(self, value: str):
|
|
241
|
+
valid_statuses = [
|
|
242
|
+
self.STATUS_PENDING,
|
|
243
|
+
self.STATUS_SUCCEEDED,
|
|
244
|
+
self.STATUS_FAILED,
|
|
245
|
+
self.STATUS_CANCELED,
|
|
246
|
+
]
|
|
247
|
+
if value not in valid_statuses:
|
|
248
|
+
raise ValueError(f"Invalid status: {value}. Must be one of {valid_statuses}")
|
|
249
|
+
self._status = value
|
|
250
|
+
|
|
251
|
+
@property
|
|
252
|
+
def failure_reason(self) -> str | None:
|
|
253
|
+
"""Failure reason if refund failed."""
|
|
254
|
+
return self._failure_reason
|
|
255
|
+
|
|
256
|
+
@failure_reason.setter
|
|
257
|
+
def failure_reason(self, value: str | None):
|
|
258
|
+
self._failure_reason = value
|
|
259
|
+
|
|
260
|
+
# Properties - Processing Details
|
|
261
|
+
@property
|
|
262
|
+
def initiated_utc_ts(self) -> float | None:
|
|
263
|
+
"""Timestamp when refund was initiated."""
|
|
264
|
+
return self._initiated_utc_ts
|
|
265
|
+
|
|
266
|
+
@initiated_utc_ts.setter
|
|
267
|
+
def initiated_utc_ts(self, value: float | None):
|
|
268
|
+
self._initiated_utc_ts = value
|
|
269
|
+
|
|
270
|
+
@property
|
|
271
|
+
def succeeded_utc_ts(self) -> float | None:
|
|
272
|
+
"""Timestamp when refund succeeded."""
|
|
273
|
+
return self._succeeded_utc_ts
|
|
274
|
+
|
|
275
|
+
@succeeded_utc_ts.setter
|
|
276
|
+
def succeeded_utc_ts(self, value: float | None):
|
|
277
|
+
self._succeeded_utc_ts = value
|
|
278
|
+
|
|
279
|
+
@property
|
|
280
|
+
def failed_utc_ts(self) -> float | None:
|
|
281
|
+
"""Timestamp when refund failed."""
|
|
282
|
+
return self._failed_utc_ts
|
|
283
|
+
|
|
284
|
+
@failed_utc_ts.setter
|
|
285
|
+
def failed_utc_ts(self, value: float | None):
|
|
286
|
+
self._failed_utc_ts = value
|
|
287
|
+
|
|
288
|
+
# Properties - Receipt
|
|
289
|
+
@property
|
|
290
|
+
def receipt_number(self) -> str | None:
|
|
291
|
+
"""Refund receipt number."""
|
|
292
|
+
return self._receipt_number
|
|
293
|
+
|
|
294
|
+
@receipt_number.setter
|
|
295
|
+
def receipt_number(self, value: str | None):
|
|
296
|
+
self._receipt_number = value
|
|
297
|
+
|
|
298
|
+
# Properties - Metadata
|
|
299
|
+
@property
|
|
300
|
+
def initiated_by_id(self) -> str | None:
|
|
301
|
+
"""User who initiated the refund."""
|
|
302
|
+
return self._initiated_by_id
|
|
303
|
+
|
|
304
|
+
@initiated_by_id.setter
|
|
305
|
+
def initiated_by_id(self, value: str | None):
|
|
306
|
+
self._initiated_by_id = value
|
|
307
|
+
|
|
308
|
+
@property
|
|
309
|
+
def notes(self) -> str | None:
|
|
310
|
+
"""Internal notes."""
|
|
311
|
+
return self._notes
|
|
312
|
+
|
|
313
|
+
@notes.setter
|
|
314
|
+
def notes(self, value: str | None):
|
|
315
|
+
self._notes = value
|
|
316
|
+
|
|
317
|
+
# Properties - Related Records
|
|
318
|
+
@property
|
|
319
|
+
def dispute_id(self) -> str | None:
|
|
320
|
+
"""Related dispute ID (if applicable)."""
|
|
321
|
+
return self._dispute_id
|
|
322
|
+
|
|
323
|
+
@dispute_id.setter
|
|
324
|
+
def dispute_id(self, value: str | None):
|
|
325
|
+
self._dispute_id = value
|
|
326
|
+
|
|
327
|
+
# Properties - Additional Data
|
|
328
|
+
@property
|
|
329
|
+
def psp_metadata(self) -> Dict[str, Any] | None:
|
|
330
|
+
"""Raw PSP metadata."""
|
|
331
|
+
return self._psp_metadata
|
|
332
|
+
|
|
333
|
+
@psp_metadata.setter
|
|
334
|
+
def psp_metadata(self, value: Dict[str, Any] | None):
|
|
335
|
+
self._psp_metadata = value if isinstance(value, dict) else None
|
|
336
|
+
|
|
337
|
+
# Helper Methods
|
|
338
|
+
def is_pending(self) -> bool:
|
|
339
|
+
"""Check if refund is pending."""
|
|
340
|
+
return self._status == self.STATUS_PENDING
|
|
341
|
+
|
|
342
|
+
def is_succeeded(self) -> bool:
|
|
343
|
+
"""Check if refund succeeded."""
|
|
344
|
+
return self._status == self.STATUS_SUCCEEDED
|
|
345
|
+
|
|
346
|
+
def is_failed(self) -> bool:
|
|
347
|
+
"""Check if refund failed."""
|
|
348
|
+
return self._status == self.STATUS_FAILED
|
|
349
|
+
|
|
350
|
+
def is_canceled(self) -> bool:
|
|
351
|
+
"""Check if refund was canceled."""
|
|
352
|
+
return self._status == self.STATUS_CANCELED
|
|
353
|
+
|
|
354
|
+
def get_amount_dollars(self) -> float:
|
|
355
|
+
"""Get refund amount in dollars."""
|
|
356
|
+
return self._amount_cents / 100.0
|
|
357
|
+
|
|
358
|
+
def mark_as_succeeded(self):
|
|
359
|
+
"""Mark refund as succeeded."""
|
|
360
|
+
self._status = self.STATUS_SUCCEEDED
|
|
361
|
+
self._succeeded_utc_ts = dt.datetime.now(dt.UTC).timestamp()
|
|
362
|
+
|
|
363
|
+
def mark_as_failed(self, reason: str | None = None):
|
|
364
|
+
"""Mark refund as failed."""
|
|
365
|
+
self._status = self.STATUS_FAILED
|
|
366
|
+
self._failed_utc_ts = dt.datetime.now(dt.UTC).timestamp()
|
|
367
|
+
self._failure_reason = reason
|
|
368
|
+
|
|
369
|
+
def mark_as_canceled(self):
|
|
370
|
+
"""Mark refund as canceled."""
|
|
371
|
+
self._status = self.STATUS_CANCELED
|
|
372
|
+
|
|
373
|
+
def get_processing_duration_seconds(self) -> float | None:
|
|
374
|
+
"""Get processing duration in seconds (from initiation to completion)."""
|
|
375
|
+
if not self._initiated_utc_ts:
|
|
376
|
+
return None
|
|
377
|
+
|
|
378
|
+
end_ts = self._succeeded_utc_ts or self._failed_utc_ts
|
|
379
|
+
if not end_ts:
|
|
380
|
+
# Still processing
|
|
381
|
+
return dt.datetime.now(dt.UTC).timestamp() - self._initiated_utc_ts
|
|
382
|
+
|
|
383
|
+
return end_ts - self._initiated_utc_ts
|
|
384
|
+
|
|
385
|
+
def validate(self) -> tuple[bool, list[str]]:
|
|
386
|
+
"""
|
|
387
|
+
Validate the refund.
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
Tuple of (is_valid, list of error messages)
|
|
391
|
+
"""
|
|
392
|
+
errors = []
|
|
393
|
+
|
|
394
|
+
# Required fields
|
|
395
|
+
if not self.tenant_id:
|
|
396
|
+
errors.append("tenant_id is required")
|
|
397
|
+
if not self._payment_id:
|
|
398
|
+
errors.append("payment_id is required")
|
|
399
|
+
if self._amount_cents <= 0:
|
|
400
|
+
errors.append("amount_cents must be greater than 0")
|
|
401
|
+
if not self._currency_code:
|
|
402
|
+
errors.append("currency_code is required")
|
|
403
|
+
|
|
404
|
+
return (len(errors) == 0, errors)
|