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.
- ai_lls_lib/__init__.py +1 -1
- ai_lls_lib/auth/__init__.py +4 -4
- ai_lls_lib/auth/context_parser.py +68 -68
- ai_lls_lib/cli/__init__.py +3 -3
- ai_lls_lib/cli/__main__.py +30 -30
- ai_lls_lib/cli/aws_client.py +115 -115
- ai_lls_lib/cli/commands/__init__.py +3 -3
- ai_lls_lib/cli/commands/admin.py +174 -174
- ai_lls_lib/cli/commands/cache.py +142 -142
- ai_lls_lib/cli/commands/stripe.py +377 -377
- ai_lls_lib/cli/commands/test_stack.py +216 -216
- ai_lls_lib/cli/commands/verify.py +111 -111
- ai_lls_lib/cli/env_loader.py +122 -122
- ai_lls_lib/core/__init__.py +3 -3
- ai_lls_lib/core/cache.py +106 -106
- ai_lls_lib/core/models.py +77 -77
- ai_lls_lib/core/processor.py +295 -295
- ai_lls_lib/core/verifier.py +84 -84
- ai_lls_lib/payment/__init__.py +13 -13
- ai_lls_lib/payment/credit_manager.py +186 -186
- ai_lls_lib/payment/models.py +102 -102
- ai_lls_lib/payment/stripe_manager.py +487 -487
- ai_lls_lib/payment/webhook_processor.py +215 -215
- ai_lls_lib/providers/__init__.py +7 -7
- ai_lls_lib/providers/base.py +28 -28
- ai_lls_lib/providers/external.py +87 -87
- ai_lls_lib/providers/stub.py +48 -48
- ai_lls_lib/testing/__init__.py +3 -3
- ai_lls_lib/testing/fixtures.py +104 -104
- {ai_lls_lib-1.4.0rc3.dist-info → ai_lls_lib-1.4.0rc4.dist-info}/METADATA +1 -1
- ai_lls_lib-1.4.0rc4.dist-info/RECORD +33 -0
- ai_lls_lib-1.4.0rc3.dist-info/RECORD +0 -33
- {ai_lls_lib-1.4.0rc3.dist-info → ai_lls_lib-1.4.0rc4.dist-info}/WHEEL +0 -0
- {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)
|