ai-lls-lib 1.1.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 (32) hide show
  1. {ai_lls_lib-1.1.0 → ai_lls_lib-1.2.0}/PKG-INFO +2 -1
  2. {ai_lls_lib-1.1.0 → ai_lls_lib-1.2.0}/pyproject.toml +2 -1
  3. {ai_lls_lib-1.1.0 → ai_lls_lib-1.2.0}/src/ai_lls_lib/__init__.py +1 -1
  4. ai_lls_lib-1.2.0/src/ai_lls_lib/auth/__init__.py +4 -0
  5. ai_lls_lib-1.2.0/src/ai_lls_lib/auth/context_parser.py +68 -0
  6. {ai_lls_lib-1.1.0 → ai_lls_lib-1.2.0}/src/ai_lls_lib/cli/__main__.py +2 -1
  7. ai_lls_lib-1.2.0/src/ai_lls_lib/cli/commands/stripe.py +307 -0
  8. ai_lls_lib-1.2.0/src/ai_lls_lib/cli/env_loader.py +122 -0
  9. ai_lls_lib-1.2.0/src/ai_lls_lib/payment/__init__.py +13 -0
  10. ai_lls_lib-1.2.0/src/ai_lls_lib/payment/credit_manager.py +174 -0
  11. ai_lls_lib-1.2.0/src/ai_lls_lib/payment/models.py +96 -0
  12. ai_lls_lib-1.2.0/src/ai_lls_lib/payment/stripe_manager.py +473 -0
  13. ai_lls_lib-1.2.0/src/ai_lls_lib/payment/webhook_processor.py +163 -0
  14. {ai_lls_lib-1.1.0 → ai_lls_lib-1.2.0}/README.md +0 -0
  15. {ai_lls_lib-1.1.0 → ai_lls_lib-1.2.0}/src/ai_lls_lib/cli/__init__.py +0 -0
  16. {ai_lls_lib-1.1.0 → ai_lls_lib-1.2.0}/src/ai_lls_lib/cli/aws_client.py +0 -0
  17. {ai_lls_lib-1.1.0 → ai_lls_lib-1.2.0}/src/ai_lls_lib/cli/commands/__init__.py +0 -0
  18. {ai_lls_lib-1.1.0 → ai_lls_lib-1.2.0}/src/ai_lls_lib/cli/commands/admin.py +0 -0
  19. {ai_lls_lib-1.1.0 → ai_lls_lib-1.2.0}/src/ai_lls_lib/cli/commands/cache.py +0 -0
  20. {ai_lls_lib-1.1.0 → ai_lls_lib-1.2.0}/src/ai_lls_lib/cli/commands/test_stack.py +0 -0
  21. {ai_lls_lib-1.1.0 → ai_lls_lib-1.2.0}/src/ai_lls_lib/cli/commands/verify.py +0 -0
  22. {ai_lls_lib-1.1.0 → ai_lls_lib-1.2.0}/src/ai_lls_lib/core/__init__.py +0 -0
  23. {ai_lls_lib-1.1.0 → ai_lls_lib-1.2.0}/src/ai_lls_lib/core/cache.py +0 -0
  24. {ai_lls_lib-1.1.0 → ai_lls_lib-1.2.0}/src/ai_lls_lib/core/models.py +0 -0
  25. {ai_lls_lib-1.1.0 → ai_lls_lib-1.2.0}/src/ai_lls_lib/core/processor.py +0 -0
  26. {ai_lls_lib-1.1.0 → ai_lls_lib-1.2.0}/src/ai_lls_lib/core/verifier.py +0 -0
  27. {ai_lls_lib-1.1.0 → ai_lls_lib-1.2.0}/src/ai_lls_lib/providers/__init__.py +0 -0
  28. {ai_lls_lib-1.1.0 → ai_lls_lib-1.2.0}/src/ai_lls_lib/providers/base.py +0 -0
  29. {ai_lls_lib-1.1.0 → ai_lls_lib-1.2.0}/src/ai_lls_lib/providers/external.py +0 -0
  30. {ai_lls_lib-1.1.0 → ai_lls_lib-1.2.0}/src/ai_lls_lib/providers/stub.py +0 -0
  31. {ai_lls_lib-1.1.0 → ai_lls_lib-1.2.0}/src/ai_lls_lib/testing/__init__.py +0 -0
  32. {ai_lls_lib-1.1.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.1.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,6 +14,7 @@ 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
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "ai-lls-lib"
3
- version = "1.1.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.1.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)
@@ -0,0 +1,122 @@
1
+ """Environment variable loader for CLI commands."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Optional, Dict
6
+ import click
7
+
8
+
9
+ def load_env_file(env_path: Path) -> Dict[str, str]:
10
+ """Load environment variables from a .env file."""
11
+ env_vars = {}
12
+ if env_path.exists():
13
+ try:
14
+ with open(env_path, 'r') as f:
15
+ for line in f:
16
+ line = line.strip()
17
+ if line and not line.startswith('#') and '=' in line:
18
+ key, value = line.split('=', 1)
19
+ # Remove quotes if present
20
+ value = value.strip().strip('"').strip("'")
21
+ env_vars[key.strip()] = value
22
+ except Exception as e:
23
+ click.echo(f"Warning: Could not read {env_path}: {e}", err=True)
24
+ return env_vars
25
+
26
+
27
+ def load_environment_config() -> Dict[str, str]:
28
+ """
29
+ Load environment variables from multiple sources in order:
30
+ 1. ~/.lls/.env (user global config)
31
+ 2. ./.env (project local config)
32
+ 3. System environment variables (highest priority)
33
+
34
+ Returns merged dictionary with system env taking precedence.
35
+ """
36
+ env_vars = {}
37
+
38
+ # Load from ~/.lls/.env
39
+ home_env = Path.home() / '.lls' / '.env'
40
+ if home_env.exists():
41
+ home_vars = load_env_file(home_env)
42
+ env_vars.update(home_vars)
43
+ click.echo(f"Loaded {len(home_vars)} variables from {home_env}", err=True)
44
+
45
+ # Load from ./.env
46
+ local_env = Path('.env')
47
+ if local_env.exists():
48
+ local_vars = load_env_file(local_env)
49
+ env_vars.update(local_vars)
50
+ click.echo(f"Loaded {len(local_vars)} variables from {local_env}", err=True)
51
+
52
+ # System environment variables override file-based ones
53
+ env_vars.update(os.environ)
54
+
55
+ return env_vars
56
+
57
+
58
+ def get_stripe_key(environment: str, env_vars: Optional[Dict[str, str]] = None) -> Optional[str]:
59
+ """
60
+ Get Stripe API key for the specified environment.
61
+
62
+ Looks for keys in this order:
63
+ 1. STAGING_STRIPE_SECRET_KEY or PROD_STRIPE_SECRET_KEY (based on environment)
64
+ 2. STRIPE_SECRET_KEY (fallback)
65
+
66
+ Args:
67
+ environment: 'staging' or 'production'
68
+ env_vars: Optional pre-loaded environment variables
69
+
70
+ Returns:
71
+ Stripe API key or None if not found
72
+ """
73
+ if env_vars is None:
74
+ env_vars = load_environment_config()
75
+
76
+ # Map environment names to prefixes
77
+ env_prefix = {
78
+ 'staging': 'STAGING',
79
+ 'production': 'PROD'
80
+ }.get(environment, environment.upper())
81
+
82
+ # Try environment-specific key first
83
+ env_key = f"{env_prefix}_STRIPE_SECRET_KEY"
84
+ if env_key in env_vars:
85
+ return env_vars[env_key]
86
+
87
+ # Fall back to generic key
88
+ if 'STRIPE_SECRET_KEY' in env_vars:
89
+ return env_vars['STRIPE_SECRET_KEY']
90
+
91
+ return None
92
+
93
+
94
+ def get_env_variable(key: str, environment: Optional[str] = None,
95
+ env_vars: Optional[Dict[str, str]] = None) -> Optional[str]:
96
+ """
97
+ Get an environment variable, optionally with environment prefix.
98
+
99
+ Args:
100
+ key: Variable name (e.g., 'API_URL')
101
+ environment: Optional environment ('staging' or 'production')
102
+ env_vars: Optional pre-loaded environment variables
103
+
104
+ Returns:
105
+ Variable value or None if not found
106
+ """
107
+ if env_vars is None:
108
+ env_vars = load_environment_config()
109
+
110
+ if environment:
111
+ env_prefix = {
112
+ 'staging': 'STAGING',
113
+ 'production': 'PROD'
114
+ }.get(environment, environment.upper())
115
+
116
+ # Try environment-specific key first
117
+ env_key = f"{env_prefix}_{key}"
118
+ if env_key in env_vars:
119
+ return env_vars[env_key]
120
+
121
+ # Fall back to non-prefixed key
122
+ return env_vars.get(key)
@@ -0,0 +1,13 @@
1
+ """Payment module for Landline Scrubber."""
2
+
3
+ from .models import Plan, PlanType, SubscriptionStatus
4
+ from .stripe_manager import StripeManager
5
+ from .credit_manager import CreditManager
6
+
7
+ __all__ = [
8
+ "Plan",
9
+ "PlanType",
10
+ "SubscriptionStatus",
11
+ "StripeManager",
12
+ "CreditManager",
13
+ ]