refactor billing api

This commit is contained in:
Saumya 2025-09-08 23:35:32 +05:30
parent ccfbdb049d
commit 6b0ffb9766
7 changed files with 147 additions and 31 deletions

View File

@ -396,9 +396,13 @@ class SubscriptionService:
return
existing_account = await client.from_('credit_accounts').select('trial_status').eq('account_id', account_id).execute()
if existing_account.data and existing_account.data[0].get('trial_status') == 'active':
logger.info(f"[WEBHOOK] Trial already active for account {account_id}, skipping duplicate processing")
return
if existing_account.data:
current_status = existing_account.data[0].get('trial_status')
if current_status == 'active':
logger.info(f"[WEBHOOK] Trial already active for account {account_id}, skipping duplicate processing")
return
elif current_status == 'none':
logger.info(f"[WEBHOOK] Activating trial for account {account_id}")
trial_ends_at = datetime.fromtimestamp(subscription.trial_end, tz=timezone.utc)
@ -417,14 +421,10 @@ class SubscriptionService:
description=f'{TRIAL_DURATION_DAYS}-day free trial credits'
)
await client.from_('trial_history').insert({
await client.from_('trial_history').upsert({
'account_id': account_id,
'trial_mode': 'cc_required',
'started_at': datetime.now(timezone.utc).isoformat(),
'stripe_subscription_id': subscription['id'],
'had_payment_method': True,
'credits_granted': str(TRIAL_CREDITS)
}).on_conflict('account_id').do_nothing().execute()
'started_at': datetime.now(timezone.utc).isoformat()
}, on_conflict='account_id').execute()
logger.info(f"[WEBHOOK] Started trial for user {account_id} via Stripe subscription - granted ${TRIAL_CREDITS} credits")

View File

