mirror of https://github.com/kortix-ai/suna.git
Add project limits based on subscription tier
Co-authored-by: sharath <sharath@kortix.ai>
This commit is contained in:
parent
ffb97b8dc8
commit
d395ac1a82
|
@ -1018,6 +1018,12 @@ async def initiate_agent_with_files(
|
||||||
if not can_run:
|
if not can_run:
|
||||||
raise HTTPException(status_code=402, detail={"message": message, "subscription": subscription})
|
raise HTTPException(status_code=402, detail={"message": message, "subscription": subscription})
|
||||||
|
|
||||||
|
# Check project limits before creating a new project
|
||||||
|
from services.billing import check_project_limits
|
||||||
|
can_create_project, project_message, project_subscription = await check_project_limits(client, account_id)
|
||||||
|
if not can_create_project:
|
||||||
|
raise HTTPException(status_code=402, detail={"message": project_message, "subscription": project_subscription})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 1. Create Project
|
# 1. Create Project
|
||||||
placeholder_name = f"{prompt[:30]}..." if len(prompt) > 30 else prompt
|
placeholder_name = f"{prompt[:30]}..." if len(prompt) > 30 else prompt
|
||||||
|
|
|
@ -43,22 +43,22 @@ def get_model_pricing(model: str) -> tuple[float, float] | None:
|
||||||
|
|
||||||
|
|
||||||
SUBSCRIPTION_TIERS = {
|
SUBSCRIPTION_TIERS = {
|
||||||
config.STRIPE_FREE_TIER_ID: {'name': 'free', 'minutes': 60, 'cost': 5},
|
config.STRIPE_FREE_TIER_ID: {'name': 'free', 'minutes': 60, 'cost': 5, 'project_limit': 3},
|
||||||
config.STRIPE_TIER_2_20_ID: {'name': 'tier_2_20', 'minutes': 120, 'cost': 20 + 5}, # 2 hours
|
config.STRIPE_TIER_2_20_ID: {'name': 'tier_2_20', 'minutes': 120, 'cost': 20 + 5, 'project_limit': 10}, # 2 hours
|
||||||
config.STRIPE_TIER_6_50_ID: {'name': 'tier_6_50', 'minutes': 360, 'cost': 50 + 5}, # 6 hours
|
config.STRIPE_TIER_6_50_ID: {'name': 'tier_6_50', 'minutes': 360, 'cost': 50 + 5, 'project_limit': 25}, # 6 hours
|
||||||
config.STRIPE_TIER_12_100_ID: {'name': 'tier_12_100', 'minutes': 720, 'cost': 100 + 5}, # 12 hours
|
config.STRIPE_TIER_12_100_ID: {'name': 'tier_12_100', 'minutes': 720, 'cost': 100 + 5, 'project_limit': 50}, # 12 hours
|
||||||
config.STRIPE_TIER_25_200_ID: {'name': 'tier_25_200', 'minutes': 1500, 'cost': 200 + 5}, # 25 hours
|
config.STRIPE_TIER_25_200_ID: {'name': 'tier_25_200', 'minutes': 1500, 'cost': 200 + 5, 'project_limit': 100}, # 25 hours
|
||||||
config.STRIPE_TIER_50_400_ID: {'name': 'tier_50_400', 'minutes': 3000, 'cost': 400 + 5}, # 50 hours
|
config.STRIPE_TIER_50_400_ID: {'name': 'tier_50_400', 'minutes': 3000, 'cost': 400 + 5, 'project_limit': 200}, # 50 hours
|
||||||
config.STRIPE_TIER_125_800_ID: {'name': 'tier_125_800', 'minutes': 7500, 'cost': 800 + 5}, # 125 hours
|
config.STRIPE_TIER_125_800_ID: {'name': 'tier_125_800', 'minutes': 7500, 'cost': 800 + 5, 'project_limit': 500}, # 125 hours
|
||||||
config.STRIPE_TIER_200_1000_ID: {'name': 'tier_200_1000', 'minutes': 12000, 'cost': 1000 + 5}, # 200 hours
|
config.STRIPE_TIER_200_1000_ID: {'name': 'tier_200_1000', 'minutes': 12000, 'cost': 1000 + 5, 'project_limit': 1000}, # 200 hours
|
||||||
# Yearly tiers (same usage limits, different billing period)
|
# Yearly tiers (same usage limits, different billing period)
|
||||||
config.STRIPE_TIER_2_20_YEARLY_ID: {'name': 'tier_2_20', 'minutes': 120, 'cost': 20 + 5}, # 2 hours/month, $204/year
|
config.STRIPE_TIER_2_20_YEARLY_ID: {'name': 'tier_2_20', 'minutes': 120, 'cost': 20 + 5, 'project_limit': 10}, # 2 hours/month, $204/year
|
||||||
config.STRIPE_TIER_6_50_YEARLY_ID: {'name': 'tier_6_50', 'minutes': 360, 'cost': 50 + 5}, # 6 hours/month, $510/year
|
config.STRIPE_TIER_6_50_YEARLY_ID: {'name': 'tier_6_50', 'minutes': 360, 'cost': 50 + 5, 'project_limit': 25}, # 6 hours/month, $510/year
|
||||||
config.STRIPE_TIER_12_100_YEARLY_ID: {'name': 'tier_12_100', 'minutes': 720, 'cost': 100 + 5}, # 12 hours/month, $1020/year
|
config.STRIPE_TIER_12_100_YEARLY_ID: {'name': 'tier_12_100', 'minutes': 720, 'cost': 100 + 5, 'project_limit': 50}, # 12 hours/month, $1020/year
|
||||||
config.STRIPE_TIER_25_200_YEARLY_ID: {'name': 'tier_25_200', 'minutes': 1500, 'cost': 200 + 5}, # 25 hours/month, $2040/year
|
config.STRIPE_TIER_25_200_YEARLY_ID: {'name': 'tier_25_200', 'minutes': 1500, 'cost': 200 + 5, 'project_limit': 100}, # 25 hours/month, $2040/year
|
||||||
config.STRIPE_TIER_50_400_YEARLY_ID: {'name': 'tier_50_400', 'minutes': 3000, 'cost': 400 + 5}, # 50 hours/month, $4080/year
|
config.STRIPE_TIER_50_400_YEARLY_ID: {'name': 'tier_50_400', 'minutes': 3000, 'cost': 400 + 5, 'project_limit': 200}, # 50 hours/month, $4080/year
|
||||||
config.STRIPE_TIER_125_800_YEARLY_ID: {'name': 'tier_125_800', 'minutes': 7500, 'cost': 800 + 5}, # 125 hours/month, $8160/year
|
config.STRIPE_TIER_125_800_YEARLY_ID: {'name': 'tier_125_800', 'minutes': 7500, 'cost': 800 + 5, 'project_limit': 500}, # 125 hours/month, $8160/year
|
||||||
config.STRIPE_TIER_200_1000_YEARLY_ID: {'name': 'tier_200_1000', 'minutes': 12000, 'cost': 1000 + 5}, # 200 hours/month, $10200/year
|
config.STRIPE_TIER_200_1000_YEARLY_ID: {'name': 'tier_200_1000', 'minutes': 12000, 'cost': 1000 + 5, 'project_limit': 1000}, # 200 hours/month, $10200/year
|
||||||
}
|
}
|
||||||
|
|
||||||
# Pydantic models for request/response validation
|
# Pydantic models for request/response validation
|
||||||
|
@ -81,6 +81,8 @@ class SubscriptionStatus(BaseModel):
|
||||||
minutes_limit: Optional[int] = None
|
minutes_limit: Optional[int] = None
|
||||||
cost_limit: Optional[float] = None
|
cost_limit: Optional[float] = None
|
||||||
current_usage: Optional[float] = None
|
current_usage: Optional[float] = None
|
||||||
|
project_limit: Optional[int] = None # Added project limit
|
||||||
|
current_project_count: Optional[int] = None # Added current project count
|
||||||
# Fields for scheduled changes
|
# Fields for scheduled changes
|
||||||
has_schedule: bool = False
|
has_schedule: bool = False
|
||||||
scheduled_plan_name: Optional[str] = None
|
scheduled_plan_name: Optional[str] = None
|
||||||
|
@ -503,6 +505,63 @@ async def check_billing_status(client, user_id: str) -> Tuple[bool, str, Optiona
|
||||||
|
|
||||||
return True, "OK", subscription
|
return True, "OK", subscription
|
||||||
|
|
||||||
|
async def check_project_limits(client, user_id: str) -> Tuple[bool, str, Optional[Dict]]:
|
||||||
|
"""
|
||||||
|
Check if a user can create a new project based on their subscription and current project count.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[bool, str, Optional[Dict]]: (can_create, message, subscription_info)
|
||||||
|
"""
|
||||||
|
if config.ENV_MODE == EnvMode.LOCAL:
|
||||||
|
logger.info("Running in local development mode - project limit checks are disabled")
|
||||||
|
return True, "Local development mode - project limits disabled", {
|
||||||
|
"price_id": "local_dev",
|
||||||
|
"plan_name": "Local Development",
|
||||||
|
"project_limit": "no limit"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get current subscription
|
||||||
|
subscription = await get_user_subscription(user_id)
|
||||||
|
|
||||||
|
# If no subscription, they can use free tier
|
||||||
|
if not subscription:
|
||||||
|
subscription = {
|
||||||
|
'price_id': config.STRIPE_FREE_TIER_ID, # Free tier
|
||||||
|
'plan_name': 'free'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extract price ID from subscription items
|
||||||
|
price_id = None
|
||||||
|
if subscription.get('items') and subscription['items'].get('data') and len(subscription['items']['data']) > 0:
|
||||||
|
price_id = subscription['items']['data'][0]['price']['id']
|
||||||
|
else:
|
||||||
|
price_id = subscription.get('price_id', config.STRIPE_FREE_TIER_ID)
|
||||||
|
|
||||||
|
# Get tier info - default to free tier if not found
|
||||||
|
tier_info = SUBSCRIPTION_TIERS.get(price_id)
|
||||||
|
if not tier_info:
|
||||||
|
logger.warning(f"Unknown subscription tier: {price_id}, defaulting to free tier")
|
||||||
|
tier_info = SUBSCRIPTION_TIERS[config.STRIPE_FREE_TIER_ID]
|
||||||
|
|
||||||
|
# Get current project count for the user
|
||||||
|
project_count_result = await client.table('projects').select('project_id', count='exact').eq('account_id', user_id).execute()
|
||||||
|
current_project_count = project_count_result.count or 0
|
||||||
|
|
||||||
|
# Check if within project limits
|
||||||
|
project_limit = tier_info.get('project_limit', 3) # Default to 3 for free tier
|
||||||
|
if current_project_count >= project_limit:
|
||||||
|
return False, f"Project limit of {project_limit} reached. You currently have {current_project_count} projects. Please upgrade your plan to create more projects.", {
|
||||||
|
**subscription,
|
||||||
|
'project_limit': project_limit,
|
||||||
|
'current_project_count': current_project_count
|
||||||
|
}
|
||||||
|
|
||||||
|
return True, "OK", {
|
||||||
|
**subscription,
|
||||||
|
'project_limit': project_limit,
|
||||||
|
'current_project_count': current_project_count
|
||||||
|
}
|
||||||
|
|
||||||
# API endpoints
|
# API endpoints
|
||||||
@router.post("/create-checkout-session")
|
@router.post("/create-checkout-session")
|
||||||
async def create_checkout_session(
|
async def create_checkout_session(
|
||||||
|
@ -909,13 +968,20 @@ async def get_subscription(
|
||||||
# Default to free tier status if no active subscription for our product
|
# Default to free tier status if no active subscription for our product
|
||||||
free_tier_id = config.STRIPE_FREE_TIER_ID
|
free_tier_id = config.STRIPE_FREE_TIER_ID
|
||||||
free_tier_info = SUBSCRIPTION_TIERS.get(free_tier_id)
|
free_tier_info = SUBSCRIPTION_TIERS.get(free_tier_id)
|
||||||
|
|
||||||
|
# Get current project count for the user
|
||||||
|
project_count_result = await client.table('projects').select('project_id', count='exact').eq('account_id', current_user_id).execute()
|
||||||
|
current_project_count = project_count_result.count or 0
|
||||||
|
|
||||||
return SubscriptionStatus(
|
return SubscriptionStatus(
|
||||||
status="no_subscription",
|
status="no_subscription",
|
||||||
plan_name=free_tier_info.get('name', 'free') if free_tier_info else 'free',
|
plan_name=free_tier_info.get('name', 'free') if free_tier_info else 'free',
|
||||||
price_id=free_tier_id,
|
price_id=free_tier_id,
|
||||||
minutes_limit=free_tier_info.get('minutes') if free_tier_info else 0,
|
minutes_limit=free_tier_info.get('minutes') if free_tier_info else 0,
|
||||||
cost_limit=free_tier_info.get('cost') if free_tier_info else 0,
|
cost_limit=free_tier_info.get('cost') if free_tier_info else 0,
|
||||||
current_usage=current_usage
|
current_usage=current_usage,
|
||||||
|
project_limit=free_tier_info.get('project_limit', 3) if free_tier_info else 3,
|
||||||
|
current_project_count=current_project_count
|
||||||
)
|
)
|
||||||
|
|
||||||
# Extract current plan details
|
# Extract current plan details
|
||||||
|
@ -927,6 +993,10 @@ async def get_subscription(
|
||||||
logger.warning(f"User {current_user_id} subscribed to unknown price {current_price_id}. Defaulting info.")
|
logger.warning(f"User {current_user_id} subscribed to unknown price {current_price_id}. Defaulting info.")
|
||||||
current_tier_info = {'name': 'unknown', 'minutes': 0}
|
current_tier_info = {'name': 'unknown', 'minutes': 0}
|
||||||
|
|
||||||
|
# Get current project count for the user
|
||||||
|
project_count_result = await client.table('projects').select('project_id', count='exact').eq('account_id', current_user_id).execute()
|
||||||
|
current_project_count = project_count_result.count or 0
|
||||||
|
|
||||||
status_response = SubscriptionStatus(
|
status_response = SubscriptionStatus(
|
||||||
status=subscription['status'], # 'active', 'trialing', etc.
|
status=subscription['status'], # 'active', 'trialing', etc.
|
||||||
plan_name=subscription['plan'].get('nickname') or current_tier_info['name'],
|
plan_name=subscription['plan'].get('nickname') or current_tier_info['name'],
|
||||||
|
@ -937,6 +1007,8 @@ async def get_subscription(
|
||||||
minutes_limit=current_tier_info['minutes'],
|
minutes_limit=current_tier_info['minutes'],
|
||||||
cost_limit=current_tier_info['cost'],
|
cost_limit=current_tier_info['cost'],
|
||||||
current_usage=current_usage,
|
current_usage=current_usage,
|
||||||
|
project_limit=current_tier_info.get('project_limit', 3),
|
||||||
|
current_project_count=current_project_count,
|
||||||
has_schedule=False # Default
|
has_schedule=False # Default
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -998,6 +1070,31 @@ async def check_status(
|
||||||
logger.error(f"Error checking billing status: {str(e)}")
|
logger.error(f"Error checking billing status: {str(e)}")
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@router.get("/check-project-limits")
|
||||||
|
async def check_project_limits_endpoint(
|
||||||
|
current_user_id: str = Depends(get_current_user_id_from_jwt)
|
||||||
|
):
|
||||||
|
"""Check if the user can create a new project based on their subscription and current project count."""
|
||||||
|
try:
|
||||||
|
db = DBConnection()
|
||||||
|
client = await db.client
|
||||||
|
|
||||||
|
can_create, message, subscription = await check_project_limits(client, current_user_id)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"can_create": can_create,
|
||||||
|
"message": message,
|
||||||
|
"subscription": {
|
||||||
|
"price_id": subscription.get('price_id', 'unknown'),
|
||||||
|
"plan_name": subscription.get('plan_name', 'unknown'),
|
||||||
|
"project_limit": subscription.get('project_limit', 'unknown'),
|
||||||
|
"current_project_count": subscription.get('current_project_count', 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error checking project limits: {str(e)}")
|
||||||
|
raise HTTPException(status_code=500, detail="Internal server error")
|
||||||
|
|
||||||
@router.post("/webhook")
|
@router.post("/webhook")
|
||||||
async def stripe_webhook(request: Request):
|
async def stripe_webhook(request: Request):
|
||||||
"""Handle Stripe webhook events."""
|
"""Handle Stripe webhook events."""
|
||||||
|
|
|
@ -65,6 +65,12 @@ class SessionManager:
|
||||||
thread_id = str(uuid.uuid4())
|
thread_id = str(uuid.uuid4())
|
||||||
account_id = agent_config.get('account_id')
|
account_id = agent_config.get('account_id')
|
||||||
|
|
||||||
|
# Check project limits before creating a new project
|
||||||
|
from services.billing import check_project_limits
|
||||||
|
can_create_project, project_message, project_subscription = await check_project_limits(client, account_id)
|
||||||
|
if not can_create_project:
|
||||||
|
raise Exception(f"Project limit exceeded: {project_message}")
|
||||||
|
|
||||||
placeholder_name = f"Trigger: {agent_config.get('name', 'Agent')} - {trigger_event.trigger_id[:8]}"
|
placeholder_name = f"Trigger: {agent_config.get('name', 'Agent')} - {trigger_event.trigger_id[:8]}"
|
||||||
await client.table('projects').insert({
|
await client.table('projects').insert({
|
||||||
"project_id": project_id,
|
"project_id": project_id,
|
||||||
|
@ -96,6 +102,12 @@ class SessionManager:
|
||||||
project_id = str(uuid.uuid4())
|
project_id = str(uuid.uuid4())
|
||||||
thread_id = str(uuid.uuid4())
|
thread_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
# Check project limits before creating a new project
|
||||||
|
from services.billing import check_project_limits
|
||||||
|
can_create_project, project_message, project_subscription = await check_project_limits(client, account_id)
|
||||||
|
if not can_create_project:
|
||||||
|
raise Exception(f"Project limit exceeded: {project_message}")
|
||||||
|
|
||||||
await client.table('projects').insert({
|
await client.table('projects').insert({
|
||||||
"project_id": project_id,
|
"project_id": project_id,
|
||||||
"account_id": account_id,
|
"account_id": account_id,
|
||||||
|
|
|
@ -0,0 +1,88 @@
|
||||||
|
# Project Limits Feature
|
||||||
|
|
||||||
|
This document describes the project limits feature that restricts the number of projects users can create based on their subscription tier.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The project limits feature ensures that users on free accounts have a reasonable limit on the number of projects they can create, while paid accounts have higher limits. This helps manage system resources and encourages users to upgrade for more projects.
|
||||||
|
|
||||||
|
## Subscription Tiers and Project Limits
|
||||||
|
|
||||||
|
| Subscription Tier | Project Limit | Description |
|
||||||
|
|-------------------|---------------|-------------|
|
||||||
|
| Free | 3 projects | Basic tier for new users |
|
||||||
|
| Tier 2 ($20/month) | 10 projects | Entry-level paid tier |
|
||||||
|
| Tier 6 ($50/month) | 25 projects | Mid-tier plan |
|
||||||
|
| Tier 12 ($100/month) | 50 projects | Professional tier |
|
||||||
|
| Tier 25 ($200/month) | 100 projects | Business tier |
|
||||||
|
| Tier 50 ($400/month) | 200 projects | Enterprise tier |
|
||||||
|
| Tier 125 ($800/month) | 500 projects | Large enterprise |
|
||||||
|
| Tier 200 ($1000/month) | 1000 projects | Maximum tier |
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### Backend Changes
|
||||||
|
|
||||||
|
1. **Billing Service (`backend/services/billing.py`)**:
|
||||||
|
- Added `project_limit` field to `SUBSCRIPTION_TIERS` configuration
|
||||||
|
- Created `check_project_limits()` function to validate project creation
|
||||||
|
- Added `/billing/check-project-limits` API endpoint
|
||||||
|
- Updated subscription status to include project limit information
|
||||||
|
|
||||||
|
2. **Agent API (`backend/agent/api.py`)**:
|
||||||
|
- Added project limit check before creating projects in agent initiation
|
||||||
|
- Returns HTTP 402 error when limits are exceeded
|
||||||
|
|
||||||
|
3. **Execution Service (`backend/triggers/services/execution_service.py`)**:
|
||||||
|
- Added project limit checks in `create_agent_session()` and `create_workflow_session()`
|
||||||
|
- Prevents automatic project creation when limits are reached
|
||||||
|
|
||||||
|
### Frontend Changes
|
||||||
|
|
||||||
|
1. **API Layer (`frontend/src/lib/api.ts`)**:
|
||||||
|
- Added `checkProjectLimits()` function
|
||||||
|
- Added `ProjectLimitsResponse` interface
|
||||||
|
- Updated `SubscriptionStatus` interface to include project limits
|
||||||
|
|
||||||
|
2. **Project Mutations (`frontend/src/hooks/react-query/sidebar/use-project-mutations.ts`)**:
|
||||||
|
- Added project limit check before creating projects
|
||||||
|
- Shows error toast when limits are exceeded
|
||||||
|
|
||||||
|
3. **Billing Status Component (`frontend/src/components/billing/account-billing-status.tsx`)**:
|
||||||
|
- Added project count display in billing status
|
||||||
|
- Shows current projects vs. limit
|
||||||
|
|
||||||
|
4. **React Query Hook (`frontend/src/hooks/react-query/billing/use-project-limits.ts`)**:
|
||||||
|
- Created hook for checking project limits
|
||||||
|
- Caches results for 5 minutes
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
When project limits are exceeded:
|
||||||
|
|
||||||
|
1. **Frontend**: Shows error toast with upgrade message
|
||||||
|
2. **Backend**: Returns HTTP 402 (Payment Required) with detailed error message
|
||||||
|
3. **API Response**: Includes current project count and limit information
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
To test the feature:
|
||||||
|
|
||||||
|
1. Create projects up to your tier's limit
|
||||||
|
2. Attempt to create an additional project
|
||||||
|
3. Verify that appropriate error messages are shown
|
||||||
|
4. Check that billing status displays current project count
|
||||||
|
|
||||||
|
## Local Development
|
||||||
|
|
||||||
|
In local development mode (`ENV_MODE=LOCAL`), project limits are disabled to allow unrestricted development and testing.
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
Potential improvements to consider:
|
||||||
|
|
||||||
|
1. **Soft Limits**: Allow exceeding limits with warnings
|
||||||
|
2. **Project Archiving**: Allow archiving old projects to free up slots
|
||||||
|
3. **Team Limits**: Separate limits for team accounts
|
||||||
|
4. **Usage Analytics**: Track project creation patterns
|
||||||
|
5. **Dynamic Limits**: Adjust limits based on usage patterns
|
|
@ -105,7 +105,7 @@ export default function AccountBillingStatus({ accountId, returnUrl }: Props) {
|
||||||
|
|
||||||
{subscriptionData ? (
|
{subscriptionData ? (
|
||||||
<>
|
<>
|
||||||
<div className="mb-6">
|
<div className="mb-6 space-y-3">
|
||||||
<div className="rounded-lg border bg-background p-4">
|
<div className="rounded-lg border bg-background p-4">
|
||||||
<div className="flex justify-between items-center gap-4">
|
<div className="flex justify-between items-center gap-4">
|
||||||
<span className="text-sm font-medium text-foreground/90">
|
<span className="text-sm font-medium text-foreground/90">
|
||||||
|
@ -122,6 +122,18 @@ export default function AccountBillingStatus({ accountId, returnUrl }: Props) {
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-lg border bg-background p-4">
|
||||||
|
<div className="flex justify-between items-center gap-4">
|
||||||
|
<span className="text-sm font-medium text-foreground/90">
|
||||||
|
Projects
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-medium text-card-title">
|
||||||
|
{subscriptionData.current_project_count || 0} /{' '}
|
||||||
|
{subscriptionData.project_limit || '∞'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Plans Comparison */}
|
{/* Plans Comparison */}
|
||||||
|
@ -149,8 +161,8 @@ export default function AccountBillingStatus({ accountId, returnUrl }: Props) {
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="mb-6">
|
<div className="mb-6 space-y-3">
|
||||||
<div className="rounded-lg border bg-background p-4 gap-4">
|
<div className="rounded-lg border bg-background p-4">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-sm font-medium text-foreground/90">
|
<span className="text-sm font-medium text-foreground/90">
|
||||||
Current Plan
|
Current Plan
|
||||||
|
@ -159,7 +171,9 @@ export default function AccountBillingStatus({ accountId, returnUrl }: Props) {
|
||||||
Free
|
Free
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-lg border bg-background p-4">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-sm font-medium text-foreground/90">
|
<span className="text-sm font-medium text-foreground/90">
|
||||||
Agent Usage This Month
|
Agent Usage This Month
|
||||||
|
@ -170,6 +184,18 @@ export default function AccountBillingStatus({ accountId, returnUrl }: Props) {
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-lg border bg-background p-4">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm font-medium text-foreground/90">
|
||||||
|
Projects
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-medium text-card-title">
|
||||||
|
{subscriptionData?.current_project_count || 0} /{' '}
|
||||||
|
{subscriptionData?.project_limit || '3'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Plans Comparison */}
|
{/* Plans Comparison */}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
export { useProjectLimits } from './use-project-limits';
|
|
@ -0,0 +1,16 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { createQueryHook } from '@/hooks/use-query';
|
||||||
|
import { checkProjectLimits } from '@/lib/api';
|
||||||
|
|
||||||
|
export const useProjectLimits = createQueryHook(
|
||||||
|
() => checkProjectLimits(),
|
||||||
|
{
|
||||||
|
queryKey: ['project-limits'],
|
||||||
|
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||||
|
errorContext: {
|
||||||
|
operation: 'check project limits',
|
||||||
|
resource: 'project limits'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
|
@ -5,14 +5,30 @@ import {
|
||||||
createProject,
|
createProject,
|
||||||
updateProject,
|
updateProject,
|
||||||
deleteProject,
|
deleteProject,
|
||||||
Project
|
Project,
|
||||||
|
checkProjectLimits
|
||||||
} from '@/lib/api';
|
} from '@/lib/api';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { projectKeys } from './keys';
|
import { projectKeys } from './keys';
|
||||||
|
|
||||||
export const useCreateProject = createMutationHook(
|
export const useCreateProject = createMutationHook(
|
||||||
(data: { name: string; description: string; accountId?: string }) =>
|
async (data: { name: string; description: string; accountId?: string }) => {
|
||||||
createProject(data, data.accountId),
|
// Check project limits before creating
|
||||||
|
try {
|
||||||
|
const limits = await checkProjectLimits();
|
||||||
|
if (!limits.can_create) {
|
||||||
|
throw new Error(limits.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error && error.message.includes('Project limit')) {
|
||||||
|
toast.error(error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
// For other errors, let the backend handle the limit check
|
||||||
|
}
|
||||||
|
|
||||||
|
return createProject(data, data.accountId);
|
||||||
|
},
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success('Project created successfully');
|
toast.success('Project created successfully');
|
||||||
|
|
|
@ -1606,6 +1606,8 @@ export interface SubscriptionStatus {
|
||||||
minutes_limit?: number;
|
minutes_limit?: number;
|
||||||
cost_limit?: number;
|
cost_limit?: number;
|
||||||
current_usage?: number;
|
current_usage?: number;
|
||||||
|
project_limit?: number; // Added project limit
|
||||||
|
current_project_count?: number; // Added current project count
|
||||||
// Fields for scheduled changes
|
// Fields for scheduled changes
|
||||||
has_schedule: boolean;
|
has_schedule: boolean;
|
||||||
scheduled_plan_name?: string;
|
scheduled_plan_name?: string;
|
||||||
|
@ -1934,6 +1936,59 @@ export const checkBillingStatus = async (): Promise<BillingStatusResponse> => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface ProjectLimitsResponse {
|
||||||
|
can_create: boolean;
|
||||||
|
message: string;
|
||||||
|
subscription: {
|
||||||
|
price_id: string;
|
||||||
|
plan_name: string;
|
||||||
|
project_limit: number;
|
||||||
|
current_project_count: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const checkProjectLimits = async (): Promise<ProjectLimitsResponse> => {
|
||||||
|
try {
|
||||||
|
const supabase = createClient();
|
||||||
|
const {
|
||||||
|
data: { session },
|
||||||
|
} = await supabase.auth.getSession();
|
||||||
|
|
||||||
|
if (!session?.access_token) {
|
||||||
|
throw new NoAccessTokenAvailableError();
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${API_URL}/billing/check-project-limits`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${session.access_token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response
|
||||||
|
.text()
|
||||||
|
.catch(() => 'No error details available');
|
||||||
|
console.error(
|
||||||
|
`Error checking project limits: ${response.status} ${response.statusText}`,
|
||||||
|
errorText,
|
||||||
|
);
|
||||||
|
throw new Error(
|
||||||
|
`Error checking project limits: ${response.statusText} (${response.status})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof NoAccessTokenAvailableError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('Failed to check project limits:', error);
|
||||||
|
handleApiError(error, { operation: 'check project limits', resource: 'project limits' });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Transcription API Types
|
// Transcription API Types
|
||||||
export interface TranscriptionResponse {
|
export interface TranscriptionResponse {
|
||||||
text: string;
|
text: string;
|
||||||
|
|
Loading…
Reference in New Issue