ai-lls-lib 1.0.0__tar.gz → 1.2.0__tar.gz

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.
Files changed (34) hide show
  1. {ai_lls_lib-1.0.0 → ai_lls_lib-1.2.0}/PKG-INFO +61 -8
  2. {ai_lls_lib-1.0.0 → ai_lls_lib-1.2.0}/README.md +59 -7
  3. {ai_lls_lib-1.0.0 → ai_lls_lib-1.2.0}/pyproject.toml +2 -1
  4. {ai_lls_lib-1.0.0 → ai_lls_lib-1.2.0}/src/ai_lls_lib/__init__.py +1 -1
  5. ai_lls_lib-1.2.0/src/ai_lls_lib/auth/__init__.py +4 -0
  6. ai_lls_lib-1.2.0/src/ai_lls_lib/auth/context_parser.py +68 -0
  7. {ai_lls_lib-1.0.0 → ai_lls_lib-1.2.0}/src/ai_lls_lib/cli/__main__.py +2 -1
  8. ai_lls_lib-1.2.0/src/ai_lls_lib/cli/commands/stripe.py +307 -0
  9. {ai_lls_lib-1.0.0 → ai_lls_lib-1.2.0}/src/ai_lls_lib/cli/commands/verify.py +3 -3
  10. ai_lls_lib-1.2.0/src/ai_lls_lib/cli/env_loader.py +122 -0
  11. ai_lls_lib-1.2.0/src/ai_lls_lib/core/processor.py +295 -0
  12. ai_lls_lib-1.2.0/src/ai_lls_lib/core/verifier.py +84 -0
  13. ai_lls_lib-1.2.0/src/ai_lls_lib/payment/__init__.py +13 -0
  14. ai_lls_lib-1.2.0/src/ai_lls_lib/payment/credit_manager.py +174 -0
  15. ai_lls_lib-1.2.0/src/ai_lls_lib/payment/models.py +96 -0
  16. ai_lls_lib-1.2.0/src/ai_lls_lib/payment/stripe_manager.py +473 -0
  17. ai_lls_lib-1.2.0/src/ai_lls_lib/payment/webhook_processor.py +163 -0
  18. ai_lls_lib-1.2.0/src/ai_lls_lib/providers/__init__.py +7 -0
  19. ai_lls_lib-1.2.0/src/ai_lls_lib/providers/base.py +28 -0
  20. ai_lls_lib-1.2.0/src/ai_lls_lib/providers/external.py +87 -0
  21. ai_lls_lib-1.2.0/src/ai_lls_lib/providers/stub.py +48 -0
  22. ai_lls_lib-1.0.0/src/ai_lls_lib/core/processor.py +0 -135
  23. ai_lls_lib-1.0.0/src/ai_lls_lib/core/verifier.py +0 -95
  24. {ai_lls_lib-1.0.0 → ai_lls_lib-1.2.0}/src/ai_lls_lib/cli/__init__.py +0 -0
  25. {ai_lls_lib-1.0.0 → ai_lls_lib-1.2.0}/src/ai_lls_lib/cli/aws_client.py +0 -0
  26. {ai_lls_lib-1.0.0 → ai_lls_lib-1.2.0}/src/ai_lls_lib/cli/commands/__init__.py +0 -0
  27. {ai_lls_lib-1.0.0 → ai_lls_lib-1.2.0}/src/ai_lls_lib/cli/commands/admin.py +0 -0
  28. {ai_lls_lib-1.0.0 → ai_lls_lib-1.2.0}/src/ai_lls_lib/cli/commands/cache.py +0 -0
  29. {ai_lls_lib-1.0.0 → ai_lls_lib-1.2.0}/src/ai_lls_lib/cli/commands/test_stack.py +0 -0
  30. {ai_lls_lib-1.0.0 → ai_lls_lib-1.2.0}/src/ai_lls_lib/core/__init__.py +0 -0
  31. {ai_lls_lib-1.0.0 → ai_lls_lib-1.2.0}/src/ai_lls_lib/core/cache.py +0 -0
  32. {ai_lls_lib-1.0.0 → ai_lls_lib-1.2.0}/src/ai_lls_lib/core/models.py +0 -0
  33. {ai_lls_lib-1.0.0 → ai_lls_lib-1.2.0}/src/ai_lls_lib/testing/__init__.py +0 -0
  34. {ai_lls_lib-1.0.0 → ai_lls_lib-1.2.0}/src/ai_lls_lib/testing/fixtures.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: ai-lls-lib
