Add subcription feature 3

This commit is contained in:
Urtzi Alfaro
2026-01-15 20:45:49 +01:00
parent a4c3b7da3f
commit b674708a4c
83 changed files with 9451 additions and 6828 deletions

View File

@@ -82,15 +82,29 @@ class SubscriptionRepository(TenantBaseRepository):
else:
subscription_data["next_billing_date"] = datetime.utcnow() + timedelta(days=30)
# Check if subscription with this subscription_id already exists to prevent duplicates
if subscription_data.get('subscription_id'):
existing_subscription = await self.get_by_provider_id(subscription_data['subscription_id'])
if existing_subscription:
# Update the existing subscription instead of creating a duplicate
updated_subscription = await self.update(str(existing_subscription.id), subscription_data)
logger.info("Existing subscription updated",
subscription_id=subscription_data['subscription_id'],
tenant_id=subscription_data.get('tenant_id'),
plan=subscription_data.get('plan'))
return updated_subscription
# Create subscription
subscription = await self.create(subscription_data)
logger.info("Subscription created successfully",
subscription_id=subscription.id,
tenant_id=subscription.tenant_id,
plan=subscription.plan,
monthly_price=subscription.monthly_price)
return subscription
except (ValidationError, DuplicateRecordError):
@@ -514,7 +528,8 @@ class SubscriptionRepository(TenantBaseRepository):
"""Create a subscription not linked to any tenant (for registration flow)"""
try:
# Validate required data for tenant-independent subscription
required_fields = ["user_id", "plan", "subscription_id", "customer_id"]
# user_id may not exist during registration, so validate other required fields
required_fields = ["plan", "subscription_id", "customer_id"]
validation_result = self._validate_tenant_data(subscription_data, required_fields)
if not validation_result["is_valid"]:
@@ -567,16 +582,41 @@ class SubscriptionRepository(TenantBaseRepository):
else:
subscription_data["next_billing_date"] = datetime.utcnow() + timedelta(days=30)
# Create tenant-independent subscription
subscription = await self.create(subscription_data)
logger.info("Tenant-independent subscription created successfully",
subscription_id=subscription.id,
user_id=subscription.user_id,
plan=subscription.plan,
monthly_price=subscription.monthly_price)
return subscription
# Check if subscription with this subscription_id already exists
existing_subscription = await self.get_by_provider_id(subscription_data['subscription_id'])
if existing_subscription:
# Update the existing subscription instead of creating a duplicate
updated_subscription = await self.update(str(existing_subscription.id), subscription_data)
logger.info("Existing tenant-independent subscription updated",
subscription_id=subscription_data['subscription_id'],
user_id=subscription_data.get('user_id'),
plan=subscription_data.get('plan'))
return updated_subscription
else:
# Create new subscription, but handle potential duplicate errors
try:
subscription = await self.create(subscription_data)
logger.info("Tenant-independent subscription created successfully",
subscription_id=subscription.id,
user_id=subscription.user_id,
plan=subscription.plan,
monthly_price=subscription.monthly_price)
return subscription
except DuplicateRecordError:
# Another process may have created the subscription between our check and create
# Try to get the existing subscription and return it
final_subscription = await self.get_by_provider_id(subscription_data['subscription_id'])
if final_subscription:
logger.info("Race condition detected: subscription already created by another process",
subscription_id=subscription_data['subscription_id'])
return final_subscription
else:
# This shouldn't happen, but re-raise the error if we can't find it
raise
except (ValidationError, DuplicateRecordError):
raise
@@ -700,3 +740,29 @@ class SubscriptionRepository(TenantBaseRepository):
logger.error("Failed to cleanup orphaned subscriptions",
error=str(e))
raise DatabaseError(f"Cleanup failed: {str(e)}")
async def get_by_customer_id(self, customer_id: str) -> List[Subscription]:
"""
Get subscriptions by Stripe customer ID
Args:
customer_id: Stripe customer ID
Returns:
List of Subscription objects
"""
try:
query = select(Subscription).where(Subscription.customer_id == customer_id)
result = await self.session.execute(query)
subscriptions = result.scalars().all()
logger.debug("Found subscriptions by customer_id",
customer_id=customer_id,
count=len(subscriptions))
return subscriptions
except Exception as e:
logger.error("Error getting subscriptions by customer_id",
customer_id=customer_id,
error=str(e))
raise DatabaseError(f"Failed to get subscriptions by customer_id: {str(e)}")

View File

@@ -11,7 +11,7 @@ import structlog
import uuid
from .base import TenantBaseRepository
from app.models.tenants import Tenant
from app.models.tenants import Tenant, Subscription
from shared.database.exceptions import DatabaseError, ValidationError, DuplicateRecordError
logger = structlog.get_logger()
@@ -570,3 +570,39 @@ class TenantRepository(TenantBaseRepository):
session_id=session_id,
error=str(e))
raise DatabaseError(f"Failed to get enterprise demo tenants: {str(e)}")
async def get_by_customer_id(self, customer_id: str) -> Optional[Tenant]:
"""
Get tenant by Stripe customer ID
Args:
customer_id: Stripe customer ID
Returns:
Tenant object if found, None otherwise
"""
try:
# Find tenant by joining with subscriptions table
# Tenant doesn't have customer_id directly, so we need to find via subscription
query = select(Tenant).join(
Subscription, Subscription.tenant_id == Tenant.id
).where(Subscription.customer_id == customer_id)
result = await self.session.execute(query)
tenant = result.scalar_one_or_none()
if tenant:
logger.debug("Found tenant by customer_id",
customer_id=customer_id,
tenant_id=str(tenant.id))
return tenant
else:
logger.debug("No tenant found for customer_id",
customer_id=customer_id)
return None
except Exception as e:
logger.error("Error getting tenant by customer_id",
customer_id=customer_id,
error=str(e))
raise DatabaseError(f"Failed to get tenant by customer_id: {str(e)}")