Coverage for backend / app / payments / routers / routers.py: 44%
63 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-17 21:34 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-17 21:34 +0000
1"""Payment-related API endpoints using FastAPI and Stripe."""
3import json
5from fastapi import Request, Depends, HTTPException, APIRouter
6from sqlalchemy.orm import Session
7from starlette import status
9from app.config import settings
10from app.core.oauth2 import get_current_user
11from app.database import get_db
12from app.models import User
13from app.payments import logger, stripe
14from app.payments.checkout import build_checkout_params
15from app.payments.customer import get_or_create_stripe_customer
16from app.payments.webhooks import process_subscription_event
19payment_router = APIRouter(prefix="/payments", tags=["payments"])
22@payment_router.post("/create-subscription-checkout")
23async def create_subscription_checkout(
24 current_user: User = Depends(get_current_user),
25 db: Session = Depends(get_db),
26) -> dict:
27 """Create a Stripe Customer and Checkout Session for the current user.
28 New customers get a 14-day trial without a payment method required.
29 Returning customers must provide a payment method upfront.
30 :param current_user: Authenticated user from JWT token
31 :param db: Database session
32 :return: dict with checkout URL
33 :raises HTTPException: On Stripe or database errors"""
35 try:
36 customer_id = await get_or_create_stripe_customer(current_user, db)
37 checkout_params = await build_checkout_params(customer_id)
38 checkout_session = await stripe.checkout.Session.create_async(**checkout_params)
39 logger.info(f"Created checkout session {checkout_session.id} for user {current_user.id}")
40 return {"url": checkout_session.url}
41 except HTTPException: # re-raise internal HTTP exceptions
42 raise
43 except stripe.error.StripeError as e:
44 logger.error(f"Stripe error creating checkout for user {current_user.id}: {str(e)}", exc_info=True)
45 raise HTTPException(
46 status_code=status.HTTP_400_BAD_REQUEST,
47 detail="Payment service temporarily unavailable. Please try again.",
48 )
49 except Exception as e:
50 logger.error(f"Unexpected error creating checkout for user {current_user.id}: {str(e)}", exc_info=True)
51 raise HTTPException(
52 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
53 detail="An error occurred. Please try again.",
54 )
57@payment_router.post("/webhooks")
58async def stripe_webhook(
59 request: Request,
60 db: Session = Depends(get_db),
61) -> dict:
62 """Handle Stripe webhook events
63 :param request: Incoming HTTP request
64 :param db: Database session"""
66 payload = await request.body()
67 sig_header = request.headers.get("stripe-signature")
69 if settings.test_mode:
70 # Just parse JSON directly in dev/test mode
71 event = json.loads(payload)
72 print("[Stripe Event]", event)
73 else:
74 # Production verification
75 try:
76 event = stripe.Webhook.construct_event(payload, sig_header, settings.stripe_webhook_secret)
77 except ValueError:
78 raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid payload")
79 except stripe.error.SignatureVerificationError:
80 raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid signature")
82 obj = event["data"]["object"]
83 event_type = event.get("type")
85 await process_subscription_event(
86 customer_id=obj.get("customer"),
87 event_type=event_type,
88 db=db,
89 subscription_id=obj.get("id"),
90 trial_end=obj.get("trial_end"),
91 )
92 return {"status": "success"}
95@payment_router.post("/create-portal-session")
96async def create_portal_session(
97 current_user: User = Depends(get_current_user),
98 db: Session = Depends(get_db),
99) -> dict:
100 """Create a Stripe Customer Portal session for subscription management.
101 :param current_user: Authenticated user from JWT token
102 :param db: Database session
103 :return: dict with portal URL
104 :raises HTTPException: On customer not found or Stripe errors"""
106 try:
107 if not current_user.stripe_details.customer_id:
108 raise HTTPException(
109 status_code=status.HTTP_404_NOT_FOUND,
110 detail="No customer found. Please subscribe first.",
111 )
112 # Make sure that Stripe and JAM are synced
113 customer_id = await get_or_create_stripe_customer(current_user, db)
115 # Create portal session
116 portal_session = await stripe.billing_portal.Session.create_async(
117 customer=customer_id,
118 return_url=f"{settings.frontend_url}/settings/premium?success=true",
119 )
121 logger.info(f"Created portal session for user {current_user.id}")
123 return {"url": portal_session.url}
125 except HTTPException:
126 raise
127 except stripe.error.StripeError as e:
128 logger.error(f"Stripe error creating portal for user {current_user.id}: {str(e)}", exc_info=True)
129 raise HTTPException(status_code=503, detail="Payment service temporarily unavailable. Please try again.")
130 except Exception as e:
131 logger.error(f"Unexpected error creating portal for user {current_user.id}: {str(e)}", exc_info=True)
132 raise HTTPException(status_code=500, detail="An error occurred. Please try again.")