Coverage for backend / app / payments / webhooks.py: 96%
47 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"""Stripe webhooks module"""
3import datetime as dt
5from fastapi import HTTPException
6from sqlalchemy.orm import Session
7from starlette import status
9from app.emails.email_service import email_service
10from app.models import User
11from app.payments import logger, stripe
14async def process_subscription_event(
15 customer_id: str,
16 event_type: str,
17 db: Session,
18 subscription_id: str | None = None,
19 trial_end: float | None = None,
20) -> None:
21 """Process subscription event for a given user.
22 :param customer_id: Stripe customer id
23 :param event_type: Stripe event type
24 :param db: Database session
25 :param subscription_id: Stripe subscription id
26 :param trial_end: Stripe trial end"""
28 user = db.query(User).filter(User.stripe_details.has(customer_id=customer_id)).first()
29 logger.info(f"Received event: {event_type} for customer {customer_id}")
31 # If user not found (e.g., user was deleted), log and return early
32 if not user:
33 logger.warning(f"User not found for customer {customer_id}, skipping event {event_type}")
34 return
36 # Handle subscription creation
37 if event_type == "customer.subscription.created":
38 user.stripe_details.subscription_id = subscription_id
39 user.premium.is_active = True
40 db.commit()
41 logger.info(f"Subscription created: {subscription_id} for user {user.id}")
43 # Handle subscription deletion by setting premium as not active
44 elif event_type == "customer.subscription.deleted":
45 user.premium.is_active = False
46 db.commit()
47 logger.info(f"Subscription deleted for user {user.id}")
49 # Handle trial ending soon, send email to user
50 elif event_type == "customer.subscription.trial_will_end":
51 try:
52 trial_end_date = dt.datetime.fromtimestamp(trial_end)
53 email_service.send_trial_end_notification(user.email, trial_end_date)
54 logger.info(f"Trial ending notification sent to user {user.id}")
55 except Exception as e:
56 logger.error(f"Failed to send trial ending email to user {user.id}: {e}")
57 logger.info(f"Trial ending soon for user {user.id}")
59 elif event_type in ["billing_portal.session.created", "customer.created"]:
60 pass
62 else:
63 logger.error(f"Unhandled event type: {event_type}")
65 # Update the user's subscription status'
66 subscription_status = await get_subscription_status(user)
67 user.stripe_details.subscription_status = subscription_status["status"]
68 user.stripe_details.trial_end_date = subscription_status["trial_end"]
69 db.commit()
72async def get_subscription_status(current_user: User) -> dict:
73 """Get subscription status and trial information for the current user.
74 :param current_user: Authenticated user from JWT token
75 :return: dict with subscription status and remaining trial days"""
77 if not current_user.stripe_details.subscription_id:
78 return {"status": None, "trial_end": None}
79 try:
80 subscription = await stripe.Subscription.retrieve_async(current_user.stripe_details.subscription_id)
81 logger.info(f"Retrieved subscription status for user {current_user.id}. Trial end: {subscription.trial_end}")
82 return {"status": subscription.status, "trial_end": subscription.trial_end}
83 except stripe.error.StripeError as e:
84 logger.error(f"Failed to retrieve subscription for user {current_user.id}: {e}")
85 raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))