Files
bakery-ia/shared/clients/stripe_client.py

312 lines
13 KiB
Python
Raw Normal View History

2025-09-25 14:30:47 +02:00
"""
Stripe Payment Provider Implementation
This module implements the PaymentProvider interface for Stripe
"""
import stripe
import structlog
2026-01-11 07:50:34 +01:00
import uuid
2025-09-25 14:30:47 +02:00
from typing import Dict, Any, Optional
from datetime import datetime
from .payment_client import PaymentProvider, PaymentCustomer, PaymentMethod, Subscription, Invoice
logger = structlog.get_logger()
class StripeProvider(PaymentProvider):
"""
Stripe implementation of the PaymentProvider interface
"""
def __init__(self, api_key: str, webhook_secret: Optional[str] = None):
"""
Initialize the Stripe provider with API key
"""
stripe.api_key = api_key
self.webhook_secret = webhook_secret
async def create_customer(self, customer_data: Dict[str, Any]) -> PaymentCustomer:
"""
2026-01-11 07:50:34 +01:00
Create a customer in Stripe with idempotency key
2025-09-25 14:30:47 +02:00
"""
try:
2026-01-11 07:50:34 +01:00
idempotency_key = f"create_customer_{uuid.uuid4()}"
logger.info("Creating Stripe customer",
email=customer_data.get('email'),
customer_name=customer_data.get('name'))
2025-09-25 14:30:47 +02:00
stripe_customer = stripe.Customer.create(
email=customer_data.get('email'),
name=customer_data.get('name'),
phone=customer_data.get('phone'),
2026-01-11 07:50:34 +01:00
metadata=customer_data.get('metadata', {}),
idempotency_key=idempotency_key
2025-09-25 14:30:47 +02:00
)
2026-01-11 07:50:34 +01:00
logger.info("Stripe customer created successfully",
customer_id=stripe_customer.id)
2025-09-25 14:30:47 +02:00
return PaymentCustomer(
id=stripe_customer.id,
email=stripe_customer.email,
name=stripe_customer.name,
created_at=datetime.fromtimestamp(stripe_customer.created)
)
except stripe.error.StripeError as e:
2026-01-11 07:50:34 +01:00
logger.error("Failed to create Stripe customer",
error=str(e),
error_type=type(e).__name__,
email=customer_data.get('email'))
2025-09-25 14:30:47 +02:00
raise e
async def create_subscription(self, customer_id: str, plan_id: str, payment_method_id: str, trial_period_days: Optional[int] = None) -> Subscription:
"""
2026-01-11 07:50:34 +01:00
Create a subscription in Stripe with idempotency and enhanced error handling
2025-09-25 14:30:47 +02:00
"""
try:
2026-01-11 07:50:34 +01:00
subscription_idempotency_key = f"create_subscription_{uuid.uuid4()}"
payment_method_idempotency_key = f"attach_pm_{uuid.uuid4()}"
customer_update_idempotency_key = f"update_customer_{uuid.uuid4()}"
logger.info("Creating Stripe subscription",
customer_id=customer_id,
plan_id=plan_id,
payment_method_id=payment_method_id)
# Attach payment method to customer with idempotency
2025-09-25 14:30:47 +02:00
stripe.PaymentMethod.attach(
payment_method_id,
customer=customer_id,
2026-01-11 07:50:34 +01:00
idempotency_key=payment_method_idempotency_key
2025-09-25 14:30:47 +02:00
)
2026-01-11 07:50:34 +01:00
logger.info("Payment method attached to customer",
customer_id=customer_id,
payment_method_id=payment_method_id)
# Set customer's default payment method with idempotency
2025-09-25 14:30:47 +02:00
stripe.Customer.modify(
customer_id,
invoice_settings={
'default_payment_method': payment_method_id
2026-01-11 07:50:34 +01:00
},
idempotency_key=customer_update_idempotency_key
2025-09-25 14:30:47 +02:00
)
2026-01-11 07:50:34 +01:00
logger.info("Customer default payment method updated",
customer_id=customer_id)
2025-09-25 14:30:47 +02:00
# Create subscription with trial period if specified
subscription_params = {
'customer': customer_id,
'items': [{'price': plan_id}],
'default_payment_method': payment_method_id,
2026-01-11 07:50:34 +01:00
'idempotency_key': subscription_idempotency_key,
'expand': ['latest_invoice.payment_intent']
2025-09-25 14:30:47 +02:00
}
if trial_period_days:
subscription_params['trial_period_days'] = trial_period_days
2026-01-11 07:50:34 +01:00
logger.info("Subscription includes trial period",
trial_period_days=trial_period_days)
2025-09-25 14:30:47 +02:00
stripe_subscription = stripe.Subscription.create(**subscription_params)
2026-01-11 07:50:34 +01:00
logger.info("Stripe subscription created successfully",
subscription_id=stripe_subscription.id,
status=stripe_subscription.status,
current_period_end=stripe_subscription.current_period_end)
2025-09-25 14:30:47 +02:00
return Subscription(
id=stripe_subscription.id,
customer_id=stripe_subscription.customer,
plan_id=plan_id, # Using the price ID as plan_id
status=stripe_subscription.status,
current_period_start=datetime.fromtimestamp(stripe_subscription.current_period_start),
current_period_end=datetime.fromtimestamp(stripe_subscription.current_period_end),
created_at=datetime.fromtimestamp(stripe_subscription.created)
)
2026-01-11 07:50:34 +01:00
except stripe.error.CardError as e:
logger.error("Card error during subscription creation",
error=str(e),
error_code=e.code,
decline_code=e.decline_code,
customer_id=customer_id)
raise e
except stripe.error.InvalidRequestError as e:
logger.error("Invalid request during subscription creation",
error=str(e),
param=e.param,
customer_id=customer_id)
raise e
2025-09-25 14:30:47 +02:00
except stripe.error.StripeError as e:
2026-01-11 07:50:34 +01:00
logger.error("Failed to create Stripe subscription",
error=str(e),
error_type=type(e).__name__,
customer_id=customer_id,
plan_id=plan_id)
2025-09-25 14:30:47 +02:00
raise e
async def update_payment_method(self, customer_id: str, payment_method_id: str) -> PaymentMethod:
"""
Update the payment method for a customer in Stripe
"""
try:
# Attach payment method to customer
stripe.PaymentMethod.attach(
payment_method_id,
customer=customer_id,
)
# Set as default payment method
stripe.Customer.modify(
customer_id,
invoice_settings={
'default_payment_method': payment_method_id
}
)
stripe_payment_method = stripe.PaymentMethod.retrieve(payment_method_id)
return PaymentMethod(
id=stripe_payment_method.id,
type=stripe_payment_method.type,
brand=getattr(stripe_payment_method, stripe_payment_method.type, {}).get('brand'),
last4=getattr(stripe_payment_method, stripe_payment_method.type, {}).get('last4'),
exp_month=getattr(stripe_payment_method, stripe_payment_method.type, {}).get('exp_month'),
exp_year=getattr(stripe_payment_method, stripe_payment_method.type, {}).get('exp_year'),
)
except stripe.error.StripeError as e:
logger.error("Failed to update Stripe payment method", error=str(e))
raise e
async def cancel_subscription(self, subscription_id: str) -> Subscription:
"""
Cancel a subscription in Stripe
"""
try:
stripe_subscription = stripe.Subscription.delete(subscription_id)
return Subscription(
id=stripe_subscription.id,
customer_id=stripe_subscription.customer,
plan_id=subscription_id, # This would need to be retrieved differently in practice
status=stripe_subscription.status,
current_period_start=datetime.fromtimestamp(stripe_subscription.current_period_start),
current_period_end=datetime.fromtimestamp(stripe_subscription.current_period_end),
created_at=datetime.fromtimestamp(stripe_subscription.created)
)
except stripe.error.StripeError as e:
logger.error("Failed to cancel Stripe subscription", error=str(e))
raise e
async def get_invoices(self, customer_id: str) -> list[Invoice]:
"""
Get invoices for a customer from Stripe
"""
try:
stripe_invoices = stripe.Invoice.list(customer=customer_id, limit=100)
2025-10-31 11:54:19 +01:00
2025-09-25 14:30:47 +02:00
invoices = []
for stripe_invoice in stripe_invoices:
2025-10-31 11:54:19 +01:00
# Create base invoice object
invoice = Invoice(
2025-09-25 14:30:47 +02:00
id=stripe_invoice.id,
customer_id=stripe_invoice.customer,
subscription_id=stripe_invoice.subscription,
amount=stripe_invoice.amount_paid / 100.0, # Convert from cents
currency=stripe_invoice.currency,
status=stripe_invoice.status,
created_at=datetime.fromtimestamp(stripe_invoice.created),
due_date=datetime.fromtimestamp(stripe_invoice.due_date) if stripe_invoice.due_date else None,
description=stripe_invoice.description
2025-10-31 11:54:19 +01:00
)
# Add Stripe-specific URLs as custom attributes
invoice.invoice_pdf = stripe_invoice.invoice_pdf if hasattr(stripe_invoice, 'invoice_pdf') else None
invoice.hosted_invoice_url = stripe_invoice.hosted_invoice_url if hasattr(stripe_invoice, 'hosted_invoice_url') else None
invoices.append(invoice)
2025-09-25 14:30:47 +02:00
return invoices
except stripe.error.StripeError as e:
logger.error("Failed to retrieve Stripe invoices", error=str(e))
raise e
async def get_subscription(self, subscription_id: str) -> Subscription:
"""
Get subscription details from Stripe
"""
try:
stripe_subscription = stripe.Subscription.retrieve(subscription_id)
return Subscription(
id=stripe_subscription.id,
customer_id=stripe_subscription.customer,
plan_id=subscription_id, # This would need to be retrieved differently in practice
status=stripe_subscription.status,
current_period_start=datetime.fromtimestamp(stripe_subscription.current_period_start),
current_period_end=datetime.fromtimestamp(stripe_subscription.current_period_end),
created_at=datetime.fromtimestamp(stripe_subscription.created)
)
except stripe.error.StripeError as e:
logger.error("Failed to retrieve Stripe subscription", error=str(e))
raise e
async def get_customer(self, customer_id: str) -> PaymentCustomer:
"""
Get customer details from Stripe
"""
try:
stripe_customer = stripe.Customer.retrieve(customer_id)
return PaymentCustomer(
id=stripe_customer.id,
email=stripe_customer.email,
name=stripe_customer.name,
created_at=datetime.fromtimestamp(stripe_customer.created)
)
except stripe.error.StripeError as e:
logger.error("Failed to retrieve Stripe customer", error=str(e))
raise e
async def create_setup_intent(self) -> Dict[str, Any]:
"""
Create a setup intent for saving payment methods in Stripe
"""
try:
setup_intent = stripe.SetupIntent.create()
return {
'client_secret': setup_intent.client_secret,
'id': setup_intent.id
}
except stripe.error.StripeError as e:
logger.error("Failed to create Stripe setup intent", error=str(e))
raise e
async def create_payment_intent(self, amount: float, currency: str, customer_id: str, payment_method_id: str) -> Dict[str, Any]:
"""
Create a payment intent for one-time payments in Stripe
"""
try:
payment_intent = stripe.PaymentIntent.create(
amount=int(amount * 100), # Convert to cents
currency=currency,
customer=customer_id,
payment_method=payment_method_id,
confirm=True
)
return {
'id': payment_intent.id,
'client_secret': payment_intent.client_secret,
'status': payment_intent.status
}
except stripe.error.StripeError as e:
logger.error("Failed to create Stripe payment intent", error=str(e))
raise e