3
- Version: 1.0.0
3
+ Version: 1.2.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
@@ -14,12 +14,28 @@ Requires-Dist: httpx (>=0.25.0,<0.26.0)
14
14
  Requires-Dist: phonenumbers (>=8.13.0,<9.0.0)
15
15
  Requires-Dist: pydantic (>=2.5.0,<3.0.0)
16
16
  Requires-Dist: rich (>=14.0,<15.0)
17
+ Requires-Dist: stripe (>=12.5.1,<13.0.0)
17
18
  Description-Content-Type: text/markdown
18
19
 
19
20
  # AI LLS Library
20
21
 
21
22
  Core business logic library and CLI tools for Landline Scrubber - phone verification and DNC checking.
22
23
 
24
+ ## Version 2.1.0 - Streaming & Provider Architecture
25
+
26
+ New features:
27
+ - **Streaming support** for large CSV files to reduce memory usage
28
+ - **Provider architecture** for clean separation of verification logic
29
+ - **Contract tests** ensuring all providers behave consistently
30
+
31
+ ## Version 2.0.0 - Breaking Changes
32
+
33
+ This is a greenfield rewrite with no backwards compatibility:
34
+ - All file-based CSV processing replaced with text-based methods
35
+ - Removed `_sync` suffix from all methods (everything is sync)
36
+ - `process_csv_sync(file_path)` → `process_csv(csv_text)`
37
+ - `generate_results_csv(...)` now returns CSV string instead of writing to file
38
+
23
39
  ## Features
24
40
 
25
41
  - Phone number normalization (E.164 format)
@@ -50,7 +66,7 @@ from ai_lls_lib import PhoneVerifier, DynamoDBCache
50
66
  cache = DynamoDBCache(table_name="phone-cache")
51
67
  verifier = PhoneVerifier(cache)
52
68
 
53
- result = verifier.verify_sync("+15551234567")
69
+ result = verifier.verify("+15551234567")
54
70
  print(f"Line type: {result.line_type}")
55
71
  print(f"DNC: {result.dnc}")
56
72
  print(f"From cache: {result.cached}")
@@ -65,12 +81,49 @@ cache = DynamoDBCache(table_name="phone-cache")
65
81
  verifier = PhoneVerifier(cache)
66
82
  processor = BulkProcessor(verifier)
67
83
 
