ai-lls-lib 1.2.0__py3-none-any.whl → 1.3.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.
- ai_lls_lib/__init__.py +1 -1
- ai_lls_lib/cli/commands/stripe.py +26 -4
- ai_lls_lib/payment/credit_manager.py +19 -0
- ai_lls_lib/payment/stripe_manager.py +48 -34
- ai_lls_lib/payment/webhook_processor.py +53 -1
- {ai_lls_lib-1.2.0.dist-info → ai_lls_lib-1.3.1.dist-info}/METADATA +1 -1
- {ai_lls_lib-1.2.0.dist-info → ai_lls_lib-1.3.1.dist-info}/RECORD +9 -9
- {ai_lls_lib-1.2.0.dist-info → ai_lls_lib-1.3.1.dist-info}/WHEEL +0 -0
- {ai_lls_lib-1.2.0.dist-info → ai_lls_lib-1.3.1.dist-info}/entry_points.txt +0 -0
ai_lls_lib/__init__.py
CHANGED
@@ -13,7 +13,7 @@ from ai_lls_lib.core.verifier import PhoneVerifier
|
|
13
13
|
from ai_lls_lib.core.processor import BulkProcessor
|
14
14
|
from ai_lls_lib.core.cache import DynamoDBCache
|
15
15
|
|
16
|
-
__version__ = "1.
|
16
|
+
__version__ = "1.3.1"
|
17
17
|
__all__ = [
|
18
18
|
"PhoneVerification",
|
19
19
|
"BulkJob",
|
@@ -37,6 +37,32 @@ def seed_products(environment: str, api_key: Optional[str], dry_run: bool):
|
|
37
37
|
|
38
38
|
# Define the products and prices to create
|
39
39
|
products_config = [
|
40
|
+
{
|
41
|
+
"name": "Landline Scrubber - Variable",
|
42
|
+
"description": "Pay as you go - choose your amount",
|
43
|
+
"metadata": {
|
44
|
+
"product_type": "landline_scrubber",
|
45
|
+
"environment": environment,
|
46
|
+
"tier": "VARIABLE"
|
47
|
+
},
|
48
|
+
"price": {
|
49
|
+
"unit_amount": 500, # $5.00 minimum
|
50
|
+
"currency": "usd",
|
51
|
+
"metadata": {
|
52
|
+
"product_type": "landline_scrubber",
|
53
|
+
"environment": environment,
|
54
|
+
"plan_type": "prepaid",
|
55
|
+
"tier": "VARIABLE",
|
56
|
+
"variable_amount": "true",
|
57
|
+
"min_amount": "5",
|
58
|
+
"default_amounts": "5,10,50",
|
59
|
+
"credits_per_dollar": "285", # ~$0.00175 per credit
|
60
|
+
"plan_credits_text": "Variable credits",
|
61
|
+
"percent_off": "",
|
62
|
+
"active": "true"
|
63
|
+
}
|
64
|
+
}
|
65
|
+
},
|
40
66
|
{
|
41
67
|
"name": "Landline Scrubber - STANDARD",
|
42
68
|
"description": "One-time purchase",
|
@@ -53,7 +79,6 @@ def seed_products(environment: str, api_key: Optional[str], dry_run: bool):
|
|
53
79
|
"environment": environment,
|
54
80
|
"plan_type": "prepaid",
|
55
81
|
"tier": "STANDARD",
|
56
|
-
"plan_reference": "79541679412215", # Legacy ID for compatibility
|
57
82
|
"credits": "5000",
|
58
83
|
"plan_credits_text": "5,000 credits",
|
59
84
|
"percent_off": "",
|
@@ -77,7 +102,6 @@ def seed_products(environment: str, api_key: Optional[str], dry_run: bool):
|
|
77
102
|
"environment": environment,
|
78
103
|
"plan_type": "prepaid",
|
79
104
|
"tier": "POWER",
|
80
|
-
"plan_reference": "79541679412216", # Legacy ID for compatibility
|
81
105
|
"credits": "28500",
|
82
106
|
"plan_credits_text": "28,500 credits",
|
83
107
|
"percent_off": "12.5% OFF",
|
@@ -101,7 +125,6 @@ def seed_products(environment: str, api_key: Optional[str], dry_run: bool):
|
|
101
125
|
"environment": environment,
|
102
126
|
"plan_type": "prepaid",
|
103
127
|
"tier": "ELITE",
|
104
|
-
"plan_reference": "79541679412217", # Legacy ID for compatibility
|
105
128
|
"credits": "66666",
|
106
129
|
"plan_credits_text": "66,666 credits",
|
107
130
|
"percent_off": "25% OFF",
|
@@ -126,7 +149,6 @@ def seed_products(environment: str, api_key: Optional[str], dry_run: bool):
|
|
126
149
|
"environment": environment,
|
127
150
|
"plan_type": "postpaid",
|
128
151
|
"tier": "UNLIMITED",
|
129
|
-
"plan_reference": "price_unlimited",
|
130
152
|
"credits": "unlimited",
|
131
153
|
"plan_credits_text": "Unlimited",
|
132
154
|
"percent_off": "",
|
@@ -172,3 +172,22 @@ class CreditManager:
|
|
172
172
|
"""Check if user has unlimited access via active subscription."""
|
173
173
|
info = self.get_user_payment_info(user_id)
|
174
174
|
return info.get("subscription_status") == "active"
|
175
|
+
|
176
|
+
def set_stripe_customer_id(self, user_id: str, stripe_customer_id: str) -> None:
|
177
|
+
"""Store Stripe customer ID for a user."""
|
178
|
+
if not self.table:
|
179
|
+
return # Mock for testing
|
180
|
+
|
181
|
+
try:
|
182
|
+
self.table.update_item(
|
183
|
+
Key={"user_id": user_id},
|
184
|
+
UpdateExpression="SET stripe_customer_id = :customer_id, updated_at = :now",
|
185
|
+
ExpressionAttributeValues={
|
186
|
+
":customer_id": stripe_customer_id,
|
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
|
@@ -97,7 +97,8 @@ class StripeManager:
|
|
97
97
|
|
98
98
|
return {
|
99
99
|
"client_secret": setup_intent.client_secret,
|
100
|
-
"setup_intent_id": setup_intent.id
|
100
|
+
"setup_intent_id": setup_intent.id,
|
101
|
+
"customer_id": customer.id
|
101
102
|
}
|
102
103
|
|
103
104
|
except stripe.error.StripeError as e:
|
@@ -186,49 +187,62 @@ class StripeManager:
|
|
186
187
|
def charge_prepaid(self, user_id: str, reference_code: str, amount: Optional[float] = None) -> Dict[str, Any]:
|
187
188
|
"""
|
188
189
|
Charge saved payment method for credit purchase.
|
189
|
-
Supports both fixed-price and
|
190
|
+
Supports both fixed-price and metadata-based variable-amount plans.
|
190
191
|
"""
|
191
192
|
try:
|
192
193
|
customer = self._get_or_create_customer(user_id)
|
193
194
|
|
194
|
-
#
|
195
|
-
|
195
|
+
# Look up price from Stripe
|
196
|
+
prices = stripe.Price.list(active=True, limit=100, expand=["data.product"])
|
197
|
+
price = None
|
198
|
+
|
199
|
+
for p in prices.data:
|
200
|
+
metadata = p.metadata or {}
|
201
|
+
# Match by price ID or plan_reference in metadata
|
202
|
+
if (p.id == reference_code or
|
203
|
+
metadata.get("plan_reference") == reference_code or
|
204
|
+
(metadata.get("tier") == reference_code and
|
205
|
+
metadata.get("environment") == self.environment)):
|
206
|
+
price = p
|
207
|
+
break
|
208
|
+
|
209
|
+
if not price:
|
210
|
+
raise ValueError(f"Invalid plan reference: {reference_code}")
|
196
211
|
|
197
|
-
|
212
|
+
price_metadata = price.metadata or {}
|
213
|
+
|
214
|
+
# Check if this is a variable amount plan
|
215
|
+
if price_metadata.get("variable_amount") == "true":
|
198
216
|
# Variable amount plan - validate amount
|
199
217
|
if not amount:
|
200
218
|
raise ValueError("Amount required for variable-amount plan")
|
201
219
|
|
202
|
-
#
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
if
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
220
|
+
# Get validation rules from metadata
|
221
|
+
min_amount = float(price_metadata.get("min_amount", "5"))
|
222
|
+
if amount < min_amount:
|
223
|
+
raise ValueError(f"Amount ${amount} is below minimum ${min_amount}")
|
224
|
+
|
225
|
+
# Check against default amounts if specified
|
226
|
+
default_amounts_str = price_metadata.get("default_amounts", "")
|
227
|
+
if default_amounts_str:
|
228
|
+
allowed_amounts = [float(x.strip()) for x in default_amounts_str.split(",")]
|
229
|
+
# Allow default amounts OR any amount >= minimum
|
230
|
+
if amount not in allowed_amounts and amount < max(allowed_amounts):
|
231
|
+
logger.info(f"Amount ${amount} not in defaults {allowed_amounts}, but allowed as >= ${min_amount}")
|
232
|
+
|
233
|
+
# Calculate credits based on credits_per_dollar
|
234
|
+
credits_per_dollar = float(price_metadata.get("credits_per_dollar", "285"))
|
214
235
|
credits_to_add = int(amount * credits_per_dollar)
|
215
236
|
charge_amount = int(amount * 100) # Convert to cents
|
216
237
|
|
217
238
|
else:
|
218
|
-
# Fixed price plan
|
219
|
-
prices = stripe.Price.list(active=True, limit=100)
|
220
|
-
price = None
|
221
|
-
|
222
|
-
for p in prices.data:
|
223
|
-
if p.id == reference_code or p.metadata.get("plan_reference") == reference_code:
|
224
|
-
price = p
|
225
|
-
break
|
226
|
-
|
227
|
-
if not price:
|
228
|
-
raise ValueError(f"Invalid plan reference: {reference_code}")
|
229
|
-
|
239
|
+
# Fixed price plan
|
230
240
|
charge_amount = price.unit_amount
|
231
|
-
|
241
|
+
credits_str = price_metadata.get("credits", "0")
|
242
|
+
if credits_str.lower() == "unlimited":
|
243
|
+
credits_to_add = 0 # Subscription handles this differently
|
244
|
+
else:
|
245
|
+
credits_to_add = int(credits_str)
|
232
246
|
|
233
247
|
# Get default payment method
|
234
248
|
default_pm = customer.invoice_settings.get("default_payment_method")
|
@@ -431,7 +445,7 @@ class StripeManager:
|
|
431
445
|
"""Return mock plans for development/testing."""
|
432
446
|
return [
|
433
447
|
Plan(
|
434
|
-
plan_reference="
|
448
|
+
plan_reference="price_standard_mock",
|
435
449
|
plan_type="prepaid",
|
436
450
|
plan_name="STANDARD",
|
437
451
|
plan_subtitle="One-time purchase",
|
@@ -441,7 +455,7 @@ class StripeManager:
|
|
441
455
|
percent_off=""
|
442
456
|
),
|
443
457
|
Plan(
|
444
|
-
plan_reference="
|
458
|
+
plan_reference="price_power_mock",
|
445
459
|
plan_type="prepaid",
|
446
460
|
plan_name="POWER",
|
447
461
|
plan_subtitle="Best value",
|
@@ -451,7 +465,7 @@ class StripeManager:
|
|
451
465
|
percent_off="12.5% OFF"
|
452
466
|
),
|
453
467
|
Plan(
|
454
|
-
plan_reference="
|
468
|
+
plan_reference="price_elite_mock",
|
455
469
|
plan_type="prepaid",
|
456
470
|
plan_name="ELITE",
|
457
471
|
plan_subtitle="Maximum savings",
|
@@ -461,7 +475,7 @@ class StripeManager:
|
|
461
475
|
percent_off="25% OFF"
|
462
476
|
),
|
463
477
|
Plan(
|
464
|
-
plan_reference="
|
478
|
+
plan_reference="price_unlimited_mock",
|
465
479
|
plan_type="postpaid",
|
466
480
|
plan_name="UNLIMITED",
|
467
481
|
plan_subtitle="Monthly subscription",
|
@@ -49,7 +49,10 @@ class WebhookProcessor:
|
|
49
49
|
|
50
50
|
logger.info(f"Processing webhook event: {event_type}")
|
51
51
|
|
52
|
-
if event_type == "
|
52
|
+
if event_type == "payment_intent.succeeded":
|
53
|
+
return self._handle_payment_intent_succeeded(event_data)
|
54
|
+
|
55
|
+
elif event_type == "checkout.session.completed":
|
53
56
|
return self._handle_checkout_completed(event_data)
|
54
57
|
|
55
58
|
elif event_type == "customer.subscription.created":
|
@@ -67,6 +70,9 @@ class WebhookProcessor:
|
|
67
70
|
elif event_type == "invoice.payment_failed":
|
68
71
|
return self._handle_invoice_failed(event_data)
|
69
72
|
|
73
|
+
elif event_type == "charge.dispute.created":
|
74
|
+
return self._handle_dispute_created(event_data)
|
75
|
+
|
70
76
|
else:
|
71
77
|
logger.info(f"Unhandled event type: {event_type}")
|
72
78
|
return {"message": f"Event {event_type} received but not processed"}
|
@@ -161,3 +167,49 @@ class WebhookProcessor:
|
|
161
167
|
logger.warning(f"Invoice payment failed for customer {customer_id}")
|
162
168
|
# Could pause subscription or send notification here
|
163
169
|
return {"status": "payment_failed"}
|
170
|
+
|
171
|
+
def _handle_payment_intent_succeeded(self, payment_intent: Dict[str, Any]) -> Dict[str, Any]:
|
172
|
+
"""Handle successful payment intent (credit purchase)."""
|
173
|
+
metadata = payment_intent.get("metadata", {})
|
174
|
+
user_id = metadata.get("user_id")
|
175
|
+
|
176
|
+
if not user_id:
|
177
|
+
logger.error("No user_id in payment_intent metadata")
|
178
|
+
return {"error": "Missing user_id"}
|
179
|
+
|
180
|
+
# Check if this is a verification charge ($1)
|
181
|
+
if metadata.get("type") == "verification":
|
182
|
+
# This was the $1 verification, credits already added in payment_setup handler
|
183
|
+
logger.info(f"Verification charge completed for user {user_id}")
|
184
|
+
return {"type": "verification", "status": "completed"}
|
185
|
+
|
186
|
+
# Get credits from metadata (set during payment creation)
|
187
|
+
credits = int(metadata.get("credits", 0))
|
188
|
+
|
189
|
+
if credits > 0:
|
190
|
+
new_balance = self.credit_manager.add_credits(user_id, credits)
|
191
|
+
logger.info(f"Added {credits} credits to user {user_id}, new balance: {new_balance}")
|
192
|
+
return {"credits_added": credits, "new_balance": new_balance}
|
193
|
+
|
194
|
+
return {"message": "Payment processed"}
|
195
|
+
|
196
|
+
def _handle_dispute_created(self, dispute: Dict[str, Any]) -> Dict[str, Any]:
|
197
|
+
"""Handle charge dispute (mark account as disputed)."""
|
198
|
+
# Get the charge and its metadata
|
199
|
+
charge_id = dispute.get("charge")
|
200
|
+
|
201
|
+
if not charge_id:
|
202
|
+
logger.error("No charge_id in dispute")
|
203
|
+
return {"error": "Missing charge_id"}
|
204
|
+
|
205
|
+
# In production, would fetch the charge from Stripe to get metadata
|
206
|
+
# For now, log the dispute for manual handling
|
207
|
+
amount = dispute.get("amount", 0) / 100.0
|
208
|
+
reason = dispute.get("reason", "unknown")
|
209
|
+
|
210
|
+
logger.warning(f"Dispute created for charge {charge_id}: ${amount}, reason: {reason}")
|
211
|
+
|
212
|
+
# TODO: Mark user account as disputed in CreditsTable
|
213
|
+
# This would prevent new purchases until resolved
|
214
|
+
|
215
|
+
return {"dispute_id": dispute.get("id"), "status": "created", "amount": amount}
|
@@ -1,4 +1,4 @@
|
|
1
|
-
ai_lls_lib/__init__.py,sha256=
|
1
|
+
ai_lls_lib/__init__.py,sha256=nkkaFByPHCiQNRa0kvk3DVE4Yh8eicP2KHfuSuE4tlc,584
|
2
2
|
ai_lls_lib/auth/__init__.py,sha256=c6zomHSB6y9Seakf84ciGsD3XgWarIty9xty6P8fxVw,194
|
3
3
|
ai_lls_lib/auth/context_parser.py,sha256=8I0vGbtykNLWqm8ldedxXjE-E3nqsCy113JgeyuiJoM,2222
|
4
4
|
ai_lls_lib/cli/__init__.py,sha256=m9qjZTW1jpENwXAUeuRrlP0b66BWRcqSO28MSjvOyCs,74
|
@@ -7,7 +7,7 @@ ai_lls_lib/cli/aws_client.py,sha256=YcCWCpTNOW9JPLxSNLRy5-F5HPKguJJk7dPNrPqhJv0,
|
|
7
7
|
ai_lls_lib/cli/commands/__init__.py,sha256=_kROrYuR_p2i110c0OvNeArfEFQbn15zR1c3pdeZOoo,28
|
8
8
|
ai_lls_lib/cli/commands/admin.py,sha256=bNBJi2fZBP0J40JQP6HP7NYadNmI214iII1TeLhooyE,6687
|
9
9
|
ai_lls_lib/cli/commands/cache.py,sha256=vWt0vy4L9CEgUEWUzfdehU6u43PE8vUvHx7xxg4e5tw,5680
|
10
|
-
ai_lls_lib/cli/commands/stripe.py,sha256=
|
10
|
+
ai_lls_lib/cli/commands/stripe.py,sha256=Iccp8ZmNE18q9rvoox-Mo7dZgByEhIVP7otRklgr4uI,12707
|
11
11
|
ai_lls_lib/cli/commands/test_stack.py,sha256=rNq4mhRXX7Ixo67kSoEPWlxqgXCzM9e2PR96qTAG7pE,7378
|
12
12
|
ai_lls_lib/cli/commands/verify.py,sha256=V5ucjmjCUxqN8_AeEJWWgrmYin8BNV3h4WbZ-iX3loU,4238
|
13
13
|
ai_lls_lib/cli/env_loader.py,sha256=YTCB6QDyknOuedPbQsW4bezB5SgHzJMGjhpSPArHFVc,3762
|
@@ -17,17 +17,17 @@ ai_lls_lib/core/models.py,sha256=ABRYaeMCWahQh4WdbkCxt3AO0-EvPWZwlL-JITQSnEM,238
|
|
17
17
|
ai_lls_lib/core/processor.py,sha256=6752IPDQ-Mz5i_CU7aBM0UjvV7IjyZFl35LKVPkHMpc,9974
|
18
18
|
ai_lls_lib/core/verifier.py,sha256=6uB_jawWoIqsNYAadTKr0lSolIWygg2gK6ykf8lrul0,2716
|
19
19
|
ai_lls_lib/payment/__init__.py,sha256=xhUWgfLnk3syXXQItswmDXdfXUyJTXTQAA0zIUuCVII,295
|
20
|
-
ai_lls_lib/payment/credit_manager.py,sha256=
|
20
|
+
ai_lls_lib/payment/credit_manager.py,sha256=ynjweRkbdHI-A6fROUoqlazNDmXOugXsIaMco7k62S0,7147
|
21
21
|
ai_lls_lib/payment/models.py,sha256=JjSmWKwpuFF85Jzmabj6y7UyolJlJlsh5CmOWRg21B8,3339
|
22
|
-
ai_lls_lib/payment/stripe_manager.py,sha256
|
23
|
-
ai_lls_lib/payment/webhook_processor.py,sha256=
|
22
|
+
ai_lls_lib/payment/stripe_manager.py,sha256=R_M4Bii9BGianL_STqvz4UU8ww8mlCByGo66V-e2C6Y,18027
|
23
|
+
ai_lls_lib/payment/webhook_processor.py,sha256=cIZqCS98Q305SCI3-4AJ1h-IJ-l-7fMby1FFy1ckP6k,8765
|
24
24
|
ai_lls_lib/providers/__init__.py,sha256=AEv3ARenWDwDo5PLCoszP2fQ70RgSHkrSLSUz7xHJDk,179
|
25
25
|
ai_lls_lib/providers/base.py,sha256=344XYOg7bxDQMWJ6Lle8U7NOHpabnCp0XYbZpeWpPAk,681
|
26
26
|
ai_lls_lib/providers/external.py,sha256=-Hhnlm8lQWRcWr5vG0dmD3sca2rohURrx0usOR2y1MM,2623
|
27
27
|
ai_lls_lib/providers/stub.py,sha256=847Tmw522B3HQ2j38BH1sdcZQy--RdtDcXsrIrFKNBQ,1150
|
28
28
|
ai_lls_lib/testing/__init__.py,sha256=RUxRYBzzPCPS15Umb6bUrE6rL5BQXBQf4SJM2E3ffrg,39
|
29
29
|
ai_lls_lib/testing/fixtures.py,sha256=_n6bbr95LnQf9Dvu1qKs2HsvHEA7AAbe59B75qxE10w,3310
|
30
|
-
ai_lls_lib-1.
|
31
|
-
ai_lls_lib-1.
|
32
|
-
ai_lls_lib-1.
|
33
|
-
ai_lls_lib-1.
|
30
|
+
ai_lls_lib-1.3.1.dist-info/METADATA,sha256=AxasNt6DhVulTID8lw_C7FFuDCJMs_dPLFicO7fPK6s,7248
|
31
|
+
ai_lls_lib-1.3.1.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
32
|
+
ai_lls_lib-1.3.1.dist-info/entry_points.txt,sha256=Pi0V_HBViEKGFbNQKatl5lhhnHHBXlxaom-5gH9gXZ0,55
|
33
|
+
ai_lls_lib-1.3.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|