geek-cafe-saas-sdk 0.6.0__py3-none-any.whl → 0.7.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of geek-cafe-saas-sdk might be problematic. Click here for more details.

Files changed (94) hide show
  1. geek_cafe_saas_sdk/__init__.py +2 -2
  2. geek_cafe_saas_sdk/domains/files/handlers/README.md +446 -0
  3. geek_cafe_saas_sdk/domains/files/handlers/__init__.py +6 -0
  4. geek_cafe_saas_sdk/domains/files/handlers/files/create/app.py +121 -0
  5. geek_cafe_saas_sdk/domains/files/handlers/files/download/app.py +80 -0
  6. geek_cafe_saas_sdk/domains/files/handlers/files/get/app.py +62 -0
  7. geek_cafe_saas_sdk/domains/files/handlers/files/list/app.py +72 -0
  8. geek_cafe_saas_sdk/domains/files/handlers/lineage/create_derived/app.py +99 -0
  9. geek_cafe_saas_sdk/domains/files/handlers/lineage/create_main/app.py +104 -0
  10. geek_cafe_saas_sdk/domains/files/handlers/lineage/download_bundle/app.py +99 -0
  11. geek_cafe_saas_sdk/domains/files/handlers/lineage/get_lineage/app.py +68 -0
  12. geek_cafe_saas_sdk/domains/files/handlers/lineage/prepare_bundle/app.py +76 -0
  13. geek_cafe_saas_sdk/domains/files/models/__init__.py +17 -0
  14. geek_cafe_saas_sdk/domains/files/models/directory.py +42 -6
  15. geek_cafe_saas_sdk/domains/files/models/file.py +158 -16
  16. geek_cafe_saas_sdk/domains/files/models/file_share.py +33 -0
  17. geek_cafe_saas_sdk/domains/files/models/file_version.py +24 -0
  18. geek_cafe_saas_sdk/domains/files/services/__init__.py +21 -0
  19. geek_cafe_saas_sdk/domains/files/services/directory_service.py +54 -135
  20. geek_cafe_saas_sdk/domains/files/services/file_lineage_service.py +487 -0
  21. geek_cafe_saas_sdk/domains/files/services/file_share_service.py +37 -120
  22. geek_cafe_saas_sdk/domains/files/services/file_system_service.py +67 -103
  23. geek_cafe_saas_sdk/domains/files/services/file_version_service.py +44 -124
  24. geek_cafe_saas_sdk/domains/messaging/services/contact_thread_service.py +55 -7
  25. geek_cafe_saas_sdk/domains/notifications/__init__.py +18 -0
  26. geek_cafe_saas_sdk/domains/notifications/handlers/__init__.py +1 -0
  27. geek_cafe_saas_sdk/domains/notifications/handlers/create_webhook/app.py +73 -0
  28. geek_cafe_saas_sdk/domains/notifications/handlers/get/app.py +40 -0
  29. geek_cafe_saas_sdk/domains/notifications/handlers/get_preferences/app.py +34 -0
  30. geek_cafe_saas_sdk/domains/notifications/handlers/list/app.py +43 -0
  31. geek_cafe_saas_sdk/domains/notifications/handlers/list_webhooks/app.py +40 -0
  32. geek_cafe_saas_sdk/domains/notifications/handlers/mark_read/app.py +40 -0
  33. geek_cafe_saas_sdk/domains/notifications/handlers/send/app.py +83 -0
  34. geek_cafe_saas_sdk/domains/notifications/handlers/update_preferences/app.py +45 -0
  35. geek_cafe_saas_sdk/domains/notifications/models/__init__.py +16 -0
  36. geek_cafe_saas_sdk/domains/notifications/models/notification.py +717 -0
  37. geek_cafe_saas_sdk/domains/notifications/models/notification_preference.py +365 -0
  38. geek_cafe_saas_sdk/domains/notifications/models/webhook_subscription.py +339 -0
  39. geek_cafe_saas_sdk/domains/notifications/services/__init__.py +10 -0
  40. geek_cafe_saas_sdk/domains/notifications/services/notification_service.py +576 -0
  41. geek_cafe_saas_sdk/domains/payments/__init__.py +16 -0
  42. geek_cafe_saas_sdk/domains/payments/handlers/README.md +334 -0
  43. geek_cafe_saas_sdk/domains/payments/handlers/__init__.py +6 -0
  44. geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/create/app.py +105 -0
  45. geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/get/app.py +60 -0
  46. geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/update/app.py +97 -0
  47. geek_cafe_saas_sdk/domains/payments/handlers/payment_intents/create/app.py +97 -0
  48. geek_cafe_saas_sdk/domains/payments/handlers/payment_intents/get/app.py +60 -0
  49. geek_cafe_saas_sdk/domains/payments/handlers/payments/get/app.py +60 -0
  50. geek_cafe_saas_sdk/domains/payments/handlers/payments/list/app.py +68 -0
  51. geek_cafe_saas_sdk/domains/payments/handlers/payments/record/app.py +118 -0
  52. geek_cafe_saas_sdk/domains/payments/handlers/refunds/create/app.py +89 -0
  53. geek_cafe_saas_sdk/domains/payments/handlers/refunds/get/app.py +60 -0
  54. geek_cafe_saas_sdk/domains/payments/models/__init__.py +17 -0
  55. geek_cafe_saas_sdk/domains/payments/models/billing_account.py +521 -0
  56. geek_cafe_saas_sdk/domains/payments/models/payment.py +639 -0
  57. geek_cafe_saas_sdk/domains/payments/models/payment_intent_ref.py +539 -0
  58. geek_cafe_saas_sdk/domains/payments/models/refund.py +404 -0
  59. geek_cafe_saas_sdk/domains/payments/services/__init__.py +11 -0
  60. geek_cafe_saas_sdk/domains/payments/services/payment_service.py +405 -0
  61. geek_cafe_saas_sdk/domains/subscriptions/__init__.py +19 -0
  62. geek_cafe_saas_sdk/domains/subscriptions/handlers/README.md +408 -0
  63. geek_cafe_saas_sdk/domains/subscriptions/handlers/__init__.py +1 -0
  64. geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/create/app.py +81 -0
  65. geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/get/app.py +48 -0
  66. geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/list/app.py +54 -0
  67. geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/update/app.py +54 -0
  68. geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/create/app.py +83 -0
  69. geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/get/app.py +47 -0
  70. geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/validate/app.py +62 -0
  71. geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/create/app.py +82 -0
  72. geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/get/app.py +48 -0
  73. geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/list/app.py +66 -0
  74. geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/update/app.py +54 -0
  75. geek_cafe_saas_sdk/domains/subscriptions/handlers/usage/aggregate/app.py +72 -0
  76. geek_cafe_saas_sdk/domains/subscriptions/handlers/usage/record/app.py +89 -0
  77. geek_cafe_saas_sdk/domains/subscriptions/models/__init__.py +13 -0
  78. geek_cafe_saas_sdk/domains/subscriptions/models/addon.py +604 -0
  79. geek_cafe_saas_sdk/domains/subscriptions/models/discount.py +492 -0
  80. geek_cafe_saas_sdk/domains/subscriptions/models/plan.py +569 -0
  81. geek_cafe_saas_sdk/domains/subscriptions/models/usage_record.py +300 -0
  82. geek_cafe_saas_sdk/domains/subscriptions/services/__init__.py +10 -0
  83. geek_cafe_saas_sdk/domains/subscriptions/services/subscription_manager_service.py +694 -0
  84. geek_cafe_saas_sdk/domains/tenancy/models/subscription.py +123 -1
  85. geek_cafe_saas_sdk/domains/tenancy/services/subscription_service.py +213 -0
  86. geek_cafe_saas_sdk/lambda_handlers/_base/base_handler.py +7 -0
  87. geek_cafe_saas_sdk/services/database_service.py +10 -6
  88. geek_cafe_saas_sdk/utilities/cognito_utility.py +16 -26
  89. geek_cafe_saas_sdk/utilities/environment_variables.py +16 -0
  90. geek_cafe_saas_sdk/utilities/logging_utility.py +77 -0
  91. {geek_cafe_saas_sdk-0.6.0.dist-info → geek_cafe_saas_sdk-0.7.1.dist-info}/METADATA +11 -11
  92. {geek_cafe_saas_sdk-0.6.0.dist-info → geek_cafe_saas_sdk-0.7.1.dist-info}/RECORD +94 -23
  93. {geek_cafe_saas_sdk-0.6.0.dist-info → geek_cafe_saas_sdk-0.7.1.dist-info}/WHEEL +0 -0
  94. {geek_cafe_saas_sdk-0.6.0.dist-info → geek_cafe_saas_sdk-0.7.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,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