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,639 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Payment 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, List
|
|
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 Payment(BaseModel):
|
|
15
|
+
"""
|
|
16
|
+
Payment - Settled payment record with gross, fees, and net amounts.
|
|
17
|
+
|
|
18
|
+
Represents a completed payment transaction with full financial details.
|
|
19
|
+
This is an immutable record created after a payment intent succeeds.
|
|
20
|
+
Contains gross amount, processing fees, and net amount deposited.
|
|
21
|
+
|
|
22
|
+
Multi-Tenancy:
|
|
23
|
+
- tenant_id: Organization/company receiving the payment
|
|
24
|
+
|
|
25
|
+
Access Patterns (DynamoDB Keys):
|
|
26
|
+
- pk: PAYMENT#{tenant_id}#{payment_id}
|
|
27
|
+
- sk: metadata
|
|
28
|
+
- gsi1_pk: tenant#{tenant_id}
|
|
29
|
+
- gsi1_sk: PAYMENT#{settled_utc_ts}
|
|
30
|
+
- gsi2_pk: BILLING_ACCOUNT#{billing_account_id}
|
|
31
|
+
- gsi2_sk: PAYMENT#{settled_utc_ts}
|
|
32
|
+
- gsi3_pk: PSP_TRANSACTION#{psp_type}#{psp_transaction_id}
|
|
33
|
+
- gsi3_sk: metadata
|
|
34
|
+
|
|
35
|
+
Immutability:
|
|
36
|
+
After settlement, core financial fields should not be modified.
|
|
37
|
+
Only status and metadata fields can be updated (e.g., for disputes).
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self):
|
|
41
|
+
super().__init__()
|
|
42
|
+
|
|
43
|
+
# Identity (inherited from BaseModel: id, tenant_id)
|
|
44
|
+
self._billing_account_id: str | None = None # Associated billing account
|
|
45
|
+
self._payment_intent_ref_id: str | None = None # Related payment intent
|
|
46
|
+
|
|
47
|
+
# PSP Information
|
|
48
|
+
self._psp_type: str = "stripe" # "stripe", "paypal", "square", etc.
|
|
49
|
+
self._psp_transaction_id: str | None = None # PSP's transaction ID
|
|
50
|
+
self._psp_charge_id: str | None = None # PSP's charge ID (Stripe specific)
|
|
51
|
+
self._psp_balance_transaction_id: str | None = None # PSP balance transaction
|
|
52
|
+
|
|
53
|
+
# Financial Details (in cents to avoid float issues)
|
|
54
|
+
# Gross = Total amount charged
|
|
55
|
+
# Fees = Processing fees charged by PSP
|
|
56
|
+
# Net = Amount deposited (gross - fees)
|
|
57
|
+
self._gross_amount_cents: int = 0 # Total amount charged
|
|
58
|
+
self._fee_amount_cents: int = 0 # Processing fees
|
|
59
|
+
self._net_amount_cents: int = 0 # Net amount (gross - fees)
|
|
60
|
+
self._currency_code: str = "USD" # ISO 4217 currency code
|
|
61
|
+
|
|
62
|
+
# Fee Breakdown (optional detailed fee structure)
|
|
63
|
+
self._fee_details: Dict[str, Any] | None = None # Detailed fee breakdown
|
|
64
|
+
|
|
65
|
+
# Payment Method Details
|
|
66
|
+
self._payment_method_id: str | None = None # PSP payment method ID
|
|
67
|
+
self._payment_method_type: str | None = None # "card", "ach_debit", etc.
|
|
68
|
+
self._payment_method_last4: str | None = None # Last 4 digits
|
|
69
|
+
self._payment_method_brand: str | None = None # "visa", "mastercard", etc.
|
|
70
|
+
self._payment_method_funding: str | None = None # "credit", "debit", "prepaid"
|
|
71
|
+
|
|
72
|
+
# Settlement Details
|
|
73
|
+
self._settled_utc_ts: float | None = None # When payment settled
|
|
74
|
+
self._settlement_date: str | None = None # Expected settlement date (YYYY-MM-DD)
|
|
75
|
+
self._payout_id: str | None = None # Related payout batch ID
|
|
76
|
+
|
|
77
|
+
# Status
|
|
78
|
+
self._status: str = "succeeded" # "succeeded", "refunded", "partially_refunded", "disputed"
|
|
79
|
+
self._is_refunded: bool = False # Full refund flag
|
|
80
|
+
self._is_partially_refunded: bool = False # Partial refund flag
|
|
81
|
+
|
|
82
|
+
# Refund Tracking (in cents)
|
|
83
|
+
self._refunded_amount_cents: int = 0 # Total refunded amount
|
|
84
|
+
self._refund_count: int = 0 # Number of refunds
|
|
85
|
+
self._refund_ids: List[str] = [] # List of refund IDs
|
|
86
|
+
|
|
87
|
+
# Dispute Tracking
|
|
88
|
+
self._is_disputed: bool = False
|
|
89
|
+
self._dispute_id: str | None = None # PSP dispute ID
|
|
90
|
+
self._dispute_status: str | None = None # "warning_needs_response", "won", "lost"
|
|
91
|
+
self._dispute_reason: str | None = None # "fraudulent", "duplicate", etc.
|
|
92
|
+
|
|
93
|
+
# Related Records
|
|
94
|
+
self._invoice_id: str | None = None # Related invoice
|
|
95
|
+
self._subscription_id: str | None = None # Related subscription
|
|
96
|
+
self._customer_id: str | None = None # Customer/user ID
|
|
97
|
+
|
|
98
|
+
# Metadata
|
|
99
|
+
self._description: str | None = None # Payment description
|
|
100
|
+
self._statement_descriptor: str | None = None # Appears on card statement
|
|
101
|
+
self._receipt_number: str | None = None # Receipt number
|
|
102
|
+
self._receipt_email: str | None = None # Email receipt sent to
|
|
103
|
+
self._receipt_url: str | None = None # URL to receipt
|
|
104
|
+
|
|
105
|
+
# Reconciliation
|
|
106
|
+
self._reconciled: bool = False
|
|
107
|
+
self._reconciled_utc_ts: float | None = None
|
|
108
|
+
self._reconciliation_notes: str | None = None
|
|
109
|
+
|
|
110
|
+
# Additional PSP Data
|
|
111
|
+
self._psp_metadata: Dict[str, Any] | None = None # Raw PSP data
|
|
112
|
+
self._application_fee_amount_cents: int | None = None # Platform fee (if any)
|
|
113
|
+
|
|
114
|
+
# CRITICAL: Call _setup_indexes() as LAST line in __init__
|
|
115
|
+
self._setup_indexes()
|
|
116
|
+
|
|
117
|
+
def _setup_indexes(self):
|
|
118
|
+
"""Setup DynamoDB indexes for payment queries."""
|
|
119
|
+
|
|
120
|
+
# Primary index: Payment by ID
|
|
121
|
+
primary = DynamoDBIndex()
|
|
122
|
+
primary.name = "primary"
|
|
123
|
+
primary.partition_key.attribute_name = "pk"
|
|
124
|
+
primary.partition_key.value = lambda: DynamoDBKey.build_key(("payment", self.id))
|
|
125
|
+
primary.sort_key.attribute_name = "sk"
|
|
126
|
+
primary.sort_key.value = lambda: "metadata"
|
|
127
|
+
self.indexes.add_primary(primary)
|
|
128
|
+
|
|
129
|
+
# GSI1: Payments by tenant (for listing/reporting)
|
|
130
|
+
gsi = DynamoDBIndex()
|
|
131
|
+
gsi.name = "gsi1"
|
|
132
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
133
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(("tenant", self.tenant_id))
|
|
134
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
135
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(("payment", self.settled_utc_ts))
|
|
136
|
+
self.indexes.add_secondary(gsi)
|
|
137
|
+
|
|
138
|
+
# GSI2: Payments by billing account
|
|
139
|
+
gsi = DynamoDBIndex()
|
|
140
|
+
gsi.name = "gsi2"
|
|
141
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
142
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(("billing_account", self.billing_account_id))
|
|
143
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
144
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(("payment", self.settled_utc_ts))
|
|
145
|
+
self.indexes.add_secondary(gsi)
|
|
146
|
+
|
|
147
|
+
# GSI3: Payment by PSP transaction ID (for webhook lookups)
|
|
148
|
+
if self.psp_transaction_id:
|
|
149
|
+
gsi = DynamoDBIndex()
|
|
150
|
+
gsi.name = "gsi3"
|
|
151
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
152
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(("psp_transaction", self.psp_type), ("tx", self.psp_transaction_id))
|
|
153
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
154
|
+
gsi.sort_key.value = lambda: "metadata"
|
|
155
|
+
self.indexes.add_secondary(gsi)
|
|
156
|
+
|
|
157
|
+
# Properties - Identity
|
|
158
|
+
@property
|
|
159
|
+
def payment_id(self) -> str | None:
|
|
160
|
+
"""Unique payment ID (alias for id)."""
|
|
161
|
+
return self.id
|
|
162
|
+
|
|
163
|
+
@payment_id.setter
|
|
164
|
+
def payment_id(self, value: str | None):
|
|
165
|
+
self.id = value
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def billing_account_id(self) -> str | None:
|
|
169
|
+
"""Associated billing account ID."""
|
|
170
|
+
return self._billing_account_id
|
|
171
|
+
|
|
172
|
+
@billing_account_id.setter
|
|
173
|
+
def billing_account_id(self, value: str | None):
|
|
174
|
+
self._billing_account_id = value
|
|
175
|
+
|
|
176
|
+
@property
|
|
177
|
+
def payment_intent_ref_id(self) -> str | None:
|
|
178
|
+
"""Related payment intent reference ID."""
|
|
179
|
+
return self._payment_intent_ref_id
|
|
180
|
+
|
|
181
|
+
@payment_intent_ref_id.setter
|
|
182
|
+
def payment_intent_ref_id(self, value: str | None):
|
|
183
|
+
self._payment_intent_ref_id = value
|
|
184
|
+
|
|
185
|
+
# Properties - PSP Information
|
|
186
|
+
@property
|
|
187
|
+
def psp_type(self) -> str:
|
|
188
|
+
"""Payment service provider type."""
|
|
189
|
+
return self._psp_type
|
|
190
|
+
|
|
191
|
+
@psp_type.setter
|
|
192
|
+
def psp_type(self, value: str):
|
|
193
|
+
valid_types = ["stripe", "paypal", "square", "braintree"]
|
|
194
|
+
if value not in valid_types:
|
|
195
|
+
raise ValueError(f"Invalid psp_type: {value}. Must be one of {valid_types}")
|
|
196
|
+
self._psp_type = value
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def psp_transaction_id(self) -> str | None:
|
|
200
|
+
"""PSP transaction ID."""
|
|
201
|
+
return self._psp_transaction_id
|
|
202
|
+
|
|
203
|
+
@psp_transaction_id.setter
|
|
204
|
+
def psp_transaction_id(self, value: str | None):
|
|
205
|
+
self._psp_transaction_id = value
|
|
206
|
+
|
|
207
|
+
@property
|
|
208
|
+
def psp_charge_id(self) -> str | None:
|
|
209
|
+
"""PSP charge ID (Stripe specific)."""
|
|
210
|
+
return self._psp_charge_id
|
|
211
|
+
|
|
212
|
+
@psp_charge_id.setter
|
|
213
|
+
def psp_charge_id(self, value: str | None):
|
|
214
|
+
self._psp_charge_id = value
|
|
215
|
+
|
|
216
|
+
@property
|
|
217
|
+
def psp_balance_transaction_id(self) -> str | None:
|
|
218
|
+
"""PSP balance transaction ID."""
|
|
219
|
+
return self._psp_balance_transaction_id
|
|
220
|
+
|
|
221
|
+
@psp_balance_transaction_id.setter
|
|
222
|
+
def psp_balance_transaction_id(self, value: str | None):
|
|
223
|
+
self._psp_balance_transaction_id = value
|
|
224
|
+
|
|
225
|
+
# Properties - Financial Details
|
|
226
|
+
@property
|
|
227
|
+
def gross_amount_cents(self) -> int:
|
|
228
|
+
"""Gross amount charged in cents."""
|
|
229
|
+
return self._gross_amount_cents
|
|
230
|
+
|
|
231
|
+
@gross_amount_cents.setter
|
|
232
|
+
def gross_amount_cents(self, value: int):
|
|
233
|
+
if value < 0:
|
|
234
|
+
raise ValueError("gross_amount_cents must be non-negative")
|
|
235
|
+
self._gross_amount_cents = value
|
|
236
|
+
# Auto-calculate net amount
|
|
237
|
+
self._net_amount_cents = self._gross_amount_cents - self._fee_amount_cents
|
|
238
|
+
|
|
239
|
+
@property
|
|
240
|
+
def fee_amount_cents(self) -> int:
|
|
241
|
+
"""Processing fee in cents."""
|
|
242
|
+
return self._fee_amount_cents
|
|
243
|
+
|
|
244
|
+
@fee_amount_cents.setter
|
|
245
|
+
def fee_amount_cents(self, value: int):
|
|
246
|
+
if value < 0:
|
|
247
|
+
raise ValueError("fee_amount_cents must be non-negative")
|
|
248
|
+
self._fee_amount_cents = value
|
|
249
|
+
# Auto-calculate net amount
|
|
250
|
+
self._net_amount_cents = self._gross_amount_cents - self._fee_amount_cents
|
|
251
|
+
|
|
252
|
+
@property
|
|
253
|
+
def net_amount_cents(self) -> int:
|
|
254
|
+
"""Net amount deposited in cents (gross - fees)."""
|
|
255
|
+
return self._net_amount_cents
|
|
256
|
+
|
|
257
|
+
# Net amount is calculated, but allow setter for deserialization
|
|
258
|
+
@net_amount_cents.setter
|
|
259
|
+
def net_amount_cents(self, value: int):
|
|
260
|
+
self._net_amount_cents = value
|
|
261
|
+
|
|
262
|
+
@property
|
|
263
|
+
def currency_code(self) -> str:
|
|
264
|
+
"""ISO 4217 currency code."""
|
|
265
|
+
return self._currency_code
|
|
266
|
+
|
|
267
|
+
@currency_code.setter
|
|
268
|
+
def currency_code(self, value: str):
|
|
269
|
+
if not value or len(value) != 3:
|
|
270
|
+
raise ValueError("currency_code must be a 3-letter ISO 4217 code")
|
|
271
|
+
self._currency_code = value.upper()
|
|
272
|
+
|
|
273
|
+
@property
|
|
274
|
+
def fee_details(self) -> Dict[str, Any] | None:
|
|
275
|
+
"""Detailed fee breakdown."""
|
|
276
|
+
return self._fee_details
|
|
277
|
+
|
|
278
|
+
@fee_details.setter
|
|
279
|
+
def fee_details(self, value: Dict[str, Any] | None):
|
|
280
|
+
self._fee_details = value if isinstance(value, dict) else None
|
|
281
|
+
|
|
282
|
+
# Properties - Payment Method
|
|
283
|
+
@property
|
|
284
|
+
def payment_method_id(self) -> str | None:
|
|
285
|
+
"""PSP payment method ID."""
|
|
286
|
+
return self._payment_method_id
|
|
287
|
+
|
|
288
|
+
@payment_method_id.setter
|
|
289
|
+
def payment_method_id(self, value: str | None):
|
|
290
|
+
self._payment_method_id = value
|
|
291
|
+
|
|
292
|
+
@property
|
|
293
|
+
def payment_method_type(self) -> str | None:
|
|
294
|
+
"""Payment method type."""
|
|
295
|
+
return self._payment_method_type
|
|
296
|
+
|
|
297
|
+
@payment_method_type.setter
|
|
298
|
+
def payment_method_type(self, value: str | None):
|
|
299
|
+
self._payment_method_type = value
|
|
300
|
+
|
|
301
|
+
@property
|
|
302
|
+
def payment_method_last4(self) -> str | None:
|
|
303
|
+
"""Last 4 digits of payment method."""
|
|
304
|
+
return self._payment_method_last4
|
|
305
|
+
|
|
306
|
+
@payment_method_last4.setter
|
|
307
|
+
def payment_method_last4(self, value: str | None):
|
|
308
|
+
self._payment_method_last4 = value
|
|
309
|
+
|
|
310
|
+
@property
|
|
311
|
+
def payment_method_brand(self) -> str | None:
|
|
312
|
+
"""Payment method brand."""
|
|
313
|
+
return self._payment_method_brand
|
|
314
|
+
|
|
315
|
+
@payment_method_brand.setter
|
|
316
|
+
def payment_method_brand(self, value: str | None):
|
|
317
|
+
self._payment_method_brand = value
|
|
318
|
+
|
|
319
|
+
@property
|
|
320
|
+
def payment_method_funding(self) -> str | None:
|
|
321
|
+
"""Payment method funding type."""
|
|
322
|
+
return self._payment_method_funding
|
|
323
|
+
|
|
324
|
+
@payment_method_funding.setter
|
|
325
|
+
def payment_method_funding(self, value: str | None):
|
|
326
|
+
self._payment_method_funding = value
|
|
327
|
+
|
|
328
|
+
# Properties - Settlement
|
|
329
|
+
@property
|
|
330
|
+
def settled_utc_ts(self) -> float | None:
|
|
331
|
+
"""Timestamp when payment settled."""
|
|
332
|
+
return self._settled_utc_ts
|
|
333
|
+
|
|
334
|
+
@settled_utc_ts.setter
|
|
335
|
+
def settled_utc_ts(self, value: float | None):
|
|
336
|
+
self._settled_utc_ts = value
|
|
337
|
+
|
|
338
|
+
@property
|
|
339
|
+
def settlement_date(self) -> str | None:
|
|
340
|
+
"""Expected settlement date (YYYY-MM-DD)."""
|
|
341
|
+
return self._settlement_date
|
|
342
|
+
|
|
343
|
+
@settlement_date.setter
|
|
344
|
+
def settlement_date(self, value: str | None):
|
|
345
|
+
self._settlement_date = value
|
|
346
|
+
|
|
347
|
+
@property
|
|
348
|
+
def payout_id(self) -> str | None:
|
|
349
|
+
"""Related payout batch ID."""
|
|
350
|
+
return self._payout_id
|
|
351
|
+
|
|
352
|
+
@payout_id.setter
|
|
353
|
+
def payout_id(self, value: str | None):
|
|
354
|
+
self._payout_id = value
|
|
355
|
+
|
|
356
|
+
# Properties - Status
|
|
357
|
+
@property
|
|
358
|
+
def status(self) -> str:
|
|
359
|
+
"""Payment status."""
|
|
360
|
+
return self._status
|
|
361
|
+
|
|
362
|
+
@status.setter
|
|
363
|
+
def status(self, value: str):
|
|
364
|
+
valid_statuses = ["succeeded", "refunded", "partially_refunded", "disputed"]
|
|
365
|
+
if value not in valid_statuses:
|
|
366
|
+
raise ValueError(f"Invalid status: {value}. Must be one of {valid_statuses}")
|
|
367
|
+
self._status = value
|
|
368
|
+
|
|
369
|
+
@property
|
|
370
|
+
def is_refunded(self) -> bool:
|
|
371
|
+
"""Full refund flag."""
|
|
372
|
+
return self._is_refunded
|
|
373
|
+
|
|
374
|
+
@is_refunded.setter
|
|
375
|
+
def is_refunded(self, value: bool):
|
|
376
|
+
self._is_refunded = bool(value)
|
|
377
|
+
|
|
378
|
+
@property
|
|
379
|
+
def is_partially_refunded(self) -> bool:
|
|
380
|
+
"""Partial refund flag."""
|
|
381
|
+
return self._is_partially_refunded
|
|
382
|
+
|
|
383
|
+
@is_partially_refunded.setter
|
|
384
|
+
def is_partially_refunded(self, value: bool):
|
|
385
|
+
self._is_partially_refunded = bool(value)
|
|
386
|
+
|
|
387
|
+
# Properties - Refund Tracking
|
|
388
|
+
@property
|
|
389
|
+
def refunded_amount_cents(self) -> int:
|
|
390
|
+
"""Total refunded amount in cents."""
|
|
391
|
+
return self._refunded_amount_cents
|
|
392
|
+
|
|
393
|
+
@refunded_amount_cents.setter
|
|
394
|
+
def refunded_amount_cents(self, value: int):
|
|
395
|
+
if value < 0:
|
|
396
|
+
raise ValueError("refunded_amount_cents must be non-negative")
|
|
397
|
+
self._refunded_amount_cents = value
|
|
398
|
+
|
|
399
|
+
@property
|
|
400
|
+
def refund_count(self) -> int:
|
|
401
|
+
"""Number of refunds."""
|
|
402
|
+
return self._refund_count
|
|
403
|
+
|
|
404
|
+
@refund_count.setter
|
|
405
|
+
def refund_count(self, value: int):
|
|
406
|
+
self._refund_count = value if value is not None else 0
|
|
407
|
+
|
|
408
|
+
@property
|
|
409
|
+
def refund_ids(self) -> List[str]:
|
|
410
|
+
"""List of refund IDs."""
|
|
411
|
+
return self._refund_ids
|
|
412
|
+
|
|
413
|
+
@refund_ids.setter
|
|
414
|
+
def refund_ids(self, value: List[str] | None):
|
|
415
|
+
self._refund_ids = value if isinstance(value, list) else []
|
|
416
|
+
|
|
417
|
+
# Properties - Dispute
|
|
418
|
+
@property
|
|
419
|
+
def is_disputed(self) -> bool:
|
|
420
|
+
"""Dispute flag."""
|
|
421
|
+
return self._is_disputed
|
|
422
|
+
|
|
423
|
+
@is_disputed.setter
|
|
424
|
+
def is_disputed(self, value: bool):
|
|
425
|
+
self._is_disputed = bool(value)
|
|
426
|
+
|
|
427
|
+
@property
|
|
428
|
+
def dispute_id(self) -> str | None:
|
|
429
|
+
"""PSP dispute ID."""
|
|
430
|
+
return self._dispute_id
|
|
431
|
+
|
|
432
|
+
@dispute_id.setter
|
|
433
|
+
def dispute_id(self, value: str | None):
|
|
434
|
+
self._dispute_id = value
|
|
435
|
+
|
|
436
|
+
@property
|
|
437
|
+
def dispute_status(self) -> str | None:
|
|
438
|
+
"""Dispute status."""
|
|
439
|
+
return self._dispute_status
|
|
440
|
+
|
|
441
|
+
@dispute_status.setter
|
|
442
|
+
def dispute_status(self, value: str | None):
|
|
443
|
+
self._dispute_status = value
|
|
444
|
+
|
|
445
|
+
@property
|
|
446
|
+
def dispute_reason(self) -> str | None:
|
|
447
|
+
"""Dispute reason."""
|
|
448
|
+
return self._dispute_reason
|
|
449
|
+
|
|
450
|
+
@dispute_reason.setter
|
|
451
|
+
def dispute_reason(self, value: str | None):
|
|
452
|
+
self._dispute_reason = value
|
|
453
|
+
|
|
454
|
+
# Properties - Related Records
|
|
455
|
+
@property
|
|
456
|
+
def invoice_id(self) -> str | None:
|
|
457
|
+
"""Related invoice ID."""
|
|
458
|
+
return self._invoice_id
|
|
459
|
+
|
|
460
|
+
@invoice_id.setter
|
|
461
|
+
def invoice_id(self, value: str | None):
|
|
462
|
+
self._invoice_id = value
|
|
463
|
+
|
|
464
|
+
@property
|
|
465
|
+
def subscription_id(self) -> str | None:
|
|
466
|
+
"""Related subscription ID."""
|
|
467
|
+
return self._subscription_id
|
|
468
|
+
|
|
469
|
+
@subscription_id.setter
|
|
470
|
+
def subscription_id(self, value: str | None):
|
|
471
|
+
self._subscription_id = value
|
|
472
|
+
|
|
473
|
+
@property
|
|
474
|
+
def customer_id(self) -> str | None:
|
|
475
|
+
"""Customer/user ID."""
|
|
476
|
+
return self._customer_id
|
|
477
|
+
|
|
478
|
+
@customer_id.setter
|
|
479
|
+
def customer_id(self, value: str | None):
|
|
480
|
+
self._customer_id = value
|
|
481
|
+
|
|
482
|
+
# Properties - Metadata
|
|
483
|
+
@property
|
|
484
|
+
def description(self) -> str | None:
|
|
485
|
+
"""Payment description."""
|
|
486
|
+
return self._description
|
|
487
|
+
|
|
488
|
+
@description.setter
|
|
489
|
+
def description(self, value: str | None):
|
|
490
|
+
self._description = value
|
|
491
|
+
|
|
492
|
+
@property
|
|
493
|
+
def statement_descriptor(self) -> str | None:
|
|
494
|
+
"""Statement descriptor."""
|
|
495
|
+
return self._statement_descriptor
|
|
496
|
+
|
|
497
|
+
@statement_descriptor.setter
|
|
498
|
+
def statement_descriptor(self, value: str | None):
|
|
499
|
+
self._statement_descriptor = value
|
|
500
|
+
|
|
501
|
+
@property
|
|
502
|
+
def receipt_number(self) -> str | None:
|
|
503
|
+
"""Receipt number."""
|
|
504
|
+
return self._receipt_number
|
|
505
|
+
|
|
506
|
+
@receipt_number.setter
|
|
507
|
+
def receipt_number(self, value: str | None):
|
|
508
|
+
self._receipt_number = value
|
|
509
|
+
|
|
510
|
+
@property
|
|
511
|
+
def receipt_email(self) -> str | None:
|
|
512
|
+
"""Email receipt sent to."""
|
|
513
|
+
return self._receipt_email
|
|
514
|
+
|
|
515
|
+
@receipt_email.setter
|
|
516
|
+
def receipt_email(self, value: str | None):
|
|
517
|
+
self._receipt_email = value
|
|
518
|
+
|
|
519
|
+
@property
|
|
520
|
+
def receipt_url(self) -> str | None:
|
|
521
|
+
"""URL to receipt."""
|
|
522
|
+
return self._receipt_url
|
|
523
|
+
|
|
524
|
+
@receipt_url.setter
|
|
525
|
+
def receipt_url(self, value: str | None):
|
|
526
|
+
self._receipt_url = value
|
|
527
|
+
|
|
528
|
+
# Properties - Reconciliation
|
|
529
|
+
@property
|
|
530
|
+
def reconciled(self) -> bool:
|
|
531
|
+
"""Reconciliation flag."""
|
|
532
|
+
return self._reconciled
|
|
533
|
+
|
|
534
|
+
@reconciled.setter
|
|
535
|
+
def reconciled(self, value: bool):
|
|
536
|
+
self._reconciled = bool(value)
|
|
537
|
+
|
|
538
|
+
@property
|
|
539
|
+
def reconciled_utc_ts(self) -> float | None:
|
|
540
|
+
"""Timestamp when reconciled."""
|
|
541
|
+
return self._reconciled_utc_ts
|
|
542
|
+
|
|
543
|
+
@reconciled_utc_ts.setter
|
|
544
|
+
def reconciled_utc_ts(self, value: float | None):
|
|
545
|
+
self._reconciled_utc_ts = value
|
|
546
|
+
|
|
547
|
+
@property
|
|
548
|
+
def reconciliation_notes(self) -> str | None:
|
|
549
|
+
"""Reconciliation notes."""
|
|
550
|
+
return self._reconciliation_notes
|
|
551
|
+
|
|
552
|
+
@reconciliation_notes.setter
|
|
553
|
+
def reconciliation_notes(self, value: str | None):
|
|
554
|
+
self._reconciliation_notes = value
|
|
555
|
+
|
|
556
|
+
# Properties - Additional Data
|
|
557
|
+
@property
|
|
558
|
+
def psp_metadata(self) -> Dict[str, Any] | None:
|
|
559
|
+
"""Raw PSP metadata."""
|
|
560
|
+
return self._psp_metadata
|
|
561
|
+
|
|
562
|
+
@psp_metadata.setter
|
|
563
|
+
def psp_metadata(self, value: Dict[str, Any] | None):
|
|
564
|
+
self._psp_metadata = value if isinstance(value, dict) else None
|
|
565
|
+
|
|
566
|
+
@property
|
|
567
|
+
def application_fee_amount_cents(self) -> int | None:
|
|
568
|
+
"""Platform application fee in cents."""
|
|
569
|
+
return self._application_fee_amount_cents
|
|
570
|
+
|
|
571
|
+
@application_fee_amount_cents.setter
|
|
572
|
+
def application_fee_amount_cents(self, value: int | None):
|
|
573
|
+
self._application_fee_amount_cents = value
|
|
574
|
+
|
|
575
|
+
# Helper Methods
|
|
576
|
+
def get_gross_amount_dollars(self) -> float:
|
|
577
|
+
"""Get gross amount in dollars."""
|
|
578
|
+
return self._gross_amount_cents / 100.0
|
|
579
|
+
|
|
580
|
+
def get_fee_amount_dollars(self) -> float:
|
|
581
|
+
"""Get fee amount in dollars."""
|
|
582
|
+
return self._fee_amount_cents / 100.0
|
|
583
|
+
|
|
584
|
+
def get_net_amount_dollars(self) -> float:
|
|
585
|
+
"""Get net amount in dollars."""
|
|
586
|
+
return self._net_amount_cents / 100.0
|
|
587
|
+
|
|
588
|
+
def get_refunded_amount_dollars(self) -> float:
|
|
589
|
+
"""Get refunded amount in dollars."""
|
|
590
|
+
return self._refunded_amount_cents / 100.0
|
|
591
|
+
|
|
592
|
+
def get_remaining_amount_cents(self) -> int:
|
|
593
|
+
"""Get remaining amount after refunds (in cents)."""
|
|
594
|
+
return self._gross_amount_cents - self._refunded_amount_cents
|
|
595
|
+
|
|
596
|
+
def get_remaining_amount_dollars(self) -> float:
|
|
597
|
+
"""Get remaining amount after refunds (in dollars)."""
|
|
598
|
+
return self.get_remaining_amount_cents() / 100.0
|
|
599
|
+
|
|
600
|
+
def is_fully_refunded(self) -> bool:
|
|
601
|
+
"""Check if payment is fully refunded."""
|
|
602
|
+
return self._is_refunded or self._refunded_amount_cents >= self._gross_amount_cents
|
|
603
|
+
|
|
604
|
+
def has_refunds(self) -> bool:
|
|
605
|
+
"""Check if payment has any refunds."""
|
|
606
|
+
return self._refund_count > 0 or self._refunded_amount_cents > 0
|
|
607
|
+
|
|
608
|
+
def add_refund(self, refund_id: str, refund_amount_cents: int):
|
|
609
|
+
"""Record a refund."""
|
|
610
|
+
self._refund_ids.append(refund_id)
|
|
611
|
+
self._refund_count += 1
|
|
612
|
+
self._refunded_amount_cents += refund_amount_cents
|
|
613
|
+
|
|
614
|
+
# Update status
|
|
615
|
+
if self._refunded_amount_cents >= self._gross_amount_cents:
|
|
616
|
+
self._status = "refunded"
|
|
617
|
+
self._is_refunded = True
|
|
618
|
+
else:
|
|
619
|
+
self._status = "partially_refunded"
|
|
620
|
+
self._is_partially_refunded = True
|
|
621
|
+
|
|
622
|
+
def mark_as_disputed(self, dispute_id: str, dispute_reason: str | None = None):
|
|
623
|
+
"""Mark payment as disputed."""
|
|
624
|
+
self._is_disputed = True
|
|
625
|
+
self._dispute_id = dispute_id
|
|
626
|
+
self._dispute_reason = dispute_reason
|
|
627
|
+
self._status = "disputed"
|
|
628
|
+
|
|
629
|
+
def mark_as_reconciled(self, notes: str | None = None):
|
|
630
|
+
"""Mark payment as reconciled."""
|
|
631
|
+
self._reconciled = True
|
|
632
|
+
self._reconciled_utc_ts = dt.datetime.now(dt.UTC).timestamp()
|
|
633
|
+
self._reconciliation_notes = notes
|
|
634
|
+
|
|
635
|
+
def calculate_fee_percentage(self) -> float:
|
|
636
|
+
"""Calculate fee as percentage of gross amount."""
|
|
637
|
+
if self._gross_amount_cents == 0:
|
|
638
|
+
return 0.0
|
|
639
|
+
return (self._fee_amount_cents / self._gross_amount_cents) * 100.0
|