ai-lls-lib 1.2.0__py3-none-any.whl → 1.3.0__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.0"
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": "",
@@ -186,49 +186,62 @@ class StripeManager:
186
186
  def charge_prepaid(self, user_id: str, reference_code: str, amount: Optional[float] = None) -> Dict[str, Any]:
187
187
  """
188
188
  Charge saved payment method for credit purchase.
189
- Supports both fixed-price and legacy variable-amount plans.
189
+ Supports both fixed-price and metadata-based variable-amount plans.
190
190
  """
191
191
  try:
192
192
  customer = self._get_or_create_customer(user_id)
193
193
 
194
- # Legacy variable-amount plan references
195
- VARIABLE_AMOUNT_REFS = ["79541679412215", "79541679412216"]
194
+ # Look up price from Stripe
195
+ prices = stripe.Price.list(active=True, limit=100, expand=["data.product"])
196
+ price = None
196
197
 
197
- if reference_code in VARIABLE_AMOUNT_REFS:
198
+ for p in prices.data:
199
+ metadata = p.metadata or {}
200
+ # Match by price ID or plan_reference in metadata
201
+ if (p.id == reference_code or
202
+ metadata.get("plan_reference") == reference_code or
203
+ (metadata.get("tier") == reference_code and
204
+ metadata.get("environment") == self.environment)):
205
+ price = p
206
+ break
207
+
208
+ if not price:
209
+ raise ValueError(f"Invalid plan reference: {reference_code}")
210
+
211
+ price_metadata = price.metadata or {}
212
+
213
+ # Check if this is a variable amount plan
214
+ if price_metadata.get("variable_amount") == "true":
198
215
  # Variable amount plan - validate amount
199
216
  if not amount:
200
217
  raise ValueError("Amount required for variable-amount plan")
201
218
 
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
-
219
+ # Get validation rules from metadata
220
+ min_amount = float(price_metadata.get("min_amount", "5"))
221
+ if amount < min_amount:
222
+ raise ValueError(f"Amount ${amount} is below minimum ${min_amount}")
223
+
224
+ # Check against default amounts if specified
225
+ default_amounts_str = price_metadata.get("default_amounts", "")
226
+ if default_amounts_str:
227
+ allowed_amounts = [float(x.strip()) for x in default_amounts_str.split(",")]
228
+ # Allow default amounts OR any amount >= minimum
229
+ if amount not in allowed_amounts and amount < max(allowed_amounts):
230
+ logger.info(f"Amount ${amount} not in defaults {allowed_amounts}, but allowed as >= ${min_amount}")
231
+
232
+ # Calculate credits based on credits_per_dollar
233
+ credits_per_dollar = float(price_metadata.get("credits_per_dollar", "285"))
214
234
  credits_to_add = int(amount * credits_per_dollar)
215
235
  charge_amount = int(amount * 100) # Convert to cents
216
236
 
217
237
  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
-
238
+ # Fixed price plan
230
239
  charge_amount = price.unit_amount
231
- credits_to_add = int(price.metadata.get("credits", 0))
240
+ credits_str = price_metadata.get("credits", "0")
241
+ if credits_str.lower() == "unlimited":
242
+ credits_to_add = 0 # Subscription handles this differently
243
+ else:
244
+ credits_to_add = int(credits_str)
232
245
 
233
246
  # Get default payment method
234
247
  default_pm = customer.invoice_settings.get("default_payment_method")
@@ -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.0
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=qeDhkuaSaxKB6Moq7QFQcEkVg-preOmw_-zvG41ORc4,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
@@ -19,15 +19,15 @@ ai_lls_lib/core/verifier.py,sha256=6uB_jawWoIqsNYAadTKr0lSolIWygg2gK6ykf8lrul0,2
19
19
  ai_lls_lib/payment/__init__.py,sha256=xhUWgfLnk3syXXQItswmDXdfXUyJTXTQAA0zIUuCVII,295
20
20
  ai_lls_lib/payment/credit_manager.py,sha256=TDrdW7BYjhtlg8MX_EJEnMNct1SU_6Tdb_k19_e8kpo,6343
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=Yc-2gAlGIf1EtBPpVUAQYH7oyis2Fk8k_9qAwoWtlYE,17969
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.0.dist-info/METADATA,sha256=3rY5rpASz-JFelJRnWL9yM9o9vrFfRuu4AuGn0IcyXE,7248
31
+ ai_lls_lib-1.3.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
32
+ ai_lls_lib-1.3.0.dist-info/entry_points.txt,sha256=Pi0V_HBViEKGFbNQKatl5lhhnHHBXlxaom-5gH9gXZ0,55
33
+ ai_lls_lib-1.3.0.dist-info/RECORD,,