ai-lls-lib 1.4.0rc2__py3-none-any.whl → 1.4.0rc4__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.
- ai_lls_lib/__init__.py +1 -1
- ai_lls_lib/auth/__init__.py +4 -4
- ai_lls_lib/auth/context_parser.py +68 -68
- ai_lls_lib/cli/__init__.py +3 -3
- ai_lls_lib/cli/__main__.py +30 -30
- ai_lls_lib/cli/aws_client.py +115 -115
- ai_lls_lib/cli/commands/__init__.py +3 -3
- ai_lls_lib/cli/commands/admin.py +174 -174
- ai_lls_lib/cli/commands/cache.py +142 -142
- ai_lls_lib/cli/commands/stripe.py +377 -377
- ai_lls_lib/cli/commands/test_stack.py +216 -216
- ai_lls_lib/cli/commands/verify.py +111 -111
- ai_lls_lib/cli/env_loader.py +122 -122
- ai_lls_lib/core/__init__.py +3 -3
- ai_lls_lib/core/cache.py +106 -106
- ai_lls_lib/core/models.py +77 -77
- ai_lls_lib/core/processor.py +295 -295
- ai_lls_lib/core/verifier.py +84 -84
- ai_lls_lib/payment/__init__.py +13 -13
- ai_lls_lib/payment/credit_manager.py +186 -193
- ai_lls_lib/payment/models.py +102 -102
- ai_lls_lib/payment/stripe_manager.py +487 -487
- ai_lls_lib/payment/webhook_processor.py +215 -215
- ai_lls_lib/providers/__init__.py +7 -7
- ai_lls_lib/providers/base.py +28 -28
- ai_lls_lib/providers/external.py +87 -87
- ai_lls_lib/providers/stub.py +48 -48
- ai_lls_lib/testing/__init__.py +3 -3
- ai_lls_lib/testing/fixtures.py +104 -104
- {ai_lls_lib-1.4.0rc2.dist-info → ai_lls_lib-1.4.0rc4.dist-info}/METADATA +1 -1
- ai_lls_lib-1.4.0rc4.dist-info/RECORD +33 -0
- ai_lls_lib-1.4.0rc2.dist-info/RECORD +0 -33
- {ai_lls_lib-1.4.0rc2.dist-info → ai_lls_lib-1.4.0rc4.dist-info}/WHEEL +0 -0
- {ai_lls_lib-1.4.0rc2.dist-info → ai_lls_lib-1.4.0rc4.dist-info}/entry_points.txt +0 -0
ai_lls_lib/core/verifier.py
CHANGED
@@ -1,84 +1,84 @@
|
|
1
|
-
"""
|
2
|
-
Phone verification logic - checks line type and DNC status
|
3
|
-
"""
|
4
|
-
import os
|
5
|
-
from datetime import datetime, timezone
|
6
|
-
from typing import Optional
|
7
|
-
import phonenumbers
|
8
|
-
from aws_lambda_powertools import Logger
|
9
|
-
from .models import PhoneVerification, LineType, VerificationSource
|
10
|
-
from .cache import DynamoDBCache
|
11
|
-
from ..providers import VerificationProvider, StubProvider
|
12
|
-
|
13
|
-
logger = Logger()
|
14
|
-
|
15
|
-
|
16
|
-
class PhoneVerifier:
|
17
|
-
"""Verifies phone numbers for line type and DNC status"""
|
18
|
-
|
19
|
-
def __init__(self, cache: DynamoDBCache, provider: Optional[VerificationProvider] = None):
|
20
|
-
"""
|
21
|
-
Initialize phone verifier.
|
22
|
-
|
23
|
-
Args:
|
24
|
-
cache: DynamoDB cache for storing results
|
25
|
-
provider: Verification provider (defaults to StubProvider)
|
26
|
-
"""
|
27
|
-
self.cache = cache
|
28
|
-
self.provider = provider or StubProvider()
|
29
|
-
|
30
|
-
def normalize_phone(self, phone: str) -> str:
|
31
|
-
"""Normalize phone to E.164 format"""
|
32
|
-
try:
|
33
|
-
# Parse with US as default country
|
34
|
-
parsed = phonenumbers.parse(phone, "US")
|
35
|
-
if not phonenumbers.is_valid_number(parsed):
|
36
|
-
raise ValueError(f"Invalid phone number: {phone}")
|
37
|
-
|
38
|
-
# Format as E.164
|
39
|
-
return phonenumbers.format_number(parsed, phonenumbers.PhoneNumberFormat.E164)
|
40
|
-
except Exception as e:
|
41
|
-
logger.error(f"Phone normalization failed: {str(e)}")
|
42
|
-
raise ValueError(f"Invalid phone format: {phone}")
|
43
|
-
|
44
|
-
def verify(self, phone: str) -> PhoneVerification:
|
45
|
-
"""Verify phone number for line type and DNC status"""
|
46
|
-
normalized = self.normalize_phone(phone)
|
47
|
-
|
48
|
-
# Check cache first
|
49
|
-
cached = self.cache.get(normalized)
|
50
|
-
if cached:
|
51
|
-
return cached
|
52
|
-
|
53
|
-
# Use provider to verify
|
54
|
-
line_type, dnc_status = self.provider.verify_phone(normalized)
|
55
|
-
|
56
|
-
result = PhoneVerification(
|
57
|
-
phone_number=normalized,
|
58
|
-
line_type=line_type,
|
59
|
-
dnc=dnc_status,
|
60
|
-
cached=False,
|
61
|
-
verified_at=datetime.now(timezone.utc),
|
62
|
-
source=VerificationSource.API
|
63
|
-
)
|
64
|
-
|
65
|
-
# Store in cache
|
66
|
-
self.cache.set(normalized, result)
|
67
|
-
|
68
|
-
return result
|
69
|
-
|
70
|
-
def _check_line_type(self, phone: str) -> LineType:
|
71
|
-
"""
|
72
|
-
Check line type (for backwards compatibility with CLI).
|
73
|
-
Delegates to provider.
|
74
|
-
"""
|
75
|
-
line_type, _ = self.provider.verify_phone(phone)
|
76
|
-
return line_type
|
77
|
-
|
78
|
-
def _check_dnc(self, phone: str) -> bool:
|
79
|
-
"""
|
80
|
-
Check DNC status (for backwards compatibility with CLI).
|
81
|
-
Delegates to provider.
|
82
|
-
"""
|
83
|
-
_, dnc_status = self.provider.verify_phone(phone)
|
84
|
-
return dnc_status
|
1
|
+
"""
|
2
|
+
Phone verification logic - checks line type and DNC status
|
3
|
+
"""
|
4
|
+
import os
|
5
|
+
from datetime import datetime, timezone
|
6
|
+
from typing import Optional
|
7
|
+
import phonenumbers
|
8
|
+
from aws_lambda_powertools import Logger
|
9
|
+
from .models import PhoneVerification, LineType, VerificationSource
|
10
|
+
from .cache import DynamoDBCache
|
11
|
+
from ..providers import VerificationProvider, StubProvider
|
12
|
+
|
13
|
+
logger = Logger()
|
14
|
+
|
15
|
+
|
16
|
+
class PhoneVerifier:
|
17
|
+
"""Verifies phone numbers for line type and DNC status"""
|
18
|
+
|
19
|
+
def __init__(self, cache: DynamoDBCache, provider: Optional[VerificationProvider] = None):
|
20
|
+
"""
|
21
|
+
Initialize phone verifier.
|
22
|
+
|
23
|
+
Args:
|
24
|
+
cache: DynamoDB cache for storing results
|
25
|
+
provider: Verification provider (defaults to StubProvider)
|
26
|
+
"""
|
27
|
+
self.cache = cache
|
28
|
+
self.provider = provider or StubProvider()
|
29
|
+
|
30
|
+
def normalize_phone(self, phone: str) -> str:
|
31
|
+
"""Normalize phone to E.164 format"""
|
32
|
+
try:
|
33
|
+
# Parse with US as default country
|
34
|
+
parsed = phonenumbers.parse(phone, "US")
|
35
|
+
if not phonenumbers.is_valid_number(parsed):
|
36
|
+
raise ValueError(f"Invalid phone number: {phone}")
|
37
|
+
|
38
|
+
# Format as E.164
|
39
|
+
return phonenumbers.format_number(parsed, phonenumbers.PhoneNumberFormat.E164)
|
40
|
+
except Exception as e:
|
41
|
+
logger.error(f"Phone normalization failed: {str(e)}")
|
42
|
+
raise ValueError(f"Invalid phone format: {phone}")
|
43
|
+
|
44
|
+
def verify(self, phone: str) -> PhoneVerification:
|
45
|
+
"""Verify phone number for line type and DNC status"""
|
46
|
+
normalized = self.normalize_phone(phone)
|
47
|
+
|
48
|
+
# Check cache first
|
49
|
+
cached = self.cache.get(normalized)
|
50
|
+
if cached:
|
51
|
+
return cached
|
52
|
+
|
53
|
+
# Use provider to verify
|
54
|
+
line_type, dnc_status = self.provider.verify_phone(normalized)
|
55
|
+
|
56
|
+
result = PhoneVerification(
|
57
|
+
phone_number=normalized,
|
58
|
+
line_type=line_type,
|
59
|
+
dnc=dnc_status,
|
60
|
+
cached=False,
|
61
|
+
verified_at=datetime.now(timezone.utc),
|
62
|
+
source=VerificationSource.API
|
63
|
+
)
|
64
|
+
|
65
|
+
# Store in cache
|
66
|
+
self.cache.set(normalized, result)
|
67
|
+
|
68
|
+
return result
|
69
|
+
|
70
|
+
def _check_line_type(self, phone: str) -> LineType:
|
71
|
+
"""
|
72
|
+
Check line type (for backwards compatibility with CLI).
|
73
|
+
Delegates to provider.
|
74
|
+
"""
|
75
|
+
line_type, _ = self.provider.verify_phone(phone)
|
76
|
+
return line_type
|
77
|
+
|
78
|
+
def _check_dnc(self, phone: str) -> bool:
|
79
|
+
"""
|
80
|
+
Check DNC status (for backwards compatibility with CLI).
|
81
|
+
Delegates to provider.
|
82
|
+
"""
|
83
|
+
_, dnc_status = self.provider.verify_phone(phone)
|
84
|
+
return dnc_status
|
ai_lls_lib/payment/__init__.py
CHANGED
@@ -1,13 +1,13 @@
|
|
1
|
-
"""Payment module for Landline Scrubber."""
|
2
|
-
|
3
|
-
from .models import Plan, PlanType, SubscriptionStatus
|
4
|
-
from .stripe_manager import StripeManager
|
5
|
-
from .credit_manager import CreditManager
|
6
|
-
|
7
|
-
__all__ = [
|
8
|
-
"Plan",
|
9
|
-
"PlanType",
|
10
|
-
"SubscriptionStatus",
|
11
|
-
"StripeManager",
|
12
|
-
"CreditManager",
|
13
|
-
]
|
1
|
+
"""Payment module for Landline Scrubber."""
|
2
|
+
|
3
|
+
from .models import Plan, PlanType, SubscriptionStatus
|
4
|
+
from .stripe_manager import StripeManager
|
5
|
+
from .credit_manager import CreditManager
|
6
|
+
|
7
|
+
__all__ = [
|
8
|
+
"Plan",
|
9
|
+
"PlanType",
|
10
|
+
"SubscriptionStatus",
|
11
|
+
"StripeManager",
|
12
|
+
"CreditManager",
|
13
|
+
]
|
@@ -1,193 +1,186 @@
|
|
1
|
-
"""Credit balance management with DynamoDB."""
|
2
|
-
|
3
|
-
import os
|
4
|
-
from typing import Optional, Dict, Any
|
5
|
-
from decimal import Decimal
|
6
|
-
import logging
|
7
|
-
from datetime import datetime
|
8
|
-
|
9
|
-
try:
|
10
|
-
import boto3
|
11
|
-
from botocore.exceptions import ClientError
|
12
|
-
except ImportError:
|
13
|
-
boto3 = None # Handle gracefully for testing
|
14
|
-
|
15
|
-
logger = logging.getLogger(__name__)
|
16
|
-
|
17
|
-
|
18
|
-
class CreditManager:
|
19
|
-
"""
|
20
|
-
Manages user credit balances in DynamoDB CreditsTable.
|
21
|
-
"""
|
22
|
-
|
23
|
-
def __init__(self, table_name: Optional[str] = None):
|
24
|
-
"""Initialize with DynamoDB table."""
|
25
|
-
if not boto3:
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
":
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
)
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
"
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
"
|
159
|
-
"
|
160
|
-
"
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
":now": datetime.utcnow().isoformat()
|
188
|
-
}
|
189
|
-
)
|
190
|
-
logger.info(f"Stored Stripe customer ID {stripe_customer_id} for user {user_id}")
|
191
|
-
except ClientError as e:
|
192
|
-
logger.error(f"Error storing Stripe customer ID for {user_id}: {e}")
|
193
|
-
raise
|
1
|
+
"""Credit balance management with DynamoDB."""
|
2
|
+
|
3
|
+
import os
|
4
|
+
from typing import Optional, Dict, Any
|
5
|
+
from decimal import Decimal
|
6
|
+
import logging
|
7
|
+
from datetime import datetime
|
8
|
+
|
9
|
+
try:
|
10
|
+
import boto3
|
11
|
+
from botocore.exceptions import ClientError
|
12
|
+
except ImportError:
|
13
|
+
boto3 = None # Handle gracefully for testing
|
14
|
+
|
15
|
+
logger = logging.getLogger(__name__)
|
16
|
+
|
17
|
+
|
18
|
+
class CreditManager:
|
19
|
+
"""
|
20
|
+
Manages user credit balances in DynamoDB CreditsTable.
|
21
|
+
"""
|
22
|
+
|
23
|
+
def __init__(self, table_name: Optional[str] = None):
|
24
|
+
"""Initialize with DynamoDB table."""
|
25
|
+
if not boto3:
|
26
|
+
raise RuntimeError("boto3 is required for CreditManager")
|
27
|
+
|
28
|
+
self.dynamodb = boto3.resource("dynamodb")
|
29
|
+
self.table_name = table_name if table_name else os.environ['CREDITS_TABLE']
|
30
|
+
|
31
|
+
try:
|
32
|
+
self.table = self.dynamodb.Table(self.table_name)
|
33
|
+
except Exception as e:
|
34
|
+
logger.error(f"Failed to connect to DynamoDB table {self.table_name}: {e}")
|
35
|
+
self.table = None
|
36
|
+
|
37
|
+
def get_balance(self, user_id: str) -> int:
|
38
|
+
"""Get current credit balance for a user."""
|
39
|
+
if not self.table:
|
40
|
+
raise RuntimeError(f"DynamoDB table {self.table_name} not accessible")
|
41
|
+
|
42
|
+
try:
|
43
|
+
response = self.table.get_item(Key={"user_id": user_id})
|
44
|
+
if "Item" in response:
|
45
|
+
return int(response["Item"].get("credits", 0))
|
46
|
+
return 0
|
47
|
+
except ClientError as e:
|
48
|
+
logger.error(f"Error getting balance for {user_id}: {e}")
|
49
|
+
return 0
|
50
|
+
|
51
|
+
def add_credits(self, user_id: str, amount: int) -> int:
|
52
|
+
"""Add credits to user balance and return new balance."""
|
53
|
+
if not self.table:
|
54
|
+
raise RuntimeError(f"DynamoDB table {self.table_name} not accessible")
|
55
|
+
|
56
|
+
try:
|
57
|
+
response = self.table.update_item(
|
58
|
+
Key={"user_id": user_id},
|
59
|
+
UpdateExpression="ADD credits :amount SET updated_at = :now",
|
60
|
+
ExpressionAttributeValues={
|
61
|
+
":amount": Decimal(amount),
|
62
|
+
":now": datetime.utcnow().isoformat()
|
63
|
+
},
|
64
|
+
ReturnValues="ALL_NEW"
|
65
|
+
)
|
66
|
+
return int(response["Attributes"]["credits"])
|
67
|
+
except ClientError as e:
|
68
|
+
logger.error(f"Error adding credits for {user_id}: {e}")
|
69
|
+
raise
|
70
|
+
|
71
|
+
def deduct_credits(self, user_id: str, amount: int) -> bool:
|
72
|
+
"""
|
73
|
+
Deduct credits from user balance.
|
74
|
+
Returns True if successful, False if insufficient balance.
|
75
|
+
"""
|
76
|
+
if not self.table:
|
77
|
+
raise RuntimeError(f"DynamoDB table {self.table_name} not accessible")
|
78
|
+
|
79
|
+
try:
|
80
|
+
# Conditional update - only deduct if balance >= amount
|
81
|
+
self.table.update_item(
|
82
|
+
Key={"user_id": user_id},
|
83
|
+
UpdateExpression="ADD credits :negative_amount SET updated_at = :now",
|
84
|
+
ConditionExpression="credits >= :amount",
|
85
|
+
ExpressionAttributeValues={
|
86
|
+
":negative_amount": Decimal(-amount),
|
87
|
+
":amount": Decimal(amount),
|
88
|
+
":now": datetime.utcnow().isoformat()
|
89
|
+
}
|
90
|
+
)
|
91
|
+
return True
|
92
|
+
except ClientError as e:
|
93
|
+
if e.response["Error"]["Code"] == "ConditionalCheckFailedException":
|
94
|
+
logger.info(f"Insufficient credits for {user_id}")
|
95
|
+
return False
|
96
|
+
logger.error(f"Error deducting credits for {user_id}: {e}")
|
97
|
+
raise
|
98
|
+
|
99
|
+
def set_subscription_state(
|
100
|
+
self,
|
101
|
+
user_id: str,
|
102
|
+
status: str,
|
103
|
+
stripe_customer_id: Optional[str] = None,
|
104
|
+
stripe_subscription_id: Optional[str] = None
|
105
|
+
) -> None:
|
106
|
+
"""Update subscription state in CreditsTable."""
|
107
|
+
if not self.table:
|
108
|
+
raise RuntimeError(f"DynamoDB table {self.table_name} not accessible")
|
109
|
+
|
110
|
+
try:
|
111
|
+
update_expr = "SET subscription_status = :status, updated_at = :now"
|
112
|
+
expr_values = {
|
113
|
+
":status": status,
|
114
|
+
":now": datetime.utcnow().isoformat()
|
115
|
+
}
|
116
|
+
|
117
|
+
if stripe_customer_id:
|
118
|
+
update_expr += ", stripe_customer_id = :customer_id"
|
119
|
+
expr_values[":customer_id"] = stripe_customer_id
|
120
|
+
|
121
|
+
if stripe_subscription_id:
|
122
|
+
update_expr += ", stripe_subscription_id = :subscription_id"
|
123
|
+
expr_values[":subscription_id"] = stripe_subscription_id
|
124
|
+
|
125
|
+
self.table.update_item(
|
126
|
+
Key={"user_id": user_id},
|
127
|
+
UpdateExpression=update_expr,
|
128
|
+
ExpressionAttributeValues=expr_values
|
129
|
+
)
|
130
|
+
except ClientError as e:
|
131
|
+
logger.error(f"Error updating subscription state for {user_id}: {e}")
|
132
|
+
raise
|
133
|
+
|
134
|
+
def get_user_payment_info(self, user_id: str) -> Dict[str, Any]:
|
135
|
+
"""Get user's payment-related information."""
|
136
|
+
if not self.table:
|
137
|
+
raise RuntimeError(f"DynamoDB table {self.table_name} not accessible")
|
138
|
+
|
139
|
+
try:
|
140
|
+
response = self.table.get_item(Key={"user_id": user_id})
|
141
|
+
if "Item" in response:
|
142
|
+
item = response["Item"]
|
143
|
+
return {
|
144
|
+
"credits": int(item.get("credits", 0)),
|
145
|
+
"stripe_customer_id": item.get("stripe_customer_id"),
|
146
|
+
"stripe_subscription_id": item.get("stripe_subscription_id"),
|
147
|
+
"subscription_status": item.get("subscription_status")
|
148
|
+
}
|
149
|
+
return {
|
150
|
+
"credits": 0,
|
151
|
+
"stripe_customer_id": None,
|
152
|
+
"stripe_subscription_id": None,
|
153
|
+
"subscription_status": None
|
154
|
+
}
|
155
|
+
except ClientError as e:
|
156
|
+
logger.error(f"Error getting payment info for {user_id}: {e}")
|
157
|
+
return {
|
158
|
+
"credits": 0,
|
159
|
+
"stripe_customer_id": None,
|
160
|
+
"stripe_subscription_id": None,
|
161
|
+
"subscription_status": None
|
162
|
+
}
|
163
|
+
|
164
|
+
def has_unlimited_access(self, user_id: str) -> bool:
|
165
|
+
"""Check if user has unlimited access via active subscription."""
|
166
|
+
info = self.get_user_payment_info(user_id)
|
167
|
+
return info.get("subscription_status") == "active"
|
168
|
+
|
169
|
+
def set_stripe_customer_id(self, user_id: str, stripe_customer_id: str) -> None:
|
170
|
+
"""Store Stripe customer ID for a user."""
|
171
|
+
if not self.table:
|
172
|
+
raise RuntimeError(f"DynamoDB table {self.table_name} not accessible")
|
173
|
+
|
174
|
+
try:
|
175
|
+
self.table.update_item(
|
176
|
+
Key={"user_id": user_id},
|
177
|
+
UpdateExpression="SET stripe_customer_id = :customer_id, updated_at = :now",
|
178
|
+
ExpressionAttributeValues={
|
179
|
+
":customer_id": stripe_customer_id,
|
180
|
+
":now": datetime.utcnow().isoformat()
|
181
|
+
}
|
182
|
+
)
|
183
|
+
logger.info(f"Stored Stripe customer ID {stripe_customer_id} for user {user_id}")
|
184
|
+
except ClientError as e:
|
185
|
+
logger.error(f"Error storing Stripe customer ID for {user_id}: {e}")
|
186
|
+
raise
|