HEX
Server: LiteSpeed
System: Linux houston.panomity.com 6.8.0-100-generic #100-Ubuntu SMP PREEMPT_DYNAMIC Tue Jan 13 16:40:06 UTC 2026 x86_64
User: nudepix (1011)
PHP: 7.4.33
Disabled: pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare,
Upload Files
File: //opt/agentcloud/webapp/src/controllers/stripe.ts
'use strict';

import { dynamicResponse } from '@dr';
import { setStripeCustomerId, setStripePlan, updateStripeCustomer } from 'db/account';
import { addCheckoutSession, getCheckoutSessionByAccountId } from 'db/checkoutsession';
import { addPaymentLink, unsafeGetPaymentLinkById } from 'db/paymentlink';
import { addPortalLink } from 'db/portallink';
import debug from 'debug';
import { stripe } from 'lib/stripe';
import toObjectId from 'misc/toobjectid';
import { planToPriceMap, priceToPlanMap, priceToProductMap,SubscriptionPlan } from 'struct/billing';
const log = debug('webapp:stripe');

function destructureSubscription(sub) {
	let planSub
		, addonUsersSub
		, addonStorageSub;
	// for (let sub of subscriptionData) {
	if (Array.isArray(sub?.items?.data) && sub.items.data[0]?.price?.id) {
		switch (sub.items.data[0]?.price?.id) {
			case process.env.STRIPE_FREE_PLAN_PRICE_ID:
			case process.env.STRIPE_PRO_PLAN_PRICE_ID:
			case process.env.STRIPE_TEAMS_PLAN_PRICE_ID:
				planSub = sub;
				break;
			case process.env.STRIPE_ADDON_USERS_PRICE_ID:
				addonUsersSub = sub;
				break;
			case process.env.STRIPE_ADDON_STORAGE_PRICE_ID:
				addonStorageSub = sub;
				break;
		}
	}
	// }
	return { planSub, addonUsersSub, addonStorageSub };
} 

async function getSubscriptionsDetails(stripeCustomerId: string) {
	try {
		const body: any = {
			customer: stripeCustomerId,
			status: 'trialing',
			limit: 1 // fetch only the first subscription
		};
		let subscriptions = await stripe.subscriptions.list(body);
		if (!subscriptions || subscriptions?.data?.length === 0) {
			//They are not on a trial
			body.status = 'active';
			subscriptions = await stripe.subscriptions.list(body);
		}
		return destructureSubscription(subscriptions.data[0]);
	} catch (error) {
		console.error('Error fetching subscriptions:', error);
		throw error;
	}
}

/**
 * @api {post} /stripe-webhook
 */
export async function webhookHandler(req, res, next) {

	if (!process.env['STRIPE_WEBHOOK_SECRET']) {
		log('Received stripe webhook but STRIPE_WEBHOOK_SECRET is not set');
		return res.status(400).send('missing STRIPE_WEBHOOK_SECRET');
	}

	const sig = req.headers['stripe-signature'];
	let event;
	try {
		event = stripe.webhooks.constructEvent(req.body, sig, process.env['STRIPE_WEBHOOK_SECRET']);
	} catch (err) {
		log(err);
		return res.status(400).send(`Webhook Error: ${err.message}`);
	}

	log(`Stripe webhook "${event.type}":`, JSON.stringify(event, null, '\t'));

	// Handle the event
	switch (event.type) {

		case 'checkout.session.completed': {
			const checkoutSession = event.data.object;
			const paymentLink = checkoutSession.payment_link;
			if (!paymentLink) {
				log('Completed checkout session without .data.object.payment_link:', checkoutSession);
				break;
			}
			const foundPaymentLink = await unsafeGetPaymentLinkById(paymentLink);
			if (!foundPaymentLink) {
				log('No payment link found for payment link id:', paymentLink);
				break;
			}
			await addCheckoutSession({
				accountId: foundPaymentLink.accountId,
				checkoutSessionId: checkoutSession.id,
				payload: checkoutSession,
				createdDate: new Date(),
			});
			await setStripeCustomerId(foundPaymentLink.accountId, checkoutSession.customer);
			const { planSub, addonUsersSub, addonStorageSub } = await getSubscriptionsDetails(checkoutSession.customer);
			//Note: 0 to set them on else case
			await updateStripeCustomer(checkoutSession.customer, {
				stripePlan: planToPriceMap[planSub?.items.data[0].price.id],
				stripeAddons: {
					users: addonUsersSub ? addonUsersSub?.items.data[0].quantity : 0,
					storage: addonStorageSub ? addonStorageSub?.items.data[0].quantity : 0,
				},
				stripeEndsAt: planSub?.current_period_end*1000,
			});
			break;
		}

		case 'customer.subscription.updated': {
			const subscriptionUpdated = event.data.object;
			const { planSub, addonUsersSub, addonStorageSub } = destructureSubscription(subscriptionUpdated);
			//Note: null to not update them unless required
			const update = {
				stripePlan: priceToPlanMap[planSub?.items.data[0].price.id],
				stripeAddons: {
					users: addonUsersSub ? addonUsersSub?.items.data[0].quantity : null,
					storage: addonStorageSub ? addonStorageSub?.items.data[0].quantity : null,
				},
				stripeEndsAt: planSub?.current_period_end ? planSub.current_period_end*1000 : null,
				stripeTrial: planSub?.status === 'trialing', // https://docs.stripe.com/api/subscriptions/object#subscription_object-status
			};
			if (subscriptionUpdated['cancel_at_period_end'] === true) {
				log(`${subscriptionUpdated.customer} subscription will cancel at end of period`);
				update['stripeEndsAt'] = subscriptionUpdated.cancel_at;
				update['stripeCancelled'] = true;
			}
			if (subscriptionUpdated['status'] === 'canceled') {
				log(`${subscriptionUpdated.customer} canceled their subscription`);
				update['stripeEndsAt'] = subscriptionUpdated.cancel_at;
				update['stripeCancelled'] = true;
			}
			log('Customer subscription update %O', update);
			await updateStripeCustomer(subscriptionUpdated.customer, update);
			break;
		}

		case 'customer.subscription.created': {
			const subscriptionCreated = event.data.object;
			//TODO: check we actually need this
			await updateStripeCustomer(subscriptionCreated.customer, {
				stripeEndsAt: subscriptionCreated.current_period_end,
			});
			break;
		}
		
		// case 'customer.subscription.trial_will_end': {
		// 	break;
		// }
		
		//TODO:
		// case 'customer.subscription.deleted': {
		// 	break;
		// }

		default: {
			log(`Unhandled stripe webhook event type "${event.type}"`);
		}
	}

	// Return a 200 response to acknowledge receipt of the event
	res.status(200).send();

}

