feat(cloud): replace novu for all notification triggers

This commit is contained in:
Henry
2026-08-08 13:37:51 +01:00
parent b4e9dd3811
commit e9ebd06eaa
16 changed files with 139 additions and 455 deletions

View File

@@ -1,4 +1,9 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { env } from "next-runtime-env";
import { beforeEach, describe, expect, it, vi } from "vitest";
import * as memberRepo from "@kan/db/repository/member.repo";
import { createDatabaseHooks } from "./hooks";
vi.mock("next-runtime-env", () => ({
env: vi.fn(),
@@ -15,11 +20,11 @@ vi.mock("@kan/db/repository/user.repo", () => ({
}));
vi.mock("@kan/email", () => ({
notificationClient: null,
createSubscriber: vi.fn(),
triggerSubscriberWorkflow: vi.fn(),
}));
vi.mock("@kan/shared", () => ({
createEmailUnsubscribeLink: vi.fn(),
createS3Client: vi.fn(),
}));
@@ -27,17 +32,10 @@ vi.mock("@aws-sdk/client-s3", () => ({
PutObjectCommand: vi.fn(),
}));
vi.mock("@novu/api/models/components", () => ({
ChatOrPushProviderEnum: { Discord: "discord" },
}));
import { env } from "next-runtime-env";
import * as memberRepo from "@kan/db/repository/member.repo";
import { createDatabaseHooks } from "./hooks";
const mockEnv = env as ReturnType<typeof vi.fn>;
const mockGetByEmailAndStatus =
memberRepo.getByEmailAndStatus as ReturnType<typeof vi.fn>;
const mockGetByEmailAndStatus = memberRepo.getByEmailAndStatus as ReturnType<
typeof vi.fn
>;
const db = {} as Parameters<typeof createDatabaseHooks>[0];

View File

@@ -1,14 +1,13 @@
import { PutObjectCommand } from "@aws-sdk/client-s3";
import { ChatOrPushProviderEnum } from "@novu/api/models/components";
import { createAuthMiddleware } from "better-auth/api";
import { env } from "next-runtime-env";
import type { dbClient } from "@kan/db/client";
import * as memberRepo from "@kan/db/repository/member.repo";
import * as userRepo from "@kan/db/repository/user.repo";
import { createSubscriber, notificationClient } from "@kan/email";
import { createSubscriber, triggerSubscriberWorkflow } from "@kan/email";
import { createLogger } from "@kan/logger";
import { createEmailUnsubscribeLink, createS3Client } from "@kan/shared";
import { createS3Client } from "@kan/shared";
import { downloadImage } from "./utils";
@@ -94,80 +93,49 @@ export function createDatabaseHooks(db: dbClient) {
}
}
if (notificationClient) {
const [firstName, ...rest] = (user.name || "")
.split(" ")
.filter(Boolean);
const lastName = rest.length ? rest.join(" ") : undefined;
const [firstName, ...rest] = (user.name || "")
.split(" ")
.filter(Boolean);
const lastName = rest.length ? rest.join(" ") : undefined;
try {
const avatarUrl = avatarKey
? `${env("NEXT_PUBLIC_STORAGE_URL")}/${env("NEXT_PUBLIC_AVATAR_BUCKET_NAME")}/${avatarKey}`
: undefined;
try {
const avatarUrl = avatarKey
? `${env("NEXT_PUBLIC_STORAGE_URL")}/${env("NEXT_PUBLIC_AVATAR_BUCKET_NAME")}/${avatarKey}`
: undefined;
const unsubscribeUrl = await createEmailUnsubscribeLink(user.id);
await createSubscriber({
publicId: user.id,
email: user.email,
externalId: user.id,
firstName,
lastName,
name: user.name,
attributes: {
avatarUrl,
emailVerified: user.emailVerified,
stripeCustomerId: user.stripeCustomerId,
createdAt: user.createdAt,
updatedAt: user.updatedAt,
},
});
} catch (error) {
log.error({ err: error }, "Error creating subscriber");
}
log.info(
{
workflowId: "user-signup",
userId: user.id,
email: user.email,
},
"Triggering Novu workflow",
);
await notificationClient.trigger({
to: {
subscriberId: user.id,
firstName: firstName,
lastName: lastName,
email: user.email,
avatar: avatarUrl,
data: {
emailVerified: user.emailVerified,
stripeCustomerId: user.stripeCustomerId,
createdAt: user.createdAt,
updatedAt: user.updatedAt,
},
},
payload: {
emailUnsubscribeUrl: unsubscribeUrl,
},
workflowId: "user-signup",
});
log.info(
{ workflowId: "user-signup", userId: user.id },
"Novu workflow triggered",
);
await notificationClient.subscribers.credentials.update(
{
providerId: ChatOrPushProviderEnum.Discord,
credentials: {
webhookUrl: env("DISCORD_WEBHOOK_URL"),
},
integrationIdentifier: "discord",
},
user.id,
);
} catch (error) {
log.error(
{ err: error },
"Error adding user to notification client",
);
}
try {
await createSubscriber({
publicId: user.id,
email: user.email,
externalId: user.id,
firstName,
lastName,
name: user.name,
});
} catch (error) {
log.error({ err: error }, "Error creating subscriber");
}
try {
log.info(
{ workflowId: "user-signup", userId: user.id, email: user.email },
"Triggering user-signup workflow",
);
await triggerSubscriberWorkflow("user-signup", {
publicId: user.id,
});
log.info(
{ workflowId: "user-signup", userId: user.id },
"user-signup workflow triggered",
);
} catch (error) {
log.error({ err: error }, "Error triggering user-signup workflow");
}
},
},

View File

@@ -3,9 +3,8 @@ import type Stripe from "stripe";
import type { dbClient } from "@kan/db/client";
import * as userRepo from "@kan/db/repository/user.repo";
import { notificationClient } from "@kan/email";
import { triggerSubscriberWorkflow } from "@kan/email";
import { createLogger } from "@kan/logger";
import { createEmailUnsubscribeLink } from "@kan/shared";
const log = createLogger("auth");
@@ -24,30 +23,22 @@ export async function triggerWorkflow(
cancellationDetails?: Stripe.Subscription.CancellationDetails | null,
) {
try {
if (!subscription.stripeCustomerId || !notificationClient) return;
if (!subscription.stripeCustomerId) return;
const user = await userRepo.getByStripeCustomerId(
db,
subscription.stripeCustomerId,
);
if (!user || !notificationClient) return;
if (!user) return;
const unsubscribeUrl = await createEmailUnsubscribeLink(user.id);
log.info({ workflowId, userId: user.id }, "Triggering Novu workflow");
await notificationClient.trigger({
to: {
subscriberId: user.id,
},
payload: {
...subscription,
cancellationDetails,
emailUnsubscribeUrl: unsubscribeUrl,
},
log.info({ workflowId, userId: user.id }, "Triggering workflow");
await triggerSubscriberWorkflow(
workflowId,
});
log.info({ workflowId, userId: user.id }, "Novu workflow triggered");
{ publicId: user.id },
{ ...subscription, cancellationDetails },
);
log.info({ workflowId, userId: user.id }, "Workflow triggered");
} catch (error) {
log.error({ err: error, workflowId }, "Error triggering workflow");
}