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,539 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PaymentIntentRef 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 PaymentIntentRef(BaseModel):
|
|
15
|
+
"""
|
|
16
|
+
PaymentIntentRef - PSP payment intent reference and status tracking.
|
|
17
|
+
|
|
18
|
+
Represents a payment intent created with a payment service provider (PSP)
|
|
19
|
+
like Stripe. Tracks the intent lifecycle from creation through completion
|
|
20
|
+
or cancellation. Used to handle async payment flows and webhooks.
|
|
21
|
+
|
|
22
|
+
Multi-Tenancy:
|
|
23
|
+
- tenant_id: Organization/company initiating the payment
|
|
24
|
+
|
|
25
|
+
Access Patterns (DynamoDB Keys):
|
|
26
|
+
- pk: PAYMENT_INTENT#{tenant_id}#{intent_ref_id}
|
|
27
|
+
- sk: metadata
|
|
28
|
+
- gsi1_pk: tenant#{tenant_id}
|
|
29
|
+
- gsi1_sk: PAYMENT_INTENT#{created_utc_ts}
|
|
30
|
+
- gsi2_pk: PSP_INTENT#{psp_type}#{psp_intent_id}
|
|
31
|
+
- gsi2_sk: metadata
|
|
32
|
+
- gsi3_pk: BILLING_ACCOUNT#{billing_account_id}
|
|
33
|
+
- gsi3_sk: PAYMENT_INTENT#{created_utc_ts}
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self):
|
|
37
|
+
super().__init__()
|
|
38
|
+
|
|
39
|
+
# Identity (inherited from BaseModel: id, tenant_id)
|
|
40
|
+
self._billing_account_id: str | None = None # Associated billing account
|
|
41
|
+
|
|
42
|
+
# PSP Information
|
|
43
|
+
self._psp_type: str = "stripe" # "stripe", "paypal", "square", etc.
|
|
44
|
+
self._psp_intent_id: str | None = None # PSP's intent identifier
|
|
45
|
+
self._psp_client_secret: str | None = None # Client secret for frontend
|
|
46
|
+
|
|
47
|
+
# Amount (in cents to avoid float issues)
|
|
48
|
+
self._amount_cents: int = 0 # Total amount
|
|
49
|
+
self._currency_code: str = "USD" # ISO 4217 currency code
|
|
50
|
+
|
|
51
|
+
# Payment Method
|
|
52
|
+
self._payment_method_id: str | None = None # PSP payment method ID
|
|
53
|
+
self._payment_method_type: str | None = None # "card", "ach_debit", etc.
|
|
54
|
+
self._payment_method_last4: str | None = None # Last 4 digits
|
|
55
|
+
self._payment_method_brand: str | None = None # "visa", "mastercard", etc.
|
|
56
|
+
|
|
57
|
+
# Status Tracking
|
|
58
|
+
self._status: str = "created" # Status of the payment intent
|
|
59
|
+
self._status_history: list[Dict[str, Any]] = [] # Status change log
|
|
60
|
+
self._last_status_change_utc_ts: float | None = None
|
|
61
|
+
|
|
62
|
+
# Processing Details
|
|
63
|
+
self._setup_future_usage: str | None = None # "on_session", "off_session"
|
|
64
|
+
self._capture_method: str = "automatic" # "automatic", "manual"
|
|
65
|
+
self._confirmation_method: str = "automatic" # "automatic", "manual"
|
|
66
|
+
|
|
67
|
+
# Metadata
|
|
68
|
+
self._description: str | None = None # Payment description
|
|
69
|
+
self._statement_descriptor: str | None = None # Appears on card statement
|
|
70
|
+
self._receipt_email: str | None = None # Email for receipt
|
|
71
|
+
|
|
72
|
+
# Error Tracking
|
|
73
|
+
self._error_code: str | None = None # PSP error code
|
|
74
|
+
self._error_message: str | None = None # Human-readable error
|
|
75
|
+
self._error_type: str | None = None # "card_error", "invalid_request", etc.
|
|
76
|
+
|
|
77
|
+
# Webhooks & Events
|
|
78
|
+
self._last_webhook_utc_ts: float | None = None # Last webhook received
|
|
79
|
+
self._webhook_count: int = 0 # Number of webhooks received
|
|
80
|
+
|
|
81
|
+
# Related Records
|
|
82
|
+
self._payment_id: str | None = None # Settled Payment record (if succeeded)
|
|
83
|
+
self._invoice_id: str | None = None # Related invoice
|
|
84
|
+
self._subscription_id: str | None = None # Related subscription
|
|
85
|
+
|
|
86
|
+
# Cancellation
|
|
87
|
+
self._canceled_utc_ts: float | None = None
|
|
88
|
+
self._cancellation_reason: str | None = None # "duplicate", "fraudulent", etc.
|
|
89
|
+
|
|
90
|
+
# Additional PSP Data
|
|
91
|
+
self._psp_metadata: Dict[str, Any] | None = None # Raw PSP data
|
|
92
|
+
|
|
93
|
+
# CRITICAL: Call _setup_indexes() as LAST line in __init__
|
|
94
|
+
self._setup_indexes()
|
|
95
|
+
|
|
96
|
+
def _setup_indexes(self):
|
|
97
|
+
"""Setup DynamoDB indexes for payment intent queries."""
|
|
98
|
+
|
|
99
|
+
# Primary index: Payment intent by ID
|
|
100
|
+
primary = DynamoDBIndex()
|
|
101
|
+
primary.name = "primary"
|
|
102
|
+
primary.partition_key.attribute_name = "pk"
|
|
103
|
+
primary.partition_key.value = lambda: DynamoDBKey.build_key(("payment_intent", self.id))
|
|
104
|
+
primary.sort_key.attribute_name = "sk"
|
|
105
|
+
primary.sort_key.value = lambda: "metadata"
|
|
106
|
+
self.indexes.add_primary(primary)
|
|
107
|
+
|
|
108
|
+
# GSI1: Payment intents by tenant
|
|
109
|
+
gsi = DynamoDBIndex()
|
|
110
|
+
gsi.name = "gsi1"
|
|
111
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
112
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(("tenant", self.tenant_id))
|
|
113
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
114
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(("payment_intent", self.created_utc_ts))
|
|
115
|
+
self.indexes.add_secondary(gsi)
|
|
116
|
+
|
|
117
|
+
# GSI2: Payment intent by PSP intent ID (for webhook lookups)
|
|
118
|
+
if self.psp_intent_id:
|
|
119
|
+
gsi = DynamoDBIndex()
|
|
120
|
+
gsi.name = "gsi2"
|
|
121
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
122
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(("psp_intent", self.psp_type), ("intent", self.psp_intent_id))
|
|
123
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
124
|
+
gsi.sort_key.value = lambda: "metadata"
|
|
125
|
+
self.indexes.add_secondary(gsi)
|
|
126
|
+
|
|
127
|
+
# Status Constants
|
|
128
|
+
STATUS_CREATED = "created"
|
|
129
|
+
STATUS_PROCESSING = "processing"
|
|
130
|
+
STATUS_REQUIRES_ACTION = "requires_action" # e.g., 3D Secure
|
|
131
|
+
STATUS_REQUIRES_CONFIRMATION = "requires_confirmation"
|
|
132
|
+
STATUS_REQUIRES_PAYMENT_METHOD = "requires_payment_method"
|
|
133
|
+
STATUS_SUCCEEDED = "succeeded"
|
|
134
|
+
STATUS_CANCELED = "canceled"
|
|
135
|
+
STATUS_FAILED = "failed"
|
|
136
|
+
|
|
137
|
+
# Properties - Identity
|
|
138
|
+
@property
|
|
139
|
+
def intent_ref_id(self) -> str | None:
|
|
140
|
+
"""Unique intent reference ID (alias for id)."""
|
|
141
|
+
return self.id
|
|
142
|
+
|
|
143
|
+
@intent_ref_id.setter
|
|
144
|
+
def intent_ref_id(self, value: str | None):
|
|
145
|
+
self.id = value
|
|
146
|
+
|
|
147
|
+
@property
|
|
148
|
+
def billing_account_id(self) -> str | None:
|
|
149
|
+
"""Associated billing account ID."""
|
|
150
|
+
return self._billing_account_id
|
|
151
|
+
|
|
152
|
+
@billing_account_id.setter
|
|
153
|
+
def billing_account_id(self, value: str | None):
|
|
154
|
+
self._billing_account_id = value
|
|
155
|
+
|
|
156
|
+
# Properties - PSP Information
|
|
157
|
+
@property
|
|
158
|
+
def psp_type(self) -> str:
|
|
159
|
+
"""Payment service provider type."""
|
|
160
|
+
return self._psp_type
|
|
161
|
+
|
|
162
|
+
@psp_type.setter
|
|
163
|
+
def psp_type(self, value: str):
|
|
164
|
+
valid_types = ["stripe", "paypal", "square", "braintree"]
|
|
165
|
+
if value not in valid_types:
|
|
166
|
+
raise ValueError(f"Invalid psp_type: {value}. Must be one of {valid_types}")
|
|
167
|
+
self._psp_type = value
|
|
168
|
+
|
|
169
|
+
@property
|
|
170
|
+
def psp_intent_id(self) -> str | None:
|
|
171
|
+
"""PSP's payment intent identifier."""
|
|
172
|
+
return self._psp_intent_id
|
|
173
|
+
|
|
174
|
+
@psp_intent_id.setter
|
|
175
|
+
def psp_intent_id(self, value: str | None):
|
|
176
|
+
self._psp_intent_id = value
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def psp_client_secret(self) -> str | None:
|
|
180
|
+
"""Client secret for frontend confirmation."""
|
|
181
|
+
return self._psp_client_secret
|
|
182
|
+
|
|
183
|
+
@psp_client_secret.setter
|
|
184
|
+
def psp_client_secret(self, value: str | None):
|
|
185
|
+
self._psp_client_secret = value
|
|
186
|
+
|
|
187
|
+
# Properties - Amount
|
|
188
|
+
@property
|
|
189
|
+
def amount_cents(self) -> int:
|
|
190
|
+
"""Payment amount in cents."""
|
|
191
|
+
return self._amount_cents
|
|
192
|
+
|
|
193
|
+
@amount_cents.setter
|
|
194
|
+
def amount_cents(self, value: int):
|
|
195
|
+
if value < 0:
|
|
196
|
+
raise ValueError("amount_cents must be non-negative")
|
|
197
|
+
self._amount_cents = value
|
|
198
|
+
|
|
199
|
+
@property
|
|
200
|
+
def currency_code(self) -> str:
|
|
201
|
+
"""ISO 4217 currency code."""
|
|
202
|
+
return self._currency_code
|
|
203
|
+
|
|
204
|
+
@currency_code.setter
|
|
205
|
+
def currency_code(self, value: str):
|
|
206
|
+
if not value or len(value) != 3:
|
|
207
|
+
raise ValueError("currency_code must be a 3-letter ISO 4217 code")
|
|
208
|
+
self._currency_code = value.upper()
|
|
209
|
+
|
|
210
|
+
# Properties - Payment Method
|
|
211
|
+
@property
|
|
212
|
+
def payment_method_id(self) -> str | None:
|
|
213
|
+
"""PSP payment method ID."""
|
|
214
|
+
return self._payment_method_id
|
|
215
|
+
|
|
216
|
+
@payment_method_id.setter
|
|
217
|
+
def payment_method_id(self, value: str | None):
|
|
218
|
+
self._payment_method_id = value
|
|
219
|
+
|
|
220
|
+
@property
|
|
221
|
+
def payment_method_type(self) -> str | None:
|
|
222
|
+
"""Payment method type (e.g., 'card', 'ach_debit')."""
|
|
223
|
+
return self._payment_method_type
|
|
224
|
+
|
|
225
|
+
@payment_method_type.setter
|
|
226
|
+
def payment_method_type(self, value: str | None):
|
|
227
|
+
self._payment_method_type = value
|
|
228
|
+
|
|
229
|
+
@property
|
|
230
|
+
def payment_method_last4(self) -> str | None:
|
|
231
|
+
"""Last 4 digits of payment method."""
|
|
232
|
+
return self._payment_method_last4
|
|
233
|
+
|
|
234
|
+
@payment_method_last4.setter
|
|
235
|
+
def payment_method_last4(self, value: str | None):
|
|
236
|
+
self._payment_method_last4 = value
|
|
237
|
+
|
|
238
|
+
@property
|
|
239
|
+
def payment_method_brand(self) -> str | None:
|
|
240
|
+
"""Payment method brand (e.g., 'visa', 'mastercard')."""
|
|
241
|
+
return self._payment_method_brand
|
|
242
|
+
|
|
243
|
+
@payment_method_brand.setter
|
|
244
|
+
def payment_method_brand(self, value: str | None):
|
|
245
|
+
self._payment_method_brand = value
|
|
246
|
+
|
|
247
|
+
# Properties - Status
|
|
248
|
+
@property
|
|
249
|
+
def status(self) -> str:
|
|
250
|
+
"""Current payment intent status."""
|
|
251
|
+
return self._status
|
|
252
|
+
|
|
253
|
+
@status.setter
|
|
254
|
+
def status(self, value: str):
|
|
255
|
+
valid_statuses = [
|
|
256
|
+
self.STATUS_CREATED,
|
|
257
|
+
self.STATUS_PROCESSING,
|
|
258
|
+
self.STATUS_REQUIRES_ACTION,
|
|
259
|
+
self.STATUS_REQUIRES_CONFIRMATION,
|
|
260
|
+
self.STATUS_REQUIRES_PAYMENT_METHOD,
|
|
261
|
+
self.STATUS_SUCCEEDED,
|
|
262
|
+
self.STATUS_CANCELED,
|
|
263
|
+
self.STATUS_FAILED,
|
|
264
|
+
]
|
|
265
|
+
if value not in valid_statuses:
|
|
266
|
+
raise ValueError(f"Invalid status: {value}. Must be one of {valid_statuses}")
|
|
267
|
+
|
|
268
|
+
# Record status change in history
|
|
269
|
+
if value != self._status:
|
|
270
|
+
self._status_history.append({
|
|
271
|
+
"from_status": self._status,
|
|
272
|
+
"to_status": value,
|
|
273
|
+
"timestamp": dt.datetime.now(dt.UTC).timestamp(),
|
|
274
|
+
})
|
|
275
|
+
self._last_status_change_utc_ts = dt.datetime.now(dt.UTC).timestamp()
|
|
276
|
+
|
|
277
|
+
self._status = value
|
|
278
|
+
|
|
279
|
+
@property
|
|
280
|
+
def status_history(self) -> list[Dict[str, Any]]:
|
|
281
|
+
"""Status change history."""
|
|
282
|
+
return self._status_history
|
|
283
|
+
|
|
284
|
+
@status_history.setter
|
|
285
|
+
def status_history(self, value: list[Dict[str, Any]] | None):
|
|
286
|
+
self._status_history = value if isinstance(value, list) else []
|
|
287
|
+
|
|
288
|
+
@property
|
|
289
|
+
def last_status_change_utc_ts(self) -> float | None:
|
|
290
|
+
"""Timestamp of last status change."""
|
|
291
|
+
return self._last_status_change_utc_ts
|
|
292
|
+
|
|
293
|
+
@last_status_change_utc_ts.setter
|
|
294
|
+
def last_status_change_utc_ts(self, value: float | None):
|
|
295
|
+
self._last_status_change_utc_ts = value
|
|
296
|
+
|
|
297
|
+
# Properties - Processing Details
|
|
298
|
+
@property
|
|
299
|
+
def setup_future_usage(self) -> str | None:
|
|
300
|
+
"""Setup for future usage: 'on_session', 'off_session'."""
|
|
301
|
+
return self._setup_future_usage
|
|
302
|
+
|
|
303
|
+
@setup_future_usage.setter
|
|
304
|
+
def setup_future_usage(self, value: str | None):
|
|
305
|
+
if value and value not in ["on_session", "off_session"]:
|
|
306
|
+
raise ValueError("setup_future_usage must be 'on_session' or 'off_session'")
|
|
307
|
+
self._setup_future_usage = value
|
|
308
|
+
|
|
309
|
+
@property
|
|
310
|
+
def capture_method(self) -> str:
|
|
311
|
+
"""Capture method: 'automatic', 'manual'."""
|
|
312
|
+
return self._capture_method
|
|
313
|
+
|
|
314
|
+
@capture_method.setter
|
|
315
|
+
def capture_method(self, value: str):
|
|
316
|
+
if value not in ["automatic", "manual"]:
|
|
317
|
+
raise ValueError("capture_method must be 'automatic' or 'manual'")
|
|
318
|
+
self._capture_method = value
|
|
319
|
+
|
|
320
|
+
@property
|
|
321
|
+
def confirmation_method(self) -> str:
|
|
322
|
+
"""Confirmation method: 'automatic', 'manual'."""
|
|
323
|
+
return self._confirmation_method
|
|
324
|
+
|
|
325
|
+
@confirmation_method.setter
|
|
326
|
+
def confirmation_method(self, value: str):
|
|
327
|
+
if value not in ["automatic", "manual"]:
|
|
328
|
+
raise ValueError("confirmation_method must be 'automatic' or 'manual'")
|
|
329
|
+
self._confirmation_method = value
|
|
330
|
+
|
|
331
|
+
# Properties - Metadata
|
|
332
|
+
@property
|
|
333
|
+
def description(self) -> str | None:
|
|
334
|
+
"""Payment description."""
|
|
335
|
+
return self._description
|
|
336
|
+
|
|
337
|
+
@description.setter
|
|
338
|
+
def description(self, value: str | None):
|
|
339
|
+
self._description = value
|
|
340
|
+
|
|
341
|
+
@property
|
|
342
|
+
def statement_descriptor(self) -> str | None:
|
|
343
|
+
"""Statement descriptor (appears on card statement)."""
|
|
344
|
+
return self._statement_descriptor
|
|
345
|
+
|
|
346
|
+
@statement_descriptor.setter
|
|
347
|
+
def statement_descriptor(self, value: str | None):
|
|
348
|
+
self._statement_descriptor = value
|
|
349
|
+
|
|
350
|
+
@property
|
|
351
|
+
def receipt_email(self) -> str | None:
|
|
352
|
+
"""Email for receipt."""
|
|
353
|
+
return self._receipt_email
|
|
354
|
+
|
|
355
|
+
@receipt_email.setter
|
|
356
|
+
def receipt_email(self, value: str | None):
|
|
357
|
+
self._receipt_email = value
|
|
358
|
+
|
|
359
|
+
# Properties - Error Tracking
|
|
360
|
+
@property
|
|
361
|
+
def error_code(self) -> str | None:
|
|
362
|
+
"""PSP error code."""
|
|
363
|
+
return self._error_code
|
|
364
|
+
|
|
365
|
+
@error_code.setter
|
|
366
|
+
def error_code(self, value: str | None):
|
|
367
|
+
self._error_code = value
|
|
368
|
+
|
|
369
|
+
@property
|
|
370
|
+
def error_message(self) -> str | None:
|
|
371
|
+
"""Human-readable error message."""
|
|
372
|
+
return self._error_message
|
|
373
|
+
|
|
374
|
+
@error_message.setter
|
|
375
|
+
def error_message(self, value: str | None):
|
|
376
|
+
self._error_message = value
|
|
377
|
+
|
|
378
|
+
@property
|
|
379
|
+
def error_type(self) -> str | None:
|
|
380
|
+
"""Error type (e.g., 'card_error', 'invalid_request')."""
|
|
381
|
+
return self._error_type
|
|
382
|
+
|
|
383
|
+
@error_type.setter
|
|
384
|
+
def error_type(self, value: str | None):
|
|
385
|
+
self._error_type = value
|
|
386
|
+
|
|
387
|
+
# Properties - Webhooks
|
|
388
|
+
@property
|
|
389
|
+
def last_webhook_utc_ts(self) -> float | None:
|
|
390
|
+
"""Timestamp of last webhook received."""
|
|
391
|
+
return self._last_webhook_utc_ts
|
|
392
|
+
|
|
393
|
+
@last_webhook_utc_ts.setter
|
|
394
|
+
def last_webhook_utc_ts(self, value: float | None):
|
|
395
|
+
self._last_webhook_utc_ts = value
|
|
396
|
+
|
|
397
|
+
@property
|
|
398
|
+
def webhook_count(self) -> int:
|
|
399
|
+
"""Number of webhooks received."""
|
|
400
|
+
return self._webhook_count
|
|
401
|
+
|
|
402
|
+
@webhook_count.setter
|
|
403
|
+
def webhook_count(self, value: int):
|
|
404
|
+
self._webhook_count = value if value is not None else 0
|
|
405
|
+
|
|
406
|
+
# Properties - Related Records
|
|
407
|
+
@property
|
|
408
|
+
def payment_id(self) -> str | None:
|
|
409
|
+
"""ID of settled Payment record (if succeeded)."""
|
|
410
|
+
return self._payment_id
|
|
411
|
+
|
|
412
|
+
@payment_id.setter
|
|
413
|
+
def payment_id(self, value: str | None):
|
|
414
|
+
self._payment_id = value
|
|
415
|
+
|
|
416
|
+
@property
|
|
417
|
+
def invoice_id(self) -> str | None:
|
|
418
|
+
"""Related invoice ID."""
|
|
419
|
+
return self._invoice_id
|
|
420
|
+
|
|
421
|
+
@invoice_id.setter
|
|
422
|
+
def invoice_id(self, value: str | None):
|
|
423
|
+
self._invoice_id = value
|
|
424
|
+
|
|
425
|
+
@property
|
|
426
|
+
def subscription_id(self) -> str | None:
|
|
427
|
+
"""Related subscription ID."""
|
|
428
|
+
return self._subscription_id
|
|
429
|
+
|
|
430
|
+
@subscription_id.setter
|
|
431
|
+
def subscription_id(self, value: str | None):
|
|
432
|
+
self._subscription_id = value
|
|
433
|
+
|
|
434
|
+
# Properties - Cancellation
|
|
435
|
+
@property
|
|
436
|
+
def canceled_utc_ts(self) -> float | None:
|
|
437
|
+
"""Timestamp when canceled."""
|
|
438
|
+
return self._canceled_utc_ts
|
|
439
|
+
|
|
440
|
+
@canceled_utc_ts.setter
|
|
441
|
+
def canceled_utc_ts(self, value: float | None):
|
|
442
|
+
self._canceled_utc_ts = value
|
|
443
|
+
|
|
444
|
+
@property
|
|
445
|
+
def cancellation_reason(self) -> str | None:
|
|
446
|
+
"""Reason for cancellation."""
|
|
447
|
+
return self._cancellation_reason
|
|
448
|
+
|
|
449
|
+
@cancellation_reason.setter
|
|
450
|
+
def cancellation_reason(self, value: str | None):
|
|
451
|
+
self._cancellation_reason = value
|
|
452
|
+
|
|
453
|
+
# Properties - PSP Data
|
|
454
|
+
@property
|
|
455
|
+
def psp_metadata(self) -> Dict[str, Any] | None:
|
|
456
|
+
"""Raw PSP metadata."""
|
|
457
|
+
return self._psp_metadata
|
|
458
|
+
|
|
459
|
+
@psp_metadata.setter
|
|
460
|
+
def psp_metadata(self, value: Dict[str, Any] | None):
|
|
461
|
+
self._psp_metadata = value if isinstance(value, dict) else None
|
|
462
|
+
|
|
463
|
+
# Helper Methods
|
|
464
|
+
def is_succeeded(self) -> bool:
|
|
465
|
+
"""Check if payment succeeded."""
|
|
466
|
+
return self._status == self.STATUS_SUCCEEDED
|
|
467
|
+
|
|
468
|
+
def is_failed(self) -> bool:
|
|
469
|
+
"""Check if payment failed."""
|
|
470
|
+
return self._status == self.STATUS_FAILED
|
|
471
|
+
|
|
472
|
+
def is_canceled(self) -> bool:
|
|
473
|
+
"""Check if payment was canceled."""
|
|
474
|
+
return self._status == self.STATUS_CANCELED
|
|
475
|
+
|
|
476
|
+
def is_processing(self) -> bool:
|
|
477
|
+
"""Check if payment is processing."""
|
|
478
|
+
return self._status == self.STATUS_PROCESSING
|
|
479
|
+
|
|
480
|
+
def requires_action(self) -> bool:
|
|
481
|
+
"""Check if payment requires user action."""
|
|
482
|
+
return self._status == self.STATUS_REQUIRES_ACTION
|
|
483
|
+
|
|
484
|
+
def is_pending(self) -> bool:
|
|
485
|
+
"""Check if payment is in any pending state."""
|
|
486
|
+
pending_statuses = [
|
|
487
|
+
self.STATUS_CREATED,
|
|
488
|
+
self.STATUS_PROCESSING,
|
|
489
|
+
self.STATUS_REQUIRES_ACTION,
|
|
490
|
+
self.STATUS_REQUIRES_CONFIRMATION,
|
|
491
|
+
self.STATUS_REQUIRES_PAYMENT_METHOD,
|
|
492
|
+
]
|
|
493
|
+
return self._status in pending_statuses
|
|
494
|
+
|
|
495
|
+
def is_terminal(self) -> bool:
|
|
496
|
+
"""Check if payment is in a terminal state."""
|
|
497
|
+
terminal_statuses = [
|
|
498
|
+
self.STATUS_SUCCEEDED,
|
|
499
|
+
self.STATUS_CANCELED,
|
|
500
|
+
self.STATUS_FAILED,
|
|
501
|
+
]
|
|
502
|
+
return self._status in terminal_statuses
|
|
503
|
+
|
|
504
|
+
def get_amount_dollars(self) -> float:
|
|
505
|
+
"""Get amount in dollars."""
|
|
506
|
+
return self._amount_cents / 100.0
|
|
507
|
+
|
|
508
|
+
def has_error(self) -> bool:
|
|
509
|
+
"""Check if payment has an error."""
|
|
510
|
+
return self._error_code is not None or self._error_message is not None
|
|
511
|
+
|
|
512
|
+
def increment_webhook_count(self):
|
|
513
|
+
"""Increment webhook counter."""
|
|
514
|
+
self._webhook_count += 1
|
|
515
|
+
self._last_webhook_utc_ts = dt.datetime.now(dt.UTC).timestamp()
|
|
516
|
+
|
|
517
|
+
def set_error(self, error_code: str, error_message: str, error_type: str | None = None):
|
|
518
|
+
"""Set error information."""
|
|
519
|
+
self._error_code = error_code
|
|
520
|
+
self._error_message = error_message
|
|
521
|
+
self._error_type = error_type
|
|
522
|
+
|
|
523
|
+
def clear_error(self):
|
|
524
|
+
"""Clear error information."""
|
|
525
|
+
self._error_code = None
|
|
526
|
+
self._error_message = None
|
|
527
|
+
self._error_type = None
|
|
528
|
+
|
|
529
|
+
def mark_as_canceled(self, reason: str | None = None):
|
|
530
|
+
"""Mark the intent as canceled."""
|
|
531
|
+
self.status = self.STATUS_CANCELED
|
|
532
|
+
self._canceled_utc_ts = dt.datetime.now(dt.UTC).timestamp()
|
|
533
|
+
self._cancellation_reason = reason
|
|
534
|
+
|
|
535
|
+
def get_status_duration_seconds(self) -> float | None:
|
|
536
|
+
"""Get duration in current status (in seconds)."""
|
|
537
|
+
if not self._last_status_change_utc_ts:
|
|
538
|
+
return None
|
|
539
|
+
return dt.datetime.now(dt.UTC).timestamp() - self._last_status_change_utc_ts
|