geek-cafe-saas-sdk 0.7.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 (79) hide show
  1. geek_cafe_saas_sdk/__init__.py +1 -1
  2. geek_cafe_saas_sdk/domains/files/models/directory.py +42 -6
  3. geek_cafe_saas_sdk/domains/files/models/file.py +40 -4
  4. geek_cafe_saas_sdk/domains/files/models/file_share.py +33 -0
  5. geek_cafe_saas_sdk/domains/files/models/file_version.py +24 -0
  6. geek_cafe_saas_sdk/domains/files/services/directory_service.py +54 -135
  7. geek_cafe_saas_sdk/domains/files/services/file_share_service.py +37 -120
  8. geek_cafe_saas_sdk/domains/files/services/file_system_service.py +40 -102
  9. geek_cafe_saas_sdk/domains/files/services/file_version_service.py +44 -124
  10. geek_cafe_saas_sdk/domains/messaging/services/contact_thread_service.py +55 -7
  11. geek_cafe_saas_sdk/domains/notifications/__init__.py +18 -0
  12. geek_cafe_saas_sdk/domains/notifications/handlers/__init__.py +1 -0
  13. geek_cafe_saas_sdk/domains/notifications/handlers/create_webhook/app.py +73 -0
  14. geek_cafe_saas_sdk/domains/notifications/handlers/get/app.py +40 -0
  15. geek_cafe_saas_sdk/domains/notifications/handlers/get_preferences/app.py +34 -0
  16. geek_cafe_saas_sdk/domains/notifications/handlers/list/app.py +43 -0
  17. geek_cafe_saas_sdk/domains/notifications/handlers/list_webhooks/app.py +40 -0
  18. geek_cafe_saas_sdk/domains/notifications/handlers/mark_read/app.py +40 -0
  19. geek_cafe_saas_sdk/domains/notifications/handlers/send/app.py +83 -0
  20. geek_cafe_saas_sdk/domains/notifications/handlers/update_preferences/app.py +45 -0
  21. geek_cafe_saas_sdk/domains/notifications/models/__init__.py +16 -0
  22. geek_cafe_saas_sdk/domains/notifications/models/notification.py +717 -0
  23. geek_cafe_saas_sdk/domains/notifications/models/notification_preference.py +365 -0
  24. geek_cafe_saas_sdk/domains/notifications/models/webhook_subscription.py +339 -0
  25. geek_cafe_saas_sdk/domains/notifications/services/__init__.py +10 -0
  26. geek_cafe_saas_sdk/domains/notifications/services/notification_service.py +576 -0
  27. geek_cafe_saas_sdk/domains/payments/__init__.py +16 -0
  28. geek_cafe_saas_sdk/domains/payments/handlers/README.md +334 -0
  29. geek_cafe_saas_sdk/domains/payments/handlers/__init__.py +6 -0
  30. geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/create/app.py +105 -0
  31. geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/get/app.py +60 -0
  32. geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/update/app.py +97 -0
  33. geek_cafe_saas_sdk/domains/payments/handlers/payment_intents/create/app.py +97 -0
  34. geek_cafe_saas_sdk/domains/payments/handlers/payment_intents/get/app.py +60 -0
  35. geek_cafe_saas_sdk/domains/payments/handlers/payments/get/app.py +60 -0
  36. geek_cafe_saas_sdk/domains/payments/handlers/payments/list/app.py +68 -0
  37. geek_cafe_saas_sdk/domains/payments/handlers/payments/record/app.py +118 -0
  38. geek_cafe_saas_sdk/domains/payments/handlers/refunds/create/app.py +89 -0
  39. geek_cafe_saas_sdk/domains/payments/handlers/refunds/get/app.py +60 -0
  40. geek_cafe_saas_sdk/domains/payments/models/__init__.py +17 -0
  41. geek_cafe_saas_sdk/domains/payments/models/billing_account.py +521 -0
  42. geek_cafe_saas_sdk/domains/payments/models/payment.py +639 -0
  43. geek_cafe_saas_sdk/domains/payments/models/payment_intent_ref.py +539 -0
  44. geek_cafe_saas_sdk/domains/payments/models/refund.py +404 -0
  45. geek_cafe_saas_sdk/domains/payments/services/__init__.py +11 -0
  46. geek_cafe_saas_sdk/domains/payments/services/payment_service.py +405 -0
  47. geek_cafe_saas_sdk/domains/subscriptions/__init__.py +19 -0
  48. geek_cafe_saas_sdk/domains/subscriptions/handlers/README.md +408 -0
  49. geek_cafe_saas_sdk/domains/subscriptions/handlers/__init__.py +1 -0
  50. geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/create/app.py +81 -0
  51. geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/get/app.py +48 -0
  52. geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/list/app.py +54 -0
  53. geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/update/app.py +54 -0
  54. geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/create/app.py +83 -0
  55. geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/get/app.py +47 -0
  56. geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/validate/app.py +62 -0
  57. geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/create/app.py +82 -0
  58. geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/get/app.py +48 -0
  59. geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/list/app.py +66 -0
  60. geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/update/app.py +54 -0
  61. geek_cafe_saas_sdk/domains/subscriptions/handlers/usage/aggregate/app.py +72 -0
  62. geek_cafe_saas_sdk/domains/subscriptions/handlers/usage/record/app.py +89 -0
  63. geek_cafe_saas_sdk/domains/subscriptions/models/__init__.py +13 -0
  64. geek_cafe_saas_sdk/domains/subscriptions/models/addon.py +604 -0
  65. geek_cafe_saas_sdk/domains/subscriptions/models/discount.py +492 -0
  66. geek_cafe_saas_sdk/domains/subscriptions/models/plan.py +569 -0
  67. geek_cafe_saas_sdk/domains/subscriptions/models/usage_record.py +300 -0
  68. geek_cafe_saas_sdk/domains/subscriptions/services/__init__.py +10 -0
  69. geek_cafe_saas_sdk/domains/subscriptions/services/subscription_manager_service.py +694 -0
  70. geek_cafe_saas_sdk/domains/tenancy/models/subscription.py +123 -1
  71. geek_cafe_saas_sdk/domains/tenancy/services/subscription_service.py +213 -0
  72. geek_cafe_saas_sdk/lambda_handlers/_base/base_handler.py +7 -0
  73. geek_cafe_saas_sdk/services/database_service.py +10 -6
  74. geek_cafe_saas_sdk/utilities/environment_variables.py +16 -0
  75. geek_cafe_saas_sdk/utilities/logging_utility.py +77 -0
  76. {geek_cafe_saas_sdk-0.7.0.dist-info → geek_cafe_saas_sdk-0.7.1.dist-info}/METADATA +1 -1
  77. {geek_cafe_saas_sdk-0.7.0.dist-info → geek_cafe_saas_sdk-0.7.1.dist-info}/RECORD +79 -20
  78. {geek_cafe_saas_sdk-0.7.0.dist-info → geek_cafe_saas_sdk-0.7.1.dist-info}/WHEEL +0 -0
  79. {geek_cafe_saas_sdk-0.7.0.dist-info → geek_cafe_saas_sdk-0.7.1.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)
@@ -0,0 +1,11 @@
1
+ """Payment services.
2
+
3
+ Geek Cafe, LLC
4
+ MIT License. See Project Root for the license information.
5
+ """
6
+
7
+ from .payment_service import PaymentService
8
+
9
+ __all__ = [
10
+ "PaymentService",
11
+ ]