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 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.2.0"
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 legacy variable-amount plans.
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
- # Legacy variable-amount plan references
195
- VARIABLE_AMOUNT_REFS = ["79541679412215", "79541679412216"]
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
- if reference_code in VARIABLE_AMOUNT_REFS:
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
- # Validate amount against allowed values from metadata
203
- # For now, allow common amounts
204
- allowed_amounts = [5, 10, 20, 25, 50, 75, 100, 150, 200, 250, 500]
205
- if amount not in allowed_amounts:
206
- raise ValueError(f"Invalid amount ${amount}. Allowed amounts: {allowed_amounts}")
207
-
208
- # Calculate credits based on tier
209
- if reference_code == "79541679412215": # STANDARD
210
- credits_per_dollar = 500
211
- else: # POWER
212
- credits_per_dollar = 570
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 - look up from Stripe
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
- credits_to_add = int(price.metadata.get("credits", 0))
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="79541679412215",
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="79541679412216",
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="79541679412217",
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="price_unlimited",
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 == "checkout.session.completed":
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,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: ai-lls-lib
3
- Version: 1.2.0
3
+ Version: 1.3.1
4
4
  Summary: Landline Scrubber core library - phone verification and DNC checking
5
5
  Author: LandlineScrubber Team
6
6
  Requires-Python: >=3.12,<4.0
@@ -1,4 +1,4 @@
1
- ai_lls_lib/__init__.py,sha256=vX28RZKgWt_4PDkQ_H69AwCCp7Jt9nHwhxt4RhKOotU,584
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=x5QtirbCPiBrjdMRVTR0aeQN2gy0_p6jcGzELObFfzg,12006
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=TDrdW7BYjhtlg8MX_EJEnMNct1SU_6Tdb_k19_e8kpo,6343
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=-L329stuu-6V_QKzRdkm9qnoi74SbWzST0fuf2WAJXA,17111
23
- ai_lls_lib/payment/webhook_processor.py,sha256=UVngeAGcou5ieRAp-49pqdWh0wsJQVwpiRoLmyl5Adc,6448
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.2.0.dist-info/METADATA,sha256=o76nenDApQ5pITFYnlmX7IS-maEi39JgU75iUyI9udk,7248
31
- ai_lls_lib-1.2.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
32
- ai_lls_lib-1.2.0.dist-info/entry_points.txt,sha256=Pi0V_HBViEKGFbNQKatl5lhhnHHBXlxaom-5gH9gXZ0,55
33
- ai_lls_lib-1.2.0.dist-info/RECORD,,
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,,