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.
- {ai_lls_lib-1.0.0 → ai_lls_lib-1.2.0}/PKG-INFO +61 -8
- {ai_lls_lib-1.0.0 → ai_lls_lib-1.2.0}/README.md +59 -7
- {ai_lls_lib-1.0.0 → ai_lls_lib-1.2.0}/pyproject.toml +2 -1
- {ai_lls_lib-1.0.0 → ai_lls_lib-1.2.0}/src/ai_lls_lib/__init__.py +1 -1
- ai_lls_lib-1.2.0/src/ai_lls_lib/auth/__init__.py +4 -0
- ai_lls_lib-1.2.0/src/ai_lls_lib/auth/context_parser.py +68 -0
- {ai_lls_lib-1.0.0 → ai_lls_lib-1.2.0}/src/ai_lls_lib/cli/__main__.py +2 -1
- ai_lls_lib-1.2.0/src/ai_lls_lib/cli/commands/stripe.py +307 -0
- {ai_lls_lib-1.0.0 → ai_lls_lib-1.2.0}/src/ai_lls_lib/cli/commands/verify.py +3 -3
- ai_lls_lib-1.2.0/src/ai_lls_lib/cli/env_loader.py +122 -0
- ai_lls_lib-1.2.0/src/ai_lls_lib/core/processor.py +295 -0
- ai_lls_lib-1.2.0/src/ai_lls_lib/core/verifier.py +84 -0
- ai_lls_lib-1.2.0/src/ai_lls_lib/payment/__init__.py +13 -0
- ai_lls_lib-1.2.0/src/ai_lls_lib/payment/credit_manager.py +174 -0
- ai_lls_lib-1.2.0/src/ai_lls_lib/payment/models.py +96 -0
- ai_lls_lib-1.2.0/src/ai_lls_lib/payment/stripe_manager.py +473 -0
- ai_lls_lib-1.2.0/src/ai_lls_lib/payment/webhook_processor.py +163 -0
- ai_lls_lib-1.2.0/src/ai_lls_lib/providers/__init__.py +7 -0
- ai_lls_lib-1.2.0/src/ai_lls_lib/providers/base.py +28 -0
- ai_lls_lib-1.2.0/src/ai_lls_lib/providers/external.py +87 -0
- ai_lls_lib-1.2.0/src/ai_lls_lib/providers/stub.py +48 -0
- ai_lls_lib-1.0.0/src/ai_lls_lib/core/processor.py +0 -135
- ai_lls_lib-1.0.0/src/ai_lls_lib/core/verifier.py +0 -95
- {ai_lls_lib-1.0.0 → ai_lls_lib-1.2.0}/src/ai_lls_lib/cli/__init__.py +0 -0
- {ai_lls_lib-1.0.0 → ai_lls_lib-1.2.0}/src/ai_lls_lib/cli/aws_client.py +0 -0
- {ai_lls_lib-1.0.0 → ai_lls_lib-1.2.0}/src/ai_lls_lib/cli/commands/__init__.py +0 -0
- {ai_lls_lib-1.0.0 → ai_lls_lib-1.2.0}/src/ai_lls_lib/cli/commands/admin.py +0 -0
- {ai_lls_lib-1.0.0 → ai_lls_lib-1.2.0}/src/ai_lls_lib/cli/commands/cache.py +0 -0
- {ai_lls_lib-1.0.0 → ai_lls_lib-1.2.0}/src/ai_lls_lib/cli/commands/test_stack.py +0 -0
- {ai_lls_lib-1.0.0 → ai_lls_lib-1.2.0}/src/ai_lls_lib/core/__init__.py +0 -0
- {ai_lls_lib-1.0.0 → ai_lls_lib-1.2.0}/src/ai_lls_lib/core/cache.py +0 -0
- {ai_lls_lib-1.0.0 → ai_lls_lib-1.2.0}/src/ai_lls_lib/core/models.py +0 -0
- {ai_lls_lib-1.0.0 → ai_lls_lib-1.2.0}/src/ai_lls_lib/testing/__init__.py +0 -0
- {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.
|
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.
|
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
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
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.
|
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
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
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.
|
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.
|
16
|
+
__version__ = "1.2.0"
|
17
17
|
__all__ = [
|
18
18
|
"PhoneVerification",
|
19
19
|
"BulkJob",
|
@@ -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.
|
39
|
-
dnc = verifier.
|
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.
|
49
|
+
result = verifier.verify(phone_number)
|
50
50
|
result = result.dict() if hasattr(result, 'dict') else result
|
51
51
|
|
52
52
|
# Display results
|