68
- results = processor.process_csv_sync("/path/to/phones.csv")
69
- processor.generate_results_csv(
70
- original_path="/path/to/phones.csv",
71
- results=results,
72
- output_path="/path/to/results.csv"
73
- )
84
+ # Process CSV text content
85
+ csv_text = "name,phone\nJohn,+15551234567\nJane,+15551234568"
86
+ results = processor.process_csv(csv_text)
87
+
88
+ # Generate results CSV
89
+ results_csv = processor.generate_results_csv(csv_text, results)
90
+ print(results_csv) # CSV string with added line_type, dnc, cached columns
91
+ ```
92
+
93
+ ### Streaming Large Files
94
+
95
+ For memory-efficient processing of large CSV files:
96
+
97
+ ```python
98
+ from ai_lls_lib import BulkProcessor, PhoneVerifier, DynamoDBCache
99
+
100
+ cache = DynamoDBCache(table_name="phone-cache")
101
+ verifier = PhoneVerifier(cache)
102
+ processor = BulkProcessor(verifier)
103
+
104
+ # Process CSV as a stream, yielding batches
105
+ csv_lines = open('large_file.csv').readlines()
106
+ for batch in processor.process_csv_stream(csv_lines, batch_size=100):
107
+ print(f"Processed batch of {len(batch)} phones")
108
+ # Each batch is a list of PhoneVerification objects
109
+ ```
110
+
111
+ ### Custom Verification Providers
112
+
113
+ Use different verification providers based on your needs:
114
+
115
+ ```python
116
+ from ai_lls_lib import PhoneVerifier, DynamoDBCache
117
+ from ai_lls_lib.providers import StubProvider
118
+
119
+ # Use stub provider for testing
120
+ cache = DynamoDBCache(table_name="phone-cache")
121
+ provider = StubProvider() # Deterministic testing provider
122
+ verifier = PhoneVerifier(cache, provider=provider)
123
+
124
+ # When external APIs are ready, switch to:
125
+ # from ai_lls_lib.providers.external import ExternalAPIProvider
126
+ # provider = ExternalAPIProvider(phone_api_key="...", dnc_api_key="...")
74
127
  ```
75
128
 
76
129
  ## CLI Usage
@@ -2,6 +2,21 @@
2
2
 
3
3
  Core business logic library and CLI tools for Landline Scrubber - phone verification and DNC checking.
4
4
 
5
+ ## Version 2.1.0 - Streaming & Provider Architecture
6
+
7
+ New features:
8
+ - **Streaming support** for large CSV files to reduce memory usage
9
+ - **Provider architecture** for clean separation of verification logic
10
+ - **Contract tests** ensuring all providers behave consistently
11
+
12
+ ## Version 2.0.0 - Breaking Changes
13
+
14
+ This is a greenfield rewrite with no backwards compatibility:
15
+ - All file-based CSV processing replaced with text-based methods
16
+ - Removed `_sync` suffix from all methods (everything is sync)
17
+ - `process_csv_sync(file_path)` → `process_csv(csv_text)`
18
+ - `generate_results_csv(...)` now returns CSV string instead of writing to file
19
+
5
20
  ## Features
6
21
 
7
22
  - Phone number normalization (E.164 format)
@@ -32,7 +47,7 @@ from ai_lls_lib import PhoneVerifier, DynamoDBCache
32
47
  cache = DynamoDBCache(table_name="phone-cache")
33
48
  verifier = PhoneVerifier(cache)
34
49
 
35
- result = verifier.verify_sync("+15551234567")
50
+ result = verifier.verify("+15551234567")
36
51
  print(f"Line type: {result.line_type}")
37
52
  print(f"DNC: {result.dnc}")
38
53
  print(f"From cache: {result.cached}")
@@ -47,12 +62,49 @@ cache = DynamoDBCache(table_name="phone-cache")
47
62
  verifier = PhoneVerifier(cache)
48
63
  processor = BulkProcessor(verifier)
49
64
 
50
- results = processor.process_csv_sync("/path/to/phones.csv")
51
- processor.generate_results_csv(
52
- original_path="/path/to/phones.csv",
53
- results=results,
54
- output_path="/path/to/results.csv"
55
- )
65
+ # Process CSV text content
66
+ csv_text = "name,phone\nJohn,+15551234567\nJane,+15551234568"
67
+ results = processor.process_csv(csv_text)
68
+
69
+ # Generate results CSV
70
+ results_csv = processor.generate_results_csv(csv_text, results)
71
+ print(results_csv) # CSV string with added line_type, dnc, cached columns
72
+ ```
73
+
74
+ ### Streaming Large Files
75
+
76
+ For memory-efficient processing of large CSV files:
77
+
78
+ ```python
79
+ from ai_lls_lib import BulkProcessor, PhoneVerifier, DynamoDBCache
80
+
81
+ cache = DynamoDBCache(table_name="phone-cache")
82
+ verifier = PhoneVerifier(cache)
83
+ processor = BulkProcessor(verifier)
84
+
85
+ # Process CSV as a stream, yielding batches
86
+ csv_lines = open('large_file.csv').readlines()
87
+ for batch in processor.process_csv_stream(csv_lines, batch_size=100):
88
+ print(f"Processed batch of {len(batch)} phones")
89
+ # Each batch is a list of PhoneVerification objects
90
+ ```
91
+
92
+ ### Custom Verification Providers
93
+
94
+ Use different verification providers based on your needs:
95
+
96
+ ```python
97
+ from ai_lls_lib import PhoneVerifier, DynamoDBCache
98
+ from ai_lls_lib.providers import StubProvider
99
+
100
+ # Use stub provider for testing
101
+ cache = DynamoDBCache(table_name="phone-cache")
102
+ provider = StubProvider() # Deterministic testing provider
103
+ verifier = PhoneVerifier(cache, provider=provider)
104
+
105
+ # When external APIs are ready, switch to:
106
+ # from ai_lls_lib.providers.external import ExternalAPIProvider
107
+ # provider = ExternalAPIProvider(phone_api_key="...", dnc_api_key="...")
56
108
  ```
