mirror of https://github.com/kortix-ai/suna.git
Merge pull request #1602 from escapade-mckv/billing-improvements-clean
disable trial check for local mode
This commit is contained in:
commit
9f8abca2ce
|
@ -22,7 +22,7 @@ from .subscription_service import subscription_service
|
|||
from .trial_service import trial_service
|
||||
from .payment_service import payment_service
|
||||
|
||||
router = APIRouter(prefix="/billing/v2", tags=["billing"])
|
||||
router = APIRouter(prefix="/billing", tags=["billing"])
|
||||
|
||||
stripe.api_key = config.STRIPE_SECRET_KEY
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ from billing.api import calculate_token_cost
|
|||
from billing.credit_manager import credit_manager
|
||||
from core.utils.config import config, EnvMode
|
||||
from core.utils.logger import logger
|
||||
from core.services.supabase import DBConnection
|
||||
|
||||
class BillingIntegration:
|
||||
@staticmethod
|
||||
|
|
|
@ -4,6 +4,7 @@ from typing import Optional, Dict, List, Any
|
|||
from core.services.supabase import DBConnection
|
||||
from core.utils.logger import logger
|
||||
from core.utils.cache import Cache
|
||||
from core.utils.config import config, EnvMode
|
||||
from billing.config import FREE_TIER_INITIAL_CREDITS, TRIAL_ENABLED
|
||||
|
||||
class CreditService:
|
||||
|
@ -36,57 +37,82 @@ class CreditService:
|
|||
if result.data and len(result.data) > 0:
|
||||
balance = Decimal(str(result.data[0]['balance']))
|
||||
else:
|
||||
trial_mode = TRIAL_ENABLED
|
||||
logger.info(f"Creating new user {user_id} with free tier (trial migration will handle conversion)")
|
||||
|
||||
if trial_mode == TRIAL_ENABLED:
|
||||
account_data = {
|
||||
'account_id': user_id,
|
||||
'balance': str(FREE_TIER_INITIAL_CREDITS),
|
||||
'tier': 'free'
|
||||
}
|
||||
|
||||
try:
|
||||
logger.info(f"Creating FREE TIER account for new user {user_id}")
|
||||
|
||||
try:
|
||||
test_data = {**account_data, 'last_grant_date': datetime.now(timezone.utc).isoformat()}
|
||||
await client.from_('credit_accounts').insert(test_data).execute()
|
||||
logger.info(f"Successfully created FREE TIER account for user {user_id}")
|
||||
except Exception as e1:
|
||||
logger.warning(f"Creating account without last_grant_date: {e1}")
|
||||
await client.from_('credit_accounts').insert(account_data).execute()
|
||||
logger.info(f"Successfully created minimal FREE TIER account for user {user_id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create FREE TIER account for user {user_id}: {e}")
|
||||
raise
|
||||
|
||||
balance = FREE_TIER_INITIAL_CREDITS
|
||||
|
||||
await client.from_('credit_ledger').insert({
|
||||
'account_id': user_id,
|
||||
'amount': str(FREE_TIER_INITIAL_CREDITS),
|
||||
'type': 'tier_grant',
|
||||
'description': 'Welcome to Suna! Free tier initial credits',
|
||||
'balance_after': str(FREE_TIER_INITIAL_CREDITS)
|
||||
}).execute()
|
||||
else:
|
||||
if config.ENV_MODE == EnvMode.LOCAL:
|
||||
logger.info(f"LOCAL mode: Creating user {user_id} with tier='none' (no free tier in local mode)")
|
||||
account_data = {
|
||||
'account_id': user_id,
|
||||
'balance': '0',
|
||||
'tier': 'free'
|
||||
'tier': 'none',
|
||||
'trial_status': 'none'
|
||||
}
|
||||
|
||||
try:
|
||||
logger.info(f"Creating TRIAL PENDING account for new user {user_id}")
|
||||
await client.from_('credit_accounts').insert(account_data).execute()
|
||||
logger.info(f"Successfully created TRIAL PENDING account for user {user_id}")
|
||||
logger.info(f"Successfully created tier='none' account for user {user_id} in LOCAL mode")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create TRIAL PENDING account for user {user_id}: {e}")
|
||||
logger.error(f"Failed to create account for user {user_id}: {e}")
|
||||
raise
|
||||
|
||||
balance = Decimal('0')
|
||||
|
||||
await client.from_('credit_ledger').insert({
|
||||
'account_id': user_id,
|
||||
'amount': '0',
|
||||
'type': 'initial',
|
||||
'description': 'Account created - no free tier in local mode',
|
||||
'balance_after': '0'
|
||||
}).execute()
|
||||
else:
|
||||
trial_mode = TRIAL_ENABLED
|
||||
logger.info(f"Creating new user {user_id} with free tier (trial migration will handle conversion)")
|
||||
|
||||
if trial_mode == TRIAL_ENABLED:
|
||||
account_data = {
|
||||
'account_id': user_id,
|
||||
'balance': str(FREE_TIER_INITIAL_CREDITS),
|
||||
'tier': 'free'
|
||||
}
|
||||
|
||||
try:
|
||||
logger.info(f"Creating FREE TIER account for new user {user_id}")
|
||||
|
||||
try:
|
||||
test_data = {**account_data, 'last_grant_date': datetime.now(timezone.utc).isoformat()}
|
||||
await client.from_('credit_accounts').insert(test_data).execute()
|
||||
logger.info(f"Successfully created FREE TIER account for user {user_id}")
|
||||
except Exception as e1:
|
||||
logger.warning(f"Creating account without last_grant_date: {e1}")
|
||||
await client.from_('credit_accounts').insert(account_data).execute()
|
||||
logger.info(f"Successfully created minimal FREE TIER account for user {user_id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create FREE TIER account for user {user_id}: {e}")
|
||||
raise
|
||||
|
||||
balance = FREE_TIER_INITIAL_CREDITS
|
||||
|
||||
await client.from_('credit_ledger').insert({
|
||||
'account_id': user_id,
|
||||
'amount': str(FREE_TIER_INITIAL_CREDITS),
|
||||
'type': 'tier_grant',
|
||||
'description': 'Welcome to Suna! Free tier initial credits',
|
||||
'balance_after': str(FREE_TIER_INITIAL_CREDITS)
|
||||
}).execute()
|
||||
else:
|
||||
account_data = {
|
||||
'account_id': user_id,
|
||||
'balance': '0',
|
||||
'tier': 'free'
|
||||
}
|
||||
try:
|
||||
logger.info(f"Creating TRIAL PENDING account for new user {user_id}")
|
||||
await client.from_('credit_accounts').insert(account_data).execute()
|
||||
logger.info(f"Successfully created TRIAL PENDING account for user {user_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create TRIAL PENDING account for user {user_id}: {e}")
|
||||
raise
|
||||
|
||||
balance = Decimal('0')
|
||||
|
||||
if self.cache:
|
||||
await self.cache.set(cache_key, str(balance), ttl=300)
|
||||
|
|
|
@ -25,7 +25,7 @@ export default function SubscriptionRequiredPage() {
|
|||
|
||||
const checkBillingStatus = async () => {
|
||||
try {
|
||||
const response = await backendApi.get('/billing/v2/subscription');
|
||||
const response = await backendApi.get('/billing/subscription');
|
||||
setBillingStatus(response.data);
|
||||
const hasActiveSubscription = response.data.subscription &&
|
||||
response.data.subscription.status === 'active' &&
|
||||
|
|
|
@ -66,7 +66,7 @@ export function useTransactions(
|
|||
params.append('type_filter', typeFilter);
|
||||
}
|
||||
|
||||
const response = await backendApi.get(`/billing/v2/transactions?${params.toString()}`);
|
||||
const response = await backendApi.get(`/billing/transactions?${params.toString()}`);
|
||||
if (response.error) {
|
||||
throw new Error(response.error.message);
|
||||
}
|
||||
|
@ -80,7 +80,7 @@ export function useTransactionsSummary(days: number = 30) {
|
|||
return useQuery<TransactionsSummary>({
|
||||
queryKey: ['billing', 'transactions', 'summary', days],
|
||||
queryFn: async () => {
|
||||
const response = await backendApi.get(`/billing/v2/transactions/summary?days=${days}`);
|
||||
const response = await backendApi.get(`/billing/transactions/summary?days=${days}`);
|
||||
if (response.error) {
|
||||
throw new Error(response.error.message);
|
||||
}
|
||||
|
|
|
@ -1898,7 +1898,7 @@ export const createCheckoutSession = async (
|
|||
const requestBody = { ...request, tolt_referral: window.tolt_referral };
|
||||
|
||||
// Use the new billing v2 API endpoint
|
||||
const response = await fetch(`${API_URL}/billing/v2/create-checkout-session`, {
|
||||
const response = await fetch(`${API_URL}/billing/create-checkout-session`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
@ -1956,7 +1956,7 @@ export const createPortalSession = async (
|
|||
throw new NoAccessTokenAvailableError();
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_URL}/billing/v2/create-portal-session`, {
|
||||
const response = await fetch(`${API_URL}/billing/create-portal-session`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
@ -2002,7 +2002,7 @@ export const getSubscription = async (): Promise<SubscriptionStatus> => {
|
|||
throw new NoAccessTokenAvailableError();
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_URL}/billing/v2/subscription`, {
|
||||
const response = await fetch(`${API_URL}/billing/subscription`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${session.access_token}`,
|
||||
},
|
||||
|
@ -2057,7 +2057,7 @@ export const getSubscriptionCommitment = async (subscriptionId: string): Promise
|
|||
}
|
||||
|
||||
// Use the new billing v2 API endpoint
|
||||
const response = await fetch(`${API_URL}/billing/v2/subscription-commitment/${subscriptionId}`, {
|
||||
const response = await fetch(`${API_URL}/billing/subscription-commitment/${subscriptionId}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${session.access_token}`,
|
||||
},
|
||||
|
@ -2099,7 +2099,7 @@ export const getAvailableModels = async (): Promise<AvailableModelsResponse> =>
|
|||
throw new NoAccessTokenAvailableError();
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_URL}/billing/v2/available-models`, {
|
||||
const response = await fetch(`${API_URL}/billing/available-models`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${session.access_token}`,
|
||||
},
|
||||
|
@ -2142,7 +2142,7 @@ export const checkBillingStatus = async (): Promise<BillingStatusResponse> => {
|
|||
throw new NoAccessTokenAvailableError();
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_URL}/billing/v2/check-status`, {
|
||||
const response = await fetch(`${API_URL}/billing/check-status`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${session.access_token}`,
|
||||
},
|
||||
|
@ -2183,7 +2183,7 @@ export const cancelSubscription = async (): Promise<CancelSubscriptionResponse>
|
|||
throw new NoAccessTokenAvailableError();
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_URL}/billing/v2/cancel-subscription`, {
|
||||
const response = await fetch(`${API_URL}/billing/cancel-subscription`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
@ -2224,7 +2224,7 @@ export const reactivateSubscription = async (): Promise<ReactivateSubscriptionRe
|
|||
throw new NoAccessTokenAvailableError();
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_URL}/billing/v2/reactivate-subscription`, {
|
||||
const response = await fetch(`${API_URL}/billing/reactivate-subscription`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
|
|
@ -179,32 +179,32 @@ export interface TrialCheckoutResponse {
|
|||
|
||||
export const billingApiV2 = {
|
||||
async getSubscription() {
|
||||
const response = await backendApi.get<SubscriptionInfo>('/billing/v2/subscription');
|
||||
const response = await backendApi.get<SubscriptionInfo>('/billing/subscription');
|
||||
if (response.error) throw response.error;
|
||||
return response.data!;
|
||||
},
|
||||
|
||||
async checkBillingStatus() {
|
||||
const response = await backendApi.post<BillingStatus>('/billing/v2/check');
|
||||
const response = await backendApi.post<BillingStatus>('/billing/check');
|
||||
if (response.error) throw response.error;
|
||||
return response.data!;
|
||||
},
|
||||
|
||||
async getCreditBalance() {
|
||||
const response = await backendApi.get<CreditBalance>('/billing/v2/balance');
|
||||
const response = await backendApi.get<CreditBalance>('/billing/balance');
|
||||
if (response.error) throw response.error;
|
||||
return response.data!;
|
||||
},
|
||||
|
||||
async deductTokenUsage(usage: TokenUsage) {
|
||||
const response = await backendApi.post<DeductResult>('/billing/v2/deduct', usage);
|
||||
const response = await backendApi.post<DeductResult>('/billing/deduct', usage);
|
||||
if (response.error) throw response.error;
|
||||
return response.data!;
|
||||
},
|
||||
|
||||
async createCheckoutSession(request: CreateCheckoutSessionRequest) {
|
||||
const response = await backendApi.post<CreateCheckoutSessionResponse>(
|
||||
'/billing/v2/create-checkout-session',
|
||||
'/billing/create-checkout-session',
|
||||
request
|
||||
);
|
||||
if (response.error) throw response.error;
|
||||
|
@ -213,7 +213,7 @@ export const billingApiV2 = {
|
|||
|
||||
async createPortalSession(request: CreatePortalSessionRequest) {
|
||||
const response = await backendApi.post<CreatePortalSessionResponse>(
|
||||
'/billing/v2/create-portal-session',
|
||||
'/billing/create-portal-session',
|
||||
request
|
||||
);
|
||||
if (response.error) throw response.error;
|
||||
|
@ -222,7 +222,7 @@ export const billingApiV2 = {
|
|||
|
||||
async cancelSubscription(request?: CancelSubscriptionRequest) {
|
||||
const response = await backendApi.post<CancelSubscriptionResponse>(
|
||||
'/billing/v2/cancel-subscription',
|
||||
'/billing/cancel-subscription',
|
||||
request || {}
|
||||
);
|
||||
if (response.error) throw response.error;
|
||||
|
@ -231,7 +231,7 @@ export const billingApiV2 = {
|
|||
|
||||
async reactivateSubscription() {
|
||||
const response = await backendApi.post<ReactivateSubscriptionResponse>(
|
||||
'/billing/v2/reactivate-subscription'
|
||||
'/billing/reactivate-subscription'
|
||||
);
|
||||
if (response.error) throw response.error;
|
||||
return response.data!;
|
||||
|
@ -239,7 +239,7 @@ export const billingApiV2 = {
|
|||
|
||||
async purchaseCredits(request: PurchaseCreditsRequest) {
|
||||
const response = await backendApi.post<PurchaseCreditsResponse>(
|
||||
'/billing/v2/purchase-credits',
|
||||
'/billing/purchase-credits',
|
||||
request
|
||||
);
|
||||
if (response.error) throw response.error;
|
||||
|
@ -248,7 +248,7 @@ export const billingApiV2 = {
|
|||
|
||||
async getTransactions(limit = 50, offset = 0) {
|
||||
const response = await backendApi.get<{ transactions: Transaction[]; count: number }>(
|
||||
`/billing/v2/transactions?limit=${limit}&offset=${offset}`
|
||||
`/billing/transactions?limit=${limit}&offset=${offset}`
|
||||
);
|
||||
if (response.error) throw response.error;
|
||||
return response.data!;
|
||||
|
@ -256,33 +256,33 @@ export const billingApiV2 = {
|
|||
|
||||
async getUsageHistory(days = 30) {
|
||||
const response = await backendApi.get<UsageHistory>(
|
||||
`/billing/v2/usage-history?days=${days}`
|
||||
`/billing/usage-history?days=${days}`
|
||||
);
|
||||
if (response.error) throw response.error;
|
||||
return response.data!;
|
||||
},
|
||||
|
||||
async triggerTestRenewal() {
|
||||
const response = await backendApi.post<TestRenewalResponse>('/billing/v2/test/trigger-renewal');
|
||||
const response = await backendApi.post<TestRenewalResponse>('/billing/test/trigger-renewal');
|
||||
if (response.error) throw response.error;
|
||||
return response.data!;
|
||||
},
|
||||
|
||||
async getTrialStatus() {
|
||||
const response = await backendApi.get<TrialStatus>('/billing/v2/trial/status');
|
||||
const response = await backendApi.get<TrialStatus>('/billing/trial/status');
|
||||
if (response.error) throw response.error;
|
||||
return response.data!;
|
||||
},
|
||||
|
||||
async startTrial(request: TrialStartRequest) {
|
||||
const response = await backendApi.post<TrialStartResponse>('/billing/v2/trial/start', request);
|
||||
const response = await backendApi.post<TrialStartResponse>('/billing/trial/start', request);
|
||||
if (response.error) throw response.error;
|
||||
return response.data!;
|
||||
},
|
||||
|
||||
async createTrialCheckout(request: TrialCheckoutRequest) {
|
||||
const response = await backendApi.post<TrialCheckoutResponse>(
|
||||
'/billing/v2/trial/create-checkout',
|
||||
'/billing/trial/create-checkout',
|
||||
request
|
||||
);
|
||||
if (response.error) throw response.error;
|
||||
|
@ -291,7 +291,7 @@ export const billingApiV2 = {
|
|||
|
||||
async cancelTrial() {
|
||||
const response = await backendApi.post<{ success: boolean; message: string; subscription_status: string }>(
|
||||
'/billing/v2/trial/cancel',
|
||||
'/billing/trial/cancel',
|
||||
{}
|
||||
);
|
||||
if (response.error) throw response.error;
|
||||
|
|
|
@ -66,6 +66,10 @@ export async function middleware(request: NextRequest) {
|
|||
return NextResponse.redirect(url);
|
||||
}
|
||||
|
||||
const isLocalMode = process.env.NEXT_PUBLIC_ENV_MODE === 'local'
|
||||
if (isLocalMode) {
|
||||
return supabaseResponse;
|
||||
}
|
||||
|
||||
if (!BILLING_ROUTES.includes(pathname) && pathname !== '/') {
|
||||
const { data: accounts } = await supabase
|
||||
|
@ -77,7 +81,6 @@ export async function middleware(request: NextRequest) {
|
|||
.single();
|
||||
|
||||
if (!accounts) {
|
||||
// Don't redirect if already on activate-trial page
|
||||
if (pathname === '/activate-trial') {
|
||||
return supabaseResponse;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue