Support subcription payments
This commit is contained in:
@@ -145,23 +145,58 @@ class BaseAlertService:
|
||||
|
||||
# PATTERN 3: Database Triggers
|
||||
async def start_database_listener(self):
|
||||
"""Listen for database notifications"""
|
||||
"""Listen for database notifications with connection management"""
|
||||
try:
|
||||
import asyncpg
|
||||
# Convert SQLAlchemy URL format to plain PostgreSQL for asyncpg
|
||||
database_url = self.config.DATABASE_URL
|
||||
if database_url.startswith('postgresql+asyncpg://'):
|
||||
database_url = database_url.replace('postgresql+asyncpg://', 'postgresql://')
|
||||
|
||||
conn = await asyncpg.connect(database_url)
|
||||
|
||||
# Register listeners based on service
|
||||
await self.register_db_listeners(conn)
|
||||
|
||||
logger.info("Database listeners registered", service=self.config.SERVICE_NAME)
|
||||
|
||||
|
||||
# Add connection timeout and retry logic
|
||||
max_retries = 3
|
||||
retry_count = 0
|
||||
conn = None
|
||||
|
||||
while retry_count < max_retries and not conn:
|
||||
try:
|
||||
conn = await asyncio.wait_for(
|
||||
asyncpg.connect(database_url),
|
||||
timeout=10.0
|
||||
)
|
||||
break
|
||||
except (asyncio.TimeoutError, Exception) as e:
|
||||
retry_count += 1
|
||||
if retry_count < max_retries:
|
||||
logger.warning(f"DB listener connection attempt {retry_count} failed, retrying...",
|
||||
service=self.config.SERVICE_NAME, error=str(e))
|
||||
await asyncio.sleep(2)
|
||||
else:
|
||||
raise
|
||||
|
||||
if conn:
|
||||
# Register listeners based on service
|
||||
await self.register_db_listeners(conn)
|
||||
logger.info("Database listeners registered", service=self.config.SERVICE_NAME)
|
||||
|
||||
# Keep connection alive with periodic ping
|
||||
asyncio.create_task(self._maintain_db_connection(conn))
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to setup database listeners", service=self.config.SERVICE_NAME, error=str(e))
|
||||
|
||||
async def _maintain_db_connection(self, conn):
|
||||
"""Maintain database connection for listeners"""
|
||||
try:
|
||||
while not conn.is_closed():
|
||||
await asyncio.sleep(30) # Check every 30 seconds
|
||||
try:
|
||||
await conn.fetchval("SELECT 1")
|
||||
except Exception as e:
|
||||
logger.error("DB listener connection lost", service=self.config.SERVICE_NAME, error=str(e))
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error("Error maintaining DB connection", service=self.config.SERVICE_NAME, error=str(e))
|
||||
|
||||
async def register_db_listeners(self, conn):
|
||||
"""Register database listeners - Override in service"""
|
||||
|
||||
121
shared/clients/payment_client.py
Normal file
121
shared/clients/payment_client.py
Normal file
@@ -0,0 +1,121 @@
|
||||
"""
|
||||
Payment Client Interface and Implementation
|
||||
This module provides an abstraction layer for payment providers to make the system payment-agnostic
|
||||
"""
|
||||
|
||||
import abc
|
||||
from typing import Dict, Any, Optional
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
@dataclass
|
||||
class PaymentCustomer:
|
||||
id: str
|
||||
email: str
|
||||
name: str
|
||||
created_at: datetime
|
||||
|
||||
|
||||
@dataclass
|
||||
class PaymentMethod:
|
||||
id: str
|
||||
type: str
|
||||
brand: Optional[str] = None
|
||||
last4: Optional[str] = None
|
||||
exp_month: Optional[int] = None
|
||||
exp_year: Optional[int] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class Subscription:
|
||||
id: str
|
||||
customer_id: str
|
||||
plan_id: str
|
||||
status: str # active, canceled, past_due, etc.
|
||||
current_period_start: datetime
|
||||
current_period_end: datetime
|
||||
created_at: datetime
|
||||
|
||||
|
||||
@dataclass
|
||||
class Invoice:
|
||||
id: str
|
||||
customer_id: str
|
||||
subscription_id: str
|
||||
amount: float
|
||||
currency: str
|
||||
status: str # draft, open, paid, void, etc.
|
||||
created_at: datetime
|
||||
due_date: Optional[datetime] = None
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
class PaymentProvider(abc.ABC):
|
||||
"""
|
||||
Abstract base class for payment providers.
|
||||
All payment providers should implement this interface.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
async def create_customer(self, customer_data: Dict[str, Any]) -> PaymentCustomer:
|
||||
"""
|
||||
Create a customer in the payment provider system
|
||||
"""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
async def create_subscription(self, customer_id: str, plan_id: str, payment_method_id: str, trial_period_days: Optional[int] = None) -> Subscription:
|
||||
"""
|
||||
Create a subscription for a customer
|
||||
"""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
async def update_payment_method(self, customer_id: str, payment_method_id: str) -> PaymentMethod:
|
||||
"""
|
||||
Update the payment method for a customer
|
||||
"""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
async def cancel_subscription(self, subscription_id: str) -> Subscription:
|
||||
"""
|
||||
Cancel a subscription
|
||||
"""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
async def get_invoices(self, customer_id: str) -> list[Invoice]:
|
||||
"""
|
||||
Get invoices for a customer
|
||||
"""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
async def get_subscription(self, subscription_id: str) -> Subscription:
|
||||
"""
|
||||
Get subscription details
|
||||
"""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
async def get_customer(self, customer_id: str) -> PaymentCustomer:
|
||||
"""
|
||||
Get customer details
|
||||
"""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
async def create_setup_intent(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Create a setup intent for saving payment methods
|
||||
"""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
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
|
||||
"""
|
||||
pass
|
||||
246
shared/clients/stripe_client.py
Normal file
246
shared/clients/stripe_client.py
Normal file
@@ -0,0 +1,246 @@
|
||||
"""
|
||||
Stripe Payment Provider Implementation
|
||||
This module implements the PaymentProvider interface for Stripe
|
||||
"""
|
||||
|
||||
import stripe
|
||||
import structlog
|
||||
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:
|
||||
"""
|
||||
Create a customer in Stripe
|
||||
"""
|
||||
try:
|
||||
stripe_customer = stripe.Customer.create(
|
||||
email=customer_data.get('email'),
|
||||
name=customer_data.get('name'),
|
||||
phone=customer_data.get('phone'),
|
||||
metadata=customer_data.get('metadata', {})
|
||||
)
|
||||
|
||||
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 create Stripe customer", error=str(e))
|
||||
raise e
|
||||
|
||||
async def create_subscription(self, customer_id: str, plan_id: str, payment_method_id: str, trial_period_days: Optional[int] = None) -> Subscription:
|
||||
"""
|
||||
Create a subscription in Stripe
|
||||
"""
|
||||
try:
|
||||
# Attach payment method to customer
|
||||
stripe.PaymentMethod.attach(
|
||||
payment_method_id,
|
||||
customer=customer_id,
|
||||
)
|
||||
|
||||
# Set customer's default payment method
|
||||
stripe.Customer.modify(
|
||||
customer_id,
|
||||
invoice_settings={
|
||||
'default_payment_method': payment_method_id
|
||||
}
|
||||
)
|
||||
|
||||
# Create subscription with trial period if specified
|
||||
subscription_params = {
|
||||
'customer': customer_id,
|
||||
'items': [{'price': plan_id}],
|
||||
'default_payment_method': payment_method_id,
|
||||
}
|
||||
|
||||
if trial_period_days:
|
||||
subscription_params['trial_period_days'] = trial_period_days
|
||||
|
||||
stripe_subscription = stripe.Subscription.create(**subscription_params)
|
||||
|
||||
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)
|
||||
)
|
||||
except stripe.error.StripeError as e:
|
||||
logger.error("Failed to create Stripe subscription", error=str(e))
|
||||
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)
|
||||
|
||||
invoices = []
|
||||
for stripe_invoice in stripe_invoices:
|
||||
invoices.append(Invoice(
|
||||
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
|
||||
))
|
||||
|
||||
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
|
||||
@@ -187,6 +187,11 @@ class BaseServiceSettings(BaseSettings):
|
||||
WHATSAPP_BASE_URL: str = os.getenv("WHATSAPP_BASE_URL", "https://api.twilio.com")
|
||||
WHATSAPP_FROM_NUMBER: str = os.getenv("WHATSAPP_FROM_NUMBER", "")
|
||||
|
||||
# Stripe Payment Configuration
|
||||
STRIPE_PUBLISHABLE_KEY: str = os.getenv("STRIPE_PUBLISHABLE_KEY", "")
|
||||
STRIPE_SECRET_KEY: str = os.getenv("STRIPE_SECRET_KEY", "")
|
||||
STRIPE_WEBHOOK_SECRET: str = os.getenv("STRIPE_WEBHOOK_SECRET", "")
|
||||
|
||||
# ================================================================
|
||||
# ML & AI CONFIGURATION
|
||||
# ================================================================
|
||||
@@ -395,4 +400,4 @@ class BaseServiceSettings(BaseSettings):
|
||||
for setting in critical_settings:
|
||||
value = getattr(self, setting)
|
||||
if not value or 'change' in value.lower() or 'default' in value.lower():
|
||||
raise ValueError(f"{setting} must be properly configured for production")
|
||||
raise ValueError(f"{setting} must be properly configured for production")
|
||||
|
||||
Reference in New Issue
Block a user