ai-lls-lib 1.4.0rc3__py3-none-any.whl → 1.4.0rc4__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.
Files changed (34) hide show
  1. ai_lls_lib/__init__.py +1 -1
  2. ai_lls_lib/auth/__init__.py +4 -4
  3. ai_lls_lib/auth/context_parser.py +68 -68
  4. ai_lls_lib/cli/__init__.py +3 -3
  5. ai_lls_lib/cli/__main__.py +30 -30
  6. ai_lls_lib/cli/aws_client.py +115 -115
  7. ai_lls_lib/cli/commands/__init__.py +3 -3
  8. ai_lls_lib/cli/commands/admin.py +174 -174
  9. ai_lls_lib/cli/commands/cache.py +142 -142
  10. ai_lls_lib/cli/commands/stripe.py +377 -377
  11. ai_lls_lib/cli/commands/test_stack.py +216 -216
  12. ai_lls_lib/cli/commands/verify.py +111 -111
  13. ai_lls_lib/cli/env_loader.py +122 -122
  14. ai_lls_lib/core/__init__.py +3 -3
  15. ai_lls_lib/core/cache.py +106 -106
  16. ai_lls_lib/core/models.py +77 -77
  17. ai_lls_lib/core/processor.py +295 -295
  18. ai_lls_lib/core/verifier.py +84 -84
  19. ai_lls_lib/payment/__init__.py +13 -13
  20. ai_lls_lib/payment/credit_manager.py +186 -186
  21. ai_lls_lib/payment/models.py +102 -102
  22. ai_lls_lib/payment/stripe_manager.py +487 -487
  23. ai_lls_lib/payment/webhook_processor.py +215 -215
  24. ai_lls_lib/providers/__init__.py +7 -7
  25. ai_lls_lib/providers/base.py +28 -28
  26. ai_lls_lib/providers/external.py +87 -87
  27. ai_lls_lib/providers/stub.py +48 -48
  28. ai_lls_lib/testing/__init__.py +3 -3
  29. ai_lls_lib/testing/fixtures.py +104 -104
  30. {ai_lls_lib-1.4.0rc3.dist-info → ai_lls_lib-1.4.0rc4.dist-info}/METADATA +1 -1
  31. ai_lls_lib-1.4.0rc4.dist-info/RECORD +33 -0
  32. ai_lls_lib-1.4.0rc3.dist-info/RECORD +0 -33
  33. {ai_lls_lib-1.4.0rc3.dist-info → ai_lls_lib-1.4.0rc4.dist-info}/WHEEL +0 -0
  34. {ai_lls_lib-1.4.0rc3.dist-info → ai_lls_lib-1.4.0rc4.dist-info}/entry_points.txt +0 -0
