Skip to content

Commit

Permalink
Proactively cancel inactive trials 3 days before end (#4553)
Browse files Browse the repository at this point in the history
* Proactively cancel inactive trials 3 days before end

* ✂️

* ✨

* Send email when canceling trial proactively

* ✂️

* Log in Slack
  • Loading branch information
flvndvd authored Apr 4, 2024
1 parent 43e726f commit b5a5d13
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 2 deletions.
26 changes: 26 additions & 0 deletions front/lib/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,3 +189,29 @@ export async function sendAdminDataDeletionEmail({
};
return sendEmail(email, msg);
}

export async function sendProactiveTrialCancelledEmail(
email: string
): Promise<void> {
const message = {
from: {
name: "Gabriel Hubert",
email: "[email protected]",
},
subject: "[Dust] Your Pro plan trial has been cancelled early",
html: `<p>Hi,</p>
<p>I'm Gabriel, a cofounder of Dust. Thanks for trying us out with a free trial of the Pro Plan.</p>
<p>You've not used core features of the product (adding data sources, creating custom assistants) and you haven't used assistant conversations in the past 7 days.
As a result, to avoid keeping your payment method on file while you may not intend to convert to our paid plan, we've cancelled your trial ahead of time and won't be charging you.</p>
<p>If you did intend to continue on Dust, you can subscribe again. If you'd like to extend further, feel free to just email me.</p>
<p>Thanks again for trying Dust out. If you have a second, please let me know if you have any thoughts about what we could do to improve Dust for your needs!</p>
Gabriel`,
};

return sendEmail(email, message);
}
56 changes: 55 additions & 1 deletion front/lib/plans/subscription.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import type { WorkspaceType } from "@dust-tt/types";
import type { PlanType, SubscriptionType } from "@dust-tt/types";
import type { WorkspaceType } from "@dust-tt/types";
import { sendUserOperationMessage } from "@dust-tt/types";
import type Stripe from "stripe";

import type { Authenticator } from "@app/lib/auth";
import { sendProactiveTrialCancelledEmail } from "@app/lib/email";
import { Plan, Subscription, Workspace } from "@app/lib/models";
import type { PlanAttributes } from "@app/lib/plans/free_plans";
import { FREE_NO_PLAN_DATA } from "@app/lib/plans/free_plans";
Expand All @@ -20,6 +23,8 @@ import {
import { redisClient } from "@app/lib/redis";
import { frontSequelize } from "@app/lib/resources/storage";
import { generateModelSId } from "@app/lib/utils";
import { getWorkspaceFirstAdmin } from "@app/lib/workspace";
import { checkWorkspaceActivity } from "@app/lib/workspace_usage";
import logger from "@app/logger/logger";

// Helper function to render PlanType from PlanAttributes
Expand Down Expand Up @@ -463,3 +468,52 @@ export const updateWorkspacePerMonthlyActiveUsersSubscriptionUsage = async ({
}
}
};

/**
* Proactively cancel inactive trials.
*/
export async function maybeCancelInactiveTrials(
stripeSubscription: Stripe.Subscription
) {
const { id: stripeSubscriptionId } = stripeSubscription;

const subscription = await Subscription.findOne({
where: { stripeSubscriptionId },
include: [Workspace],
});

if (!subscription || !subscription.trialing) {
return;
}

const { workspace } = subscription;
const isWorkspaceActive = await checkWorkspaceActivity(workspace);

if (!isWorkspaceActive) {
logger.info(
{ action: "cancelling-trial", workspaceId: workspace.sId },
"Cancelling inactive trial."
);

await cancelSubscriptionImmediately({
stripeSubscriptionId,
});

const firstAdmin = await getWorkspaceFirstAdmin(workspace);
if (!firstAdmin) {
logger.info(
{ action: "cancelling-trial", workspaceId: workspace.sId },
"No first adming found -- skipping email."
);

return;
} else {
await sendProactiveTrialCancelledEmail(firstAdmin.email);
}

await sendUserOperationMessage({
logger,
message: `Trial for workspace ${workspace.sId} cancelled proactively!`,
});
}
}
19 changes: 19 additions & 0 deletions front/lib/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import type {
} from "@dust-tt/types";

import type { Workspace } from "@app/lib/models";
import { User } from "@app/lib/models";
import { MembershipModel } from "@app/lib/resources/storage/models/membership";

export function renderLightWorkspaceType({
workspace,
Expand All @@ -21,3 +23,20 @@ export function renderLightWorkspaceType({
role,
};
}

// TODO: This belong to the WorkspaceResource.
export async function getWorkspaceFirstAdmin(workspace: Workspace) {
return User.findOne({
include: [
{
model: MembershipModel,
where: {
role: "admin",
workspaceId: workspace.id,
},
required: true,
},
],
order: [["createdAt", "ASC"]],
});
}
30 changes: 29 additions & 1 deletion front/lib/workspace_usage.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { format } from "date-fns/format";
import { QueryTypes } from "sequelize";
import { Op, QueryTypes } from "sequelize";

import type { Workspace } from "@app/lib/models";
import { AgentConfiguration, Conversation, DataSource } from "@app/lib/models";

import { frontSequelize } from "./resources/storage";

Expand Down Expand Up @@ -94,3 +97,28 @@ export async function unsafeGetUsageData(

return csvHeader + csvContent;
}

/**
* Check if a workspace is active during a trial based on the following conditions:
* - Existence of a connected data source
* - Existence of a custom assistant
* - A conversation occurred within the past 7 days
*/
export async function checkWorkspaceActivity(workspace: Workspace) {
const sevenDaysAgo = new Date();
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);

const hasDataSource = await DataSource.findOne({
where: { workspaceId: workspace.id },
});

const hasCreatedAssistant = await AgentConfiguration.findOne({
where: { workspaceId: workspace.id },
});

const hasRecentConversation = await Conversation.findOne({
where: { workspaceId: workspace.id, updatedAt: { [Op.gte]: sevenDaysAgo } },
});

return hasDataSource || hasCreatedAssistant || hasRecentConversation;
}
9 changes: 9 additions & 0 deletions front/pages/api/stripe/webhook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from "@app/lib/email";
import { Plan, Subscription, Workspace } from "@app/lib/models";
import { createCustomerPortalSession } from "@app/lib/plans/stripe";
import { maybeCancelInactiveTrials } from "@app/lib/plans/subscription";
import { countActiveSeatsInWorkspace } from "@app/lib/plans/workspace_usage";
import { frontSequelize } from "@app/lib/resources/storage";
import { generateModelSId } from "@app/lib/utils";
Expand Down Expand Up @@ -562,6 +563,14 @@ async function handler(
}

break;

case "customer.subscription.trial_will_end":
stripeSubscription = event.data.object as Stripe.Subscription;

await maybeCancelInactiveTrials(stripeSubscription);

break;

default:
// Unhandled event type
}
Expand Down

0 comments on commit b5a5d13

Please sign in to comment.