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

1"""Payment-related API endpoints using FastAPI and Stripe.""" 

2 

3import json 

4 

5from fastapi import Request, Depends, HTTPException, APIRouter 

6from sqlalchemy.orm import Session 

7from starlette import status 

8 

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 

17 

18 

19payment_router = APIRouter(prefix="/payments", tags=["payments"]) 

20 

21 

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""" 

34 

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 ) 

55 

56 

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""" 

65 

66 payload = await request.body() 

67 sig_header = request.headers.get("stripe-signature") 

68 

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") 

81 

82 obj = event["data"]["object"] 

83 event_type = event.get("type") 

84 

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"} 

93 

94 

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""" 

105 

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) 

114 

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 ) 

120 

121 logger.info(f"Created portal session for user {current_user.id}") 

122 

123 return {"url": portal_session.url} 

124 

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.")