mirror of https://github.com/kortix-ai/suna.git
refactor billing api
This commit is contained in:
parent
ccfbdb049d
commit
6b0ffb9766
|
@ -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")
|
||||
|
||||
|
|
|
@ -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': {
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
|
@ -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
|
||||
|
|
|
@ -14,7 +14,6 @@ export function useTrialStatus() {
|
|||
|
||||
export function useStartTrial() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: startTrial,
|
||||
onSuccess: (data) => {
|
||||
|
|
Loading…
Reference in New Issue