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.
- geek_cafe_saas_sdk/__init__.py +2 -2
- geek_cafe_saas_sdk/domains/files/handlers/README.md +446 -0
- geek_cafe_saas_sdk/domains/files/handlers/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/files/handlers/files/create/app.py +121 -0
- geek_cafe_saas_sdk/domains/files/handlers/files/download/app.py +80 -0
- geek_cafe_saas_sdk/domains/files/handlers/files/get/app.py +62 -0
- geek_cafe_saas_sdk/domains/files/handlers/files/list/app.py +72 -0
- geek_cafe_saas_sdk/domains/files/handlers/lineage/create_derived/app.py +99 -0
- geek_cafe_saas_sdk/domains/files/handlers/lineage/create_main/app.py +104 -0
- geek_cafe_saas_sdk/domains/files/handlers/lineage/download_bundle/app.py +99 -0
- geek_cafe_saas_sdk/domains/files/handlers/lineage/get_lineage/app.py +68 -0
- geek_cafe_saas_sdk/domains/files/handlers/lineage/prepare_bundle/app.py +76 -0
- geek_cafe_saas_sdk/domains/files/models/__init__.py +17 -0
- geek_cafe_saas_sdk/domains/files/models/directory.py +42 -6
- geek_cafe_saas_sdk/domains/files/models/file.py +158 -16
- 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/__init__.py +21 -0
- geek_cafe_saas_sdk/domains/files/services/directory_service.py +54 -135
- geek_cafe_saas_sdk/domains/files/services/file_lineage_service.py +487 -0
- geek_cafe_saas_sdk/domains/files/services/file_share_service.py +37 -120
- geek_cafe_saas_sdk/domains/files/services/file_system_service.py +67 -103
- geek_cafe_saas_sdk/domains/files/services/file_version_service.py +44 -124
- 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/cognito_utility.py +16 -26
- 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.6.0.dist-info → geek_cafe_saas_sdk-0.7.1.dist-info}/METADATA +11 -11
- {geek_cafe_saas_sdk-0.6.0.dist-info → geek_cafe_saas_sdk-0.7.1.dist-info}/RECORD +94 -23
- {geek_cafe_saas_sdk-0.6.0.dist-info → geek_cafe_saas_sdk-0.7.1.dist-info}/WHEEL +0 -0
- {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,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)
|