@ -82,7 +82,7 @@ class TrialService:
logger.info(f"[TRIAL CANCEL] Cancelled Stripe subscription {stripe_subscription_id} for account {account_id}")
await client.from_('credit_accounts').update({
'trial_status': 'expired',
'trial_status': 'cancelled',
'tier': 'none',
'balance': 0.00,
'stripe_subscription_id': None
@ -145,6 +145,7 @@ class TrialService:
from .subscription_service import subscription_service
customer_id = await subscription_service.get_or_create_stripe_customer(account_id)
logger.info(f"[TRIAL] Creating checkout session for account {account_id}")
session = await stripe.checkout.Session.create_async(
customer=customer_id,
payment_method_types=['card'],
@ -165,6 +166,10 @@ class TrialService:
mode='subscription',
success_url=success_url,
cancel_url=cancel_url,
metadata={
'account_id': account_id,
'trial_start': 'true'
},
subscription_data={
'trial_period_days': TRIAL_DURATION_DAYS,
'metadata': {

View File

@ -107,24 +107,111 @@ class WebhookService:
logger.error(f"[WEBHOOK] Credit amount: ${credit_amount}")
async def _handle_subscription_checkout(self, session, client):
if session.get('metadata', {}).get('trial_start') == 'true':
logger.info(f"[WEBHOOK] Trial checkout detected - will be handled by subscription change event")
logger.info(f"[WEBHOOK] Checkout completed for new subscription: {session['subscription']}")
logger.info(f"[WEBHOOK] Session metadata: {session.get('metadata', {})}")
if session.get('metadata', {}).get('trial_start') == 'true':
account_id = session['metadata'].get('account_id')
logger.info(f"[WEBHOOK] Trial checkout detected for account {account_id}")
if session.get('subscription'):
subscription_id = session['subscription']
subscription = stripe.Subscription.retrieve(subscription_id, expand=['default_payment_method'])
if subscription.status == 'trialing':
logger.info(f"[WEBHOOK] Trial subscription created for account {account_id}")
price_id = subscription['items']['data'][0]['price']['id'] if subscription.get('items') else None
tier_info = get_tier_by_price_id(price_id)
tier_name = tier_info.name if tier_info else 'tier_2_20'
trial_ends_at = datetime.fromtimestamp(subscription.trial_end, tz=timezone.utc) if subscription.get('trial_end') else None
await client.from_('credit_accounts').update({
'trial_status': 'active',
'trial_started_at': datetime.now(timezone.utc).isoformat(),
'trial_ends_at': trial_ends_at.isoformat() if trial_ends_at else None,
'stripe_subscription_id': subscription['id'],
'tier': tier_name
}).eq('account_id', account_id).execute()
logger.info(f"[WEBHOOK] Activated trial for account {account_id}, tier: {tier_name}")
await credit_manager.add_credits(
account_id=account_id,
amount=TRIAL_CREDITS,
is_expiring=False,
description=f'{TRIAL_DURATION_DAYS}-day free trial credits'
)
await client.from_('trial_history').upsert({
'account_id': account_id,
'started_at': datetime.now(timezone.utc).isoformat(),
'stripe_checkout_session_id': session.get('id')
}, on_conflict='account_id').execute()
logger.info(f"[WEBHOOK] Trial fully activated for account {account_id}")
else:
logger.info(f"[WEBHOOK] Subscription status: {subscription.status}, not trialing")
async def _handle_subscription_created_or_updated(self, event, client):
subscription = event.data.object
logger.info(f"[WEBHOOK] Subscription event: type={event.type}, status={subscription.status}")
logger.info(f"[WEBHOOK] Subscription metadata: {subscription.get('metadata', {})}")
if event.type == 'customer.subscription.updated':
await self._handle_subscription_updated(event, subscription, client)
if subscription.status in ['active', 'trialing']:
logger.info(f"[WEBHOOK] Processing subscription change for customer: {subscription['customer']}")
if subscription.status == 'trialing' and not subscription.get('metadata', {}).get('account_id'):
customer_result = await client.schema('basejump').from_('billing_customers')\
.select('account_id')\
.eq('id', subscription['customer'])\
.execute()
if customer_result.data and customer_result.data[0].get('account_id'):
account_id = customer_result.data[0]['account_id']
logger.info(f"[WEBHOOK] Found account_id {account_id} for customer {subscription['customer']}")
try:
await stripe.Subscription.modify_async(
subscription['id'],
metadata={'account_id': account_id, 'trial_start': 'true'}
)
subscription['metadata'] = {'account_id': account_id, 'trial_start': 'true'}
logger.info(f"[WEBHOOK] Updated subscription metadata with account_id")
except Exception as e:
logger.error(f"[WEBHOOK] Failed to update subscription metadata: {e}")
from .subscription_service import subscription_service
await subscription_service.handle_subscription_change(subscription)
async def _handle_subscription_updated(self, event, subscription, client):
previous_attributes = event.data.get('previous_attributes', {})
prev_status = previous_attributes.get('status')
prev_default_payment = previous_attributes.get('default_payment_method')
if subscription.status == 'trialing' and subscription.get('default_payment_method') and not prev_default_payment:
account_id = subscription.metadata.get('account_id')
if account_id:
logger.info(f"[WEBHOOK] Payment method added to trial for account {account_id}")
price_id = subscription['items']['data'][0]['price']['id'] if subscription.get('items') else None
tier_info = get_tier_by_price_id(price_id)
tier_name = tier_info.name if tier_info else 'tier_2_20'
await client.from_('credit_accounts').update({
'trial_status': 'converted',
'tier': tier_name
}).eq('account_id', account_id).execute()
await client.from_('trial_history').update({
'converted_to_paid': True,
'ended_at': datetime.now(timezone.utc).isoformat()
}).eq('account_id', account_id).is_('ended_at', 'null').execute()
logger.info(f"[WEBHOOK] Marked trial as converted (payment added) for account {account_id}, tier: {tier_name}")
if prev_status == 'trialing' and subscription.status != 'trialing':
account_id = subscription.metadata.get('account_id')
@ -133,16 +220,36 @@ class WebhookService:
if subscription.status == 'active':
logger.info(f"[WEBHOOK] Trial converted to paid for account {account_id}")
price_id = subscription['items']['data'][0]['price']['id']
tier_info = get_tier_by_price_id(price_id)
tier_name = tier_info.name if tier_info else 'tier_2_20'
await client.from_('credit_accounts').update({
'trial_status': 'converted',
'tier': 'tier_2_20'
'tier': tier_name
}).eq('account_id', account_id).execute()
await client.from_('trial_history').update({
'ended_at': datetime.now(timezone.utc).isoformat(),
'converted_to_paid': True
}).eq('account_id', account_id).is_('ended_at', 'null').execute()
logger.info(f"[WEBHOOK] Trial conversion completed for account {account_id} to tier {tier_name}")
elif subscription.status == 'canceled':
logger.info(f"[WEBHOOK] Trial cancelled for account {account_id}")
await client.from_('credit_accounts').update({
'trial_status': 'cancelled',
'tier': 'none',
'stripe_subscription_id': None
}).eq('account_id', account_id).execute()
await client.from_('trial_history').update({
'ended_at': datetime.now(timezone.utc).isoformat(),
'converted_to_paid': False
}).eq('account_id', account_id).is_('ended_at', 'null').execute()
else:
logger.info(f"[WEBHOOK] Trial expired without conversion for account {account_id}, status: {subscription.status}")
@ -156,7 +263,7 @@ class WebhookService:
await client.from_('trial_history').update({
'ended_at': datetime.now(timezone.utc).isoformat(),
'converted_to_paid': False
}).eq('account_id', account_id).execute()
}).eq('account_id', account_id).is_('ended_at', 'null').execute()
await client.from_('credit_ledger').insert({
'account_id': account_id,
@ -303,9 +410,13 @@ class WebhookService:
return
existing_account = await client.from_('credit_accounts').select('trial_status').eq('account_id', account_id).execute()
if existing_account.data and existing_account.data[0].get('trial_status') == 'active':
logger.info(f"[WEBHOOK] Trial already active for account {account_id}, skipping duplicate processing")
return
if existing_account.data:
current_status = existing_account.data[0].get('trial_status')
if current_status == 'active':
logger.info(f"[WEBHOOK] Trial already active for account {account_id}, skipping duplicate processing")
return
elif current_status == 'none':
logger.info(f"[WEBHOOK] Activating trial for account {account_id}")
trial_ends_at = datetime.fromtimestamp(subscription.trial_end, tz=timezone.utc)
@ -324,14 +435,10 @@ class WebhookService:
description=f'{TRIAL_DURATION_DAYS}-day free trial credits'
)
await client.from_('trial_history').insert({
await client.from_('trial_history').upsert({
'account_id': account_id,
'trial_mode': 'cc_required',
'started_at': datetime.now(timezone.utc).isoformat(),
'stripe_subscription_id': subscription['id'],
'had_payment_method': True,
'credits_granted': str(TRIAL_CREDITS)
}).on_conflict('account_id').do_nothing().execute()
'started_at': datetime.now(timezone.utc).isoformat()
}, on_conflict='account_id').execute()
logger.info(f"[WEBHOOK] Started trial for user {account_id} via Stripe subscription - granted ${TRIAL_CREDITS} credits")

View File

@ -21,7 +21,6 @@ BEGIN
ON CONFLICT (account_id) DO NOTHING;
RAISE LOG 'Created account for new user % with no credits (must start trial)', NEW.id;
END IF;
RETURN NEW;
EXCEPTION WHEN OTHERS THEN
RAISE WARNING 'Error in initialize_free_tier_credits for user %: %', NEW.id, SQLERRM;

View File

@ -0,0 +1,10 @@
BEGIN;
ALTER TABLE credit_accounts
DROP CONSTRAINT IF EXISTS credit_accounts_trial_status_check;
ALTER TABLE credit_accounts
ADD CONSTRAINT credit_accounts_trial_status_check
CHECK (trial_status IN ('none', 'active', 'expired', 'converted', 'cancelled'));
COMMIT;

View File

@ -119,7 +119,6 @@ cd suna`
<DocsBody className="mb-8">
<h2 id="step-3-provide-credentials">Step 3: Provide Your Credentials</h2>
<p className="mb-4">The wizard will walk you through configuring these services in 17 steps. Don't worry - it saves your progress!</p>
<h3 id="required-services" className="mb-4">Required Services</h3>
<div className="space-y-4 mb-6">
<div className="border rounded-lg p-4">
@ -130,7 +129,6 @@ cd suna`
<p className="text-sm text-muted-foreground mb-2">Self-hosted option available. Handles user data, conversations, and agent configs.</p>
<p className="text-sm">Get it at: <a href="https://supabase.com/dashboard/projects" className="text-blue-500 hover:underline">supabase.com</a></p>
</div>
<div className="border rounded-lg p-4">
<div className="flex items-center gap-2 mb-2">
<CheckCircle2 className="h-5 w-5 text-green-500" />
@ -139,7 +137,6 @@ cd suna`
<p className="text-sm text-muted-foreground mb-2">Provides secure environments for agents to run code safely.</p>
<p className="text-sm">Get it at: <a href="https://app.daytona.io/" className="text-blue-500 hover:underline">daytona.io</a></p>
</div>
<div className="border rounded-lg p-4">
<div className="flex items-center gap-2 mb-2">
<CheckCircle2 className="h-5 w-5 text-green-500" />
@ -272,7 +269,6 @@ docker compose down`
</CodeBlockBody>
</CodeBlock>
</div>
<div>
<h4 className="font-medium mb-2">Terminal 2 - Frontend:</h4>
<CodeBlock

View File

@ -14,7 +14,6 @@ export function useTrialStatus() {
export function useStartTrial() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: startTrial,
onSuccess: (data) => {