Compare commits

..

4 Commits

Author SHA1 Message Date
Henry
eafe4d0cd8 fix: remove unused params from invite paths 2025-11-26 20:09:35 +00:00
Henry
50382e9186 feat(cloud): free trial notifications (#254) 2025-11-25 23:29:04 +00:00
Henry
00be5fb058 feat(cloud): add discord creds to subscribers 2025-11-25 13:22:43 +00:00
Henry
fc65f53e0c feat(cloud): add notification client (#253) 2025-11-24 22:20:34 +00:00
6 changed files with 85 additions and 22 deletions

View File

@@ -35,7 +35,10 @@ services:
- SMTP_PASSWORD=${SMTP_PASSWORD}
- SMTP_SECURE=${SMTP_SECURE}
- EMAIL_FROM=${EMAIL_FROM}
# Notifications
- NOVU_API_KEY=${NOVU_API_KEY}
- DISCORD_WEBHOOK_URL=${DISCORD_WEBHOOK_URL}
# S3 storage
- S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID}

View File

@@ -476,7 +476,7 @@ export const memberRouter = createTRPCRouter({
openapi: {
summary: "Get invite information by code",
method: "GET",
path: "/workspaces/{workspacePublicId}/invites/{inviteCode}",
path: "/invites/{inviteCode}",
description: "Get invite information by invite code",
tags: ["Invites"],
protect: false,
@@ -521,7 +521,7 @@ export const memberRouter = createTRPCRouter({
openapi: {
summary: "Accept an invite link",
method: "POST",
path: "/workspaces/{workspacePublicId}/invites/accept",
path: "/invites/accept",
description: "Accepts an invitation via invite link",
tags: ["Invites"],
protect: false,

View File

@@ -1,5 +1,8 @@
import type { Subscription } from "@better-auth/stripe";
import type Stripe from "stripe";
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { stripe } from "@better-auth/stripe";
import { ChatOrPushProviderEnum } from "@novu/api/models/components";
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { createAuthEndpoint, createAuthMiddleware } from "better-auth/api";
@@ -178,19 +181,17 @@ export const initAuth = (db: dbClient) => {
freeTrial: {
days: 14,
onTrialStart: async (subscription) => {
// Called when a trial starts
// @todo: send trial start email
// await sendTrialStartEmail(subscription.referenceId);
await triggerWorkflow(db, "trial-start", subscription);
},
onTrialEnd: async ({ subscription }, request) => {
// Called when a trial ends
// @todo: send trial end email
// await sendTrialEndEmail(user.email);
onTrialEnd: async ({ subscription }) => {
await triggerWorkflow(db, "trial-end", subscription);
},
onTrialExpired: async (subscription) => {
// Called when a trial expires without conversion
// @todo: send trial expired email
// await sendTrialExpiredEmail(subscription.referenceId);
await triggerWorkflow(
db,
"trial-expired",
subscription,
);
},
},
},
@@ -202,19 +203,17 @@ export const initAuth = (db: dbClient) => {
freeTrial: {
days: 14,
onTrialStart: async (subscription) => {
// Called when a trial starts
// @todo: send trial start email
// await sendTrialStartEmail(subscription.referenceId);
await triggerWorkflow(db, "trial-start", subscription);
},
onTrialEnd: async ({ subscription }, request) => {
// Called when a trial ends
// @todo: send trial end email
// await sendTrialEndEmail(user.email);
onTrialEnd: async ({ subscription }) => {
await triggerWorkflow(db, "trial-end", subscription);
},
onTrialExpired: async (subscription) => {
// Called when a trial expires without conversion
// @todo: send trial expired email
// await sendTrialExpiredEmail(subscription.referenceId);
await triggerWorkflow(
db,
"trial-expired",
subscription,
);
},
},
},
@@ -418,6 +417,17 @@ export const initAuth = (db: dbClient) => {
},
workflowId: "user-signup",
});
await notificationClient.subscribers.credentials.update(
{
providerId: ChatOrPushProviderEnum.Discord,
credentials: {
webhookUrl: process.env.DISCORD_WEBHOOK_URL!,
},
integrationIdentifier: "discord",
},
user.id,
);
} catch (error) {
console.error(
"Error adding user to notification client",
@@ -462,3 +472,34 @@ export const initAuth = (db: dbClient) => {
},
});
};
async function triggerWorkflow(
db: dbClient,
workflowId: string,
subscription: Subscription,
cancellationDetails?: Stripe.Subscription.CancellationDetails | null,
) {
try {
if (!subscription.stripeCustomerId || !notificationClient) return;
const user = await userRepo.getByStripeCustomerId(
db,
subscription.stripeCustomerId,
);
if (!user || !notificationClient) return;
await notificationClient.trigger({
to: {
subscriberId: user.id,
},
payload: {
...subscription,
cancellationDetails,
},
workflowId,
});
} catch (error) {
console.error("Error triggering workflow", error);
}
}

View File

@@ -28,6 +28,15 @@ export const getById = async (db: dbClient, userId: string) => {
});
};
export const getByStripeCustomerId = async (
db: dbClient,
stripeCustomerId: string,
) => {
return await db.query.users.findFirst({
where: eq(users.stripeCustomerId, stripeCustomerId),
});
};
export const getByEmail = (db: dbClient, email: string) => {
return db.query.users.findFirst({
columns: {

View File

@@ -71,4 +71,13 @@ export const updateSubscriptionSeats = async (
return updatedSubscription;
};
export const getCancellationDetails = async (
stripeSubscriptionId: string,
): Promise<Stripe.Subscription.CancellationDetails | null> => {
const stripe = createStripeClient();
const stripeSubscription =
await stripe.subscriptions.retrieve(stripeSubscriptionId);
return stripeSubscription.cancellation_details ?? null;
};
export { createStripeClient };

View File

@@ -99,6 +99,7 @@
"NEXT_PUBLIC_KAN_ENV",
"NEXT_PUBLIC_WHITE_LABEL_HIDE_POWERED_BY",
"STRIPE_SECRET_KEY",
"DISCORD_WEBHOOK_URL",
"STRIPE_WEBHOOK_SECRET",
"STRIPE_WEBHOOK_SECRET_LEGACY",
"STRIPE_PRO_PLAN_MONTHLY_PRICE_ID",