import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { organization, subscription, user } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { generateId } from '@sim/utils/id' import { eq } from 'drizzle-orm' import type Stripe from 'stripe' import { getEmailSubject, renderEnterpriseSubscriptionEmail } from '@/components/emails' import { stripeWebhookIdempotency } from '@/lib/billing/webhooks/idempotency' import { sendEmail } from '@/lib/messaging/email/mailer' import { getFromEmailAddress } from '@/lib/messaging/email/utils' import { captureServerEvent } from '@/lib/posthog/server' import { parseEnterpriseSubscriptionMetadata } from '../types' const logger = createLogger('BillingEnterprise') export async function handleManualEnterpriseSubscription(event: Stripe.Event) { const stripeSubscription = event.data.object as Stripe.Subscription const metaPlan = (stripeSubscription.metadata?.plan as string | undefined)?.toLowerCase() || '' if (metaPlan !== 'enterprise') { logger.info('[subscription.created] Skipping non-enterprise subscription', { subscriptionId: stripeSubscription.id, plan: metaPlan || 'unknown', }) return } const stripeCustomerId = stripeSubscription.customer as string if (!stripeCustomerId) { logger.error('[subscription.created] Missing Stripe customer ID', { subscriptionId: stripeSubscription.id, }) throw new Error('Missing Stripe customer ID on subscription') } const metadata = stripeSubscription.metadata || {} const referenceId = typeof metadata.referenceId === 'string' && metadata.referenceId.length > 0 ? metadata.referenceId : null if (!referenceId) { logger.error('[subscription.created] Unable to resolve referenceId', { subscriptionId: stripeSubscription.id, stripeCustomerId, }) throw new Error('Unable to resolve referenceId for subscription') } const enterpriseMetadata = parseEnterpriseSubscriptionMetadata(metadata) if (!enterpriseMetadata) { logger.error('[subscription.created] Invalid enterprise metadata shape', { subscriptionId: stripeSubscription.id, metadata, }) throw new Error('Invalid enterprise metadata for subscription') } const { seats, monthlyPrice } = enterpriseMetadata // Get the first subscription item which contains the period information const referenceItem = stripeSubscription.items?.data?.[0] const subscriptionRow = { id: generateId(), plan: 'enterprise', referenceId, stripeCustomerId, stripeSubscriptionId: stripeSubscription.id, status: stripeSubscription.status || null, periodStart: referenceItem?.current_period_start ? new Date(referenceItem.current_period_start * 1000) : null, periodEnd: referenceItem?.current_period_end ? new Date(referenceItem.current_period_end * 1000) : null, cancelAtPeriodEnd: stripeSubscription.cancel_at_period_end ?? null, seats: 1, // Enterprise uses metadata.seats for actual seat count, column is always 1 trialStart: stripeSubscription.trial_start ? new Date(stripeSubscription.trial_start * 1000) : null, trialEnd: stripeSubscription.trial_end ? new Date(stripeSubscription.trial_end * 1000) : null, metadata: metadata as Record, } const existing = await db .select({ id: subscription.id }) .from(subscription) .where(eq(subscription.stripeSubscriptionId, stripeSubscription.id)) .limit(1) if (existing.length > 0) { await db .update(subscription) .set({ plan: subscriptionRow.plan, referenceId: subscriptionRow.referenceId, stripeCustomerId: subscriptionRow.stripeCustomerId, status: subscriptionRow.status, periodStart: subscriptionRow.periodStart, periodEnd: subscriptionRow.periodEnd, cancelAtPeriodEnd: subscriptionRow.cancelAtPeriodEnd, seats: 1, // Enterprise uses metadata.seats for actual seat count, column is always 1 trialStart: subscriptionRow.trialStart, trialEnd: subscriptionRow.trialEnd, metadata: subscriptionRow.metadata, }) .where(eq(subscription.stripeSubscriptionId, stripeSubscription.id)) } else { await db.insert(subscription).values(subscriptionRow) } // Update the organization's usage limit to match the monthly price // The referenceId for enterprise plans is the organization ID try { await db .update(organization) .set({ orgUsageLimit: monthlyPrice.toFixed(2), updatedAt: new Date(), }) .where(eq(organization.id, referenceId)) logger.info('[subscription.created] Updated organization usage limit', { organizationId: referenceId, usageLimit: monthlyPrice, }) } catch (error) { logger.error('[subscription.created] Failed to update organization usage limit', { organizationId: referenceId, usageLimit: monthlyPrice, error, }) // Don't throw - the subscription was created successfully, just log the error } logger.info('[subscription.created] Upserted enterprise subscription', { subscriptionId: subscriptionRow.id, referenceId: subscriptionRow.referenceId, plan: subscriptionRow.plan, status: subscriptionRow.status, monthlyPrice, seats, note: 'Seats from metadata, Stripe quantity set to 1', }) let actorId = referenceId try { const [provisioningUser] = await db .select({ id: user.id }) .from(user) .where(eq(user.stripeCustomerId, stripeCustomerId)) .limit(1) actorId = provisioningUser?.id ?? referenceId } catch (error) { logger.warn('Failed to resolve enterprise provisioning actor; falling back to reference id', { referenceId, error, }) } // Dedupe against Stripe redelivery — the upsert above is idempotent, but // recordAudit/captureServerEvent are pure appends and would double-record. await stripeWebhookIdempotency.executeWithIdempotency( 'enterprise-subscription-provisioned', event.id, async () => { recordAudit({ actorId, action: AuditAction.ENTERPRISE_SUBSCRIPTION_PROVISIONED, resourceType: AuditResourceType.SUBSCRIPTION, resourceId: subscriptionRow.id, description: `Enterprise subscription provisioned for organization ${referenceId} (${seats} seats)`, metadata: { organizationId: referenceId, stripeCustomerId, stripeSubscriptionId: stripeSubscription.id, seats, monthlyPrice, currency: 'usd', }, }) captureServerEvent(actorId, 'enterprise_subscription_created', { reference_id: referenceId, seats, monthly_price: monthlyPrice, currency: 'usd', }) } ) try { const userDetails = await db .select({ id: user.id, name: user.name, email: user.email, }) .from(user) .where(eq(user.stripeCustomerId, stripeCustomerId)) .limit(1) const orgDetails = await db .select({ id: organization.id, name: organization.name, }) .from(organization) .where(eq(organization.id, referenceId)) .limit(1) if (userDetails.length > 0 && orgDetails.length > 0) { const user = userDetails[0] const org = orgDetails[0] const html = await renderEnterpriseSubscriptionEmail(user.name || user.email) const emailResult = await sendEmail({ to: user.email, subject: getEmailSubject('enterprise-subscription'), html, from: getFromEmailAddress(), emailType: 'transactional', }) if (emailResult.success) { logger.info('[subscription.created] Enterprise subscription email sent successfully', { userId: user.id, email: user.email, organizationId: org.id, subscriptionId: subscriptionRow.id, }) } else { logger.warn('[subscription.created] Failed to send enterprise subscription email', { userId: user.id, email: user.email, error: emailResult.message, }) } } else { logger.warn( '[subscription.created] Could not find user or organization for email notification', { userFound: userDetails.length > 0, orgFound: orgDetails.length > 0, stripeCustomerId, referenceId, } ) } } catch (emailError) { logger.error('[subscription.created] Error sending enterprise subscription email', { error: emailError, stripeCustomerId, referenceId, subscriptionId: subscriptionRow.id, }) } }