forked from andika/membership-be
first commit
This commit is contained in:
180
payment_service.py
Normal file
180
payment_service.py
Normal file
@@ -0,0 +1,180 @@
|
||||
"""
|
||||
Payment service for Stripe integration.
|
||||
Handles subscription creation, checkout sessions, and webhook processing.
|
||||
"""
|
||||
|
||||
import stripe
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv()
|
||||
|
||||
# Initialize Stripe with secret key
|
||||
stripe.api_key = os.getenv("STRIPE_SECRET_KEY")
|
||||
|
||||
# Stripe webhook secret for signature verification
|
||||
STRIPE_WEBHOOK_SECRET = os.getenv("STRIPE_WEBHOOK_SECRET")
|
||||
|
||||
def create_checkout_session(
|
||||
user_id: str,
|
||||
user_email: str,
|
||||
plan_id: str,
|
||||
stripe_price_id: str,
|
||||
success_url: str,
|
||||
cancel_url: str
|
||||
):
|
||||
"""
|
||||
Create a Stripe Checkout session for subscription payment.
|
||||
|
||||
Args:
|
||||
user_id: User's UUID
|
||||
user_email: User's email address
|
||||
plan_id: SubscriptionPlan UUID
|
||||
stripe_price_id: Stripe Price ID for the plan
|
||||
success_url: URL to redirect after successful payment
|
||||
cancel_url: URL to redirect if user cancels
|
||||
|
||||
Returns:
|
||||
dict: Checkout session object with session ID and URL
|
||||
"""
|
||||
try:
|
||||
# Create Checkout Session
|
||||
checkout_session = stripe.checkout.Session.create(
|
||||
customer_email=user_email,
|
||||
payment_method_types=["card"],
|
||||
line_items=[
|
||||
{
|
||||
"price": stripe_price_id,
|
||||
"quantity": 1,
|
||||
}
|
||||
],
|
||||
mode="subscription",
|
||||
success_url=success_url,
|
||||
cancel_url=cancel_url,
|
||||
metadata={
|
||||
"user_id": str(user_id),
|
||||
"plan_id": str(plan_id),
|
||||
},
|
||||
subscription_data={
|
||||
"metadata": {
|
||||
"user_id": str(user_id),
|
||||
"plan_id": str(plan_id),
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"session_id": checkout_session.id,
|
||||
"url": checkout_session.url
|
||||
}
|
||||
|
||||
except stripe.error.StripeError as e:
|
||||
raise Exception(f"Stripe error: {str(e)}")
|
||||
|
||||
|
||||
def verify_webhook_signature(payload: bytes, sig_header: str) -> dict:
|
||||
"""
|
||||
Verify Stripe webhook signature and construct event.
|
||||
|
||||
Args:
|
||||
payload: Raw webhook payload bytes
|
||||
sig_header: Stripe signature header
|
||||
|
||||
Returns:
|
||||
dict: Verified webhook event
|
||||
|
||||
Raises:
|
||||
ValueError: If signature verification fails
|
||||
"""
|
||||
try:
|
||||
event = stripe.Webhook.construct_event(
|
||||
payload, sig_header, STRIPE_WEBHOOK_SECRET
|
||||
)
|
||||
return event
|
||||
except ValueError as e:
|
||||
raise ValueError(f"Invalid payload: {str(e)}")
|
||||
except stripe.error.SignatureVerificationError as e:
|
||||
raise ValueError(f"Invalid signature: {str(e)}")
|
||||
|
||||
|
||||
def get_subscription_end_date(billing_cycle: str = "yearly") -> datetime:
|
||||
"""
|
||||
Calculate subscription end date based on billing cycle.
|
||||
|
||||
Args:
|
||||
billing_cycle: "yearly" or "monthly"
|
||||
|
||||
Returns:
|
||||
datetime: End date for the subscription
|
||||
"""
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
if billing_cycle == "yearly":
|
||||
# Add 1 year
|
||||
return now + timedelta(days=365)
|
||||
elif billing_cycle == "monthly":
|
||||
# Add 1 month (approximation)
|
||||
return now + timedelta(days=30)
|
||||
else:
|
||||
# Default to yearly
|
||||
return now + timedelta(days=365)
|
||||
|
||||
|
||||
def create_stripe_price(
|
||||
product_name: str,
|
||||
price_cents: int,
|
||||
billing_cycle: str = "yearly"
|
||||
) -> str:
|
||||
"""
|
||||
Create a Stripe Price object for a subscription plan.
|
||||
|
||||
Args:
|
||||
product_name: Name of the product/plan
|
||||
price_cents: Price in cents
|
||||
billing_cycle: "yearly" or "monthly"
|
||||
|
||||
Returns:
|
||||
str: Stripe Price ID
|
||||
"""
|
||||
try:
|
||||
# Create a product first
|
||||
product = stripe.Product.create(name=product_name)
|
||||
|
||||
# Determine recurring interval
|
||||
interval = "year" if billing_cycle == "yearly" else "month"
|
||||
|
||||
# Create price
|
||||
price = stripe.Price.create(
|
||||
product=product.id,
|
||||
unit_amount=price_cents,
|
||||
currency="usd",
|
||||
recurring={"interval": interval},
|
||||
)
|
||||
|
||||
return price.id
|
||||
|
||||
except stripe.error.StripeError as e:
|
||||
raise Exception(f"Stripe error creating price: {str(e)}")
|
||||
|
||||
|
||||
def get_customer_portal_url(stripe_customer_id: str, return_url: str) -> str:
|
||||
"""
|
||||
Create a Stripe Customer Portal session for subscription management.
|
||||
|
||||
Args:
|
||||
stripe_customer_id: Stripe Customer ID
|
||||
return_url: URL to return to after portal session
|
||||
|
||||
Returns:
|
||||
str: Customer portal URL
|
||||
"""
|
||||
try:
|
||||
session = stripe.billing_portal.Session.create(
|
||||
customer=stripe_customer_id,
|
||||
return_url=return_url,
|
||||
)
|
||||
return session.url
|
||||
except stripe.error.StripeError as e:
|
||||
raise Exception(f"Stripe error creating portal session: {str(e)}")
|
||||
Reference in New Issue
Block a user