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,521 @@
1
+ """
2
+ BillingAccount model for payment system.
3
+
4
+ Geek Cafe, LLC
5
+ MIT License. See Project Root for the license information.
6
+ """
7
+
8
+ from typing import Optional, Dict, Any
9
+ from geek_cafe_saas_sdk.models.base_model import BaseModel
10
+ from boto3_assist.dynamodb.dynamodb_index import DynamoDBIndex, DynamoDBKey
11
+
12
+
13
+ class BillingAccount(BaseModel):
14
+ """
15
+ BillingAccount - Payor/payee configuration with PSP integration.
16
+
17
+ Represents billing configuration for a tenant or specific entity,
18
+ including payment service provider (PSP) customer references,
19
+ currency settings, and tax configuration.
20
+
21
+ Multi-Tenancy:
22
+ - tenant_id: Organization/company that owns this billing account
23
+ - account_holder_id: Specific entity (user/org) this account belongs to
24
+
25
+ Access Patterns (DynamoDB Keys):
26
+ - pk: BILLING_ACCOUNT#{tenant_id}#{account_id}
27
+ - sk: metadata
28
+ - gsi1_pk: tenant#{tenant_id}
29
+ - gsi1_sk: BILLING_ACCOUNT#{created_utc_ts}
30
+ - gsi2_pk: STRIPE_CUSTOMER#{stripe_customer_id}
31
+ - gsi2_sk: metadata
32
+ """
33
+
34
+ def __init__(self):
35
+ super().__init__()
36
+
37
+ # Identity (inherited from BaseModel: id, tenant_id)
38
+ self._account_holder_id: str | None = None # Entity that owns this account
39
+ self._account_holder_type: str = "user" # "user", "organization", "property"
40
+
41
+ # PSP Integration (Stripe)
42
+ self._stripe_customer_id: str | None = None # Stripe customer reference
43
+ self._stripe_account_id: str | None = None # Connected account (for payees)
44
+
45
+ # Currency & Localization
46
+ self._currency_code: str = "USD" # ISO 4217 currency code
47
+ self._country_code: str | None = None # ISO 3166-1 alpha-2 country code
48
+ self._locale: str | None = None # Locale for formatting (e.g., "en_US")
49
+
50
+ # Tax Configuration
51
+ self._tax_id: str | None = None # VAT/tax ID number
52
+ self._tax_id_type: str | None = None # "us_ein", "eu_vat", etc.
53
+ self._tax_exempt: bool = False # Tax exemption status
54
+ self._tax_metadata: Dict[str, Any] | None = None # Additional tax info
55
+
56
+ # Billing Details
57
+ self._billing_email: str | None = None # Email for invoices/receipts
58
+ self._billing_name: str | None = None # Name on account
59
+ self._billing_phone: str | None = None # Contact phone
60
+
61
+ # Address
62
+ self._address_line1: str | None = None
63
+ self._address_line2: str | None = None
64
+ self._address_city: str | None = None
65
+ self._address_state: str | None = None
66
+ self._address_postal_code: str | None = None
67
+ self._address_country: str | None = None # ISO 3166-1 alpha-2
68
+
69
+ # Payment Method Configuration
70
+ self._default_payment_method_id: str | None = None # Stripe payment method ID
71
+ self._allowed_payment_methods: list[str] = ["card"] # card, ach_debit, etc.
72
+
73
+ # Account Settings
74
+ self._auto_charge_enabled: bool = False # Auto-charge for recurring
75
+ self._require_cvv: bool = True # Require CVV for payments
76
+ self._send_receipts: bool = True # Auto-send receipt emails
77
+
78
+ # Balance & Limits (in cents to avoid float issues)
79
+ self._balance_cents: int = 0 # Current account balance (negative = credit)
80
+ self._credit_limit_cents: int | None = None # Maximum credit allowed
81
+
82
+ # Status
83
+ self._status: str = "active" # "active", "suspended", "closed"
84
+ self._status_reason: str | None = None # Reason for status change
85
+
86
+ # Metadata
87
+ self._notes: str | None = None # Internal notes
88
+ self._external_reference: str | None = None # External system reference
89
+
90
+ # CRITICAL: Call _setup_indexes() as LAST line in __init__
91
+ self._setup_indexes()
92
+
93
+ def _setup_indexes(self):
94
+ """Setup DynamoDB indexes for billing account queries."""
95
+
96
+ # Primary index: Billing account by ID
97
+ primary = DynamoDBIndex()
98
+ primary.name = "primary"
99
+ primary.partition_key.attribute_name = "pk"
100
+ primary.partition_key.value = lambda: DynamoDBKey.build_key(("billing_account", self.id))
101
+ primary.sort_key.attribute_name = "sk"
102
+ primary.sort_key.value = lambda: "metadata"
103
+ self.indexes.add_primary(primary)
104
+
105
+ # GSI1: Billing accounts by tenant
106
+ gsi = DynamoDBIndex()
107
+ gsi.name = "gsi1"
108
+ gsi.partition_key.attribute_name = f"{gsi.name}_pk"
109
+ gsi.partition_key.value = lambda: DynamoDBKey.build_key(("tenant", self.tenant_id))
110
+ gsi.sort_key.attribute_name = f"{gsi.name}_sk"
111
+ gsi.sort_key.value = lambda: DynamoDBKey.build_key(("billing_account", self.created_utc_ts))
112
+ self.indexes.add_secondary(gsi)
113
+
114
+ # GSI2: Billing account by Stripe customer ID (for webhook lookups)
115
+ if self.stripe_customer_id:
116
+ gsi = DynamoDBIndex()
117
+ gsi.name = "gsi2"
118
+ gsi.partition_key.attribute_name = f"{gsi.name}_pk"
119
+ gsi.partition_key.value = lambda: DynamoDBKey.build_key(("stripe_customer", self.stripe_customer_id))
120
+ gsi.sort_key.attribute_name = f"{gsi.name}_sk"
121
+ gsi.sort_key.value = lambda: "metadata"
122
+ self.indexes.add_secondary(gsi)
123
+
124
+ # Properties - Account Identity
125
+ @property
126
+ def account_id(self) -> str | None:
127
+ """Unique account ID (alias for id)."""
128
+ return self.id
129
+
130
+ @account_id.setter
131
+ def account_id(self, value: str | None):
132
+ self.id = value
133
+
134
+ @property
135
+ def account_holder_id(self) -> str | None:
136
+ """Entity that owns this billing account."""
137
+ return self._account_holder_id
138
+
139
+ @account_holder_id.setter
140
+ def account_holder_id(self, value: str | None):
141
+ self._account_holder_id = value
142
+
143
+ @property
144
+ def account_holder_type(self) -> str:
145
+ """Type of account holder: 'user', 'organization', 'property'."""
146
+ return self._account_holder_type
147
+
148
+ @account_holder_type.setter
149
+ def account_holder_type(self, value: str):
150
+ valid_types = ["user", "organization", "property"]
151
+ if value not in valid_types:
152
+ raise ValueError(f"Invalid account_holder_type: {value}. Must be one of {valid_types}")
153
+ self._account_holder_type = value
154
+
155
+ # Properties - PSP Integration
156
+ @property
157
+ def stripe_customer_id(self) -> str | None:
158
+ """Stripe customer reference ID."""
159
+ return self._stripe_customer_id
160
+
161
+ @stripe_customer_id.setter
162
+ def stripe_customer_id(self, value: str | None):
163
+ self._stripe_customer_id = value
164
+
165
+ @property
166
+ def stripe_account_id(self) -> str | None:
167
+ """Stripe connected account ID (for payees)."""
168
+ return self._stripe_account_id
169
+
170
+ @stripe_account_id.setter
171
+ def stripe_account_id(self, value: str | None):
172
+ self._stripe_account_id = value
173
+
174
+ # Properties - Currency & Localization
175
+ @property
176
+ def currency_code(self) -> str:
177
+ """ISO 4217 currency code."""
178
+ return self._currency_code
179
+
180
+ @currency_code.setter
181
+ def currency_code(self, value: str):
182
+ if not value or len(value) != 3:
183
+ raise ValueError("currency_code must be a 3-letter ISO 4217 code")
184
+ self._currency_code = value.upper()
185
+
186
+ @property
187
+ def country_code(self) -> str | None:
188
+ """ISO 3166-1 alpha-2 country code."""
189
+ return self._country_code
190
+
191
+ @country_code.setter
192
+ def country_code(self, value: str | None):
193
+ if value and len(value) != 2:
194
+ raise ValueError("country_code must be a 2-letter ISO 3166-1 alpha-2 code")
195
+ self._country_code = value.upper() if value else None
196
+
197
+ @property
198
+ def locale(self) -> str | None:
199
+ """Locale for formatting (e.g., 'en_US')."""
200
+ return self._locale
201
+
202
+ @locale.setter
203
+ def locale(self, value: str | None):
204
+ self._locale = value
205
+
206
+ # Properties - Tax Configuration
207
+ @property
208
+ def tax_id(self) -> str | None:
209
+ """VAT/tax ID number."""
210
+ return self._tax_id
211
+
212
+ @tax_id.setter
213
+ def tax_id(self, value: str | None):
214
+ self._tax_id = value
215
+
216
+ @property
217
+ def tax_id_type(self) -> str | None:
218
+ """Type of tax ID: 'us_ein', 'eu_vat', etc."""
219
+ return self._tax_id_type
220
+
221
+ @tax_id_type.setter
222
+ def tax_id_type(self, value: str | None):
223
+ self._tax_id_type = value
224
+
225
+ @property
226
+ def tax_exempt(self) -> bool:
227
+ """Tax exemption status."""
228
+ return self._tax_exempt
229
+
230
+ @tax_exempt.setter
231
+ def tax_exempt(self, value: bool):
232
+ self._tax_exempt = bool(value)
233
+
234
+ @property
235
+ def tax_metadata(self) -> Dict[str, Any] | None:
236
+ """Additional tax information."""
237
+ return self._tax_metadata
238
+
239
+ @tax_metadata.setter
240
+ def tax_metadata(self, value: Dict[str, Any] | None):
241
+ self._tax_metadata = value if isinstance(value, dict) else None
242
+
243
+ # Properties - Billing Details
244
+ @property
245
+ def billing_email(self) -> str | None:
246
+ """Email for invoices and receipts."""
247
+ return self._billing_email
248
+
249
+ @billing_email.setter
250
+ def billing_email(self, value: str | None):
251
+ self._billing_email = value
252
+
253
+ @property
254
+ def billing_name(self) -> str | None:
255
+ """Name on billing account."""
256
+ return self._billing_name
257
+
258
+ @billing_name.setter
259
+ def billing_name(self, value: str | None):
260
+ self._billing_name = value
261
+
262
+ @property
263
+ def billing_phone(self) -> str | None:
264
+ """Billing contact phone."""
265
+ return self._billing_phone
266
+
267
+ @billing_phone.setter
268
+ def billing_phone(self, value: str | None):
269
+ self._billing_phone = value
270
+
271
+ # Properties - Address
272
+ @property
273
+ def address_line1(self) -> str | None:
274
+ """Address line 1."""
275
+ return self._address_line1
276
+
277
+ @address_line1.setter
278
+ def address_line1(self, value: str | None):
279
+ self._address_line1 = value
280
+
281
+ @property
282
+ def address_line2(self) -> str | None:
283
+ """Address line 2."""
284
+ return self._address_line2
285
+
286
+ @address_line2.setter
287
+ def address_line2(self, value: str | None):
288
+ self._address_line2 = value
289
+
290
+ @property
291
+ def address_city(self) -> str | None:
292
+ """City."""
293
+ return self._address_city
294
+
295
+ @address_city.setter
296
+ def address_city(self, value: str | None):
297
+ self._address_city = value
298
+
299
+ @property
300
+ def address_state(self) -> str | None:
301
+ """State/province."""
302
+ return self._address_state
303
+
304
+ @address_state.setter
305
+ def address_state(self, value: str | None):
306
+ self._address_state = value
307
+
308
+ @property
309
+ def address_postal_code(self) -> str | None:
310
+ """Postal/ZIP code."""
311
+ return self._address_postal_code
312
+
313
+ @address_postal_code.setter
314
+ def address_postal_code(self, value: str | None):
315
+ self._address_postal_code = value
316
+
317
+ @property
318
+ def address_country(self) -> str | None:
319
+ """Country (ISO 3166-1 alpha-2)."""
320
+ return self._address_country
321
+
322
+ @address_country.setter
323
+ def address_country(self, value: str | None):
324
+ if value and len(value) != 2:
325
+ raise ValueError("address_country must be a 2-letter ISO 3166-1 alpha-2 code")
326
+ self._address_country = value.upper() if value else None
327
+
328
+ # Properties - Payment Method Configuration
329
+ @property
330
+ def default_payment_method_id(self) -> str | None:
331
+ """Default Stripe payment method ID."""
332
+ return self._default_payment_method_id
333
+
334
+ @default_payment_method_id.setter
335
+ def default_payment_method_id(self, value: str | None):
336
+ self._default_payment_method_id = value
337
+
338
+ @property
339
+ def allowed_payment_methods(self) -> list[str]:
340
+ """Allowed payment method types."""
341
+ return self._allowed_payment_methods
342
+
343
+ @allowed_payment_methods.setter
344
+ def allowed_payment_methods(self, value: list[str] | None):
345
+ self._allowed_payment_methods = value if isinstance(value, list) else ["card"]
346
+
347
+ # Properties - Account Settings
348
+ @property
349
+ def auto_charge_enabled(self) -> bool:
350
+ """Auto-charge enabled for recurring payments."""
351
+ return self._auto_charge_enabled
352
+
353
+ @auto_charge_enabled.setter
354
+ def auto_charge_enabled(self, value: bool):
355
+ self._auto_charge_enabled = bool(value)
356
+
357
+ @property
358
+ def require_cvv(self) -> bool:
359
+ """Require CVV for card payments."""
360
+ return self._require_cvv
361
+
362
+ @require_cvv.setter
363
+ def require_cvv(self, value: bool):
364
+ self._require_cvv = bool(value)
365
+
366
+ @property
367
+ def send_receipts(self) -> bool:
368
+ """Auto-send receipt emails."""
369
+ return self._send_receipts
370
+
371
+ @send_receipts.setter
372
+ def send_receipts(self, value: bool):
373
+ self._send_receipts = bool(value)
374
+
375
+ # Properties - Balance & Limits
376
+ @property
377
+ def balance_cents(self) -> int:
378
+ """Current account balance in cents (negative = credit)."""
379
+ return self._balance_cents
380
+
381
+ @balance_cents.setter
382
+ def balance_cents(self, value: int):
383
+ self._balance_cents = value if value is not None else 0
384
+
385
+ @property
386
+ def credit_limit_cents(self) -> int | None:
387
+ """Maximum credit limit in cents."""
388
+ return self._credit_limit_cents
389
+
390
+ @credit_limit_cents.setter
391
+ def credit_limit_cents(self, value: int | None):
392
+ self._credit_limit_cents = value
393
+
394
+ # Properties - Status
395
+ @property
396
+ def status(self) -> str:
397
+ """Account status: 'active', 'suspended', 'closed'."""
398
+ return self._status
399
+
400
+ @status.setter
401
+ def status(self, value: str):
402
+ valid_statuses = ["active", "suspended", "closed"]
403
+ if value not in valid_statuses:
404
+ raise ValueError(f"Invalid status: {value}. Must be one of {valid_statuses}")
405
+ self._status = value
406
+
407
+ @property
408
+ def status_reason(self) -> str | None:
409
+ """Reason for status change."""
410
+ return self._status_reason
411
+
412
+ @status_reason.setter
413
+ def status_reason(self, value: str | None):
414
+ self._status_reason = value
415
+
416
+ # Properties - Metadata
417
+ @property
418
+ def notes(self) -> str | None:
419
+ """Internal notes."""
420
+ return self._notes
421
+
422
+ @notes.setter
423
+ def notes(self, value: str | None):
424
+ self._notes = value
425
+
426
+ @property
427
+ def external_reference(self) -> str | None:
428
+ """External system reference."""
429
+ return self._external_reference
430
+
431
+ @external_reference.setter
432
+ def external_reference(self, value: str | None):
433
+ self._external_reference = value
434
+
435
+ # Helper Methods
436
+ def is_active(self) -> bool:
437
+ """Check if account is active."""
438
+ return self._status == "active"
439
+
440
+ def is_suspended(self) -> bool:
441
+ """Check if account is suspended."""
442
+ return self._status == "suspended"
443
+
444
+ def is_closed(self) -> bool:
445
+ """Check if account is closed."""
446
+ return self._status == "closed"
447
+
448
+ def has_credit(self) -> bool:
449
+ """Check if account has credit balance."""
450
+ return self._balance_cents < 0
451
+
452
+ def has_debit(self) -> bool:
453
+ """Check if account has debit balance."""
454
+ return self._balance_cents > 0
455
+
456
+ def get_balance_dollars(self) -> float:
457
+ """Get balance in dollars."""
458
+ return self._balance_cents / 100.0
459
+
460
+ def get_credit_limit_dollars(self) -> float | None:
461
+ """Get credit limit in dollars."""
462
+ return self._credit_limit_cents / 100.0 if self._credit_limit_cents else None
463
+
464
+ def has_stripe_customer(self) -> bool:
465
+ """Check if Stripe customer ID is set."""
466
+ return self._stripe_customer_id is not None and self._stripe_customer_id != ""
467
+
468
+ def has_default_payment_method(self) -> bool:
469
+ """Check if default payment method is set."""
470
+ return self._default_payment_method_id is not None and self._default_payment_method_id != ""
471
+
472
+ def get_full_address(self) -> str | None:
473
+ """Get formatted full address."""
474
+ parts = []
475
+ if self._address_line1:
476
+ parts.append(self._address_line1)
477
+ if self._address_line2:
478
+ parts.append(self._address_line2)
479
+
480
+ city_state_zip = []
481
+ if self._address_city:
482
+ city_state_zip.append(self._address_city)
483
+ if self._address_state:
484
+ city_state_zip.append(self._address_state)
485
+ if self._address_postal_code:
486
+ city_state_zip.append(self._address_postal_code)
487
+
488
+ if city_state_zip:
489
+ parts.append(", ".join(city_state_zip))
490
+
491
+ if self._address_country:
492
+ parts.append(self._address_country)
493
+
494
+ return "\n".join(parts) if parts else None
495
+
496
+ def validate(self) -> tuple[bool, list[str]]:
497
+ """
498
+ Validate the billing account.
499
+
500
+ Returns:
501
+ Tuple of (is_valid, list of error messages)
502
+ """
503
+ errors = []
504
+
505
+ # Required fields
506
+ if not self.tenant_id:
507
+ errors.append("tenant_id is required")
508
+ if not self._account_holder_id:
509
+ errors.append("account_holder_id is required")
510
+ if not self._currency_code:
511
+ errors.append("currency_code is required")
512
+
513
+ # Email validation (basic)
514
+ if self._billing_email and "@" not in self._billing_email:
515
+ errors.append("billing_email must be a valid email address")
516
+
517
+ # Balance and credit limit validation
518
+ if self._credit_limit_cents is not None and self._credit_limit_cents < 0:
519
+ errors.append("credit_limit_cents must be non-negative")
520
+
521
+ return (len(errors) == 0, errors)