Files
pangolin/server/private/routers/billing/hooks/handleSubscriptionCreated.ts
2026-02-04 15:49:49 -08:00

218 lines
7.6 KiB
TypeScript

/*
* 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";
export async function handleSubscriptionCreated(
subscription: Stripe.Subscription
): Promise<void> {
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}`
);
const response = await fetch(
`${privateConfig.getRawPrivateConfig().server.fossorial_api}/api/v1/license-internal/enterprise/paid-for`, // this says enterprise but it does both
{
method: "POST",
headers: {
"api-key":
privateConfig.getRawPrivateConfig().server
.fossorial_api_key!,
"Content-Type": "application/json"
},
body: JSON.stringify({
licenseId: parseInt(licenseId)
})
}
);
const data = await response.json();
logger.debug("Fossorial API response:", { data });
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;
}