@@ -1,377 +1,377 @@
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 - 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
- },
66
- {
67
- "name": "Landline Scrubber - STANDARD",
68
- "description": "One-time purchase",
69
- "metadata": {
70
- "product_type": "landline_scrubber",
71
- "environment": environment,
72
- "tier": "STANDARD"
73
- },
74
- "price": {
75
- "unit_amount": 1000, # $10.00
76
- "currency": "usd",
77
- "metadata": {
78
- "product_type": "landline_scrubber",
79
- "environment": environment,
80
- "plan_type": "prepaid",
81
- "tier": "STANDARD",
82
- "credits": "5000",
83
- "plan_credits_text": "5,000 credits",
84
- "percent_off": "",
85
- "active": "true"
86
- }
87
- }
88
- },
89
- {
90
- "name": "Landline Scrubber - POWER",
91
- "description": "Best value",
92
- "metadata": {
93
- "product_type": "landline_scrubber",
94
- "environment": environment,
95
- "tier": "POWER"
96
- },
97
- "price": {
98
- "unit_amount": 5000, # $50.00
99
- "currency": "usd",
100
- "metadata": {
101
- "product_type": "landline_scrubber",
102
- "environment": environment,
103
- "plan_type": "prepaid",
104
- "tier": "POWER",
105
- "credits": "28500",
106
- "plan_credits_text": "28,500 credits",
107
- "percent_off": "12.5% OFF",
108
- "active": "true"
109
- }
110
- }
111
- },
112
- {
113
- "name": "Landline Scrubber - ELITE",
114
- "description": "Maximum savings",
115
- "metadata": {
116
- "product_type": "landline_scrubber",
117
- "environment": environment,
118
- "tier": "ELITE"
119
- },
120
- "price": {
121
- "unit_amount": 10000, # $100.00
122
- "currency": "usd",
123
- "metadata": {
124
- "product_type": "landline_scrubber",
125
- "environment": environment,
126
- "plan_type": "prepaid",
127
- "tier": "ELITE",
128
- "credits": "66666",
129
- "plan_credits_text": "66,666 credits",
130
- "percent_off": "25% OFF",
131
- "active": "true"
132
- }
133
- }
134
- },
135
- {
136
- "name": "Landline Scrubber - UNLIMITED",
137
- "description": "Monthly subscription",
138
- "metadata": {
139
- "product_type": "landline_scrubber",
140
- "environment": environment,
141
- "tier": "UNLIMITED"
142
- },
143
- "price": {
144
- "unit_amount": 29900, # $299.00
145
- "currency": "usd",
146
- "recurring": {"interval": "month"},
147
- "metadata": {
148
- "product_type": "landline_scrubber",
149
- "environment": environment,
150
- "plan_type": "postpaid",
151
- "tier": "UNLIMITED",
152
- "credits": "unlimited",
153
- "plan_credits_text": "Unlimited",
154
- "percent_off": "",
155
- "active": "true"
156
- }
157
- }
158
- }
159
- ]
160
-
161
- if dry_run:
162
- click.echo("DRY RUN - Would create the following:")
163
- for config in products_config:
164
- click.echo(f"\nProduct: {config['name']}")
165
- click.echo(f" Description: {config['description']}")
166
- click.echo(f" Price: ${config['price']['unit_amount'] / 100:.2f}")
167
- if "recurring" in config["price"]:
168
- click.echo(f" Billing: Monthly subscription")
169
- else:
170
- click.echo(f" Billing: One-time payment")
171
- return
172
-
173
- created_prices = []
174
-
175
- for config in products_config:
176
- try:
177
- # Check if product already exists
178
- existing_products = stripe.Product.list(limit=100)
179
- product = None
180
- for p in existing_products.data:
181
- if (p.metadata.get("product_type") == "landline_scrubber" and
182
- p.metadata.get("environment") == environment and
183
- p.metadata.get("tier") == config["metadata"]["tier"] and
184
- p.active): # Only use active products
185
- product = p
186
- click.echo(f"Found existing product: {product.name}")
187
- break
188
-
189
- if not product:
190
- # Create new product
191
- product = stripe.Product.create(
192
- name=config["name"],
193
- description=config["description"],
194
- metadata=config["metadata"]
195
- )
196
- click.echo(f"Created product: {product.name}")
197
-
198
- # Create price (always create new prices, don't modify existing)
199
- price_data = {
200
- "product": product.id,
201
- "unit_amount": config["price"]["unit_amount"],
202
- "currency": config["price"]["currency"],
203
- "metadata": config["price"]["metadata"]
204
- }
205
-
206
- if "recurring" in config["price"]:
207
- price_data["recurring"] = config["price"]["recurring"]
208
-
209
- price = stripe.Price.create(**price_data)
210
- created_prices.append(price.id)
211
- click.echo(f" Created price: {price.id} (${price.unit_amount / 100:.2f})")
212
-
213
- except stripe.error.StripeError as e:
214
- click.echo(f"Error creating {config['name']}: {e}", err=True)
215
-
216
- if created_prices:
217
- click.echo(f"\nCreated {len(created_prices)} prices for {environment} environment")
218
- click.echo("\nPrice IDs:")
219
- for price_id in created_prices:
220
- click.echo(f" {price_id}")
221
-
222
-
223
- @stripe_group.command("clean")
224
- @click.option("--environment", type=click.Choice(["staging", "production"]), default="staging")
225
- @click.option("--api-key", help="Stripe API key (overrides environment)")
226
- @click.option("--force", is_flag=True, help="Skip confirmation")
227
- def clean_products(environment: str, api_key: Optional[str], force: bool):
228
- """Remove all Landline Scrubber products and prices."""
229
- import stripe
230
-
231
- # Load API key from environment if not provided
232
- if not api_key:
233
- api_key = get_stripe_key(environment)
234
- if not api_key:
235
- click.echo(f"Error: No Stripe API key found for {environment} environment", err=True)
236
- return
237
-
238
- stripe.api_key = api_key
239
-
240
- if not force:
241
- if not click.confirm(f"This will DELETE all Landline Scrubber products in {environment}. Continue?"):
242
- return
243
-
244
- try:
245
- # List all products
246
- products = stripe.Product.list(limit=100)
247
- deleted_count = 0
248
-
249
- for product in products.data:
250
- if (product.metadata.get("product_type") == "landline_scrubber" and
251
- product.metadata.get("environment") == environment):
252
- # Archive all prices first
253
- prices = stripe.Price.list(product=product.id, limit=100)
254
- for price in prices.data:
255
- if price.active:
256
- stripe.Price.modify(price.id, active=False)
257
- click.echo(f" Archived price: {price.id}")
258
-
259
- # Archive the product
260
- stripe.Product.modify(product.id, active=False)
261
- click.echo(f"Archived product: {product.name}")
262
- deleted_count += 1
263
-
264
- click.echo(f"\nArchived {deleted_count} products in {environment} environment")
265
-
266
- except stripe.error.StripeError as e:
267
- click.echo(f"Error: {e}", err=True)
268
-
269
-
270
- @stripe_group.command("list")
271
- @click.option("--environment", type=click.Choice(["staging", "production"]), default="staging")
272
- @click.option("--api-key", help="Stripe API key (overrides environment)")
273
- @click.option("--json", "output_json", is_flag=True, help="Output as JSON")
274
- def list_products(environment: str, api_key: Optional[str], output_json: bool):
275
- """List all products and prices with metadata."""
276
- try:
277
- from ai_lls_lib.payment import StripeManager
278
- except ImportError:
279
- click.echo("Error: Payment module not found", err=True)
280
- return
281
-
282
- # Load API key from environment if not provided
283
- if not api_key:
284
- api_key = get_stripe_key(environment)
285
- if not api_key:
286
- click.echo(f"Error: No Stripe API key found for {environment} environment", err=True)
287
- click.echo(f"Set {environment.upper()}_STRIPE_SECRET_KEY or STRIPE_SECRET_KEY", err=True)
288
- return
289
-
290
- try:
291
- manager = StripeManager(api_key=api_key, environment=environment)
292
- plans = manager.list_plans()
293
-
294
- if output_json:
295
- output = [plan.to_dict() for plan in plans]
296
- click.echo(json.dumps(output, indent=2))
297
- else:
298
- click.echo(f"Active plans for {environment} environment:\n")
299
- for plan in plans:
300
- click.echo(f"{plan.plan_name}:")
301
- click.echo(f" Price: ${plan.plan_amount:.2f}")
302
- click.echo(f" Credits: {plan.plan_credits_text}")
303
- click.echo(f" Type: {plan.plan_type}")
304
- click.echo(f" Reference: {plan.plan_reference}")
305
- if plan.percent_off:
306
- click.echo(f" Discount: {plan.percent_off}")
307
- click.echo()
308
-
309
- except Exception as e:
310
- click.echo(f"Error: {e}", err=True)
311
-
312
-
313
- @stripe_group.command("webhook")
314
- @click.option("--endpoint-url", help="Webhook endpoint URL")
315
- @click.option("--environment", type=click.Choice(["staging", "production"]), default="staging")
316
- @click.option("--api-key", help="Stripe API key (overrides environment)")
317
- @click.option("--print-secret", is_flag=True, help="Print the webhook signing secret")
318
- def setup_webhook(endpoint_url: Optional[str], environment: str, api_key: Optional[str], print_secret: bool):
319
- """Configure or display webhook endpoint."""
320
- try:
321
- import stripe
322
- except ImportError:
323
- click.echo("Error: stripe package not installed. Run: pip install stripe", err=True)
324
- return
325
-
326
- # Load API key from environment if not provided
327
- if not api_key:
328
- api_key = get_stripe_key(environment)
329
- if not api_key:
330
- click.echo(f"Error: No Stripe API key found for {environment} environment", err=True)
331
- click.echo(f"Set {environment.upper()}_STRIPE_SECRET_KEY or STRIPE_SECRET_KEY", err=True)
332
- return
333
-
334
- stripe.api_key = api_key
335
-
336
- if print_secret:
337
- # List existing webhooks
338
- webhooks = stripe.WebhookEndpoint.list(limit=10)
339
- if webhooks.data:
340
- click.echo("Existing webhook endpoints:\n")
341
- for webhook in webhooks.data:
342
- click.echo(f"URL: {webhook.url}")
343
- click.echo(f"ID: {webhook.id}")
344
- click.echo(f"Secret: {webhook.secret}")
345
- click.echo(f"Status: {webhook.status}")
346
- click.echo()
347
- else:
348
- click.echo("No webhook endpoints configured")
349
- return
350
-
351
- if not endpoint_url:
352
- click.echo("Error: --endpoint-url required to create webhook", err=True)
353
- return
354
-
355
- try:
356
- # Create webhook endpoint
357
- webhook = stripe.WebhookEndpoint.create(
358
- url=endpoint_url,
359
- enabled_events=[
360
- "checkout.session.completed",
361
- "customer.subscription.created",
362
- "customer.subscription.updated",
363
- "customer.subscription.deleted",
364
- "invoice.payment_succeeded",
365
- "invoice.payment_failed"
366
- ]
367
- )
368
-
369
- click.echo(f"Webhook endpoint created:")
370
- click.echo(f" URL: {webhook.url}")
371
- click.echo(f" ID: {webhook.id}")
372
- click.echo(f" Secret: {webhook.secret}")
373
- click.echo(f"\nAdd this to your environment:")
374
- click.echo(f" {environment.upper()}_STRIPE_WEBHOOK_SECRET={webhook.secret}")
375
-
376
- except stripe.error.StripeError as e:
377
- click.echo(f"Error creating webhook: {e}", err=True)
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 - 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
+ },
66
+ {
67
+ "name": "Landline Scrubber - STANDARD",
68
+ "description": "One-time purchase",
69
+ "metadata": {
70
+ "product_type": "landline_scrubber",
71
+ "environment": environment,
72
+ "tier": "STANDARD"
73
+ },
74
+ "price": {
75
+ "unit_amount": 1000, # $10.00
76
+ "currency": "usd",
77
+ "metadata": {
78
+ "product_type": "landline_scrubber",
79
+ "environment": environment,
80
+ "plan_type": "prepaid",
81
+ "tier": "STANDARD",
82
+ "credits": "5000",
83
+ "plan_credits_text": "5,000 credits",
84
+ "percent_off": "",
85
+ "active": "true"
86
+ }
87
+ }
88
+ },
89
+ {
90
+ "name": "Landline Scrubber - POWER",
91
+ "description": "Best value",
92
+ "metadata": {
93
+ "product_type": "landline_scrubber",
94
+ "environment": environment,
95
+ "tier": "POWER"
96
+ },
97
+ "price": {
98
+ "unit_amount": 5000, # $50.00
99
+ "currency": "usd",
100
+ "metadata": {
101
+ "product_type": "landline_scrubber",
102
+ "environment": environment,
103
+ "plan_type": "prepaid",
104
+ "tier": "POWER",
105
+ "credits": "28500",
106
+ "plan_credits_text": "28,500 credits",
107
+ "percent_off": "12.5% OFF",
108
+ "active": "true"
109
+ }
110
+ }
111
+ },
112
+ {
113
+ "name": "Landline Scrubber - ELITE",
114
+ "description": "Maximum savings",
115
+ "metadata": {
116
+ "product_type": "landline_scrubber",
117
+ "environment": environment,
118
+ "tier": "ELITE"
119
+ },
120
+ "price": {
121
+ "unit_amount": 10000, # $100.00
122
+ "currency": "usd",
123
+ "metadata": {
124
+ "product_type": "landline_scrubber",
125
+ "environment": environment,
126
+ "plan_type": "prepaid",
127
+ "tier": "ELITE",
128
+ "credits": "66666",
129
+ "plan_credits_text": "66,666 credits",
130
+ "percent_off": "25% OFF",
131
+ "active": "true"
132
+ }
133
+ }
134
+ },
135
+ {
136
+ "name": "Landline Scrubber - UNLIMITED",
137
+ "description": "Monthly subscription",
138
+ "metadata": {
139
+ "product_type": "landline_scrubber",
140
+ "environment": environment,
141
+ "tier": "UNLIMITED"
142
+ },
143
+ "price": {
144
+ "unit_amount": 29900, # $299.00
145
+ "currency": "usd",
146
+ "recurring": {"interval": "month"},
147
+ "metadata": {
148
+ "product_type": "landline_scrubber",
149
+ "environment": environment,
150
+ "plan_type": "postpaid",
151
+ "tier": "UNLIMITED",
152
+ "credits": "unlimited",
153
+ "plan_credits_text": "Unlimited",
154
+ "percent_off": "",
155
+ "active": "true"
156
+ }
157
+ }
158
+ }
159
+ ]
160
+
161
+ if dry_run:
162
+ click.echo("DRY RUN - Would create the following:")
163
+ for config in products_config:
164
+ click.echo(f"\nProduct: {config['name']}")
165
+ click.echo(f" Description: {config['description']}")
166
+ click.echo(f" Price: ${config['price']['unit_amount'] / 100:.2f}")
167
+ if "recurring" in config["price"]:
168
+ click.echo(f" Billing: Monthly subscription")
169
+ else:
170
+ click.echo(f" Billing: One-time payment")
171
+ return
172
+
173
+ created_prices = []
174
+
175
+ for config in products_config:
176
+ try:
177
+ # Check if product already exists
178
+ existing_products = stripe.Product.list(limit=100)
179
+ product = None
180
+ for p in existing_products.data:
181
+ if (p.metadata.get("product_type") == "landline_scrubber" and
182
+ p.metadata.get("environment") == environment and
183
+ p.metadata.get("tier") == config["metadata"]["tier"] and
184
+ p.active): # Only use active products
185
+ product = p
186
+ click.echo(f"Found existing product: {product.name}")
187
+ break
188
+
189
+ if not product:
190
+ # Create new product
191
+ product = stripe.Product.create(
192
+ name=config["name"],
193
+ description=config["description"],
194
+ metadata=config["metadata"]
195
+ )
196
+ click.echo(f"Created product: {product.name}")
197
+
198
+ # Create price (always create new prices, don't modify existing)
199
+ price_data = {
200
+ "product": product.id,
201
+ "unit_amount": config["price"]["unit_amount"],
202
+ "currency": config["price"]["currency"],
203
+ "metadata": config["price"]["metadata"]
204
+ }
205
+
206
+ if "recurring" in config["price"]:
207
+ price_data["recurring"] = config["price"]["recurring"]
208
+
209
+ price = stripe.Price.create(**price_data)
210
+ created_prices.append(price.id)
211
+ click.echo(f" Created price: {price.id} (${price.unit_amount / 100:.2f})")
212
+
213
+ except stripe.error.StripeError as e:
214
+ click.echo(f"Error creating {config['name']}: {e}", err=True)
215
+
216
+ if created_prices:
217
+ click.echo(f"\nCreated {len(created_prices)} prices for {environment} environment")
218
+ click.echo("\nPrice IDs:")
219
+ for price_id in created_prices:
220
+ click.echo(f" {price_id}")
221
+
222
+
223
+ @stripe_group.command("clean")
224
+ @click.option("--environment", type=click.Choice(["staging", "production"]), default="staging")
225
+ @click.option("--api-key", help="Stripe API key (overrides environment)")
226
+ @click.option("--force", is_flag=True, help="Skip confirmation")
227
+ def clean_products(environment: str, api_key: Optional[str], force: bool):
228
+ """Remove all Landline Scrubber products and prices."""
229
+ import stripe
230
+
231
+ # Load API key from environment if not provided
232
+ if not api_key:
233
+ api_key = get_stripe_key(environment)
234
+ if not api_key:
235
+ click.echo(f"Error: No Stripe API key found for {environment} environment", err=True)
236
+ return
237
+
238
+ stripe.api_key = api_key
239
+
240
+ if not force:
241
+ if not click.confirm(f"This will DELETE all Landline Scrubber products in {environment}. Continue?"):
242
+ return
243
+
244
+ try:
245
+ # List all products
246
+ products = stripe.Product.list(limit=100)
247
+ deleted_count = 0
248
+
249
+ for product in products.data:
250
+ if (product.metadata.get("product_type") == "landline_scrubber" and
251
+ product.metadata.get("environment") == environment):
252
+ # Archive all prices first
253
+ prices = stripe.Price.list(product=product.id, limit=100)
254
+ for price in prices.data:
255
+ if price.active:
256
+ stripe.Price.modify(price.id, active=False)
257
+ click.echo(f" Archived price: {price.id}")
258
+
259
+ # Archive the product
260
+ stripe.Product.modify(product.id, active=False)
261
+ click.echo(f"Archived product: {product.name}")
262
+ deleted_count += 1
263
+
264
+ click.echo(f"\nArchived {deleted_count} products in {environment} environment")
265
+
266
+ except stripe.error.StripeError as e:
267
+ click.echo(f"Error: {e}", err=True)
268
+
269
+
270
+ @stripe_group.command("list")
271
+ @click.option("--environment", type=click.Choice(["staging", "production"]), default="staging")
272
+ @click.option("--api-key", help="Stripe API key (overrides environment)")
273
+ @click.option("--json", "output_json", is_flag=True, help="Output as JSON")
274
+ def list_products(environment: str, api_key: Optional[str], output_json: bool):
275
+ """List all products and prices with metadata."""
276
+ try:
277
+ from ai_lls_lib.payment import StripeManager
278
+ except ImportError:
279
+ click.echo("Error: Payment module not found", err=True)
280
+ return
281
+
282
+ # Load API key from environment if not provided
283
+ if not api_key:
284
+ api_key = get_stripe_key(environment)
285
+ if not api_key:
286
+ click.echo(f"Error: No Stripe API key found for {environment} environment", err=True)
287
+ click.echo(f"Set {environment.upper()}_STRIPE_SECRET_KEY or STRIPE_SECRET_KEY", err=True)
288
+ return
289
+
290
+ try:
291
+ manager = StripeManager(api_key=api_key, environment=environment)
292
+ plans = manager.list_plans()
293
+
294
+ if output_json:
295
+ output = [plan.to_dict() for plan in plans]
296
+ click.echo(json.dumps(output, indent=2))
297
+ else:
298
+ click.echo(f"Active plans for {environment} environment:\n")
299
+ for plan in plans:
300
+ click.echo(f"{plan.plan_name}:")
301
+ click.echo(f" Price: ${plan.plan_amount:.2f}")
302
+ click.echo(f" Credits: {plan.plan_credits_text}")
303
+ click.echo(f" Type: {plan.plan_type}")
304
+ click.echo(f" Reference: {plan.plan_reference}")
305
+ if plan.percent_off:
306
+ click.echo(f" Discount: {plan.percent_off}")
307
+ click.echo()
308
+
309
+ except Exception as e:
310
+ click.echo(f"Error: {e}", err=True)
311
+
312
+
313
+ @stripe_group.command("webhook")
314
+ @click.option("--endpoint-url", help="Webhook endpoint URL")
315
+ @click.option("--environment", type=click.Choice(["staging", "production"]), default="staging")
316
+ @click.option("--api-key", help="Stripe API key (overrides environment)")
317
+ @click.option("--print-secret", is_flag=True, help="Print the webhook signing secret")
318
+ def setup_webhook(endpoint_url: Optional[str], environment: str, api_key: Optional[str], print_secret: bool):
319
+ """Configure or display webhook endpoint."""
320
+ try:
321
+ import stripe
322
+ except ImportError:
323
+ click.echo("Error: stripe package not installed. Run: pip install stripe", err=True)
324
+ return
325
+
326
+ # Load API key from environment if not provided
327
+ if not api_key:
328
+ api_key = get_stripe_key(environment)
329
+ if not api_key:
330
+ click.echo(f"Error: No Stripe API key found for {environment} environment", err=True)
331
+ click.echo(f"Set {environment.upper()}_STRIPE_SECRET_KEY or STRIPE_SECRET_KEY", err=True)
332
+ return
333
+
334
+ stripe.api_key = api_key
335
+
336
+ if print_secret:
337
+ # List existing webhooks
338
+ webhooks = stripe.WebhookEndpoint.list(limit=10)
339
+ if webhooks.data:
340
+ click.echo("Existing webhook endpoints:\n")
341
+ for webhook in webhooks.data:
342
+ click.echo(f"URL: {webhook.url}")
343
+ click.echo(f"ID: {webhook.id}")
344
+ click.echo(f"Secret: {webhook.secret}")
345
+ click.echo(f"Status: {webhook.status}")
346
+ click.echo()
347
+ else:
348
+ click.echo("No webhook endpoints configured")
349
+ return
350
+
351
+ if not endpoint_url:
352
+ click.echo("Error: --endpoint-url required to create webhook", err=True)
353
+ return
354
+
355
+ try:
356
+ # Create webhook endpoint
357
+ webhook = stripe.WebhookEndpoint.create(
358
+ url=endpoint_url,
359
+ enabled_events=[
360
+ "checkout.session.completed",
361
+ "customer.subscription.created",
362
+ "customer.subscription.updated",
363
+ "customer.subscription.deleted",
364
+ "invoice.payment_succeeded",
365
+ "invoice.payment_failed"
366
+ ]
367
+ )
368
+
369
+ click.echo(f"Webhook endpoint created:")
370
+ click.echo(f" URL: {webhook.url}")
371
+ click.echo(f" ID: {webhook.id}")
372
+ click.echo(f" Secret: {webhook.secret}")
373
+ click.echo(f"\nAdd this to your environment:")
374
+ click.echo(f" {environment.upper()}_STRIPE_WEBHOOK_SECRET={webhook.secret}")
375
+
376
+ except stripe.error.StripeError as e:
377
+ click.echo(f"Error creating webhook: {e}", err=True)