57
109
 
58
110
  ## CLI Usage
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "ai-lls-lib"
3
- version = "1.0.0"
3
+ version = "1.2.0"
4
4
  description = "Landline Scrubber core library - phone verification and DNC checking"
5
5
  authors = ["LandlineScrubber Team"]
6
6
  readme = "README.md"
@@ -15,6 +15,7 @@ httpx = "^0.25.0"
15
15
  aws-lambda-powertools = "^2.30.0"
16
16
  click = "^8.1.0"
17
17
  rich = "^14.0"
18
+ stripe = "^12.5.1"
18
19
 
19
20
  [tool.poetry.group.dev.dependencies]
20
21
  pytest = "^7.4.0"
@@ -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.0.0"
16
+ __version__ = "1.2.0"
17
17
  __all__ = [
18
18
  "PhoneVerification",
19
19
  "BulkJob",
@@ -0,0 +1,4 @@
1
+ """Auth module for handling authentication and authorization."""
2
+ from .context_parser import get_email_from_event, get_user_from_event
3
+
4
+ __all__ = ["get_user_from_event", "get_email_from_event"]
@@ -0,0 +1,68 @@
1
+ """Auth context parser for HTTP API v2.0 events."""
2
+ from typing import Any, Dict, Optional
3
+
4
+
5
+ def get_user_from_event(event: Dict[str, Any]) -> Optional[str]:
6
+ """
7
+ Extract user ID from HTTP API v2.0 event with all possible paths.
8
+ Handles both JWT and API key authentication contexts.
9
+
10
+ This function handles the complexities of AWS API Gateway authorizer contexts,
11
+ especially when EnableSimpleResponses is set to false, which wraps the
12
+ context in a 'lambda' key.
13
+
14
+ Args:
15
+ event: The Lambda event from API Gateway HTTP API v2.0
16
+
17
+ Returns:
18
+ User ID string if found, None otherwise
19
+ """
20
+ request_context = event.get("requestContext", {})
21
+ auth = request_context.get("authorizer", {})
22
+
23
+ # Handle lambda-wrapped context (EnableSimpleResponses: false)
24
+ # When EnableSimpleResponses is false, the authorizer context is wrapped
25
+ lam_ctx = auth.get("lambda", auth) if isinstance(auth.get("lambda"), dict) else auth
26
+
27
+ # Try all possible paths for user_id in priority order
28
+ user_id = (
29
+ # Lambda authorizer paths (most common with current setup)
30
+ lam_ctx.get("principal_id") or
31
+ lam_ctx.get("principalId") or
32
+ lam_ctx.get("sub") or
33
+ lam_ctx.get("user_id") or
34
+ # JWT paths (when using JWT authorizer directly)
35
+ auth.get("jwt", {}).get("claims", {}).get("sub") or
36
+ # Direct auth paths (fallback)
37
+ auth.get("principal_id") or
38
+ auth.get("principalId") or
39
+ auth.get("sub")
40
+ )
41
+
42
+ return user_id
43
+
44
+
45
+ def get_email_from_event(event: Dict[str, Any]) -> Optional[str]:
46
+ """
47
+ Extract email from HTTP API v2.0 event.
48
+
49
+ Args:
50
+ event: The Lambda event from API Gateway HTTP API v2.0
51
+
52
+ Returns:
53
+ Email string if found, None otherwise
54
+ """
55
+ request_context = event.get("requestContext", {})
56
+ auth = request_context.get("authorizer", {})
57
+
58
+ # Handle lambda-wrapped context
59
+ lam_ctx = auth.get("lambda", auth) if isinstance(auth.get("lambda"), dict) else auth
60
+
61
+ # Try to get email from various locations
62
+ email = (
63
+ lam_ctx.get("email") or
64
+ auth.get("jwt", {}).get("claims", {}).get("email") or
65
+ auth.get("email")
66
+ )
67
+
68
+ return email
@@ -3,7 +3,7 @@ Landline Scrubber CLI entry point
3
3
  """
4
4
  import click
5
5
  import sys
6
- from ai_lls_lib.cli.commands import verify, cache, admin, test_stack
6
+ from ai_lls_lib.cli.commands import verify, cache, admin, test_stack, stripe
7
7
 
8
8
  @click.group()
9
9
  @click.version_option(version="0.1.0", prog_name="ai-lls")
@@ -16,6 +16,7 @@ cli.add_command(verify.verify_group)
16
16
  cli.add_command(cache.cache_group)
17
17
  cli.add_command(admin.admin_group)
18
18
  cli.add_command(test_stack.test_stack_group)
19
+ cli.add_command(stripe.stripe_group)
19
20
 
20
21
  def main():
21
22
  """Main entry point"""
@@ -0,0 +1,307 @@
1
+ """Stripe management CLI commands."""
2
+
3
+ import click
4
+ import os
5
+ import json
6
+ from typing import Optional
7
+ from ..env_loader import load_environment_config, get_stripe_key
8
+
9
+ @click.group(name="stripe")
10
+ def stripe_group():
11
+ """Manage Stripe products and prices."""
12
+ pass
13
+
14
+
15
+ @stripe_group.command("seed")
16
+ @click.option("--environment", type=click.Choice(["staging", "production"]), required=True)
17
+ @click.option("--api-key", help="Stripe API key (overrides environment)")
18
+ @click.option("--dry-run", is_flag=True, help="Show what would be created without making changes")
19
+ def seed_products(environment: str, api_key: Optional[str], dry_run: bool):
20
+ """Create or update Stripe products and prices with metadata."""
21
+ try:
22
+ import stripe
23
+ except ImportError:
24
+ click.echo("Error: stripe package not installed. Run: pip install stripe", err=True)
25
+ return
26
+
27
+ # Load API key from environment if not provided
28
+ if not api_key:
29
+ api_key = get_stripe_key(environment)
30
+ if not api_key:
31
+ click.echo(f"Error: No Stripe API key found for {environment} environment", err=True)
32
+ click.echo(f"Set {environment.upper()}_STRIPE_SECRET_KEY or STRIPE_SECRET_KEY", err=True)
33
+ return
34
+ click.echo(f"Using Stripe key for {environment} environment", err=True)
35
+
36
+ stripe.api_key = api_key
37
+
38
+ # Define the products and prices to create
39
+ products_config = [
40
+ {
41
+ "name": "Landline Scrubber - STANDARD",
42
+ "description": "One-time purchase",
43
+ "metadata": {
44
+ "product_type": "landline_scrubber",
45
+ "environment": environment,
46
+ "tier": "STANDARD"
47
+ },
48
+ "price": {
49
+ "unit_amount": 1000, # $10.00
50
+ "currency": "usd",
51
+ "metadata": {
52
+ "product_type": "landline_scrubber",
53
+ "environment": environment,
54
+ "plan_type": "prepaid",
55
+ "tier": "STANDARD",
56
+ "plan_reference": "79541679412215", # Legacy ID for compatibility
57
+ "credits": "5000",
58
+ "plan_credits_text": "5,000 credits",
59
+ "percent_off": "",
60
+ "active": "true"
61
+ }
62
+ }
63
+ },
64
+ {
65
+ "name": "Landline Scrubber - POWER",
66
+ "description": "Best value",
67
+ "metadata": {
68
+ "product_type": "landline_scrubber",
69
+ "environment": environment,
70
+ "tier": "POWER"
71
+ },
72
+ "price": {
73
+ "unit_amount": 5000, # $50.00
74
+ "currency": "usd",
75
+ "metadata": {
76
+ "product_type": "landline_scrubber",
77
+ "environment": environment,
78
+ "plan_type": "prepaid",
79
+ "tier": "POWER",
80
+ "plan_reference": "79541679412216", # Legacy ID for compatibility
81
+ "credits": "28500",
82
+ "plan_credits_text": "28,500 credits",
83
+ "percent_off": "12.5% OFF",
84
+ "active": "true"
85
+ }
86
+ }
87
+ },
88
+ {
89
+ "name": "Landline Scrubber - ELITE",
90
+ "description": "Maximum savings",
91
+ "metadata": {
92
+ "product_type": "landline_scrubber",
93
+ "environment": environment,
94
+ "tier": "ELITE"
95
+ },
96
+ "price": {
97
+ "unit_amount": 10000, # $100.00
98
+ "currency": "usd",
99
+ "metadata": {
100
+ "product_type": "landline_scrubber",
101
+ "environment": environment,
102
+ "plan_type": "prepaid",
103
+ "tier": "ELITE",
104
+ "plan_reference": "79541679412217", # Legacy ID for compatibility
105
+ "credits": "66666",
106
+ "plan_credits_text": "66,666 credits",
107
+ "percent_off": "25% OFF",
108
+ "active": "true"
109
+ }
110
+ }
111
+ },
112
+ {
113
+ "name": "Landline Scrubber - UNLIMITED",
114
+ "description": "Monthly subscription",
115
+ "metadata": {
116
+ "product_type": "landline_scrubber",
117
+ "environment": environment,
118
+ "tier": "UNLIMITED"
119
+ },
120
+ "price": {
121
+ "unit_amount": 29900, # $299.00
122
+ "currency": "usd",
123
+ "recurring": {"interval": "month"},
124
+ "metadata": {
125
+ "product_type": "landline_scrubber",
126
+ "environment": environment,
127
+ "plan_type": "postpaid",
128
+ "tier": "UNLIMITED",
129
+ "plan_reference": "price_unlimited",
130
+ "credits": "unlimited",
131
+ "plan_credits_text": "Unlimited",
132
+ "percent_off": "",
133
+ "active": "true"
134
+ }
135
+ }
136
+ }
137
+ ]
138
+
139
+ if dry_run:
140
+ click.echo("DRY RUN - Would create the following:")
141
+ for config in products_config:
142
+ click.echo(f"\nProduct: {config['name']}")
143
+ click.echo(f" Description: {config['description']}")
144
+ click.echo(f" Price: ${config['price']['unit_amount'] / 100:.2f}")
145
+ if "recurring" in config["price"]:
146
+ click.echo(f" Billing: Monthly subscription")
147
+ else:
148
+ click.echo(f" Billing: One-time payment")
149
+ return
150
+
151
+ created_prices = []
152
+
153
+ for config in products_config:
154
+ try:
155
+ # Check if product already exists
156
+ existing_products = stripe.Product.list(limit=100)
157
+ product = None
158
+ for p in existing_products.data:
159
+ if (p.metadata.get("product_type") == "landline_scrubber" and
160
+ p.metadata.get("environment") == environment and
161
+ p.metadata.get("tier") == config["metadata"]["tier"]):
162
+ product = p
163
+ click.echo(f"Found existing product: {product.name}")
164
+ break
165
+
166
+ if not product:
167
+ # Create new product
168
+ product = stripe.Product.create(
169
+ name=config["name"],
170
+ description=config["description"],
171
+ metadata=config["metadata"]
172
+ )
173
+ click.echo(f"Created product: {product.name}")
174
+
175
+ # Create price (always create new prices, don't modify existing)
176
+ price_data = {
177
+ "product": product.id,
178
+ "unit_amount": config["price"]["unit_amount"],
179
+ "currency": config["price"]["currency"],
180
+ "metadata": config["price"]["metadata"]
181
+ }
182
+
183
+ if "recurring" in config["price"]:
184
+ price_data["recurring"] = config["price"]["recurring"]
185
+
186
+ price = stripe.Price.create(**price_data)
187
+ created_prices.append(price.id)
188
+ click.echo(f" Created price: {price.id} (${price.unit_amount / 100:.2f})")
189
+
190
+ except stripe.error.StripeError as e:
191
+ click.echo(f"Error creating {config['name']}: {e}", err=True)
192
+
193
+ if created_prices:
194
+ click.echo(f"\nCreated {len(created_prices)} prices for {environment} environment")
195
+ click.echo("\nPrice IDs:")
196
+ for price_id in created_prices:
197
+ click.echo(f" {price_id}")
198
+
199
+
200
+ @stripe_group.command("list")
201
+ @click.option("--environment", type=click.Choice(["staging", "production"]), default="staging")
202
+ @click.option("--api-key", help="Stripe API key (overrides environment)")
203
+ @click.option("--json", "output_json", is_flag=True, help="Output as JSON")
204
+ def list_products(environment: str, api_key: Optional[str], output_json: bool):
205
+ """List all products and prices with metadata."""
206
+ try:
207
+ from ai_lls_lib.payment import StripeManager
208
+ except ImportError:
209
+ click.echo("Error: Payment module not found", err=True)
210
+ return
211
+
212
+ # Load API key from environment if not provided
213
+ if not api_key:
214
+ api_key = get_stripe_key(environment)
215
+ if not api_key:
216
+ click.echo(f"Error: No Stripe API key found for {environment} environment", err=True)
217
+ click.echo(f"Set {environment.upper()}_STRIPE_SECRET_KEY or STRIPE_SECRET_KEY", err=True)
218
+ return
219
+
220
+ try:
221
+ manager = StripeManager(api_key=api_key, environment=environment)
222
+ plans = manager.list_plans()
223
+
224
+ if output_json:
225
+ output = [plan.to_dict() for plan in plans]
226
+ click.echo(json.dumps(output, indent=2))
227
+ else:
228
+ click.echo(f"Active plans for {environment} environment:\n")
229
+ for plan in plans:
230
+ click.echo(f"{plan.plan_name}:")
231
+ click.echo(f" Price: ${plan.plan_amount:.2f}")
232
+ click.echo(f" Credits: {plan.plan_credits_text}")
233
+ click.echo(f" Type: {plan.plan_type}")
234
+ click.echo(f" Reference: {plan.plan_reference}")
235
+ if plan.percent_off:
236
+ click.echo(f" Discount: {plan.percent_off}")
237
+ click.echo()
238
+
239
+ except Exception as e:
240
+ click.echo(f"Error: {e}", err=True)
241
+
242
+
243
+ @stripe_group.command("webhook")
244
+ @click.option("--endpoint-url", help="Webhook endpoint URL")
245
+ @click.option("--environment", type=click.Choice(["staging", "production"]), default="staging")
246
+ @click.option("--api-key", help="Stripe API key (overrides environment)")
247
+ @click.option("--print-secret", is_flag=True, help="Print the webhook signing secret")
248
+ def setup_webhook(endpoint_url: Optional[str], environment: str, api_key: Optional[str], print_secret: bool):
249
+ """Configure or display webhook endpoint."""
250
+ try:
251
+ import stripe
252
+ except ImportError:
253
+ click.echo("Error: stripe package not installed. Run: pip install stripe", err=True)
254
+ return
255
+
256
+ # Load API key from environment if not provided
257
+ if not api_key:
258
+ api_key = get_stripe_key(environment)
259
+ if not api_key:
260
+ click.echo(f"Error: No Stripe API key found for {environment} environment", err=True)
261
+ click.echo(f"Set {environment.upper()}_STRIPE_SECRET_KEY or STRIPE_SECRET_KEY", err=True)
262
+ return
263
+
264
+ stripe.api_key = api_key
265
+
266
+ if print_secret:
267
+ # List existing webhooks
268
+ webhooks = stripe.WebhookEndpoint.list(limit=10)
269
+ if webhooks.data:
270
+ click.echo("Existing webhook endpoints:\n")
271
+ for webhook in webhooks.data:
272
+ click.echo(f"URL: {webhook.url}")
273
+ click.echo(f"ID: {webhook.id}")
274
+ click.echo(f"Secret: {webhook.secret}")
275
+ click.echo(f"Status: {webhook.status}")
276
+ click.echo()
277
+ else:
278
+ click.echo("No webhook endpoints configured")
279
+ return
280
+
281
+ if not endpoint_url:
282
+ click.echo("Error: --endpoint-url required to create webhook", err=True)
283
+ return
284
+
285
+ try:
286
+ # Create webhook endpoint
287
+ webhook = stripe.WebhookEndpoint.create(
288
+ url=endpoint_url,
289
+ enabled_events=[
290
+ "checkout.session.completed",
291
+ "customer.subscription.created",
292
+ "customer.subscription.updated",
293
+ "customer.subscription.deleted",
294
+ "invoice.payment_succeeded",
295
+ "invoice.payment_failed"
296
+ ]
297
+ )
298
+
299
+ click.echo(f"Webhook endpoint created:")
300
+ click.echo(f" URL: {webhook.url}")
301
+ click.echo(f" ID: {webhook.id}")
302
+ click.echo(f" Secret: {webhook.secret}")
303
+ click.echo(f"\nAdd this to your environment:")
304
+ click.echo(f" {environment.upper()}_STRIPE_WEBHOOK_SECRET={webhook.secret}")
305
+
306
+ except stripe.error.StripeError as e:
307
+ click.echo(f"Error creating webhook: {e}", err=True)
@@ -35,8 +35,8 @@ def verify_phone(phone_number, stack, skip_cache, profile, region):
35
35
  if skip_cache:
36
36
  # Force fresh lookup
37
37
  normalized = verifier.normalize_phone(phone_number)
38
- line_type = verifier._check_line_type_sync(normalized)
39
- dnc = verifier._check_dnc_sync(normalized)
38
+ line_type = verifier._check_line_type(normalized)
39
+ dnc = verifier._check_dnc(normalized)
40
40
  result = {
41
41
  "phone_number": normalized,
42
42
  "line_type": line_type,
@@ -46,7 +46,7 @@ def verify_phone(phone_number, stack, skip_cache, profile, region):
46
46
  "source": "cli-direct"
47
47
  }
48
48
  else:
49
- result = verifier.verify_sync(phone_number)
49
+ result = verifier.verify(phone_number)
50
50
  result = result.dict() if hasattr(result, 'dict') else result
51
51
 
52
52
  # Display results