/* * This file is part of a proprietary work. * * Copyright (c) 2025 Fossorial, Inc. * All rights reserved. * * This file is licensed under the Fossorial Commercial License. * You may not use this file except in compliance with the License. * Unauthorized use, copying, modification, or distribution is strictly prohibited. * * This file is not licensed under the AGPLv3. */ import Stripe from "stripe"; import { customers, subscriptions, db, subscriptionItems, userOrgs, users } from "@server/db"; import { eq, and } from "drizzle-orm"; import logger from "@server/logger"; import stripe from "#private/lib/stripe"; import { handleSubscriptionLifesycle } from "../subscriptionLifecycle"; import { AudienceIds, moveEmailToAudience } from "#private/lib/resend"; import { getSubType } from "./getSubType"; import privateConfig from "#private/lib/config"; import { getLicensePriceSet, LicenseId } from "@server/lib/billing/licenses"; import { sendEmail } from "@server/emails"; import EnterpriseEditionKeyGenerated from "@server/emails/templates/EnterpriseEditionKeyGenerated"; import config from "@server/lib/config"; export async function handleSubscriptionCreated( subscription: Stripe.Subscription ): Promise { try { // Fetch the subscription from Stripe with expanded price.tiers const fullSubscription = await stripe!.subscriptions.retrieve( subscription.id, { expand: ["items.data.price.tiers"] } ); logger.info(JSON.stringify(fullSubscription, null, 2)); // Check if subscription already exists const [existingSubscription] = await db .select() .from(subscriptions) .where(eq(subscriptions.subscriptionId, subscription.id)) .limit(1); if (existingSubscription) { logger.info( `Subscription with ID ${subscription.id} already exists.` ); return; } const newSubscription = { subscriptionId: subscription.id, customerId: subscription.customer as string, status: subscription.status, canceledAt: subscription.canceled_at ? subscription.canceled_at : null, createdAt: subscription.created }; await db.insert(subscriptions).values(newSubscription); logger.info( `Subscription with ID ${subscription.id} created successfully.` ); // Insert subscription items if (Array.isArray(fullSubscription.items?.data)) { const itemsToInsertPromises = fullSubscription.items.data.map( async (item) => { // try to get the product name from stripe and add it to the item let name = null; if (item.price.product) { const product = await stripe!.products.retrieve( item.price.product as string ); name = product.name || null; } return { subscriptionId: subscription.id, planId: item.plan.id, priceId: item.price.id, meterId: item.plan.meter, unitAmount: item.price.unit_amount || 0, currentPeriodStart: item.current_period_start, currentPeriodEnd: item.current_period_end, tiers: item.price.tiers ? JSON.stringify(item.price.tiers) : null, interval: item.plan.interval, name }; } ); // wait for all items to be processed const itemsToInsert = await Promise.all(itemsToInsertPromises); if (itemsToInsert.length > 0) { await db.insert(subscriptionItems).values(itemsToInsert); logger.info( `Inserted ${itemsToInsert.length} subscription items for subscription ${subscription.id}.` ); } } // Lookup customer to get orgId const [customer] = await db .select() .from(customers) .where(eq(customers.customerId, subscription.customer as string)) .limit(1); if (!customer) { logger.error( `Customer with ID ${subscription.customer} not found for subscription ${subscription.id}.` ); return; } const type = getSubType(fullSubscription); if (type === "saas") { logger.debug( `Handling SAAS subscription lifecycle for org ${customer.orgId}` ); // we only need to handle the limit lifecycle for saas subscriptions not for the licenses await handleSubscriptionLifesycle( customer.orgId, subscription.status ); const [orgUserRes] = await db .select() .from(userOrgs) .where( and( eq(userOrgs.orgId, customer.orgId), eq(userOrgs.isOwner, true) ) ) .innerJoin(users, eq(userOrgs.userId, users.userId)); if (orgUserRes) { const email = orgUserRes.user.email; if (email) { moveEmailToAudience(email, AudienceIds.Subscribed); } } } else if (type === "license") { logger.debug( `License subscription created for org ${customer.orgId}, no lifecycle handling needed.` ); // Retrieve the client_reference_id from the checkout session let licenseId: string | null = null; try { const sessions = await stripe!.checkout.sessions.list({ subscription: subscription.id, limit: 1 }); if (sessions.data.length > 0) { licenseId = sessions.data[0].client_reference_id || null; } if (!licenseId) { logger.error( `No client_reference_id found for subscription ${subscription.id}` ); return; } logger.debug( `Retrieved licenseId ${licenseId} from checkout session for subscription ${subscription.id}` ); // Determine users and sites based on license type const priceSet = getLicensePriceSet(); const subscriptionPriceId = fullSubscription.items.data[0]?.price.id; let numUsers: number; let numSites: number; if (subscriptionPriceId === priceSet[LicenseId.SMALL_LICENSE]) { numUsers = 25; numSites = 25; } else if ( subscriptionPriceId === priceSet[LicenseId.BIG_LICENSE] ) { numUsers = 50; numSites = 50; } else { logger.error( `Unknown price ID ${subscriptionPriceId} for subscription ${subscription.id}` ); return; } logger.debug( `License type determined: ${numUsers} users, ${numSites} sites for subscription ${subscription.id}` ); const response = await fetch( `${privateConfig.getRawPrivateConfig().server.fossorial_api}/api/v1/license-internal/enterprise/paid-for`, { method: "POST", headers: { "api-key": privateConfig.getRawPrivateConfig().server .fossorial_api_key!, "Content-Type": "application/json" }, body: JSON.stringify({ licenseId: parseInt(licenseId), paidFor: true, users: numUsers, sites: numSites }) } ); const data = await response.json(); logger.debug(`Fossorial API response: ${JSON.stringify(data)}`); if (customer.email) { logger.debug( `Sending license key email to ${customer.email} for subscription ${subscription.id}` ); await sendEmail( EnterpriseEditionKeyGenerated({ keyValue: data.data.licenseKey, personalUseOnly: false, users: numUsers, sites: numSites, modifySubscriptionLink: `${config.getRawConfig().app.dashboard_url}/${customer.orgId}/settings/billing` }), { to: customer.email, from: config.getNoReplyEmail(), subject: "Your Enterprise Edition license key is ready" } ); } else { logger.error( `No email found for customer ${customer.customerId} to send license key.` ); } return data; } catch (error) { console.error("Error creating new license:", error); throw error; } } } catch (error) { logger.error( `Error handling subscription created event for ID ${subscription.id}:`, error ); } return; }