diff --git a/server/private/routers/billing/hooks/handleSubscriptionCreated.ts b/server/private/routers/billing/hooks/handleSubscriptionCreated.ts index 4370d088..9f2ee2ad 100644 --- a/server/private/routers/billing/hooks/handleSubscriptionCreated.ts +++ b/server/private/routers/billing/hooks/handleSubscriptionCreated.ts @@ -28,6 +28,9 @@ 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 @@ -211,7 +214,7 @@ export async function handleSubscriptionCreated( ); const response = await fetch( - `${privateConfig.getRawPrivateConfig().server.fossorial_api}/api/v1/license-internal/enterprise/paid-for`, // this says enterprise but it does both + `${privateConfig.getRawPrivateConfig().server.fossorial_api}/api/v1/license-internal/enterprise/paid-for`, { method: "POST", headers: { @@ -231,7 +234,32 @@ export async function handleSubscriptionCreated( const data = await response.json(); - logger.debug("Fossorial API response:", { data }); + 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, + }), + { + 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); diff --git a/server/private/routers/billing/hooks/handleSubscriptionDeleted.ts b/server/private/routers/billing/hooks/handleSubscriptionDeleted.ts index 56fca02b..003110aa 100644 --- a/server/private/routers/billing/hooks/handleSubscriptionDeleted.ts +++ b/server/private/routers/billing/hooks/handleSubscriptionDeleted.ts @@ -26,6 +26,7 @@ import { handleSubscriptionLifesycle } from "../subscriptionLifecycle"; import { AudienceIds, moveEmailToAudience } from "#private/lib/resend"; import { getSubType } from "./getSubType"; import stripe from "#private/lib/stripe"; +import privateConfig from "#private/lib/config"; export async function handleSubscriptionDeleted( subscription: Stripe.Subscription @@ -76,9 +77,14 @@ export async function handleSubscriptionDeleted( const type = getSubType(fullSubscription); if (type === "saas") { - logger.debug(`Handling SaaS subscription deletion for orgId ${customer.orgId} and subscription ID ${subscription.id}`); + logger.debug( + `Handling SaaS subscription deletion for orgId ${customer.orgId} and subscription ID ${subscription.id}` + ); - await handleSubscriptionLifesycle(customer.orgId, subscription.status); + await handleSubscriptionLifesycle( + customer.orgId, + subscription.status + ); const [orgUserRes] = await db .select() @@ -99,8 +105,33 @@ export async function handleSubscriptionDeleted( } } } else if (type === "license") { - logger.debug(`Handling license subscription deletion for orgId ${customer.orgId} and subscription ID ${subscription.id}`); - + logger.debug( + `Handling license subscription deletion for orgId ${customer.orgId} and subscription ID ${subscription.id}` + ); + try { + // WARNING: + // this invalidates ALL OF THE ENTERPRISE LICENSES for this orgId + await fetch( + `${privateConfig.getRawPrivateConfig().server.fossorial_api}/api/v1/license-internal/enterprise/invalidate`, + { + method: "POST", + headers: { + "api-key": + privateConfig.getRawPrivateConfig().server + .fossorial_api_key!, + "Content-Type": "application/json" + }, + body: JSON.stringify({ + orgId: customer.orgId, + }) + } + ); + } catch (error) { + logger.error( + `Error notifying Fossorial API of license subscription deletion for orgId ${customer.orgId} and subscription ID ${subscription.id}:`, + error + ); + } } } catch (error) { logger.error( diff --git a/server/private/routers/billing/hooks/handleSubscriptionUpdated.ts b/server/private/routers/billing/hooks/handleSubscriptionUpdated.ts index 8e6f901e..21943354 100644 --- a/server/private/routers/billing/hooks/handleSubscriptionUpdated.ts +++ b/server/private/routers/billing/hooks/handleSubscriptionUpdated.ts @@ -27,6 +27,7 @@ import { getFeatureIdByMetricId } from "@server/lib/billing/features"; import stripe from "#private/lib/stripe"; import { handleSubscriptionLifesycle } from "../subscriptionLifecycle"; import { getSubType } from "./getSubType"; +import privateConfig from "#private/lib/config"; export async function handleSubscriptionUpdated( subscription: Stripe.Subscription, @@ -57,7 +58,7 @@ export async function handleSubscriptionUpdated( } // get the customer - const [existingCustomer] = await db + const [customer] = await db .select() .from(customers) .where(eq(customers.customerId, subscription.customer as string)) @@ -150,7 +151,7 @@ export async function handleSubscriptionUpdated( continue; } - const orgId = existingCustomer.orgId; + const orgId = customer.orgId; if (!orgId) { logger.warn( @@ -235,16 +236,41 @@ export async function handleSubscriptionUpdated( const type = getSubType(fullSubscription); if (type === "saas") { - logger.debug(`Handling SAAS subscription lifecycle for org ${existingCustomer.orgId}`); + 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( - existingCustomer.orgId, + customer.orgId, subscription.status ); } else { - logger.debug( - `Subscription ${subscription.id} is of type ${type}. No lifecycle handling needed.` - ); + if (subscription.status === "canceled" || subscription.status == "unpaid" || subscription.status == "incomplete_expired") { + try { + // WARNING: + // this invalidates ALL OF THE ENTERPRISE LICENSES for this orgId + await fetch( + `${privateConfig.getRawPrivateConfig().server.fossorial_api}/api/v1/license-internal/enterprise/invalidate`, + { + method: "POST", + headers: { + "api-key": + privateConfig.getRawPrivateConfig() + .server.fossorial_api_key!, + "Content-Type": "application/json" + }, + body: JSON.stringify({ + orgId: customer.orgId + }) + } + ); + } catch (error) { + logger.error( + `Error notifying Fossorial API of license subscription deletion for orgId ${customer.orgId} and subscription ID ${subscription.id}:`, + error + ); + } + } } } } catch (error) { diff --git a/src/components/NewPricingLicenseForm.tsx b/src/components/NewPricingLicenseForm.tsx index cc2c9286..d4f76bb9 100644 --- a/src/components/NewPricingLicenseForm.tsx +++ b/src/components/NewPricingLicenseForm.tsx @@ -178,20 +178,42 @@ export default function NewPricingLicenseForm({ ): Promise => { setLoading(true); try { - const response = await api.put< - AxiosResponse - >(`/org/${orgId}/license`, payload); + // Check if this is a business/enterprise license request + if (!personalUseOnly) { + const response = await api.put>( + `/org/${orgId}/license/enterprise`, + { ...payload, tier: TIER_TO_LICENSE_ID[selectedTier] } + ); - if (response.data.data?.licenseKey?.licenseKey) { - setGeneratedKey(response.data.data.licenseKey.licenseKey); - onGenerated?.(); - toast({ - title: t("generateLicenseKeyForm.toasts.success.title"), - description: t( - "generateLicenseKeyForm.toasts.success.description" - ), - variant: "default" - }); + console.log("Checkout session response:", response.data); + const checkoutUrl = response.data.data; + if (checkoutUrl) { + window.location.href = checkoutUrl; + } else { + toast({ + title: "Failed to get checkout URL", + description: "Please try again later", + variant: "destructive" + }); + setLoading(false); + } + } else { + // Personal license flow + const response = await api.put< + AxiosResponse + >(`/org/${orgId}/license`, payload); + + if (response.data.data?.licenseKey?.licenseKey) { + setGeneratedKey(response.data.data.licenseKey.licenseKey); + onGenerated?.(); + toast({ + title: t("generateLicenseKeyForm.toasts.success.title"), + description: t( + "generateLicenseKeyForm.toasts.success.description" + ), + variant: "default" + }); + } } } catch (e) { console.error(e); @@ -229,44 +251,38 @@ export default function NewPricingLicenseForm({ }); }; - const handleContinueToCheckout = async () => { - const valid = await businessForm.trigger(); - if (!valid) return; - - const values = businessForm.getValues(); - setLoading(true); - try { - const tier = TIER_TO_LICENSE_ID[selectedTier]; - const response = await api.post>( - `/org/${orgId}/billing/create-checkout-session-license`, - { tier } - ); - const checkoutUrl = response.data.data; - if (checkoutUrl) { - window.location.href = checkoutUrl; - } else { - toast({ - title: t( - "newPricingLicenseForm.toasts.checkoutError.title" - ), - description: t( - "newPricingLicenseForm.toasts.checkoutError.description" - ), - variant: "destructive" - }); - setLoading(false); + const onSubmitBusiness = async (values: BusinessFormData) => { + const payload = { + email: values.email, + useCaseType: "business", + personal: undefined, + business: { + firstName: values.firstName, + lastName: values.lastName, + jobTitle: "N/A", + aboutYou: { + primaryUse: values.primaryUse, + industry: values.industry, + prospectiveUsers: 100, + prospectiveSites: 100 + }, + companyInfo: { + companyName: values.companyName, + countryOfResidence: "N/A", + stateProvinceRegion: "N/A", + postalZipCode: "N/A", + companyWebsite: values.companyWebsite || "", + companyPhoneNumber: values.companyPhoneNumber || "" + } + }, + consent: { + agreedToTerms: values.agreedToTerms, + acknowledgedPrivacyPolicy: values.agreedToTerms, + complianceConfirmed: values.complianceConfirmed } - } catch (error) { - toast({ - title: t("newPricingLicenseForm.toasts.checkoutError.title"), - description: formatAxiosError( - error, - t("newPricingLicenseForm.toasts.checkoutError.description") - ), - variant: "destructive" - }); - setLoading(false); - } + }; + + await submitLicenseRequest(payload); }; const handleClose = () => { @@ -608,6 +624,9 @@ export default function NewPricingLicenseForm({ {!personalUseOnly && (
@@ -877,7 +896,8 @@ export default function NewPricingLicenseForm({ {!generatedKey && !personalUseOnly && (