quantumflow-sdk 0.1.0__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.
- api/__init__.py +1 -0
- api/auth.py +208 -0
- api/main.py +403 -0
- api/models.py +137 -0
- api/routes/__init__.py +1 -0
- api/routes/auth_routes.py +234 -0
- api/routes/teleport_routes.py +415 -0
- db/__init__.py +15 -0
- db/crud.py +319 -0
- db/database.py +93 -0
- db/models.py +197 -0
- quantumflow/__init__.py +47 -0
- quantumflow/algorithms/__init__.py +48 -0
- quantumflow/algorithms/compression/__init__.py +7 -0
- quantumflow/algorithms/compression/amplitude_amplification.py +189 -0
- quantumflow/algorithms/compression/qft_compression.py +133 -0
- quantumflow/algorithms/compression/token_compression.py +261 -0
- quantumflow/algorithms/cryptography/__init__.py +6 -0
- quantumflow/algorithms/cryptography/qkd.py +205 -0
- quantumflow/algorithms/cryptography/qrng.py +231 -0
- quantumflow/algorithms/machine_learning/__init__.py +7 -0
- quantumflow/algorithms/machine_learning/qnn.py +276 -0
- quantumflow/algorithms/machine_learning/qsvm.py +249 -0
- quantumflow/algorithms/machine_learning/vqe.py +229 -0
- quantumflow/algorithms/optimization/__init__.py +7 -0
- quantumflow/algorithms/optimization/grover.py +223 -0
- quantumflow/algorithms/optimization/qaoa.py +251 -0
- quantumflow/algorithms/optimization/quantum_annealing.py +237 -0
- quantumflow/algorithms/utility/__init__.py +6 -0
- quantumflow/algorithms/utility/circuit_optimizer.py +194 -0
- quantumflow/algorithms/utility/error_correction.py +330 -0
- quantumflow/api/__init__.py +1 -0
- quantumflow/api/routes/__init__.py +4 -0
- quantumflow/api/routes/billing_routes.py +520 -0
- quantumflow/backends/__init__.py +33 -0
- quantumflow/backends/base_backend.py +184 -0
- quantumflow/backends/braket_backend.py +345 -0
- quantumflow/backends/ibm_backend.py +112 -0
- quantumflow/backends/simulator_backend.py +86 -0
- quantumflow/billing/__init__.py +25 -0
- quantumflow/billing/models.py +126 -0
- quantumflow/billing/stripe_service.py +619 -0
- quantumflow/core/__init__.py +12 -0
- quantumflow/core/entanglement.py +164 -0
- quantumflow/core/memory.py +147 -0
- quantumflow/core/quantum_backprop.py +394 -0
- quantumflow/core/quantum_compressor.py +309 -0
- quantumflow/core/teleportation.py +386 -0
- quantumflow/integrations/__init__.py +107 -0
- quantumflow/integrations/autogen_tools.py +501 -0
- quantumflow/integrations/crewai_agents.py +425 -0
- quantumflow/integrations/crewai_tools.py +407 -0
- quantumflow/integrations/langchain_memory.py +385 -0
- quantumflow/integrations/langchain_tools.py +366 -0
- quantumflow/integrations/mcp_server.py +575 -0
- quantumflow_sdk-0.1.0.dist-info/METADATA +190 -0
- quantumflow_sdk-0.1.0.dist-info/RECORD +60 -0
- quantumflow_sdk-0.1.0.dist-info/WHEEL +5 -0
- quantumflow_sdk-0.1.0.dist-info/entry_points.txt +2 -0
- quantumflow_sdk-0.1.0.dist-info/top_level.txt +3 -0
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Billing API Routes - Stripe subscription and payment management.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Optional
|
|
7
|
+
from fastapi import APIRouter, Depends, HTTPException, Header, Request
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
from api.auth import get_current_user
|
|
11
|
+
from db.models import User
|
|
12
|
+
from quantumflow.billing import (
|
|
13
|
+
StripeService,
|
|
14
|
+
SubscriptionTier,
|
|
15
|
+
TIER_PRICES,
|
|
16
|
+
TIER_LIMITS,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
router = APIRouter(prefix="/billing", tags=["billing"])
|
|
20
|
+
|
|
21
|
+
# Initialize Stripe service (lazy init to handle missing keys gracefully)
|
|
22
|
+
_stripe_service: Optional[StripeService] = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_stripe_service() -> StripeService:
|
|
26
|
+
"""Get or create Stripe service instance."""
|
|
27
|
+
global _stripe_service
|
|
28
|
+
if _stripe_service is None:
|
|
29
|
+
_stripe_service = StripeService()
|
|
30
|
+
return _stripe_service
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ==================== Request/Response Models ====================
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class CreateCustomerRequest(BaseModel):
|
|
37
|
+
"""Request to create a Stripe customer."""
|
|
38
|
+
email: str
|
|
39
|
+
name: Optional[str] = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class CreateSubscriptionRequest(BaseModel):
|
|
43
|
+
"""Request to create a subscription."""
|
|
44
|
+
tier: SubscriptionTier
|
|
45
|
+
payment_method_id: Optional[str] = None
|
|
46
|
+
trial_days: int = 0
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class UpdateSubscriptionRequest(BaseModel):
|
|
50
|
+
"""Request to update subscription tier."""
|
|
51
|
+
tier: SubscriptionTier
|
|
52
|
+
prorate: bool = True
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class CheckoutSessionRequest(BaseModel):
|
|
56
|
+
"""Request to create checkout session."""
|
|
57
|
+
tier: SubscriptionTier
|
|
58
|
+
success_url: str
|
|
59
|
+
cancel_url: str
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class BillingPortalRequest(BaseModel):
|
|
63
|
+
"""Request for billing portal session."""
|
|
64
|
+
return_url: str
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class SubscriptionResponse(BaseModel):
|
|
68
|
+
"""Subscription details response."""
|
|
69
|
+
id: str
|
|
70
|
+
tier: str
|
|
71
|
+
status: str
|
|
72
|
+
current_period_start: datetime
|
|
73
|
+
current_period_end: datetime
|
|
74
|
+
cancel_at_period_end: bool
|
|
75
|
+
trial_end: Optional[datetime] = None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class PricingResponse(BaseModel):
|
|
79
|
+
"""Pricing information response."""
|
|
80
|
+
tier: str
|
|
81
|
+
monthly_price: float
|
|
82
|
+
api_calls_included: int
|
|
83
|
+
overage_price_per_call: float
|
|
84
|
+
features: dict
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class InvoiceResponse(BaseModel):
|
|
88
|
+
"""Invoice response."""
|
|
89
|
+
id: str
|
|
90
|
+
amount_due: float
|
|
91
|
+
amount_paid: float
|
|
92
|
+
currency: str
|
|
93
|
+
status: str
|
|
94
|
+
invoice_pdf: Optional[str] = None
|
|
95
|
+
hosted_invoice_url: Optional[str] = None
|
|
96
|
+
period_start: Optional[datetime] = None
|
|
97
|
+
period_end: Optional[datetime] = None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class UsageSummaryResponse(BaseModel):
|
|
101
|
+
"""Usage summary response."""
|
|
102
|
+
total_usage: int
|
|
103
|
+
period_start: Optional[datetime] = None
|
|
104
|
+
period_end: Optional[datetime] = None
|
|
105
|
+
tier: str
|
|
106
|
+
limit: int
|
|
107
|
+
remaining: int
|
|
108
|
+
overage: int
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# ==================== Pricing Endpoints ====================
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@router.get("/pricing", response_model=list[PricingResponse])
|
|
115
|
+
async def get_pricing():
|
|
116
|
+
"""Get pricing information for all tiers."""
|
|
117
|
+
pricing = []
|
|
118
|
+
for tier in SubscriptionTier:
|
|
119
|
+
price_info = TIER_PRICES[tier]
|
|
120
|
+
tier_limits = TIER_LIMITS[tier]
|
|
121
|
+
pricing.append(PricingResponse(
|
|
122
|
+
tier=tier.value,
|
|
123
|
+
monthly_price=price_info.monthly_price_dollars,
|
|
124
|
+
api_calls_included=price_info.api_calls_included,
|
|
125
|
+
overage_price_per_call=price_info.overage_price_per_call,
|
|
126
|
+
features=tier_limits,
|
|
127
|
+
))
|
|
128
|
+
return pricing
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# ==================== Customer Endpoints ====================
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@router.post("/customers")
|
|
135
|
+
async def create_customer(
|
|
136
|
+
request: CreateCustomerRequest,
|
|
137
|
+
current_user: User = Depends(get_current_user),
|
|
138
|
+
stripe: StripeService = Depends(get_stripe_service),
|
|
139
|
+
):
|
|
140
|
+
"""Create a Stripe customer for the current user."""
|
|
141
|
+
try:
|
|
142
|
+
customer = stripe.create_customer(
|
|
143
|
+
email=request.email,
|
|
144
|
+
name=request.name,
|
|
145
|
+
user_id=str(current_user.id),
|
|
146
|
+
metadata={"user_id": str(current_user.id)},
|
|
147
|
+
)
|
|
148
|
+
return {
|
|
149
|
+
"customer_id": customer.stripe_customer_id,
|
|
150
|
+
"email": customer.email,
|
|
151
|
+
"name": customer.name,
|
|
152
|
+
}
|
|
153
|
+
except Exception as e:
|
|
154
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@router.get("/customers/me")
|
|
158
|
+
async def get_current_customer(
|
|
159
|
+
current_user: User = Depends(get_current_user),
|
|
160
|
+
stripe: StripeService = Depends(get_stripe_service),
|
|
161
|
+
):
|
|
162
|
+
"""Get current user's Stripe customer details."""
|
|
163
|
+
stripe_customer_id = getattr(current_user, 'stripe_customer_id', None)
|
|
164
|
+
if not stripe_customer_id:
|
|
165
|
+
raise HTTPException(status_code=404, detail="No billing account found")
|
|
166
|
+
|
|
167
|
+
customer = stripe.get_customer(stripe_customer_id)
|
|
168
|
+
if not customer:
|
|
169
|
+
raise HTTPException(status_code=404, detail="Customer not found")
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
"customer_id": customer.stripe_customer_id,
|
|
173
|
+
"email": customer.email,
|
|
174
|
+
"name": customer.name,
|
|
175
|
+
"has_payment_method": customer.has_payment_method,
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# ==================== Subscription Endpoints ====================
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@router.post("/subscriptions", response_model=SubscriptionResponse)
|
|
183
|
+
async def create_subscription(
|
|
184
|
+
request: CreateSubscriptionRequest,
|
|
185
|
+
current_user: User = Depends(get_current_user),
|
|
186
|
+
stripe: StripeService = Depends(get_stripe_service),
|
|
187
|
+
):
|
|
188
|
+
"""Create a new subscription."""
|
|
189
|
+
stripe_customer_id = getattr(current_user, 'stripe_customer_id', None)
|
|
190
|
+
if not stripe_customer_id:
|
|
191
|
+
raise HTTPException(
|
|
192
|
+
status_code=400,
|
|
193
|
+
detail="Create a billing account first"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
subscription = stripe.create_subscription(
|
|
198
|
+
customer_id=stripe_customer_id,
|
|
199
|
+
tier=request.tier,
|
|
200
|
+
trial_days=request.trial_days,
|
|
201
|
+
payment_method_id=request.payment_method_id,
|
|
202
|
+
)
|
|
203
|
+
return SubscriptionResponse(
|
|
204
|
+
id=subscription.stripe_subscription_id,
|
|
205
|
+
tier=subscription.tier,
|
|
206
|
+
status=subscription.status.value,
|
|
207
|
+
current_period_start=subscription.current_period_start,
|
|
208
|
+
current_period_end=subscription.current_period_end,
|
|
209
|
+
cancel_at_period_end=subscription.cancel_at_period_end,
|
|
210
|
+
trial_end=subscription.trial_end,
|
|
211
|
+
)
|
|
212
|
+
except Exception as e:
|
|
213
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@router.get("/subscriptions/current", response_model=SubscriptionResponse)
|
|
217
|
+
async def get_current_subscription(
|
|
218
|
+
current_user: User = Depends(get_current_user),
|
|
219
|
+
stripe: StripeService = Depends(get_stripe_service),
|
|
220
|
+
):
|
|
221
|
+
"""Get current user's active subscription."""
|
|
222
|
+
subscription_id = getattr(current_user, 'stripe_subscription_id', None)
|
|
223
|
+
if not subscription_id:
|
|
224
|
+
raise HTTPException(status_code=404, detail="No active subscription")
|
|
225
|
+
|
|
226
|
+
subscription = stripe.get_subscription(subscription_id)
|
|
227
|
+
if not subscription:
|
|
228
|
+
raise HTTPException(status_code=404, detail="Subscription not found")
|
|
229
|
+
|
|
230
|
+
return SubscriptionResponse(
|
|
231
|
+
id=subscription.stripe_subscription_id,
|
|
232
|
+
tier=subscription.tier,
|
|
233
|
+
status=subscription.status.value,
|
|
234
|
+
current_period_start=subscription.current_period_start,
|
|
235
|
+
current_period_end=subscription.current_period_end,
|
|
236
|
+
cancel_at_period_end=subscription.cancel_at_period_end,
|
|
237
|
+
trial_end=subscription.trial_end,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@router.patch("/subscriptions/current", response_model=SubscriptionResponse)
|
|
242
|
+
async def update_subscription(
|
|
243
|
+
request: UpdateSubscriptionRequest,
|
|
244
|
+
current_user: User = Depends(get_current_user),
|
|
245
|
+
stripe: StripeService = Depends(get_stripe_service),
|
|
246
|
+
):
|
|
247
|
+
"""Update subscription tier (upgrade/downgrade)."""
|
|
248
|
+
subscription_id = getattr(current_user, 'stripe_subscription_id', None)
|
|
249
|
+
if not subscription_id:
|
|
250
|
+
raise HTTPException(status_code=404, detail="No active subscription")
|
|
251
|
+
|
|
252
|
+
try:
|
|
253
|
+
subscription = stripe.update_subscription_tier(
|
|
254
|
+
stripe_subscription_id=subscription_id,
|
|
255
|
+
new_tier=request.tier,
|
|
256
|
+
prorate=request.prorate,
|
|
257
|
+
)
|
|
258
|
+
return SubscriptionResponse(
|
|
259
|
+
id=subscription.stripe_subscription_id,
|
|
260
|
+
tier=subscription.tier,
|
|
261
|
+
status=subscription.status.value,
|
|
262
|
+
current_period_start=subscription.current_period_start,
|
|
263
|
+
current_period_end=subscription.current_period_end,
|
|
264
|
+
cancel_at_period_end=subscription.cancel_at_period_end,
|
|
265
|
+
)
|
|
266
|
+
except Exception as e:
|
|
267
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@router.delete("/subscriptions/current")
|
|
271
|
+
async def cancel_subscription(
|
|
272
|
+
at_period_end: bool = True,
|
|
273
|
+
current_user: User = Depends(get_current_user),
|
|
274
|
+
stripe: StripeService = Depends(get_stripe_service),
|
|
275
|
+
):
|
|
276
|
+
"""Cancel current subscription."""
|
|
277
|
+
subscription_id = getattr(current_user, 'stripe_subscription_id', None)
|
|
278
|
+
if not subscription_id:
|
|
279
|
+
raise HTTPException(status_code=404, detail="No active subscription")
|
|
280
|
+
|
|
281
|
+
try:
|
|
282
|
+
subscription = stripe.cancel_subscription(
|
|
283
|
+
stripe_subscription_id=subscription_id,
|
|
284
|
+
at_period_end=at_period_end,
|
|
285
|
+
)
|
|
286
|
+
return {
|
|
287
|
+
"message": "Subscription cancelled",
|
|
288
|
+
"cancel_at_period_end": subscription.cancel_at_period_end,
|
|
289
|
+
"current_period_end": subscription.current_period_end,
|
|
290
|
+
}
|
|
291
|
+
except Exception as e:
|
|
292
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
@router.post("/subscriptions/current/reactivate")
|
|
296
|
+
async def reactivate_subscription(
|
|
297
|
+
current_user: User = Depends(get_current_user),
|
|
298
|
+
stripe: StripeService = Depends(get_stripe_service),
|
|
299
|
+
):
|
|
300
|
+
"""Reactivate a cancelled subscription."""
|
|
301
|
+
subscription_id = getattr(current_user, 'stripe_subscription_id', None)
|
|
302
|
+
if not subscription_id:
|
|
303
|
+
raise HTTPException(status_code=404, detail="No subscription found")
|
|
304
|
+
|
|
305
|
+
try:
|
|
306
|
+
subscription = stripe.reactivate_subscription(subscription_id)
|
|
307
|
+
return {
|
|
308
|
+
"message": "Subscription reactivated",
|
|
309
|
+
"status": subscription.status.value,
|
|
310
|
+
}
|
|
311
|
+
except Exception as e:
|
|
312
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# ==================== Usage Endpoints ====================
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@router.get("/usage", response_model=UsageSummaryResponse)
|
|
319
|
+
async def get_usage_summary(
|
|
320
|
+
current_user: User = Depends(get_current_user),
|
|
321
|
+
stripe: StripeService = Depends(get_stripe_service),
|
|
322
|
+
):
|
|
323
|
+
"""Get usage summary for current billing period."""
|
|
324
|
+
subscription_item_id = getattr(current_user, 'stripe_subscription_item_id', None)
|
|
325
|
+
tier = getattr(current_user, 'tier', 'free')
|
|
326
|
+
tier_enum = SubscriptionTier(tier)
|
|
327
|
+
limit = TIER_LIMITS[tier_enum]["api_calls_monthly"]
|
|
328
|
+
|
|
329
|
+
if subscription_item_id:
|
|
330
|
+
summary = stripe.get_usage_summary(subscription_item_id)
|
|
331
|
+
total_usage = summary["total_usage"]
|
|
332
|
+
else:
|
|
333
|
+
# For free tier or no subscription, use internal tracking
|
|
334
|
+
total_usage = 0 # Would come from usage tracking
|
|
335
|
+
summary = {"period_start": None, "period_end": None}
|
|
336
|
+
|
|
337
|
+
return UsageSummaryResponse(
|
|
338
|
+
total_usage=total_usage,
|
|
339
|
+
period_start=summary.get("period_start"),
|
|
340
|
+
period_end=summary.get("period_end"),
|
|
341
|
+
tier=tier,
|
|
342
|
+
limit=limit,
|
|
343
|
+
remaining=max(0, limit - total_usage),
|
|
344
|
+
overage=max(0, total_usage - limit),
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
# ==================== Payment Methods ====================
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
@router.post("/payment-methods/setup")
|
|
352
|
+
async def create_setup_intent(
|
|
353
|
+
current_user: User = Depends(get_current_user),
|
|
354
|
+
stripe: StripeService = Depends(get_stripe_service),
|
|
355
|
+
):
|
|
356
|
+
"""Create a SetupIntent for adding a payment method."""
|
|
357
|
+
stripe_customer_id = getattr(current_user, 'stripe_customer_id', None)
|
|
358
|
+
if not stripe_customer_id:
|
|
359
|
+
raise HTTPException(status_code=400, detail="Create billing account first")
|
|
360
|
+
|
|
361
|
+
intent = stripe.create_setup_intent(stripe_customer_id)
|
|
362
|
+
return intent
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
@router.get("/payment-methods")
|
|
366
|
+
async def list_payment_methods(
|
|
367
|
+
current_user: User = Depends(get_current_user),
|
|
368
|
+
stripe: StripeService = Depends(get_stripe_service),
|
|
369
|
+
):
|
|
370
|
+
"""List saved payment methods."""
|
|
371
|
+
stripe_customer_id = getattr(current_user, 'stripe_customer_id', None)
|
|
372
|
+
if not stripe_customer_id:
|
|
373
|
+
return []
|
|
374
|
+
|
|
375
|
+
methods = stripe.list_payment_methods(stripe_customer_id)
|
|
376
|
+
return methods
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
# ==================== Invoices ====================
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
@router.get("/invoices", response_model=list[InvoiceResponse])
|
|
383
|
+
async def list_invoices(
|
|
384
|
+
limit: int = 10,
|
|
385
|
+
current_user: User = Depends(get_current_user),
|
|
386
|
+
stripe: StripeService = Depends(get_stripe_service),
|
|
387
|
+
):
|
|
388
|
+
"""List invoices for current user."""
|
|
389
|
+
stripe_customer_id = getattr(current_user, 'stripe_customer_id', None)
|
|
390
|
+
if not stripe_customer_id:
|
|
391
|
+
return []
|
|
392
|
+
|
|
393
|
+
invoices = stripe.list_invoices(stripe_customer_id, limit=limit)
|
|
394
|
+
return [
|
|
395
|
+
InvoiceResponse(
|
|
396
|
+
id=inv.stripe_invoice_id,
|
|
397
|
+
amount_due=inv.amount_due_dollars,
|
|
398
|
+
amount_paid=inv.amount_paid_dollars,
|
|
399
|
+
currency=inv.currency,
|
|
400
|
+
status=inv.status.value,
|
|
401
|
+
invoice_pdf=inv.invoice_pdf,
|
|
402
|
+
hosted_invoice_url=inv.hosted_invoice_url,
|
|
403
|
+
period_start=inv.period_start,
|
|
404
|
+
period_end=inv.period_end,
|
|
405
|
+
)
|
|
406
|
+
for inv in invoices
|
|
407
|
+
]
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
@router.get("/invoices/upcoming", response_model=Optional[InvoiceResponse])
|
|
411
|
+
async def get_upcoming_invoice(
|
|
412
|
+
current_user: User = Depends(get_current_user),
|
|
413
|
+
stripe: StripeService = Depends(get_stripe_service),
|
|
414
|
+
):
|
|
415
|
+
"""Get upcoming invoice preview."""
|
|
416
|
+
stripe_customer_id = getattr(current_user, 'stripe_customer_id', None)
|
|
417
|
+
if not stripe_customer_id:
|
|
418
|
+
return None
|
|
419
|
+
|
|
420
|
+
invoice = stripe.get_upcoming_invoice(stripe_customer_id)
|
|
421
|
+
if not invoice:
|
|
422
|
+
return None
|
|
423
|
+
|
|
424
|
+
return InvoiceResponse(
|
|
425
|
+
id=invoice.stripe_invoice_id,
|
|
426
|
+
amount_due=invoice.amount_due_dollars,
|
|
427
|
+
amount_paid=invoice.amount_paid_dollars,
|
|
428
|
+
currency=invoice.currency,
|
|
429
|
+
status=invoice.status.value,
|
|
430
|
+
period_start=invoice.period_start,
|
|
431
|
+
period_end=invoice.period_end,
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
# ==================== Checkout & Portal ====================
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
@router.post("/checkout")
|
|
439
|
+
async def create_checkout_session(
|
|
440
|
+
request: CheckoutSessionRequest,
|
|
441
|
+
current_user: User = Depends(get_current_user),
|
|
442
|
+
stripe: StripeService = Depends(get_stripe_service),
|
|
443
|
+
):
|
|
444
|
+
"""Create a Stripe Checkout session."""
|
|
445
|
+
stripe_customer_id = getattr(current_user, 'stripe_customer_id', None)
|
|
446
|
+
|
|
447
|
+
# Auto-create customer if not exists
|
|
448
|
+
if not stripe_customer_id:
|
|
449
|
+
email = current_user.email
|
|
450
|
+
if not email:
|
|
451
|
+
raise HTTPException(status_code=400, detail="User email required")
|
|
452
|
+
|
|
453
|
+
customer = stripe.create_customer(
|
|
454
|
+
email=email,
|
|
455
|
+
name=current_user.name,
|
|
456
|
+
user_id=str(current_user.id),
|
|
457
|
+
metadata={"user_id": str(current_user.id)},
|
|
458
|
+
)
|
|
459
|
+
stripe_customer_id = customer.stripe_customer_id
|
|
460
|
+
# Note: In production, save stripe_customer_id to user record in database
|
|
461
|
+
|
|
462
|
+
try:
|
|
463
|
+
session = stripe.create_checkout_session(
|
|
464
|
+
customer_id=stripe_customer_id,
|
|
465
|
+
tier=request.tier,
|
|
466
|
+
success_url=request.success_url,
|
|
467
|
+
cancel_url=request.cancel_url,
|
|
468
|
+
)
|
|
469
|
+
return session
|
|
470
|
+
except Exception as e:
|
|
471
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
@router.post("/portal")
|
|
475
|
+
async def create_billing_portal(
|
|
476
|
+
request: BillingPortalRequest,
|
|
477
|
+
current_user: User = Depends(get_current_user),
|
|
478
|
+
stripe: StripeService = Depends(get_stripe_service),
|
|
479
|
+
):
|
|
480
|
+
"""Create a Stripe Billing Portal session."""
|
|
481
|
+
stripe_customer_id = getattr(current_user, 'stripe_customer_id', None)
|
|
482
|
+
if not stripe_customer_id:
|
|
483
|
+
raise HTTPException(status_code=400, detail="No billing account")
|
|
484
|
+
|
|
485
|
+
session = stripe.create_billing_portal_session(
|
|
486
|
+
customer_id=stripe_customer_id,
|
|
487
|
+
return_url=request.return_url,
|
|
488
|
+
)
|
|
489
|
+
return session
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
# ==================== Webhooks ====================
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
@router.post("/webhooks/stripe")
|
|
496
|
+
async def handle_stripe_webhook(
|
|
497
|
+
request: Request,
|
|
498
|
+
stripe_signature: str = Header(None, alias="Stripe-Signature"),
|
|
499
|
+
):
|
|
500
|
+
"""Handle Stripe webhook events."""
|
|
501
|
+
if not stripe_signature:
|
|
502
|
+
raise HTTPException(status_code=400, detail="Missing Stripe signature")
|
|
503
|
+
|
|
504
|
+
try:
|
|
505
|
+
stripe_service = get_stripe_service()
|
|
506
|
+
payload = await request.body()
|
|
507
|
+
event = stripe_service.construct_webhook_event(payload, stripe_signature)
|
|
508
|
+
result = stripe_service.handle_webhook_event(event)
|
|
509
|
+
|
|
510
|
+
# TODO: Update database based on result
|
|
511
|
+
# - subscription_created -> update user tier
|
|
512
|
+
# - subscription_deleted -> downgrade to free
|
|
513
|
+
# - payment_failed -> send notification
|
|
514
|
+
|
|
515
|
+
return {"received": True, "action": result.get("action")}
|
|
516
|
+
|
|
517
|
+
except ValueError as e:
|
|
518
|
+
raise HTTPException(status_code=400, detail=f"Invalid payload: {e}")
|
|
519
|
+
except Exception as e:
|
|
520
|
+
raise HTTPException(status_code=400, detail=f"Webhook error: {e}")
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""QuantumFlow Backends - Quantum hardware and simulator backends."""
|
|
2
|
+
|
|
3
|
+
from quantumflow.backends.base_backend import (
|
|
4
|
+
QuantumBackend,
|
|
5
|
+
BackendType,
|
|
6
|
+
ExecutionResult,
|
|
7
|
+
get_backend,
|
|
8
|
+
)
|
|
9
|
+
from quantumflow.backends.simulator_backend import SimulatorBackend
|
|
10
|
+
|
|
11
|
+
# Lazy import for optional IBM backend
|
|
12
|
+
try:
|
|
13
|
+
from quantumflow.backends.ibm_backend import IBMBackend
|
|
14
|
+
except ImportError:
|
|
15
|
+
IBMBackend = None # type: ignore
|
|
16
|
+
|
|
17
|
+
# Lazy import for optional AWS Braket backend
|
|
18
|
+
try:
|
|
19
|
+
from quantumflow.backends.braket_backend import BraketBackend, BRAKET_DEVICES
|
|
20
|
+
except ImportError:
|
|
21
|
+
BraketBackend = None # type: ignore
|
|
22
|
+
BRAKET_DEVICES = {}
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"QuantumBackend",
|
|
26
|
+
"BackendType",
|
|
27
|
+
"ExecutionResult",
|
|
28
|
+
"get_backend",
|
|
29
|
+
"SimulatorBackend",
|
|
30
|
+
"IBMBackend",
|
|
31
|
+
"BraketBackend",
|
|
32
|
+
"BRAKET_DEVICES",
|
|
33
|
+
]
|