export async function createPortalLink(req, res, next) {

	if (!process.env['STRIPE_ACCOUNT_SECRET']) {
		return dynamicResponse(req, res, 400, { error: 'Missing STRIPE_ACCOUNT_SECRET' });
	}

	if (!res.locals.account?.stripe?.stripeCustomerId) {
		return dynamicResponse(req, res, 400, { error: 'No subscription to manage' });
	}

	const { planSub } = await getSubscriptionsDetails(res.locals.account?.stripe?.stripeCustomerId);
	if (!planSub) {
		return dynamicResponse(req, res, 400, { error: 'No subscription to manage' });
	}
	
	// const configurationData: any = {
	// 	business_profile: {
	// 		//TODO: envs
	// 		headline: 'RNA Digital makers of Agentcloud',
	// 		privacy_policy_url: 'https://www.getmonita.io/legal',
	// 		terms_of_service_url: 'https://www.getmonita.io/legal',
	// 	},
	// 	features: {
	// 		subscription_update: {
	// 			enabled: true,
	// 			default_allowed_updates: ['quantity'],
	// 			products: [
	// 				{
	// 					product: priceToProductMap[planToPriceMap[res.locals.account.stripe.stripePlan]],
	// 					prices: [planToPriceMap[res.locals.account.stripe.stripePlan]],
	// 				}
	// 			]
	// 		},
	// 		payment_method_update: {
	// 			enabled: true,
	// 		},
	// 	}
	// };
	// const users = req.body.users || 0;
	// const storage = req.body.storage || 0;
	// users > 0 && configurationData.features.subscription_update.products.push({
	// 	prices: [process.env.STRIPE_ADDON_USERS_PRICE_ID],
	// 	product: process.env.STRIPE_ADDON_USERS_PRODUCT_ID,
	// });
	// storage > 0 && configurationData.features.subscription_update.products.push({
	// 	prices: [process.env.STRIPE_ADDON_STORAGE_PRICE_ID],
	// 	product: process.env.STRIPE_ADDON_STORAGE_PRODUCT_ID,
	// });
	// const customConfiguration = await stripe.billingPortal.configurations.create(configurationData);

	const portalLink = await stripe.billingPortal.sessions.create({
		customer: res.locals.account?.stripe?.stripeCustomerId,
		return_url: `${process.env.URL_APP}/auth/redirect?to=${encodeURIComponent('/billing')}`,
		// configuration: customConfiguration.id
	});

	await addPortalLink({
		accountId: toObjectId(res.locals.account._id),
		portalLinkId: portalLink.id,
		url: portalLink.url,
		payload: portalLink,
		createdDate: new Date(),
	});

	return dynamicResponse(req, res, 302, { redirect: portalLink.url });
}