Merge pull request #1602 from escapade-mckv/billing-improvements-clean

disable trial check for local mode
This commit is contained in:
Bobbie 2025-09-10 10:43:08 +05:30 committed by GitHub
commit 9f8abca2ce
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 99 additions and 69 deletions

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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' &&

View File

@ -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);
}

View File

@ -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',

View File

@ -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;

View File

@ -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;
}