253 lines
9.8 KiB
Python
253 lines
9.8 KiB
Python
"""
|
|
Payment Service for handling subscription payments
|
|
This service abstracts payment provider interactions and makes the system payment-agnostic
|
|
"""
|
|
|
|
import structlog
|
|
from typing import Dict, Any, Optional
|
|
import uuid
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.core.config import settings
|
|
from shared.clients.payment_client import PaymentProvider, PaymentCustomer, Subscription, PaymentMethod
|
|
from shared.clients.stripe_client import StripeProvider
|
|
from shared.database.base import create_database_manager
|
|
from app.repositories.subscription_repository import SubscriptionRepository
|
|
from app.repositories.coupon_repository import CouponRepository
|
|
from app.models.tenants import Subscription as SubscriptionModel
|
|
|
|
logger = structlog.get_logger()
|
|
|
|
|
|
class PaymentService:
|
|
"""Service for handling payment provider interactions"""
|
|
|
|
def __init__(self, db_session: Optional[Session] = None):
|
|
# Initialize payment provider based on configuration
|
|
# For now, we'll use Stripe, but this can be swapped for other providers
|
|
self.payment_provider: PaymentProvider = StripeProvider(
|
|
api_key=settings.STRIPE_SECRET_KEY,
|
|
webhook_secret=settings.STRIPE_WEBHOOK_SECRET
|
|
)
|
|
|
|
# Initialize database components
|
|
self.database_manager = create_database_manager(settings.DATABASE_URL, "tenant-service")
|
|
self.subscription_repo = SubscriptionRepository(SubscriptionModel, None) # Will be set in methods
|
|
self.db_session = db_session # Optional session for coupon operations
|
|
|
|
async def create_customer(self, user_data: Dict[str, Any]) -> PaymentCustomer:
|
|
"""Create a customer in the payment provider system"""
|
|
try:
|
|
customer_data = {
|
|
'email': user_data.get('email'),
|
|
'name': user_data.get('full_name'),
|
|
'metadata': {
|
|
'user_id': user_data.get('user_id'),
|
|
'tenant_id': user_data.get('tenant_id')
|
|
}
|
|
}
|
|
|
|
return await self.payment_provider.create_customer(customer_data)
|
|
except Exception as e:
|
|
logger.error("Failed to create customer in payment provider", 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 for a customer"""
|
|
try:
|
|
return await self.payment_provider.create_subscription(
|
|
customer_id,
|
|
plan_id,
|
|
payment_method_id,
|
|
trial_period_days
|
|
)
|
|
except Exception as e:
|
|
logger.error("Failed to create subscription in payment provider", error=str(e))
|
|
raise e
|
|
|
|
def validate_coupon_code(
|
|
self,
|
|
coupon_code: str,
|
|
tenant_id: str,
|
|
db_session: Session
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Validate a coupon code for a tenant.
|
|
Returns validation result with discount preview.
|
|
"""
|
|
try:
|
|
coupon_repo = CouponRepository(db_session)
|
|
validation = coupon_repo.validate_coupon(coupon_code, tenant_id)
|
|
|
|
return {
|
|
"valid": validation.valid,
|
|
"error_message": validation.error_message,
|
|
"discount_preview": validation.discount_preview,
|
|
"coupon": {
|
|
"code": validation.coupon.code,
|
|
"discount_type": validation.coupon.discount_type.value,
|
|
"discount_value": validation.coupon.discount_value
|
|
} if validation.coupon else None
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error("Failed to validate coupon", error=str(e), coupon_code=coupon_code)
|
|
return {
|
|
"valid": False,
|
|
"error_message": "Error al validar el cupón",
|
|
"discount_preview": None,
|
|
"coupon": None
|
|
}
|
|
|
|
def redeem_coupon(
|
|
self,
|
|
coupon_code: str,
|
|
tenant_id: str,
|
|
db_session: Session,
|
|
base_trial_days: int = 14
|
|
) -> tuple[bool, Optional[Dict[str, Any]], Optional[str]]:
|
|
"""
|
|
Redeem a coupon for a tenant.
|
|
Returns (success, discount_applied, error_message)
|
|
"""
|
|
try:
|
|
coupon_repo = CouponRepository(db_session)
|
|
success, redemption, error = coupon_repo.redeem_coupon(
|
|
coupon_code,
|
|
tenant_id,
|
|
base_trial_days
|
|
)
|
|
|
|
if success and redemption:
|
|
return True, redemption.discount_applied, None
|
|
else:
|
|
return False, None, error
|
|
|
|
except Exception as e:
|
|
logger.error("Failed to redeem coupon", error=str(e), coupon_code=coupon_code)
|
|
return False, None, f"Error al aplicar el cupón: {str(e)}"
|
|
|
|
async def process_registration_with_subscription(
|
|
self,
|
|
user_data: Dict[str, Any],
|
|
plan_id: str,
|
|
payment_method_id: str,
|
|
use_trial: bool = False,
|
|
coupon_code: Optional[str] = None,
|
|
db_session: Optional[Session] = None
|
|
) -> Dict[str, Any]:
|
|
"""Process user registration with subscription creation"""
|
|
try:
|
|
# Create customer in payment provider
|
|
customer = await self.create_customer(user_data)
|
|
|
|
# Determine trial period (default 14 days)
|
|
trial_period_days = 14 if use_trial else 0
|
|
|
|
# Apply coupon if provided
|
|
coupon_discount = None
|
|
if coupon_code and db_session:
|
|
# Redeem coupon
|
|
success, discount, error = self.redeem_coupon(
|
|
coupon_code,
|
|
user_data.get('tenant_id'),
|
|
db_session,
|
|
trial_period_days
|
|
)
|
|
|
|
if success and discount:
|
|
coupon_discount = discount
|
|
# Update trial period if coupon extends it
|
|
if discount.get("type") == "trial_extension":
|
|
trial_period_days = discount.get("total_trial_days", trial_period_days)
|
|
logger.info(
|
|
"Coupon applied successfully",
|
|
coupon_code=coupon_code,
|
|
extended_trial_days=trial_period_days
|
|
)
|
|
else:
|
|
logger.warning("Failed to apply coupon", error=error, coupon_code=coupon_code)
|
|
|
|
# Create subscription
|
|
subscription = await self.create_subscription(
|
|
customer.id,
|
|
plan_id,
|
|
payment_method_id,
|
|
trial_period_days if trial_period_days > 0 else None
|
|
)
|
|
|
|
# Save subscription to database
|
|
async with self.database_manager.get_session() as session:
|
|
self.subscription_repo.session = session
|
|
subscription_data = {
|
|
'id': str(uuid.uuid4()),
|
|
'tenant_id': user_data.get('tenant_id'),
|
|
'customer_id': customer.id,
|
|
'subscription_id': subscription.id,
|
|
'plan_id': plan_id,
|
|
'status': subscription.status,
|
|
'current_period_start': subscription.current_period_start,
|
|
'current_period_end': subscription.current_period_end,
|
|
'created_at': subscription.created_at,
|
|
'trial_period_days': trial_period_days if trial_period_days > 0 else None
|
|
}
|
|
subscription_record = await self.subscription_repo.create(subscription_data)
|
|
|
|
result = {
|
|
'customer_id': customer.id,
|
|
'subscription_id': subscription.id,
|
|
'status': subscription.status,
|
|
'trial_period_days': trial_period_days
|
|
}
|
|
|
|
# Include coupon info if applied
|
|
if coupon_discount:
|
|
result['coupon_applied'] = {
|
|
'code': coupon_code,
|
|
'discount': coupon_discount
|
|
}
|
|
|
|
return result
|
|
|
|
except Exception as e:
|
|
logger.error("Failed to process registration with subscription", error=str(e))
|
|
raise e
|
|
|
|
async def cancel_subscription(self, subscription_id: str) -> Subscription:
|
|
"""Cancel a subscription in the payment provider"""
|
|
try:
|
|
return await self.payment_provider.cancel_subscription(subscription_id)
|
|
except Exception as e:
|
|
logger.error("Failed to cancel subscription in payment provider", 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"""
|
|
try:
|
|
return await self.payment_provider.update_payment_method(customer_id, payment_method_id)
|
|
except Exception as e:
|
|
logger.error("Failed to update payment method in payment provider", error=str(e))
|
|
raise e
|
|
|
|
async def get_invoices(self, customer_id: str) -> list:
|
|
"""Get invoices for a customer from the payment provider"""
|
|
try:
|
|
return await self.payment_provider.get_invoices(customer_id)
|
|
except Exception as e:
|
|
logger.error("Failed to get invoices from payment provider", error=str(e))
|
|
raise e
|
|
|
|
async def get_subscription(self, subscription_id: str) -> Subscription:
|
|
"""Get subscription details from the payment provider"""
|
|
try:
|
|
return await self.payment_provider.get_subscription(subscription_id)
|
|
except Exception as e:
|
|
logger.error("Failed to get subscription from payment provider", error=str(e))
|
|
raise e
|