Add subcription feature
This commit is contained in:
@@ -22,6 +22,8 @@ from shared.auth.decorators import (
|
||||
get_current_user_dep,
|
||||
require_admin_role_dep
|
||||
)
|
||||
from app.core.database import get_db
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from shared.auth.access_control import owner_role_required, admin_role_required
|
||||
from shared.routing.route_builder import RouteBuilder
|
||||
from shared.database.base import create_database_manager
|
||||
@@ -94,7 +96,6 @@ def get_payment_service():
|
||||
logger.error("Failed to create payment service", error=str(e))
|
||||
raise HTTPException(status_code=500, detail="Payment service initialization failed")
|
||||
|
||||
# ============================================================================
|
||||
# TENANT REGISTRATION & ACCESS OPERATIONS
|
||||
# ============================================================================
|
||||
|
||||
@@ -103,81 +104,142 @@ async def register_bakery(
|
||||
bakery_data: BakeryRegistration,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service),
|
||||
payment_service: PaymentService = Depends(get_payment_service)
|
||||
payment_service: PaymentService = Depends(get_payment_service),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Register a new bakery/tenant with enhanced validation and features"""
|
||||
|
||||
try:
|
||||
# Validate coupon if provided
|
||||
# Initialize variables to avoid UnboundLocalError
|
||||
coupon_validation = None
|
||||
if bakery_data.coupon_code:
|
||||
from app.core.config import settings
|
||||
database_manager = create_database_manager(settings.DATABASE_URL, "tenant-service")
|
||||
success = None
|
||||
discount = None
|
||||
error = None
|
||||
|
||||
async with database_manager.get_session() as session:
|
||||
# Temp tenant ID for validation (will be replaced with actual after creation)
|
||||
temp_tenant_id = f"temp_{current_user['user_id']}"
|
||||
|
||||
coupon_validation = payment_service.validate_coupon_code(
|
||||
bakery_data.coupon_code,
|
||||
temp_tenant_id,
|
||||
session
|
||||
)
|
||||
|
||||
if not coupon_validation["valid"]:
|
||||
logger.warning(
|
||||
"Invalid coupon code provided during registration",
|
||||
coupon_code=bakery_data.coupon_code,
|
||||
error=coupon_validation["error_message"]
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=coupon_validation["error_message"]
|
||||
)
|
||||
|
||||
# Create bakery/tenant
|
||||
# Create bakery/tenant first
|
||||
result = await tenant_service.create_bakery(
|
||||
bakery_data,
|
||||
current_user["user_id"]
|
||||
)
|
||||
|
||||
# CRITICAL: Create default subscription for new tenant
|
||||
try:
|
||||
from app.repositories.subscription_repository import SubscriptionRepository
|
||||
from app.models.tenants import Subscription
|
||||
from datetime import datetime, timedelta, timezone
|
||||
tenant_id = result.id
|
||||
|
||||
database_manager = create_database_manager(settings.DATABASE_URL, "tenant-service")
|
||||
async with database_manager.get_session() as session:
|
||||
subscription_repo = SubscriptionRepository(Subscription, session)
|
||||
# NEW ARCHITECTURE: Check if we need to link an existing subscription
|
||||
if bakery_data.link_existing_subscription and bakery_data.subscription_id:
|
||||
logger.info("Linking existing subscription to new tenant",
|
||||
tenant_id=tenant_id,
|
||||
subscription_id=bakery_data.subscription_id,
|
||||
user_id=current_user["user_id"])
|
||||
|
||||
# Create starter subscription with 14-day trial
|
||||
trial_end_date = datetime.now(timezone.utc) + timedelta(days=14)
|
||||
next_billing_date = trial_end_date
|
||||
try:
|
||||
# Import subscription service for linking
|
||||
from app.services.subscription_service import SubscriptionService
|
||||
|
||||
await subscription_repo.create_subscription(
|
||||
tenant_id=str(result.id),
|
||||
plan="starter",
|
||||
status="active",
|
||||
billing_cycle="monthly",
|
||||
next_billing_date=next_billing_date,
|
||||
trial_ends_at=trial_end_date
|
||||
subscription_service = SubscriptionService(db)
|
||||
|
||||
# Link the subscription to the tenant
|
||||
linking_result = await subscription_service.link_subscription_to_tenant(
|
||||
subscription_id=bakery_data.subscription_id,
|
||||
tenant_id=tenant_id,
|
||||
user_id=current_user["user_id"]
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
logger.info(
|
||||
"Default subscription created for new tenant",
|
||||
tenant_id=str(result.id),
|
||||
plan="starter",
|
||||
trial_days=14
|
||||
)
|
||||
except Exception as subscription_error:
|
||||
logger.error(
|
||||
"Failed to create default subscription for tenant",
|
||||
tenant_id=str(result.id),
|
||||
error=str(subscription_error)
|
||||
logger.info("Subscription linked successfully during tenant registration",
|
||||
tenant_id=tenant_id,
|
||||
subscription_id=bakery_data.subscription_id)
|
||||
|
||||
except Exception as linking_error:
|
||||
logger.error("Error linking subscription during tenant registration",
|
||||
tenant_id=tenant_id,
|
||||
subscription_id=bakery_data.subscription_id,
|
||||
error=str(linking_error))
|
||||
# Don't fail tenant creation if subscription linking fails
|
||||
# The subscription can be linked later manually
|
||||
|
||||
elif bakery_data.coupon_code:
|
||||
# If no subscription but coupon provided, just validate and redeem coupon
|
||||
coupon_validation = payment_service.validate_coupon_code(
|
||||
bakery_data.coupon_code,
|
||||
tenant_id,
|
||||
db
|
||||
)
|
||||
# Don't fail tenant creation if subscription creation fails
|
||||
|
||||
if not coupon_validation["valid"]:
|
||||
logger.warning(
|
||||
"Invalid coupon code provided during registration",
|
||||
coupon_code=bakery_data.coupon_code,
|
||||
error=coupon_validation["error_message"]
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=coupon_validation["error_message"]
|
||||
)
|
||||
|
||||
# Redeem coupon
|
||||
success, discount, error = payment_service.redeem_coupon(
|
||||
bakery_data.coupon_code,
|
||||
tenant_id,
|
||||
db
|
||||
)
|
||||
|
||||
if success:
|
||||
logger.info("Coupon redeemed during registration",
|
||||
coupon_code=bakery_data.coupon_code,
|
||||
tenant_id=tenant_id)
|
||||
else:
|
||||
logger.warning("Failed to redeem coupon during registration",
|
||||
coupon_code=bakery_data.coupon_code,
|
||||
error=error)
|
||||
else:
|
||||
# No subscription plan provided - check if tenant already has a subscription
|
||||
# (from new registration flow where subscription is created first)
|
||||
try:
|
||||
from app.repositories.subscription_repository import SubscriptionRepository
|
||||
from app.models.tenants import Subscription
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from app.core.config import settings
|
||||
|
||||
database_manager = create_database_manager(settings.DATABASE_URL, "tenant-service")
|
||||
async with database_manager.get_session() as session:
|
||||
subscription_repo = SubscriptionRepository(Subscription, session)
|
||||
|
||||
# Check if tenant already has an active subscription
|
||||
existing_subscription = await subscription_repo.get_by_tenant_id(str(result.id))
|
||||
|
||||
if existing_subscription:
|
||||
logger.info(
|
||||
"Tenant already has an active subscription, skipping default subscription creation",
|
||||
tenant_id=str(result.id),
|
||||
existing_plan=existing_subscription.plan,
|
||||
subscription_id=str(existing_subscription.id)
|
||||
)
|
||||
else:
|
||||
# Create starter subscription with 14-day trial
|
||||
trial_end_date = datetime.now(timezone.utc) + timedelta(days=14)
|
||||
next_billing_date = trial_end_date
|
||||
|
||||
await subscription_repo.create_subscription({
|
||||
"tenant_id": str(result.id),
|
||||
"plan": "starter",
|
||||
"status": "trial",
|
||||
"billing_cycle": "monthly",
|
||||
"next_billing_date": next_billing_date,
|
||||
"trial_ends_at": trial_end_date
|
||||
})
|
||||
await session.commit()
|
||||
|
||||
logger.info(
|
||||
"Default free trial subscription created for new tenant",
|
||||
tenant_id=str(result.id),
|
||||
plan="starter",
|
||||
trial_days=14
|
||||
)
|
||||
except Exception as subscription_error:
|
||||
logger.error(
|
||||
"Failed to create default subscription for tenant",
|
||||
tenant_id=str(result.id),
|
||||
error=str(subscription_error)
|
||||
)
|
||||
|
||||
# If coupon was validated, redeem it now with actual tenant_id
|
||||
if coupon_validation and coupon_validation["valid"]:
|
||||
@@ -1068,9 +1130,101 @@ async def upgrade_subscription_plan(
|
||||
@router.post(route_builder.build_base_route("subscriptions/register-with-subscription", include_tenant_prefix=False))
|
||||
async def register_with_subscription(
|
||||
user_data: Dict[str, Any],
|
||||
plan_id: str = Query(..., description="Plan ID to subscribe to"),
|
||||
plan_id: str = Query(..., description="Plan ID to subscribe to (starter, professional, enterprise)"),
|
||||
payment_method_id: str = Query(..., description="Payment method ID from frontend"),
|
||||
use_trial: bool = Query(False, description="Whether to use trial period for pilot users"),
|
||||
coupon_code: str = Query(None, description="Coupon code for discounts or trial periods"),
|
||||
billing_interval: str = Query("monthly", description="Billing interval (monthly or yearly)"),
|
||||
payment_service: PaymentService = Depends(get_payment_service)
|
||||
):
|
||||
"""Process user registration with subscription creation"""
|
||||
|
||||
@router.post(route_builder.build_base_route("payment-customers/create", include_tenant_prefix=False))
|
||||
async def create_payment_customer(
|
||||
user_data: Dict[str, Any],
|
||||
payment_method_id: Optional[str] = Query(None, description="Optional payment method ID"),
|
||||
payment_service: PaymentService = Depends(get_payment_service)
|
||||
):
|
||||
"""
|
||||
Create a payment customer in the payment provider
|
||||
|
||||
This endpoint is designed for service-to-service communication from auth service
|
||||
during user registration. It creates a payment customer that can be used later
|
||||
for subscription creation.
|
||||
|
||||
Args:
|
||||
user_data: User data including email, name, etc.
|
||||
payment_method_id: Optional payment method ID to attach
|
||||
|
||||
Returns:
|
||||
Dictionary with payment customer details
|
||||
"""
|
||||
try:
|
||||
logger.info("Creating payment customer via service-to-service call",
|
||||
email=user_data.get('email'),
|
||||
user_id=user_data.get('user_id'))
|
||||
|
||||
# Step 1: Create payment customer
|
||||
customer = await payment_service.create_customer(user_data)
|
||||
logger.info("Payment customer created successfully",
|
||||
customer_id=customer.id,
|
||||
email=customer.email)
|
||||
|
||||
# Step 2: Attach payment method if provided
|
||||
payment_method_details = None
|
||||
if payment_method_id:
|
||||
try:
|
||||
payment_method = await payment_service.update_payment_method(
|
||||
customer.id,
|
||||
payment_method_id
|
||||
)
|
||||
payment_method_details = {
|
||||
"id": payment_method.id,
|
||||
"type": payment_method.type,
|
||||
"brand": payment_method.brand,
|
||||
"last4": payment_method.last4,
|
||||
"exp_month": payment_method.exp_month,
|
||||
"exp_year": payment_method.exp_year
|
||||
}
|
||||
logger.info("Payment method attached to customer",
|
||||
customer_id=customer.id,
|
||||
payment_method_id=payment_method.id)
|
||||
except Exception as e:
|
||||
logger.warning("Failed to attach payment method to customer",
|
||||
customer_id=customer.id,
|
||||
error=str(e),
|
||||
payment_method_id=payment_method_id)
|
||||
# Continue without attached payment method
|
||||
|
||||
# Step 3: Return comprehensive result
|
||||
return {
|
||||
"success": True,
|
||||
"payment_customer_id": customer.id,
|
||||
"payment_method": payment_method_details,
|
||||
"customer": {
|
||||
"id": customer.id,
|
||||
"email": customer.email,
|
||||
"name": customer.name,
|
||||
"created_at": customer.created_at.isoformat()
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to create payment customer via service-to-service call",
|
||||
error=str(e),
|
||||
email=user_data.get('email'),
|
||||
user_id=user_data.get('user_id'))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to create payment customer: {str(e)}"
|
||||
)
|
||||
|
||||
@router.post(route_builder.build_base_route("subscriptions/register-with-subscription", include_tenant_prefix=False))
|
||||
async def register_with_subscription(
|
||||
user_data: Dict[str, Any],
|
||||
plan_id: str = Query(..., description="Plan ID to subscribe to (starter, professional, enterprise)"),
|
||||
payment_method_id: str = Query(..., description="Payment method ID from frontend"),
|
||||
coupon_code: str = Query(None, description="Coupon code for discounts or trial periods"),
|
||||
billing_interval: str = Query("monthly", description="Billing interval (monthly or yearly)"),
|
||||
payment_service: PaymentService = Depends(get_payment_service)
|
||||
):
|
||||
"""Process user registration with subscription creation"""
|
||||
@@ -1080,7 +1234,8 @@ async def register_with_subscription(
|
||||
user_data,
|
||||
plan_id,
|
||||
payment_method_id,
|
||||
use_trial
|
||||
coupon_code,
|
||||
billing_interval
|
||||
)
|
||||
|
||||
return {
|
||||
@@ -1095,6 +1250,61 @@ async def register_with_subscription(
|
||||
detail="Failed to register with subscription"
|
||||
)
|
||||
|
||||
@router.post(route_builder.build_base_route("subscriptions/link", include_tenant_prefix=False))
|
||||
async def link_subscription_to_tenant(
|
||||
tenant_id: str = Query(..., description="Tenant ID to link subscription to"),
|
||||
subscription_id: str = Query(..., description="Subscription ID to link"),
|
||||
user_id: str = Query(..., description="User ID performing the linking"),
|
||||
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Link a pending subscription to a tenant
|
||||
|
||||
This endpoint completes the registration flow by associating the subscription
|
||||
created during registration with the tenant created during onboarding.
|
||||
|
||||
Args:
|
||||
tenant_id: Tenant ID to link to
|
||||
subscription_id: Subscription ID to link
|
||||
user_id: User ID performing the linking (for validation)
|
||||
|
||||
Returns:
|
||||
Dictionary with linking results
|
||||
"""
|
||||
try:
|
||||
logger.info("Linking subscription to tenant",
|
||||
tenant_id=tenant_id,
|
||||
subscription_id=subscription_id,
|
||||
user_id=user_id)
|
||||
|
||||
# Link subscription to tenant
|
||||
result = await tenant_service.link_subscription_to_tenant(
|
||||
tenant_id, subscription_id, user_id
|
||||
)
|
||||
|
||||
logger.info("Subscription linked to tenant successfully",
|
||||
tenant_id=tenant_id,
|
||||
subscription_id=subscription_id,
|
||||
user_id=user_id)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Subscription linked to tenant successfully",
|
||||
"data": result
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to link subscription to tenant",
|
||||
error=str(e),
|
||||
tenant_id=tenant_id,
|
||||
subscription_id=subscription_id,
|
||||
user_id=user_id)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to link subscription to tenant"
|
||||
)
|
||||
|
||||
|
||||
async def _invalidate_tenant_tokens(tenant_id: str, redis_client):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user