Add subcription feature
This commit is contained in:
@@ -502,3 +502,201 @@ class SubscriptionRepository(TenantBaseRepository):
|
||||
except Exception as e:
|
||||
logger.warning("Failed to invalidate cache (non-critical)",
|
||||
tenant_id=tenant_id, error=str(e))
|
||||
|
||||
# ========================================================================
|
||||
# TENANT-INDEPENDENT SUBSCRIPTION METHODS (New Architecture)
|
||||
# ========================================================================
|
||||
|
||||
async def create_tenant_independent_subscription(
|
||||
self,
|
||||
subscription_data: Dict[str, Any]
|
||||
) -> Subscription:
|
||||
"""Create a subscription not linked to any tenant (for registration flow)"""
|
||||
try:
|
||||
# Validate required data for tenant-independent subscription
|
||||
required_fields = ["user_id", "plan", "stripe_subscription_id", "stripe_customer_id"]
|
||||
validation_result = self._validate_tenant_data(subscription_data, required_fields)
|
||||
|
||||
if not validation_result["is_valid"]:
|
||||
raise ValidationError(f"Invalid subscription data: {validation_result['errors']}")
|
||||
|
||||
# Ensure tenant_id is not provided (this is tenant-independent)
|
||||
if "tenant_id" in subscription_data and subscription_data["tenant_id"]:
|
||||
raise ValidationError("tenant_id should not be provided for tenant-independent subscriptions")
|
||||
|
||||
# Set tenant-independent specific fields
|
||||
subscription_data["tenant_id"] = None
|
||||
subscription_data["is_tenant_linked"] = False
|
||||
subscription_data["tenant_linking_status"] = "pending"
|
||||
subscription_data["linked_at"] = None
|
||||
|
||||
# Set default values based on plan from centralized configuration
|
||||
plan = subscription_data["plan"]
|
||||
plan_info = SubscriptionPlanMetadata.get_plan_info(plan)
|
||||
|
||||
# Set defaults from centralized plan configuration
|
||||
if "monthly_price" not in subscription_data:
|
||||
billing_cycle = subscription_data.get("billing_cycle", "monthly")
|
||||
subscription_data["monthly_price"] = float(
|
||||
PlanPricing.get_price(plan, billing_cycle)
|
||||
)
|
||||
|
||||
if "max_users" not in subscription_data:
|
||||
subscription_data["max_users"] = QuotaLimits.get_limit('MAX_USERS', plan) or -1
|
||||
|
||||
if "max_locations" not in subscription_data:
|
||||
subscription_data["max_locations"] = QuotaLimits.get_limit('MAX_LOCATIONS', plan) or -1
|
||||
|
||||
if "max_products" not in subscription_data:
|
||||
subscription_data["max_products"] = QuotaLimits.get_limit('MAX_PRODUCTS', plan) or -1
|
||||
|
||||
if "features" not in subscription_data:
|
||||
subscription_data["features"] = {
|
||||
feature: True for feature in plan_info.get("features", [])
|
||||
}
|
||||
|
||||
# Set default subscription values
|
||||
if "status" not in subscription_data:
|
||||
subscription_data["status"] = "pending_tenant_linking"
|
||||
if "billing_cycle" not in subscription_data:
|
||||
subscription_data["billing_cycle"] = "monthly"
|
||||
if "next_billing_date" not in subscription_data:
|
||||
# Set next billing date based on cycle
|
||||
if subscription_data["billing_cycle"] == "yearly":
|
||||
subscription_data["next_billing_date"] = datetime.utcnow() + timedelta(days=365)
|
||||
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
|
||||
|
||||
except (ValidationError, DuplicateRecordError):
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Failed to create tenant-independent subscription",
|
||||
user_id=subscription_data.get("user_id"),
|
||||
plan=subscription_data.get("plan"),
|
||||
error=str(e))
|
||||
raise DatabaseError(f"Failed to create tenant-independent subscription: {str(e)}")
|
||||
|
||||
async def get_pending_tenant_linking_subscriptions(self) -> List[Subscription]:
|
||||
"""Get all subscriptions waiting to be linked to tenants"""
|
||||
try:
|
||||
subscriptions = await self.get_multi(
|
||||
filters={
|
||||
"tenant_linking_status": "pending",
|
||||
"is_tenant_linked": False
|
||||
},
|
||||
order_by="created_at",
|
||||
order_desc=True
|
||||
)
|
||||
return subscriptions
|
||||
except Exception as e:
|
||||
logger.error("Failed to get pending tenant linking subscriptions",
|
||||
error=str(e))
|
||||
raise DatabaseError(f"Failed to get pending subscriptions: {str(e)}")
|
||||
|
||||
async def get_pending_subscriptions_by_user(self, user_id: str) -> List[Subscription]:
|
||||
"""Get pending tenant linking subscriptions for a specific user"""
|
||||
try:
|
||||
subscriptions = await self.get_multi(
|
||||
filters={
|
||||
"user_id": user_id,
|
||||
"tenant_linking_status": "pending",
|
||||
"is_tenant_linked": False
|
||||
},
|
||||
order_by="created_at",
|
||||
order_desc=True
|
||||
)
|
||||
return subscriptions
|
||||
except Exception as e:
|
||||
logger.error("Failed to get pending subscriptions by user",
|
||||
user_id=user_id,
|
||||
error=str(e))
|
||||
raise DatabaseError(f"Failed to get pending subscriptions: {str(e)}")
|
||||
|
||||
async def link_subscription_to_tenant(
|
||||
self,
|
||||
subscription_id: str,
|
||||
tenant_id: str,
|
||||
user_id: str
|
||||
) -> Subscription:
|
||||
"""Link a pending subscription to a tenant"""
|
||||
try:
|
||||
# Get the subscription first
|
||||
subscription = await self.get_by_id(subscription_id)
|
||||
if not subscription:
|
||||
raise ValidationError(f"Subscription {subscription_id} not found")
|
||||
|
||||
# Validate subscription can be linked
|
||||
if not subscription.can_be_linked_to_tenant(user_id):
|
||||
raise ValidationError(
|
||||
f"Subscription {subscription_id} cannot be linked to tenant by user {user_id}. "
|
||||
f"Current status: {subscription.tenant_linking_status}, "
|
||||
f"User: {subscription.user_id}, "
|
||||
f"Already linked: {subscription.is_tenant_linked}"
|
||||
)
|
||||
|
||||
# Update subscription with tenant information
|
||||
update_data = {
|
||||
"tenant_id": tenant_id,
|
||||
"is_tenant_linked": True,
|
||||
"tenant_linking_status": "completed",
|
||||
"linked_at": datetime.utcnow(),
|
||||
"status": "active", # Activate subscription when linked to tenant
|
||||
"updated_at": datetime.utcnow()
|
||||
}
|
||||
|
||||
updated_subscription = await self.update(subscription_id, update_data)
|
||||
|
||||
# Invalidate cache for the tenant
|
||||
await self._invalidate_cache(tenant_id)
|
||||
|
||||
logger.info("Subscription linked to tenant successfully",
|
||||
subscription_id=subscription_id,
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id)
|
||||
|
||||
return updated_subscription
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to link subscription to tenant",
|
||||
subscription_id=subscription_id,
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id,
|
||||
error=str(e))
|
||||
raise DatabaseError(f"Failed to link subscription to tenant: {str(e)}")
|
||||
|
||||
async def cleanup_orphaned_subscriptions(self, days_old: int = 30) -> int:
|
||||
"""Clean up subscriptions that were never linked to tenants"""
|
||||
try:
|
||||
cutoff_date = datetime.utcnow() - timedelta(days=days_old)
|
||||
|
||||
query_text = """
|
||||
DELETE FROM subscriptions
|
||||
WHERE tenant_linking_status = 'pending'
|
||||
AND is_tenant_linked = FALSE
|
||||
AND created_at < :cutoff_date
|
||||
"""
|
||||
|
||||
result = await self.session.execute(text(query_text), {"cutoff_date": cutoff_date})
|
||||
deleted_count = result.rowcount
|
||||
|
||||
logger.info("Cleaned up orphaned subscriptions",
|
||||
deleted_count=deleted_count,
|
||||
days_old=days_old)
|
||||
|
||||
return deleted_count
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to cleanup orphaned subscriptions",
|
||||
error=str(e))
|
||||
raise DatabaseError(f"Cleanup failed: {str(e)}")
|
||||
|
||||
Reference in New Issue
Block a user