started implementing stripe
This commit is contained in:
444
utils/stripe_utils.py
Normal file
444
utils/stripe_utils.py
Normal file
@@ -0,0 +1,444 @@
|
||||
"""
|
||||
Stripe utility functions for managing products, prices, and checkout sessions.
|
||||
"""
|
||||
import stripe
|
||||
import os
|
||||
from models import KeyValueSettings, PricingPlan
|
||||
from flask import current_app, url_for
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def get_stripe_settings():
|
||||
"""Get Stripe settings from database"""
|
||||
stripe_settings = KeyValueSettings.get_value('stripe_settings')
|
||||
if not stripe_settings:
|
||||
return None
|
||||
return stripe_settings
|
||||
|
||||
def configure_stripe():
|
||||
"""Configure Stripe with API key from settings"""
|
||||
stripe_settings = get_stripe_settings()
|
||||
if not stripe_settings or not stripe_settings.get('secret_key'):
|
||||
raise ValueError("Stripe secret key not configured")
|
||||
|
||||
stripe.api_key = stripe_settings['secret_key']
|
||||
return stripe_settings
|
||||
|
||||
def create_stripe_product(plan):
|
||||
"""
|
||||
Create a Stripe product and prices for a pricing plan
|
||||
|
||||
Args:
|
||||
plan: PricingPlan instance
|
||||
|
||||
Returns:
|
||||
dict: Contains product_id, monthly_price_id, annual_price_id
|
||||
"""
|
||||
try:
|
||||
configure_stripe()
|
||||
|
||||
# Create product
|
||||
product = stripe.Product.create(
|
||||
name=plan.name,
|
||||
description=plan.description or f"DocuPulse {plan.name} Plan",
|
||||
metadata={
|
||||
'plan_id': plan.id,
|
||||
'plan_name': plan.name
|
||||
}
|
||||
)
|
||||
|
||||
# Create monthly price
|
||||
monthly_price = stripe.Price.create(
|
||||
product=product.id,
|
||||
unit_amount=int(plan.monthly_price * 100), # Convert to cents
|
||||
currency='eur',
|
||||
recurring={'interval': 'month'},
|
||||
metadata={
|
||||
'plan_id': plan.id,
|
||||
'billing_cycle': 'monthly',
|
||||
'plan_name': plan.name
|
||||
}
|
||||
)
|
||||
|
||||
# Create annual price
|
||||
annual_price = stripe.Price.create(
|
||||
product=product.id,
|
||||
unit_amount=int(plan.annual_price * 100), # Convert to cents
|
||||
currency='eur',
|
||||
recurring={'interval': 'year'},
|
||||
metadata={
|
||||
'plan_id': plan.id,
|
||||
'billing_cycle': 'annual',
|
||||
'plan_name': plan.name
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
'product_id': product.id,
|
||||
'monthly_price_id': monthly_price.id,
|
||||
'annual_price_id': annual_price.id
|
||||
}
|
||||
|
||||
except stripe.error.StripeError as e:
|
||||
logger.error(f"Stripe error creating product for plan {plan.name}: {str(e)}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating Stripe product for plan {plan.name}: {str(e)}")
|
||||
raise
|
||||
|
||||
def update_stripe_product(plan):
|
||||
"""
|
||||
Update an existing Stripe product and prices for a pricing plan
|
||||
|
||||
Args:
|
||||
plan: PricingPlan instance with existing Stripe IDs
|
||||
|
||||
Returns:
|
||||
dict: Updated product and price information
|
||||
"""
|
||||
try:
|
||||
configure_stripe()
|
||||
|
||||
if not plan.stripe_product_id:
|
||||
# If no product ID exists, create new product
|
||||
return create_stripe_product(plan)
|
||||
|
||||
# Update product
|
||||
product = stripe.Product.modify(
|
||||
plan.stripe_product_id,
|
||||
name=plan.name,
|
||||
description=plan.description or f"DocuPulse {plan.name} Plan",
|
||||
metadata={
|
||||
'plan_id': plan.id,
|
||||
'plan_name': plan.name
|
||||
}
|
||||
)
|
||||
|
||||
# Archive old prices and create new ones
|
||||
new_prices = {}
|
||||
|
||||
# Handle monthly price
|
||||
if plan.stripe_monthly_price_id:
|
||||
try:
|
||||
# Archive old monthly price
|
||||
stripe.Price.modify(plan.stripe_monthly_price_id, active=False)
|
||||
except stripe.error.StripeError:
|
||||
pass # Price might not exist
|
||||
|
||||
# Create new monthly price
|
||||
monthly_price = stripe.Price.create(
|
||||
product=product.id,
|
||||
unit_amount=int(plan.monthly_price * 100),
|
||||
currency='eur',
|
||||
recurring={'interval': 'month'},
|
||||
metadata={
|
||||
'plan_id': plan.id,
|
||||
'billing_cycle': 'monthly',
|
||||
'plan_name': plan.name
|
||||
}
|
||||
)
|
||||
new_prices['monthly_price_id'] = monthly_price.id
|
||||
|
||||
# Handle annual price
|
||||
if plan.stripe_annual_price_id:
|
||||
try:
|
||||
# Archive old annual price
|
||||
stripe.Price.modify(plan.stripe_annual_price_id, active=False)
|
||||
except stripe.error.StripeError:
|
||||
pass # Price might not exist
|
||||
|
||||
# Create new annual price
|
||||
annual_price = stripe.Price.create(
|
||||
product=product.id,
|
||||
unit_amount=int(plan.annual_price * 100),
|
||||
currency='eur',
|
||||
recurring={'interval': 'year'},
|
||||
metadata={
|
||||
'plan_id': plan.id,
|
||||
'billing_cycle': 'annual',
|
||||
'plan_name': plan.name
|
||||
}
|
||||
)
|
||||
new_prices['annual_price_id'] = annual_price.id
|
||||
|
||||
return {
|
||||
'product_id': product.id,
|
||||
'monthly_price_id': new_prices['monthly_price_id'],
|
||||
'annual_price_id': new_prices['annual_price_id']
|
||||
}
|
||||
|
||||
except stripe.error.StripeError as e:
|
||||
logger.error(f"Stripe error updating product for plan {plan.name}: {str(e)}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating Stripe product for plan {plan.name}: {str(e)}")
|
||||
raise
|
||||
|
||||
def create_checkout_session(plan_id, billing_cycle='monthly', success_url=None, cancel_url=None):
|
||||
"""
|
||||
Create a Stripe checkout session for a pricing plan
|
||||
|
||||
Args:
|
||||
plan_id: ID of the PricingPlan
|
||||
billing_cycle: 'monthly' or 'annual'
|
||||
success_url: URL to redirect to on successful payment
|
||||
cancel_url: URL to redirect to on cancellation
|
||||
|
||||
Returns:
|
||||
str: Checkout session URL
|
||||
"""
|
||||
logger.info(f"=== CREATE CHECKOUT SESSION START ===")
|
||||
logger.info(f"Plan ID: {plan_id}")
|
||||
logger.info(f"Billing cycle: {billing_cycle}")
|
||||
logger.info(f"Success URL: {success_url}")
|
||||
logger.info(f"Cancel URL: {cancel_url}")
|
||||
|
||||
try:
|
||||
configure_stripe()
|
||||
logger.info("Stripe configured successfully")
|
||||
|
||||
plan = PricingPlan.query.get(plan_id)
|
||||
logger.info(f"Plan lookup result: {plan}")
|
||||
|
||||
if not plan:
|
||||
logger.error(f"Pricing plan with ID {plan_id} not found")
|
||||
raise ValueError(f"Pricing plan with ID {plan_id} not found")
|
||||
|
||||
logger.info(f"Plan found: {plan.name}")
|
||||
logger.info(f"Plan stripe_monthly_price_id: {plan.stripe_monthly_price_id}")
|
||||
logger.info(f"Plan stripe_annual_price_id: {plan.stripe_annual_price_id}")
|
||||
|
||||
# Determine which price ID to use
|
||||
if billing_cycle == 'monthly':
|
||||
price_id = plan.stripe_monthly_price_id
|
||||
if not price_id:
|
||||
logger.error("Monthly price not configured for this plan")
|
||||
raise ValueError("Monthly price not configured for this plan")
|
||||
elif billing_cycle == 'annual':
|
||||
price_id = plan.stripe_annual_price_id
|
||||
if not price_id:
|
||||
logger.error("Annual price not configured for this plan")
|
||||
raise ValueError("Annual price not configured for this plan")
|
||||
else:
|
||||
logger.error(f"Invalid billing cycle: {billing_cycle}")
|
||||
raise ValueError("Invalid billing cycle. Must be 'monthly' or 'annual'")
|
||||
|
||||
logger.info(f"Using price ID: {price_id}")
|
||||
|
||||
# Set default URLs if not provided
|
||||
if not success_url:
|
||||
success_url = url_for('main.dashboard', _external=True)
|
||||
if not cancel_url:
|
||||
cancel_url = url_for('main.public_home', _external=True)
|
||||
|
||||
logger.info(f"Final success URL: {success_url}")
|
||||
logger.info(f"Final cancel URL: {cancel_url}")
|
||||
|
||||
# Create checkout session
|
||||
session_data = {
|
||||
'payment_method_types': ['card'],
|
||||
'line_items': [{
|
||||
'price': price_id,
|
||||
'quantity': 1,
|
||||
}],
|
||||
'mode': 'subscription',
|
||||
'success_url': f"{success_url}?session_id={{CHECKOUT_SESSION_ID}}",
|
||||
'cancel_url': cancel_url,
|
||||
'metadata': {
|
||||
'plan_id': plan_id,
|
||||
'plan_name': plan.name,
|
||||
'billing_cycle': billing_cycle
|
||||
},
|
||||
'customer_email': None, # Will be collected during checkout
|
||||
'allow_promotion_codes': True,
|
||||
'billing_address_collection': 'required',
|
||||
'phone_number_collection': {
|
||||
'enabled': True
|
||||
},
|
||||
'automatic_tax': {
|
||||
'enabled': True
|
||||
},
|
||||
'tax_id_collection': {
|
||||
'enabled': True
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(f"Creating Stripe session with data: {session_data}")
|
||||
|
||||
session = stripe.checkout.Session.create(**session_data)
|
||||
|
||||
logger.info(f"Stripe session created successfully: {session.id}")
|
||||
logger.info(f"Session URL: {session.url}")
|
||||
|
||||
return session.url
|
||||
|
||||
except stripe.error.StripeError as e:
|
||||
logger.error(f"Stripe error creating checkout session: {str(e)}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating checkout session: {str(e)}")
|
||||
raise
|
||||
finally:
|
||||
logger.info("=== CREATE CHECKOUT SESSION END ===")
|
||||
|
||||
def get_subscription_info(session_id):
|
||||
"""
|
||||
Get subscription information from a checkout session
|
||||
|
||||
Args:
|
||||
session_id: Stripe checkout session ID
|
||||
|
||||
Returns:
|
||||
dict: Subscription information
|
||||
"""
|
||||
try:
|
||||
configure_stripe()
|
||||
|
||||
session = stripe.checkout.Session.retrieve(session_id)
|
||||
|
||||
if session.payment_status == 'paid':
|
||||
subscription = stripe.Subscription.retrieve(session.subscription)
|
||||
|
||||
# Get customer details
|
||||
customer_details = {}
|
||||
if session.customer_details:
|
||||
customer_details = {
|
||||
'name': session.customer_details.name,
|
||||
'email': session.customer_details.email,
|
||||
'phone': session.customer_details.phone,
|
||||
'address': {
|
||||
'line1': session.customer_details.address.line1,
|
||||
'line2': session.customer_details.address.line2,
|
||||
'city': session.customer_details.address.city,
|
||||
'state': session.customer_details.address.state,
|
||||
'postal_code': session.customer_details.address.postal_code,
|
||||
'country': session.customer_details.address.country
|
||||
} if session.customer_details.address else None,
|
||||
'shipping': {
|
||||
'name': session.customer_details.shipping.name,
|
||||
'address': {
|
||||
'line1': session.customer_details.shipping.address.line1,
|
||||
'line2': session.customer_details.shipping.address.line2,
|
||||
'city': session.customer_details.shipping.address.city,
|
||||
'state': session.customer_details.shipping.address.state,
|
||||
'postal_code': session.customer_details.shipping.address.postal_code,
|
||||
'country': session.customer_details.shipping.address.country
|
||||
}
|
||||
} if session.customer_details.shipping else None,
|
||||
'tax_ids': [
|
||||
{
|
||||
'type': tax_id.type,
|
||||
'value': tax_id.value
|
||||
} for tax_id in session.customer_details.tax_ids
|
||||
] if session.customer_details.tax_ids else []
|
||||
}
|
||||
|
||||
return {
|
||||
'session_id': session_id,
|
||||
'subscription_id': subscription.id,
|
||||
'customer_id': subscription.customer,
|
||||
'status': subscription.status,
|
||||
'plan_id': session.metadata.get('plan_id'),
|
||||
'plan_name': session.metadata.get('plan_name'),
|
||||
'billing_cycle': session.metadata.get('billing_cycle'),
|
||||
'current_period_start': subscription.current_period_start,
|
||||
'current_period_end': subscription.current_period_end,
|
||||
'amount': subscription.items.data[0].price.unit_amount / 100, # Convert from cents
|
||||
'currency': subscription.currency,
|
||||
'customer_details': customer_details
|
||||
}
|
||||
else:
|
||||
return {
|
||||
'session_id': session_id,
|
||||
'payment_status': session.payment_status,
|
||||
'error': 'Payment not completed'
|
||||
}
|
||||
|
||||
except stripe.error.StripeError as e:
|
||||
logger.error(f"Stripe error getting subscription info: {str(e)}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting subscription info: {str(e)}")
|
||||
raise
|
||||
|
||||
def cancel_subscription(subscription_id):
|
||||
"""
|
||||
Cancel a Stripe subscription
|
||||
|
||||
Args:
|
||||
subscription_id: Stripe subscription ID
|
||||
|
||||
Returns:
|
||||
dict: Cancellation information
|
||||
"""
|
||||
try:
|
||||
configure_stripe()
|
||||
|
||||
subscription = stripe.Subscription.modify(
|
||||
subscription_id,
|
||||
cancel_at_period_end=True
|
||||
)
|
||||
|
||||
return {
|
||||
'subscription_id': subscription_id,
|
||||
'status': subscription.status,
|
||||
'cancel_at_period_end': subscription.cancel_at_period_end,
|
||||
'current_period_end': subscription.current_period_end
|
||||
}
|
||||
|
||||
except stripe.error.StripeError as e:
|
||||
logger.error(f"Stripe error canceling subscription: {str(e)}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error canceling subscription: {str(e)}")
|
||||
raise
|
||||
|
||||
def validate_stripe_keys():
|
||||
"""
|
||||
Validate that Stripe keys are properly configured
|
||||
|
||||
Returns:
|
||||
dict: Validation result with status and message
|
||||
"""
|
||||
try:
|
||||
stripe_settings = get_stripe_settings()
|
||||
if not stripe_settings:
|
||||
return {
|
||||
'valid': False,
|
||||
'message': 'Stripe settings not configured'
|
||||
}
|
||||
|
||||
if not stripe_settings.get('secret_key'):
|
||||
return {
|
||||
'valid': False,
|
||||
'message': 'Stripe secret key not configured'
|
||||
}
|
||||
|
||||
if not stripe_settings.get('publishable_key'):
|
||||
return {
|
||||
'valid': False,
|
||||
'message': 'Stripe publishable key not configured'
|
||||
}
|
||||
|
||||
# Test the API key
|
||||
configure_stripe()
|
||||
account = stripe.Account.retrieve()
|
||||
|
||||
return {
|
||||
'valid': True,
|
||||
'message': 'Stripe configuration is valid',
|
||||
'account_id': account.id,
|
||||
'test_mode': stripe_settings.get('test_mode', False)
|
||||
}
|
||||
|
||||
except stripe.error.AuthenticationError:
|
||||
return {
|
||||
'valid': False,
|
||||
'message': 'Invalid Stripe API key'
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
'valid': False,
|
||||
'message': f'Error validating Stripe configuration: {str(e)}'
|
||||
}
|
||||
Reference in New Issue
Block a user