diff --git a/server/private/routers/billing/hooks/handleSubscriptionCreated.ts b/server/private/routers/billing/hooks/handleSubscriptionCreated.ts index 431798dd..4370d088 100644 --- a/server/private/routers/billing/hooks/handleSubscriptionCreated.ts +++ b/server/private/routers/billing/hooks/handleSubscriptionCreated.ts @@ -27,6 +27,7 @@ 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"; export async function handleSubscriptionCreated( subscription: Stripe.Subscription @@ -182,6 +183,33 @@ export async function handleSubscriptionCreated( `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`, // this says enterprise but it does both { @@ -193,7 +221,10 @@ export async function handleSubscriptionCreated( "Content-Type": "application/json" }, body: JSON.stringify({ - licenseId: parseInt(licenseId) + licenseId: parseInt(licenseId), + paidFor: true, + users: numUsers, + sites: numSites }) } ); diff --git a/server/private/routers/billing/hooks/handleSubscriptionDeleted.ts b/server/private/routers/billing/hooks/handleSubscriptionDeleted.ts index 7a7d9149..56fca02b 100644 --- a/server/private/routers/billing/hooks/handleSubscriptionDeleted.ts +++ b/server/private/routers/billing/hooks/handleSubscriptionDeleted.ts @@ -24,11 +24,21 @@ import { eq, and } from "drizzle-orm"; import logger from "@server/logger"; import { handleSubscriptionLifesycle } from "../subscriptionLifecycle"; import { AudienceIds, moveEmailToAudience } from "#private/lib/resend"; +import { getSubType } from "./getSubType"; +import stripe from "#private/lib/stripe"; export async function handleSubscriptionDeleted( 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"] + } + ); + const [existingSubscription] = await db .select() .from(subscriptions) @@ -64,25 +74,33 @@ export async function handleSubscriptionDeleted( return; } - await handleSubscriptionLifesycle(customer.orgId, subscription.status); + const type = getSubType(fullSubscription); + if (type === "saas") { + logger.debug(`Handling SaaS subscription deletion for orgId ${customer.orgId} and subscription ID ${subscription.id}`); - const [orgUserRes] = await db - .select() - .from(userOrgs) - .where( - and( - eq(userOrgs.orgId, customer.orgId), - eq(userOrgs.isOwner, true) + 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)); + .innerJoin(users, eq(userOrgs.userId, users.userId)); - if (orgUserRes) { - const email = orgUserRes.user.email; + if (orgUserRes) { + const email = orgUserRes.user.email; - if (email) { - moveEmailToAudience(email, AudienceIds.Churned); + if (email) { + moveEmailToAudience(email, AudienceIds.Churned); + } } + } else if (type === "license") { + logger.debug(`Handling license subscription deletion for orgId ${customer.orgId} and subscription ID ${subscription.id}`); + } } catch (error) { logger.error( diff --git a/server/private/routers/billing/hooks/handleSubscriptionUpdated.ts b/server/private/routers/billing/hooks/handleSubscriptionUpdated.ts index 01086054..8e6f901e 100644 --- a/server/private/routers/billing/hooks/handleSubscriptionUpdated.ts +++ b/server/private/routers/billing/hooks/handleSubscriptionUpdated.ts @@ -26,6 +26,7 @@ import logger from "@server/logger"; import { getFeatureIdByMetricId } from "@server/lib/billing/features"; import stripe from "#private/lib/stripe"; import { handleSubscriptionLifesycle } from "../subscriptionLifecycle"; +import { getSubType } from "./getSubType"; export async function handleSubscriptionUpdated( subscription: Stripe.Subscription, @@ -74,11 +75,6 @@ export async function handleSubscriptionUpdated( }) .where(eq(subscriptions.subscriptionId, subscription.id)); - await handleSubscriptionLifesycle( - existingCustomer.orgId, - subscription.status - ); - // Upsert subscription items if (Array.isArray(fullSubscription.items?.data)) { const itemsToUpsert = fullSubscription.items.data.map((item) => ({ @@ -141,14 +137,14 @@ export async function handleSubscriptionUpdated( // This item has cycled const meterId = item.plan.meter; if (!meterId) { - logger.warn( + logger.debug( `No meterId found for subscription item ${item.id}. Skipping usage reset.` ); continue; } const featureId = getFeatureIdByMetricId(meterId); if (!featureId) { - logger.warn( + logger.debug( `No featureId found for meterId ${meterId}. Skipping usage reset.` ); continue; @@ -236,6 +232,20 @@ export async function handleSubscriptionUpdated( } } // --- end usage update --- + + const type = getSubType(fullSubscription); + if (type === "saas") { + logger.debug(`Handling SAAS subscription lifecycle for org ${existingCustomer.orgId}`); + // we only need to handle the limit lifecycle for saas subscriptions not for the licenses + await handleSubscriptionLifesycle( + existingCustomer.orgId, + subscription.status + ); + } else { + logger.debug( + `Subscription ${subscription.id} is of type ${type}. No lifecycle handling needed.` + ); + } } } catch (error) { logger.error( diff --git a/server/routers/generatedLicense/types.ts b/server/routers/generatedLicense/types.ts index d05da2de..d78f2332 100644 --- a/server/routers/generatedLicense/types.ts +++ b/server/routers/generatedLicense/types.ts @@ -6,6 +6,8 @@ export type GeneratedLicenseKey = { createdAt: string; tier: string; type: string; + users: number; + sites: number; }; export type ListGeneratedLicenseKeysResponse = GeneratedLicenseKey[]; diff --git a/src/components/GenerateLicenseKeysTable.tsx b/src/components/GenerateLicenseKeysTable.tsx index c6db4e1d..036b2fb5 100644 --- a/src/components/GenerateLicenseKeysTable.tsx +++ b/src/components/GenerateLicenseKeysTable.tsx @@ -158,6 +158,48 @@ export default function GenerateLicenseKeysTable({ : t("licenseTierPersonal"); } }, + { + accessorKey: "users", + friendlyName: t("users"), + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + const users = row.original.users; + return users === -1 ? "∞" : users; + } + }, + { + accessorKey: "sites", + friendlyName: t("sites"), + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + const sites = row.original.sites; + return sites === -1 ? "∞" : sites; + } + }, { accessorKey: "terminateAt", friendlyName: t("licenseTableValidUntil"),