From 28514487e08462c8f2bd1234d7151a2a97431ffd Mon Sep 17 00:00:00 2001 From: Piyush Gupta <56182734+gupta-piyush19@users.noreply.github.com> Date: Thu, 24 Jul 2025 17:31:39 +0530 Subject: [PATCH] chore: sunset weekly summary (#6282) --- .../environments/[environmentId]/actions.ts | 4 - .../components/EnvironmentLayout.test.tsx | 2 +- .../components/MainNavigation.test.tsx | 2 +- .../components/EditAlerts.test.tsx | 1 - .../components/EditWeeklySummary.test.tsx | 166 ----- .../components/EditWeeklySummary.tsx | 59 -- .../components/NotificationSwitch.test.tsx | 39 -- .../components/NotificationSwitch.tsx | 2 +- .../(account)/notifications/loading.test.tsx | 12 - .../(account)/notifications/loading.tsx | 5 - .../(account)/notifications/page.test.tsx | 32 +- .../settings/(account)/notifications/page.tsx | 10 - .../components/AccountSecurity.test.tsx | 2 +- .../profile/components/DeleteAccount.test.tsx | 2 +- .../EditProfileDetailsForm.test.tsx | 2 +- .../settings/(account)/profile/page.test.tsx | 2 +- .../(organization)/enterprise/page.test.tsx | 2 +- .../components/ResponseCardModal.test.tsx | 2 +- .../components/SurveyAnalysisCTA.test.tsx | 4 - .../components/share-survey-modal.test.tsx | 1 - .../lib/notificationResponse.test.ts | 276 --------- .../lib/notificationResponse.ts | 78 --- .../weekly-summary/lib/organization.test.ts | 48 -- .../cron/weekly-summary/lib/organization.ts | 10 - .../cron/weekly-summary/lib/project.test.ts | 570 ------------------ .../api/cron/weekly-summary/lib/project.ts | 111 ---- apps/web/app/api/cron/weekly-summary/route.ts | 70 --- apps/web/app/page.test.tsx | 4 - apps/web/lib/organization/service.test.ts | 4 +- apps/web/lib/organization/service.ts | 2 +- apps/web/lib/survey/__mock__/survey.mock.ts | 2 +- apps/web/lib/user/service.test.ts | 2 +- apps/web/modules/auth/invite/page.tsx | 1 - apps/web/modules/auth/lib/mock-data.ts | 2 +- apps/web/modules/auth/signup/actions.ts | 4 +- .../modules/ee/audit-logs/lib/utils.test.ts | 2 - .../components/response-timeline.test.tsx | 1 - apps/web/modules/ee/sso/lib/sso-handlers.ts | 3 - .../lib/tests/__mock__/sso-handlers.mock.ts | 2 +- .../create-reminder-notification-body.tsx | 41 -- .../live-survey-notification.tsx | 123 ---- .../no-live-survey-notification-email.tsx | 33 - .../weekly-summary/notification-footer.tsx | 34 -- .../weekly-summary/notification-header.tsx | 51 -- .../weekly-summary/notification-insight.tsx | 46 -- .../weekly-summary-notification-email.tsx | 44 -- apps/web/modules/email/index.tsx | 70 --- apps/web/modules/organization/actions.ts | 7 +- .../removed-from-organization.test.tsx | 1 - .../setup/organization/create/page.test.tsx | 10 - docker/cronjobs | 2 +- .../values-staging.yaml.gotmpl | 24 +- .../formbricks-cloud-helm/values.yaml.gotmpl | 24 +- .../migration.ts | 15 + packages/types/user.ts | 1 - packages/types/weekly-summary.ts | 104 ---- 56 files changed, 38 insertions(+), 2135 deletions(-) delete mode 100644 apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/EditWeeklySummary.test.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/EditWeeklySummary.tsx delete mode 100644 apps/web/app/api/cron/weekly-summary/lib/notificationResponse.test.ts delete mode 100644 apps/web/app/api/cron/weekly-summary/lib/notificationResponse.ts delete mode 100644 apps/web/app/api/cron/weekly-summary/lib/organization.test.ts delete mode 100644 apps/web/app/api/cron/weekly-summary/lib/organization.ts delete mode 100644 apps/web/app/api/cron/weekly-summary/lib/project.test.ts delete mode 100644 apps/web/app/api/cron/weekly-summary/lib/project.ts delete mode 100644 apps/web/app/api/cron/weekly-summary/route.ts delete mode 100644 apps/web/modules/email/emails/weekly-summary/create-reminder-notification-body.tsx delete mode 100644 apps/web/modules/email/emails/weekly-summary/live-survey-notification.tsx delete mode 100644 apps/web/modules/email/emails/weekly-summary/no-live-survey-notification-email.tsx delete mode 100644 apps/web/modules/email/emails/weekly-summary/notification-footer.tsx delete mode 100644 apps/web/modules/email/emails/weekly-summary/notification-header.tsx delete mode 100644 apps/web/modules/email/emails/weekly-summary/notification-insight.tsx delete mode 100644 apps/web/modules/email/emails/weekly-summary/weekly-summary-notification-email.tsx create mode 100644 packages/database/migration/20250722075349_sunset_weekly_summary/migration.ts delete mode 100644 packages/types/weekly-summary.ts diff --git a/apps/web/app/(app)/environments/[environmentId]/actions.ts b/apps/web/app/(app)/environments/[environmentId]/actions.ts index ce866a868b..a533636e7f 100644 --- a/apps/web/app/(app)/environments/[environmentId]/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/actions.ts @@ -71,10 +71,6 @@ export const createProjectAction = authenticatedActionClient.schema(ZCreateProje alert: { ...user.notificationSettings?.alert, }, - weeklySummary: { - ...user.notificationSettings?.weeklySummary, - [project.id]: true, - }, }; await updateUser(user.id, { diff --git a/apps/web/app/(app)/environments/[environmentId]/components/EnvironmentLayout.test.tsx b/apps/web/app/(app)/environments/[environmentId]/components/EnvironmentLayout.test.tsx index 012eeb650f..959e7fcff7 100644 --- a/apps/web/app/(app)/environments/[environmentId]/components/EnvironmentLayout.test.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/components/EnvironmentLayout.test.tsx @@ -104,7 +104,7 @@ const mockUser = { identityProvider: "email", createdAt: new Date(), updatedAt: new Date(), - notificationSettings: { alert: {}, weeklySummary: {} }, + notificationSettings: { alert: {} }, } as unknown as TUser; const mockOrganization = { diff --git a/apps/web/app/(app)/environments/[environmentId]/components/MainNavigation.test.tsx b/apps/web/app/(app)/environments/[environmentId]/components/MainNavigation.test.tsx index 56df681c75..916bd50a49 100644 --- a/apps/web/app/(app)/environments/[environmentId]/components/MainNavigation.test.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/components/MainNavigation.test.tsx @@ -106,7 +106,7 @@ const mockUser = { identityProvider: "email", createdAt: new Date(), updatedAt: new Date(), - notificationSettings: { alert: {}, weeklySummary: {} }, + notificationSettings: { alert: {} }, role: "project_manager", objective: "other", } as unknown as TUser; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/EditAlerts.test.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/EditAlerts.test.tsx index d1804af298..3f9b56d579 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/EditAlerts.test.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/EditAlerts.test.tsx @@ -49,7 +49,6 @@ const mockUser = { email: "test@example.com", notificationSettings: { alert: {}, - weeklySummary: {}, unsubscribedOrganizationIds: [], }, role: "project_manager", diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/EditWeeklySummary.test.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/EditWeeklySummary.test.tsx deleted file mode 100644 index b02933b958..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/EditWeeklySummary.test.tsx +++ /dev/null @@ -1,166 +0,0 @@ -import { cleanup, render, screen } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TUser } from "@formbricks/types/user"; -import { Membership } from "../types"; -import { EditWeeklySummary } from "./EditWeeklySummary"; - -vi.mock("lucide-react", () => ({ - UsersIcon: () =>
, -})); - -vi.mock("next/link", () => ({ - default: ({ children, href }: { children: React.ReactNode; href: string }) => ( - - {children} - - ), -})); - -const mockNotificationSwitch = vi.fn(); -vi.mock("./NotificationSwitch", () => ({ - NotificationSwitch: (props: any) => { - mockNotificationSwitch(props); - return ( -
- NotificationSwitch -
- ); - }, -})); - -const mockT = vi.fn((key) => key); -vi.mock("@tolgee/react", () => ({ - useTranslate: () => ({ - t: mockT, - }), -})); - -const mockUser = { - id: "user1", - name: "Test User", - email: "test@example.com", - notificationSettings: { - alert: {}, - weeklySummary: { - proj1: true, - proj3: false, - }, - unsubscribedOrganizationIds: [], - }, - role: "project_manager", - objective: "other", - emailVerified: new Date(), - createdAt: new Date(), - updatedAt: new Date(), - identityProvider: "email", - twoFactorEnabled: false, -} as unknown as TUser; - -const mockMemberships: Membership[] = [ - { - organization: { - id: "org1", - name: "Organization 1", - projects: [ - { id: "proj1", name: "Project 1", environments: [] }, - { id: "proj2", name: "Project 2", environments: [] }, - ], - }, - }, - { - organization: { - id: "org2", - name: "Organization 2", - projects: [{ id: "proj3", name: "Project 3", environments: [] }], - }, - }, -]; - -const environmentId = "test-env-id"; - -describe("EditWeeklySummary", () => { - afterEach(() => { - cleanup(); - vi.clearAllMocks(); - }); - - test("renders correctly with multiple memberships and projects", () => { - render(); - - expect(screen.getByText("Organization 1")).toBeInTheDocument(); - expect(screen.getByText("Project 1")).toBeInTheDocument(); - expect(screen.getByText("Project 2")).toBeInTheDocument(); - expect(screen.getByText("Organization 2")).toBeInTheDocument(); - expect(screen.getByText("Project 3")).toBeInTheDocument(); - - expect(mockNotificationSwitch).toHaveBeenCalledWith( - expect.objectContaining({ - surveyOrProjectOrOrganizationId: "proj1", - notificationSettings: mockUser.notificationSettings, - notificationType: "weeklySummary", - }) - ); - expect(screen.getByTestId("notification-switch-proj1")).toBeInTheDocument(); - - expect(mockNotificationSwitch).toHaveBeenCalledWith( - expect.objectContaining({ - surveyOrProjectOrOrganizationId: "proj2", - notificationSettings: mockUser.notificationSettings, - notificationType: "weeklySummary", - }) - ); - expect(screen.getByTestId("notification-switch-proj2")).toBeInTheDocument(); - - expect(mockNotificationSwitch).toHaveBeenCalledWith( - expect.objectContaining({ - surveyOrProjectOrOrganizationId: "proj3", - notificationSettings: mockUser.notificationSettings, - notificationType: "weeklySummary", - }) - ); - expect(screen.getByTestId("notification-switch-proj3")).toBeInTheDocument(); - - const inviteLinks = screen.getAllByTestId("link"); - expect(inviteLinks.length).toBe(mockMemberships.length); - inviteLinks.forEach((link) => { - expect(link).toHaveAttribute("href", `/environments/${environmentId}/settings/general`); - expect(link).toHaveTextContent("common.invite_them"); - }); - - expect(screen.getAllByTestId("users-icon").length).toBe(mockMemberships.length); - - expect(screen.getAllByText("common.project")[0]).toBeInTheDocument(); - expect(screen.getAllByText("common.weekly_summary")[0]).toBeInTheDocument(); - expect( - screen.getAllByText("environments.settings.notifications.want_to_loop_in_organization_mates?").length - ).toBe(mockMemberships.length); - }); - - test("renders correctly with no memberships", () => { - render(); - expect(screen.queryByText("Organization 1")).not.toBeInTheDocument(); - expect(screen.queryByTestId("users-icon")).not.toBeInTheDocument(); - }); - - test("renders correctly when an organization has no projects", () => { - const membershipsWithNoProjects: Membership[] = [ - { - organization: { - id: "org3", - name: "Organization No Projects", - projects: [], - }, - }, - ]; - render( - - ); - expect(screen.getByText("Organization No Projects")).toBeInTheDocument(); - expect(screen.queryByText("Project 1")).not.toBeInTheDocument(); // Check that no projects are listed under it - expect(mockNotificationSwitch).not.toHaveBeenCalled(); // No projects, so no switches for projects - }); -}); diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/EditWeeklySummary.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/EditWeeklySummary.tsx deleted file mode 100644 index 5f99be8309..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/EditWeeklySummary.tsx +++ /dev/null @@ -1,59 +0,0 @@ -"use client"; - -import { useTranslate } from "@tolgee/react"; -import { UsersIcon } from "lucide-react"; -import Link from "next/link"; -import { TUser } from "@formbricks/types/user"; -import { Membership } from "../types"; -import { NotificationSwitch } from "./NotificationSwitch"; - -interface EditAlertsProps { - memberships: Membership[]; - user: TUser; - environmentId: string; -} - -export const EditWeeklySummary = ({ memberships, user, environmentId }: EditAlertsProps) => { - const { t } = useTranslate(); - return ( - <> - {memberships.map((membership) => ( -
-
- - -

{membership.organization.name}

-
-
-
-
{t("common.project")}
-
{t("common.weekly_summary")}
-
-
- {membership.organization.projects.map((project) => ( -
-
{project?.name}
-
- -
-
- ))} -
-

- {t("environments.settings.notifications.want_to_loop_in_organization_mates")}?{" "} - - {t("common.invite_them")} - -

-
-
- ))} - - ); -}; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/NotificationSwitch.test.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/NotificationSwitch.test.tsx index de40d91042..7143498be2 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/NotificationSwitch.test.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/NotificationSwitch.test.tsx @@ -29,7 +29,6 @@ const organizationId = "org1"; const baseNotificationSettings: TUserNotificationSettings = { alert: {}, - weeklySummary: {}, unsubscribedOrganizationIds: [], }; @@ -68,19 +67,6 @@ describe("NotificationSwitch", () => { expect(switchInput.checked).toBe(false); }); - test("renders with initial checked state for 'weeklySummary' (true)", () => { - const settings = { ...baseNotificationSettings, weeklySummary: { [projectId]: true } }; - renderSwitch({ - surveyOrProjectOrOrganizationId: projectId, - notificationSettings: settings, - notificationType: "weeklySummary", - }); - const switchInput = screen.getByLabelText( - "toggle notification settings for weeklySummary" - ) as HTMLInputElement; - expect(switchInput.checked).toBe(true); - }); - test("renders with initial checked state for 'unsubscribedOrganizationIds' (subscribed initially, so checked is true)", () => { const settings = { ...baseNotificationSettings, unsubscribedOrganizationIds: [] }; renderSwitch({ @@ -268,31 +254,6 @@ describe("NotificationSwitch", () => { expect(toast.success).not.toHaveBeenCalled(); }); - test("shows error toast when updateNotificationSettingsAction fails for 'weeklySummary' type", async () => { - const mockErrorResponse = { serverError: "Database connection failed" }; - vi.mocked(updateNotificationSettingsAction).mockResolvedValueOnce(mockErrorResponse); - - const initialSettings = { ...baseNotificationSettings, weeklySummary: { [projectId]: true } }; - renderSwitch({ - surveyOrProjectOrOrganizationId: projectId, - notificationSettings: initialSettings, - notificationType: "weeklySummary", - }); - const switchInput = screen.getByLabelText("toggle notification settings for weeklySummary"); - - await act(async () => { - await user.click(switchInput); - }); - - expect(updateNotificationSettingsAction).toHaveBeenCalledWith({ - notificationSettings: { ...initialSettings, weeklySummary: { [projectId]: false } }, - }); - expect(toast.error).toHaveBeenCalledWith("Database connection failed", { - id: "notification-switch", - }); - expect(toast.success).not.toHaveBeenCalled(); - }); - test("shows error toast when updateNotificationSettingsAction fails for 'unsubscribedOrganizationIds' type", async () => { const mockErrorResponse = { serverError: "Permission denied" }; vi.mocked(updateNotificationSettingsAction).mockResolvedValueOnce(mockErrorResponse); diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/NotificationSwitch.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/NotificationSwitch.tsx index 9deeb3d6ad..deedc049b5 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/NotificationSwitch.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/components/NotificationSwitch.tsx @@ -12,7 +12,7 @@ import { updateNotificationSettingsAction } from "../actions"; interface NotificationSwitchProps { surveyOrProjectOrOrganizationId: string; notificationSettings: TUserNotificationSettings; - notificationType: "alert" | "weeklySummary" | "unsubscribedOrganizationIds"; + notificationType: "alert" | "unsubscribedOrganizationIds"; autoDisableNotificationType?: string; autoDisableNotificationElementId?: string; } diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/loading.test.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/loading.test.tsx index 7cac2f1c36..ea2fbf0cd0 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/loading.test.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/loading.test.tsx @@ -34,17 +34,5 @@ describe("Loading Notifications Settings", () => { .getByText("environments.settings.notifications.email_alerts_surveys") .closest("div[class*='rounded-xl']"); // Find parent card expect(alertsCard).toBeInTheDocument(); - - // Check for Weekly Summary LoadingCard - expect( - screen.getByText("environments.settings.notifications.weekly_summary_projects") - ).toBeInTheDocument(); - expect( - screen.getByText("environments.settings.notifications.stay_up_to_date_with_a_Weekly_every_Monday") - ).toBeInTheDocument(); - const weeklySummaryCard = screen - .getByText("environments.settings.notifications.weekly_summary_projects") - .closest("div[class*='rounded-xl']"); // Find parent card - expect(weeklySummaryCard).toBeInTheDocument(); }); }); diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/loading.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/loading.tsx index d636572eb1..2b074d0963 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/loading.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/loading.tsx @@ -14,11 +14,6 @@ const Loading = () => { description: t("environments.settings.notifications.set_up_an_alert_to_get_an_email_on_new_responses"), skeletonLines: [{ classes: "h-6 w-28" }, { classes: "h-10 w-128" }, { classes: "h-10 w-128" }], }, - { - title: t("environments.settings.notifications.weekly_summary_projects"), - description: t("environments.settings.notifications.stay_up_to_date_with_a_Weekly_every_Monday"), - skeletonLines: [{ classes: "h-6 w-28" }, { classes: "h-10 w-128" }, { classes: "h-10 w-128" }], - }, ]; return ( diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/page.test.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/page.test.tsx index 93075bfcfa..40a86c30cd 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/page.test.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/page.test.tsx @@ -5,7 +5,6 @@ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { prisma } from "@formbricks/database"; import { TUser } from "@formbricks/types/user"; import { EditAlerts } from "./components/EditAlerts"; -import { EditWeeklySummary } from "./components/EditWeeklySummary"; import Page from "./page"; import { Membership } from "./types"; @@ -58,9 +57,7 @@ vi.mock("@formbricks/database", () => ({ vi.mock("./components/EditAlerts", () => ({ EditAlerts: vi.fn(() =>
EditAlertsComponent
), })); -vi.mock("./components/EditWeeklySummary", () => ({ - EditWeeklySummary: vi.fn(() =>
EditWeeklySummaryComponent
), -})); + vi.mock("./components/IntegrationsTip", () => ({ IntegrationsTip: () =>
IntegrationsTipComponent
, })); @@ -71,7 +68,6 @@ const mockUser: Partial = { email: "test@example.com", notificationSettings: { alert: { "survey-old": true }, - weeklySummary: { "project-old": true }, unsubscribedOrganizationIds: ["org-unsubscribed"], }, }; @@ -137,13 +133,6 @@ describe("NotificationsPage", () => { ).toBeInTheDocument(); expect(screen.getByText("EditAlertsComponent")).toBeInTheDocument(); expect(screen.getByText("IntegrationsTipComponent")).toBeInTheDocument(); - expect( - screen.getByText("environments.settings.notifications.weekly_summary_projects") - ).toBeInTheDocument(); - expect( - screen.getByText("environments.settings.notifications.stay_up_to_date_with_a_Weekly_every_Monday") - ).toBeInTheDocument(); - expect(screen.getByText("EditWeeklySummaryComponent")).toBeInTheDocument(); // The actual `user.notificationSettings` passed to EditAlerts will be a new object // after `setCompleteNotificationSettings` processes it. @@ -157,16 +146,12 @@ describe("NotificationsPage", () => { // It iterates memberships, then projects, then environments, then surveys. // `newNotificationSettings.alert[survey.id] = notificationSettings[survey.id]?.responseFinished || (notificationSettings.alert && notificationSettings.alert[survey.id]) || false;` // This means only survey IDs found in memberships will be in the new `alert` object. - // `newNotificationSettings.weeklySummary[project.id]` also only adds project IDs from memberships. const finalExpectedSettings = { alert: { "survey-1": false, "survey-2": false, }, - weeklySummary: { - "project-1": false, - }, unsubscribedOrganizationIds: ["org-unsubscribed"], }; @@ -175,11 +160,6 @@ describe("NotificationsPage", () => { expect(editAlertsCall.environmentId).toBe(mockParams.environmentId); expect(editAlertsCall.autoDisableNotificationType).toBe(mockSearchParams.type); expect(editAlertsCall.autoDisableNotificationElementId).toBe(mockSearchParams.elementId); - - const editWeeklySummaryCall = vi.mocked(EditWeeklySummary).mock.calls[0][0]; - expect(editWeeklySummaryCall.user.notificationSettings).toEqual(finalExpectedSettings); - expect(editWeeklySummaryCall.memberships).toEqual(mockMemberships); - expect(editWeeklySummaryCall.environmentId).toBe(mockParams.environmentId); }); test("throws error if session is not found", async () => { @@ -207,21 +187,15 @@ describe("NotificationsPage", () => { render(PageComponent); expect(screen.getByText("EditAlertsComponent")).toBeInTheDocument(); - expect(screen.getByText("EditWeeklySummaryComponent")).toBeInTheDocument(); const expectedEmptySettings = { alert: {}, - weeklySummary: {}, unsubscribedOrganizationIds: [], }; const editAlertsCall = vi.mocked(EditAlerts).mock.calls[0][0]; expect(editAlertsCall.user.notificationSettings).toEqual(expectedEmptySettings); expect(editAlertsCall.memberships).toEqual([]); - - const editWeeklySummaryCall = vi.mocked(EditWeeklySummary).mock.calls[0][0]; - expect(editWeeklySummaryCall.user.notificationSettings).toEqual(expectedEmptySettings); - expect(editWeeklySummaryCall.memberships).toEqual([]); }); test("handles legacy notification settings correctly", async () => { @@ -229,7 +203,6 @@ describe("NotificationsPage", () => { id: "user-legacy", notificationSettings: { "survey-1": { responseFinished: true }, // Legacy alert for survey-1 - weeklySummary: { "project-1": true }, unsubscribedOrganizationIds: [], } as any, // To allow legacy structure }; @@ -246,9 +219,6 @@ describe("NotificationsPage", () => { "survey-1": true, // Should be true due to legacy setting "survey-2": false, // Default for other surveys in membership }, - weeklySummary: { - "project-1": true, // From user's weeklySummary - }, unsubscribedOrganizationIds: [], }; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/page.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/page.tsx index e536e64d0c..8cc08c7b02 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/page.tsx @@ -9,7 +9,6 @@ import { getServerSession } from "next-auth"; import { prisma } from "@formbricks/database"; import { TUserNotificationSettings } from "@formbricks/types/user"; import { EditAlerts } from "./components/EditAlerts"; -import { EditWeeklySummary } from "./components/EditWeeklySummary"; import { IntegrationsTip } from "./components/IntegrationsTip"; import type { Membership } from "./types"; @@ -19,14 +18,10 @@ const setCompleteNotificationSettings = ( ): TUserNotificationSettings => { const newNotificationSettings = { alert: {}, - weeklySummary: {}, unsubscribedOrganizationIds: notificationSettings.unsubscribedOrganizationIds || [], }; for (const membership of memberships) { for (const project of membership.organization.projects) { - // set default values for weekly summary - newNotificationSettings.weeklySummary[project.id] = - (notificationSettings.weeklySummary && notificationSettings.weeklySummary[project.id]) || false; // set default values for alerts for (const environment of project.environments) { for (const survey of environment.surveys) { @@ -183,11 +178,6 @@ const Page = async (props) => { /> - - - ); }; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/AccountSecurity.test.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/AccountSecurity.test.tsx index 3bd5c28285..835e669e5f 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/AccountSecurity.test.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/AccountSecurity.test.tsx @@ -20,7 +20,7 @@ const mockUser = { email: "test@example.com", notificationSettings: { alert: {}, - weeklySummary: {}, + unsubscribedOrganizationIds: [], }, twoFactorEnabled: false, diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/DeleteAccount.test.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/DeleteAccount.test.tsx index 230dbbd1f2..156be7ee0c 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/DeleteAccount.test.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/DeleteAccount.test.tsx @@ -15,7 +15,7 @@ const mockUser = { id: "user1", name: "Test User", email: "test@example.com", - notificationSettings: { alert: {}, weeklySummary: {}, unsubscribedOrganizationIds: [] }, + notificationSettings: { alert: {}, unsubscribedOrganizationIds: [] }, twoFactorEnabled: false, identityProvider: "email", createdAt: new Date(), diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/EditProfileDetailsForm.test.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/EditProfileDetailsForm.test.tsx index a9a779e55b..bbfb31327d 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/EditProfileDetailsForm.test.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/EditProfileDetailsForm.test.tsx @@ -13,7 +13,7 @@ const mockUser = { locale: "en-US", notificationSettings: { alert: {}, - weeklySummary: {}, + unsubscribedOrganizationIds: [], }, twoFactorEnabled: false, diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/page.test.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/page.test.tsx index feed9087f1..05fe5f59a3 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/page.test.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/page.test.tsx @@ -76,7 +76,7 @@ const mockUser = { imageUrl: "http://example.com/avatar.png", twoFactorEnabled: false, identityProvider: "email", - notificationSettings: { alert: {}, weeklySummary: {}, unsubscribedOrganizationIds: [] }, + notificationSettings: { alert: {}, unsubscribedOrganizationIds: [] }, createdAt: new Date(), updatedAt: new Date(), role: "project_manager", diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/enterprise/page.test.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/enterprise/page.test.tsx index d15b0a58da..8d1a82d43b 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/enterprise/page.test.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/enterprise/page.test.tsx @@ -129,7 +129,7 @@ const mockUser = { imageUrl: "", twoFactorEnabled: false, identityProvider: "email", - notificationSettings: { alert: {}, weeklySummary: {} }, + notificationSettings: { alert: {} }, role: "project_manager", objective: "other", } as unknown as TUser; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseCardModal.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseCardModal.test.tsx index 991084dbe2..54744c6655 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseCardModal.test.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseCardModal.test.tsx @@ -139,7 +139,7 @@ const mockUser = { updatedAt: new Date(), role: "project_manager", objective: "increase_conversion", - notificationSettings: { alert: {}, weeklySummary: {}, unsubscribedOrganizationIds: [] }, + notificationSettings: { alert: {}, unsubscribedOrganizationIds: [] }, } as unknown as TUser; const mockEnvironmentTags: TTag[] = [ diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA.test.tsx index 5f03cddf57..ceeda6638a 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA.test.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA.test.tsx @@ -305,12 +305,8 @@ const mockUser: TUser = { isActive: true, notificationSettings: { alert: { - weeklySummary: true, responseFinished: true, }, - weeklySummary: { - test: true, - }, unsubscribedOrganizationIds: [], }, }; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/share-survey-modal.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/share-survey-modal.test.tsx index 3d656ccce9..4855b9efbd 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/share-survey-modal.test.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/share-survey-modal.test.tsx @@ -253,7 +253,6 @@ const mockUser: TUser = { isActive: true, notificationSettings: { alert: {}, - weeklySummary: {}, unsubscribedOrganizationIds: [], }, }; diff --git a/apps/web/app/api/cron/weekly-summary/lib/notificationResponse.test.ts b/apps/web/app/api/cron/weekly-summary/lib/notificationResponse.test.ts deleted file mode 100644 index 9bdcc87cbd..0000000000 --- a/apps/web/app/api/cron/weekly-summary/lib/notificationResponse.test.ts +++ /dev/null @@ -1,276 +0,0 @@ -import { convertResponseValue } from "@/lib/responses"; -import { cleanup } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys/types"; -import { - TWeeklyEmailResponseData, - TWeeklySummaryEnvironmentData, - TWeeklySummarySurveyData, -} from "@formbricks/types/weekly-summary"; -import { getNotificationResponse } from "./notificationResponse"; - -vi.mock("@/lib/responses", () => ({ - convertResponseValue: vi.fn(), -})); - -vi.mock("@/lib/utils/recall", () => ({ - replaceHeadlineRecall: vi.fn((survey) => survey), -})); - -describe("getNotificationResponse", () => { - afterEach(() => { - cleanup(); - }); - - test("should return a notification response with calculated insights and survey data when provided with an environment containing multiple surveys", () => { - const mockSurveys = [ - { - id: "survey1", - name: "Survey 1", - status: "inProgress", - questions: [ - { - id: "question1", - headline: { default: "Question 1" }, - type: "text", - } as unknown as TSurveyQuestion, - ], - displays: [{ id: "display1" }], - responses: [ - { id: "response1", finished: true, data: { question1: "Answer 1" } }, - { id: "response2", finished: false, data: { question1: "Answer 2" } }, - ], - } as unknown as TSurvey & { responses: TWeeklyEmailResponseData[] }, - { - id: "survey2", - name: "Survey 2", - status: "inProgress", - questions: [ - { - id: "question2", - headline: { default: "Question 2" }, - type: "text", - } as unknown as TSurveyQuestion, - ], - displays: [{ id: "display2" }], - responses: [ - { id: "response3", finished: true, data: { question2: "Answer 3" } }, - { id: "response4", finished: true, data: { question2: "Answer 4" } }, - { id: "response5", finished: false, data: { question2: "Answer 5" } }, - ], - } as unknown as TSurvey & { responses: TWeeklyEmailResponseData[] }, - ] as unknown as TWeeklySummarySurveyData[]; - - const mockEnvironment = { - id: "env1", - surveys: mockSurveys, - } as unknown as TWeeklySummaryEnvironmentData; - - const projectName = "Project Name"; - - const notificationResponse = getNotificationResponse(mockEnvironment, projectName); - - expect(notificationResponse).toBeDefined(); - expect(notificationResponse.environmentId).toBe("env1"); - expect(notificationResponse.projectName).toBe(projectName); - expect(notificationResponse.surveys).toHaveLength(2); - - expect(notificationResponse.insights.totalCompletedResponses).toBe(3); - expect(notificationResponse.insights.totalDisplays).toBe(2); - expect(notificationResponse.insights.totalResponses).toBe(5); - expect(notificationResponse.insights.completionRate).toBe(60); - expect(notificationResponse.insights.numLiveSurvey).toBe(2); - - expect(notificationResponse.surveys[0].id).toBe("survey1"); - expect(notificationResponse.surveys[0].name).toBe("Survey 1"); - expect(notificationResponse.surveys[0].status).toBe("inProgress"); - expect(notificationResponse.surveys[0].responseCount).toBe(2); - - expect(notificationResponse.surveys[1].id).toBe("survey2"); - expect(notificationResponse.surveys[1].name).toBe("Survey 2"); - expect(notificationResponse.surveys[1].status).toBe("inProgress"); - expect(notificationResponse.surveys[1].responseCount).toBe(3); - }); - - test("should calculate the correct completion rate and other insights when surveys have responses with varying statuses", () => { - const mockSurveys = [ - { - id: "survey1", - name: "Survey 1", - status: "inProgress", - questions: [ - { - id: "question1", - headline: { default: "Question 1" }, - type: "text", - } as unknown as TSurveyQuestion, - ], - displays: [{ id: "display1" }], - responses: [ - { id: "response1", finished: true, data: { question1: "Answer 1" } }, - { id: "response2", finished: false, data: { question1: "Answer 2" } }, - ], - } as unknown as TSurvey & { responses: TWeeklyEmailResponseData[] }, - { - id: "survey2", - name: "Survey 2", - status: "inProgress", - questions: [ - { - id: "question2", - headline: { default: "Question 2" }, - type: "text", - } as unknown as TSurveyQuestion, - ], - displays: [{ id: "display2" }], - responses: [ - { id: "response3", finished: true, data: { question2: "Answer 3" } }, - { id: "response4", finished: true, data: { question2: "Answer 4" } }, - { id: "response5", finished: false, data: { question2: "Answer 5" } }, - ], - } as unknown as TSurvey & { responses: TWeeklyEmailResponseData[] }, - { - id: "survey3", - name: "Survey 3", - status: "inProgress", - questions: [ - { - id: "question3", - headline: { default: "Question 3" }, - type: "text", - } as unknown as TSurveyQuestion, - ], - displays: [{ id: "display3" }], - responses: [{ id: "response6", finished: false, data: { question3: "Answer 6" } }], - } as unknown as TSurvey & { responses: TWeeklyEmailResponseData[] }, - ] as unknown as TWeeklySummarySurveyData[]; - - const mockEnvironment = { - id: "env1", - surveys: mockSurveys, - } as unknown as TWeeklySummaryEnvironmentData; - - const projectName = "Project Name"; - - const notificationResponse = getNotificationResponse(mockEnvironment, projectName); - - expect(notificationResponse).toBeDefined(); - expect(notificationResponse.environmentId).toBe("env1"); - expect(notificationResponse.projectName).toBe(projectName); - expect(notificationResponse.surveys).toHaveLength(3); - - expect(notificationResponse.insights.totalCompletedResponses).toBe(3); - expect(notificationResponse.insights.totalDisplays).toBe(3); - expect(notificationResponse.insights.totalResponses).toBe(6); - expect(notificationResponse.insights.completionRate).toBe(50); - expect(notificationResponse.insights.numLiveSurvey).toBe(3); - - expect(notificationResponse.surveys[0].id).toBe("survey1"); - expect(notificationResponse.surveys[0].name).toBe("Survey 1"); - expect(notificationResponse.surveys[0].status).toBe("inProgress"); - expect(notificationResponse.surveys[0].responseCount).toBe(2); - - expect(notificationResponse.surveys[1].id).toBe("survey2"); - expect(notificationResponse.surveys[1].name).toBe("Survey 2"); - expect(notificationResponse.surveys[1].status).toBe("inProgress"); - expect(notificationResponse.surveys[1].responseCount).toBe(3); - - expect(notificationResponse.surveys[2].id).toBe("survey3"); - expect(notificationResponse.surveys[2].name).toBe("Survey 3"); - expect(notificationResponse.surveys[2].status).toBe("inProgress"); - expect(notificationResponse.surveys[2].responseCount).toBe(1); - }); - - test("should return default insights and an empty surveys array when the environment contains no surveys", () => { - const mockEnvironment = { - id: "env1", - surveys: [], - } as unknown as TWeeklySummaryEnvironmentData; - - const projectName = "Project Name"; - - const notificationResponse = getNotificationResponse(mockEnvironment, projectName); - - expect(notificationResponse).toBeDefined(); - expect(notificationResponse.environmentId).toBe("env1"); - expect(notificationResponse.projectName).toBe(projectName); - expect(notificationResponse.surveys).toHaveLength(0); - - expect(notificationResponse.insights.totalCompletedResponses).toBe(0); - expect(notificationResponse.insights.totalDisplays).toBe(0); - expect(notificationResponse.insights.totalResponses).toBe(0); - expect(notificationResponse.insights.completionRate).toBe(0); - expect(notificationResponse.insights.numLiveSurvey).toBe(0); - }); - - test("should handle missing response data gracefully when a response doesn't contain data for a question ID", () => { - const mockSurveys = [ - { - id: "survey1", - name: "Survey 1", - status: "inProgress", - questions: [ - { - id: "question1", - headline: { default: "Question 1" }, - type: "text", - } as unknown as TSurveyQuestion, - ], - displays: [{ id: "display1" }], - responses: [ - { id: "response1", finished: true, data: {} }, // Response missing data for question1 - ], - } as unknown as TSurvey & { responses: TWeeklyEmailResponseData[] }, - ] as unknown as TWeeklySummarySurveyData[]; - - const mockEnvironment = { - id: "env1", - surveys: mockSurveys, - } as unknown as TWeeklySummaryEnvironmentData; - - const projectName = "Project Name"; - - // Mock the convertResponseValue function to handle the missing data case - vi.mocked(convertResponseValue).mockReturnValue(""); - - const notificationResponse = getNotificationResponse(mockEnvironment, projectName); - - expect(notificationResponse).toBeDefined(); - expect(notificationResponse.surveys).toHaveLength(1); - expect(notificationResponse.surveys[0].responses).toHaveLength(1); - expect(notificationResponse.surveys[0].responses[0].responseValue).toBe(""); - }); - - test("should handle unsupported question types gracefully", () => { - const mockSurveys = [ - { - id: "survey1", - name: "Survey 1", - status: "inProgress", - questions: [ - { - id: "question1", - headline: { default: "Question 1" }, - type: "unsupported", - } as unknown as TSurveyQuestion, - ], - displays: [{ id: "display1" }], - responses: [{ id: "response1", finished: true, data: { question1: "Answer 1" } }], - } as unknown as TSurvey & { responses: TWeeklyEmailResponseData[] }, - ] as unknown as TWeeklySummarySurveyData[]; - - const mockEnvironment = { - id: "env1", - surveys: mockSurveys, - } as unknown as TWeeklySummaryEnvironmentData; - - const projectName = "Project Name"; - - vi.mocked(convertResponseValue).mockReturnValue("Unsupported Response"); - - const notificationResponse = getNotificationResponse(mockEnvironment, projectName); - - expect(notificationResponse).toBeDefined(); - expect(notificationResponse.surveys[0].responses[0].responseValue).toBe("Unsupported Response"); - }); -}); diff --git a/apps/web/app/api/cron/weekly-summary/lib/notificationResponse.ts b/apps/web/app/api/cron/weekly-summary/lib/notificationResponse.ts deleted file mode 100644 index b4a35ea41f..0000000000 --- a/apps/web/app/api/cron/weekly-summary/lib/notificationResponse.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { getLocalizedValue } from "@/lib/i18n/utils"; -import { convertResponseValue } from "@/lib/responses"; -import { replaceHeadlineRecall } from "@/lib/utils/recall"; -import { TSurvey } from "@formbricks/types/surveys/types"; -import { - TWeeklyEmailResponseData, - TWeeklySummaryEnvironmentData, - TWeeklySummaryNotificationDataSurvey, - TWeeklySummaryNotificationResponse, - TWeeklySummarySurveyResponseData, -} from "@formbricks/types/weekly-summary"; - -export const getNotificationResponse = ( - environment: TWeeklySummaryEnvironmentData, - projectName: string -): TWeeklySummaryNotificationResponse => { - const insights = { - totalCompletedResponses: 0, - totalDisplays: 0, - totalResponses: 0, - completionRate: 0, - numLiveSurvey: 0, - }; - - const surveys: TWeeklySummaryNotificationDataSurvey[] = []; - // iterate through the surveys and calculate the overall insights - for (const survey of environment.surveys) { - const parsedSurvey = replaceHeadlineRecall(survey as unknown as TSurvey, "default") as TSurvey & { - responses: TWeeklyEmailResponseData[]; - }; - const surveyData: TWeeklySummaryNotificationDataSurvey = { - id: parsedSurvey.id, - name: parsedSurvey.name, - status: parsedSurvey.status, - responseCount: parsedSurvey.responses.length, - responses: [], - }; - // iterate through the responses and calculate the survey insights - for (const response of parsedSurvey.responses) { - // only take the first 3 responses - if (surveyData.responses.length >= 3) { - break; - } - const surveyResponses: TWeeklySummarySurveyResponseData[] = []; - for (const question of parsedSurvey.questions) { - const headline = question.headline; - const responseValue = convertResponseValue(response.data[question.id], question); - const surveyResponse: TWeeklySummarySurveyResponseData = { - headline: getLocalizedValue(headline, "default"), - responseValue, - questionType: question.type, - }; - surveyResponses.push(surveyResponse); - } - surveyData.responses = surveyResponses; - } - surveys.push(surveyData); - // calculate the overall insights - if (survey.status == "inProgress") { - insights.numLiveSurvey += 1; - } - insights.totalCompletedResponses += survey.responses.filter((r) => r.finished).length; - insights.totalDisplays += survey.displays.length; - insights.totalResponses += survey.responses.length; - insights.completionRate = Math.round((insights.totalCompletedResponses / insights.totalResponses) * 100); - } - // build the notification response needed for the emails - const lastWeekDate = new Date(); - lastWeekDate.setDate(lastWeekDate.getDate() - 7); - return { - environmentId: environment.id, - currentDate: new Date(), - lastWeekDate, - projectName: projectName, - surveys, - insights, - }; -}; diff --git a/apps/web/app/api/cron/weekly-summary/lib/organization.test.ts b/apps/web/app/api/cron/weekly-summary/lib/organization.test.ts deleted file mode 100644 index 4fe250acd9..0000000000 --- a/apps/web/app/api/cron/weekly-summary/lib/organization.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { cleanup } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; -import { prisma } from "@formbricks/database"; -import { getOrganizationIds } from "./organization"; - -vi.mock("@formbricks/database", () => ({ - prisma: { - organization: { - findMany: vi.fn(), - }, - }, -})); - -describe("Organization", () => { - afterEach(() => { - cleanup(); - }); - - test("getOrganizationIds should return an array of organization IDs when the database contains multiple organizations", async () => { - const mockOrganizations = [{ id: "org1" }, { id: "org2" }, { id: "org3" }]; - - vi.mocked(prisma.organization.findMany).mockResolvedValue(mockOrganizations); - - const organizationIds = await getOrganizationIds(); - - expect(organizationIds).toEqual(["org1", "org2", "org3"]); - expect(prisma.organization.findMany).toHaveBeenCalledTimes(1); - expect(prisma.organization.findMany).toHaveBeenCalledWith({ - select: { - id: true, - }, - }); - }); - - test("getOrganizationIds should return an empty array when the database contains no organizations", async () => { - vi.mocked(prisma.organization.findMany).mockResolvedValue([]); - - const organizationIds = await getOrganizationIds(); - - expect(organizationIds).toEqual([]); - expect(prisma.organization.findMany).toHaveBeenCalledTimes(1); - expect(prisma.organization.findMany).toHaveBeenCalledWith({ - select: { - id: true, - }, - }); - }); -}); diff --git a/apps/web/app/api/cron/weekly-summary/lib/organization.ts b/apps/web/app/api/cron/weekly-summary/lib/organization.ts deleted file mode 100644 index bae8b74bb5..0000000000 --- a/apps/web/app/api/cron/weekly-summary/lib/organization.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { prisma } from "@formbricks/database"; - -export const getOrganizationIds = async (): Promise => { - const organizations = await prisma.organization.findMany({ - select: { - id: true, - }, - }); - return organizations.map((organization) => organization.id); -}; diff --git a/apps/web/app/api/cron/weekly-summary/lib/project.test.ts b/apps/web/app/api/cron/weekly-summary/lib/project.test.ts deleted file mode 100644 index c3de4eefe5..0000000000 --- a/apps/web/app/api/cron/weekly-summary/lib/project.test.ts +++ /dev/null @@ -1,570 +0,0 @@ -import { cleanup } from "@testing-library/react"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { prisma } from "@formbricks/database"; -import { getProjectsByOrganizationId } from "./project"; - -const mockProjects = [ - { - id: "project1", - name: "Project 1", - environments: [ - { - id: "env1", - type: "production", - surveys: [], - attributeKeys: [], - }, - ], - organization: { - memberships: [ - { - user: { - id: "user1", - email: "test@example.com", - notificationSettings: { - weeklySummary: { - project1: true, - }, - }, - locale: "en", - }, - }, - ], - }, - }, -]; - -const sevenDaysAgo = new Date(); -sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 6); // Set to 6 days ago to be within the last 7 days - -const mockProjectsWithNoEnvironments = [ - { - id: "project3", - name: "Project 3", - environments: [], - organization: { - memberships: [ - { - user: { - id: "user1", - email: "test@example.com", - notificationSettings: { - weeklySummary: { - project3: true, - }, - }, - locale: "en", - }, - }, - ], - }, - }, -]; - -vi.mock("@formbricks/database", () => ({ - prisma: { - project: { - findMany: vi.fn(), - }, - }, -})); - -describe("Project Management", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - afterEach(() => { - cleanup(); - }); - - describe("getProjectsByOrganizationId", () => { - test("retrieves projects with environments, surveys, and organization memberships for a valid organization ID", async () => { - vi.mocked(prisma.project.findMany).mockResolvedValueOnce(mockProjects); - - const organizationId = "testOrgId"; - const projects = await getProjectsByOrganizationId(organizationId); - - expect(projects).toEqual(mockProjects); - expect(prisma.project.findMany).toHaveBeenCalledWith({ - where: { - organizationId: organizationId, - }, - select: { - id: true, - name: true, - environments: { - where: { - type: "production", - }, - select: { - id: true, - surveys: { - where: { - NOT: { - AND: [ - { status: "completed" }, - { - responses: { - none: { - createdAt: { - gte: expect.any(Date), - }, - }, - }, - }, - ], - }, - status: { - not: "draft", - }, - }, - select: { - id: true, - name: true, - questions: true, - status: true, - responses: { - where: { - createdAt: { - gte: expect.any(Date), - }, - }, - select: { - id: true, - createdAt: true, - updatedAt: true, - finished: true, - data: true, - }, - orderBy: { - createdAt: "desc", - }, - }, - displays: { - where: { - createdAt: { - gte: expect.any(Date), - }, - }, - select: { - id: true, - }, - }, - hiddenFields: true, - }, - }, - attributeKeys: { - select: { - id: true, - createdAt: true, - updatedAt: true, - name: true, - description: true, - type: true, - environmentId: true, - key: true, - isUnique: true, - }, - }, - }, - }, - organization: { - select: { - memberships: { - select: { - user: { - select: { - id: true, - email: true, - notificationSettings: true, - locale: true, - }, - }, - }, - }, - }, - }, - }, - }); - }); - - test("handles date calculations correctly across DST boundaries", async () => { - const mockDate = new Date(2024, 10, 3, 0, 0, 0); // November 3, 2024, 00:00:00 (example DST boundary) - const sevenDaysAgo = new Date(mockDate); - sevenDaysAgo.setDate(mockDate.getDate() - 7); - - vi.useFakeTimers(); - vi.setSystemTime(mockDate); - - vi.mocked(prisma.project.findMany).mockResolvedValueOnce(mockProjects); - - const organizationId = "testOrgId"; - await getProjectsByOrganizationId(organizationId); - - expect(prisma.project.findMany).toHaveBeenCalledWith( - expect.objectContaining({ - where: { - organizationId: organizationId, - }, - select: expect.objectContaining({ - environments: expect.objectContaining({ - select: expect.objectContaining({ - surveys: expect.objectContaining({ - where: expect.objectContaining({ - NOT: expect.objectContaining({ - AND: expect.arrayContaining([ - expect.objectContaining({ status: "completed" }), - expect.objectContaining({ - responses: expect.objectContaining({ - none: expect.objectContaining({ - createdAt: expect.objectContaining({ - gte: sevenDaysAgo, - }), - }), - }), - }), - ]), - }), - }), - }), - }), - }), - }), - }) - ); - - vi.useRealTimers(); - }); - - test("includes surveys with 'completed' status but responses within the last 7 days", async () => { - vi.mocked(prisma.project.findMany).mockResolvedValueOnce(mockProjects); - - const organizationId = "testOrgId"; - const projects = await getProjectsByOrganizationId(organizationId); - - expect(projects).toEqual(mockProjects); - expect(prisma.project.findMany).toHaveBeenCalledWith({ - where: { - organizationId: organizationId, - }, - select: { - id: true, - name: true, - environments: { - where: { - type: "production", - }, - select: { - id: true, - surveys: { - where: { - NOT: { - AND: [ - { status: "completed" }, - { - responses: { - none: { - createdAt: { - gte: expect.any(Date), - }, - }, - }, - }, - ], - }, - status: { - not: "draft", - }, - }, - select: { - id: true, - name: true, - questions: true, - status: true, - responses: { - where: { - createdAt: { - gte: expect.any(Date), - }, - }, - select: { - id: true, - createdAt: true, - updatedAt: true, - finished: true, - data: true, - }, - orderBy: { - createdAt: "desc", - }, - }, - displays: { - where: { - createdAt: { - gte: expect.any(Date), - }, - }, - select: { - id: true, - }, - }, - hiddenFields: true, - }, - }, - attributeKeys: { - select: { - id: true, - createdAt: true, - updatedAt: true, - name: true, - description: true, - type: true, - environmentId: true, - key: true, - isUnique: true, - }, - }, - }, - }, - organization: { - select: { - memberships: { - select: { - user: { - select: { - id: true, - email: true, - notificationSettings: true, - locale: true, - }, - }, - }, - }, - }, - }, - }, - }); - }); - - test("returns an empty array when an invalid organization ID is provided", async () => { - vi.mocked(prisma.project.findMany).mockResolvedValueOnce([]); - - const invalidOrganizationId = "invalidOrgId"; - const projects = await getProjectsByOrganizationId(invalidOrganizationId); - - expect(projects).toEqual([]); - expect(prisma.project.findMany).toHaveBeenCalledWith({ - where: { - organizationId: invalidOrganizationId, - }, - select: { - id: true, - name: true, - environments: { - where: { - type: "production", - }, - select: { - id: true, - surveys: { - where: { - NOT: { - AND: [ - { status: "completed" }, - { - responses: { - none: { - createdAt: { - gte: expect.any(Date), - }, - }, - }, - }, - ], - }, - status: { - not: "draft", - }, - }, - select: { - id: true, - name: true, - questions: true, - status: true, - responses: { - where: { - createdAt: { - gte: expect.any(Date), - }, - }, - select: { - id: true, - createdAt: true, - updatedAt: true, - finished: true, - data: true, - }, - orderBy: { - createdAt: "desc", - }, - }, - displays: { - where: { - createdAt: { - gte: expect.any(Date), - }, - }, - select: { - id: true, - }, - }, - hiddenFields: true, - }, - }, - attributeKeys: { - select: { - id: true, - createdAt: true, - updatedAt: true, - name: true, - description: true, - type: true, - environmentId: true, - key: true, - isUnique: true, - }, - }, - }, - }, - organization: { - select: { - memberships: { - select: { - user: { - select: { - id: true, - email: true, - notificationSettings: true, - locale: true, - }, - }, - }, - }, - }, - }, - }, - }); - }); - - test("handles projects with no environments", async () => { - vi.mocked(prisma.project.findMany).mockResolvedValueOnce(mockProjectsWithNoEnvironments); - - const organizationId = "testOrgId"; - const projects = await getProjectsByOrganizationId(organizationId); - - expect(projects).toEqual(mockProjectsWithNoEnvironments); - expect(prisma.project.findMany).toHaveBeenCalledWith({ - where: { - organizationId: organizationId, - }, - select: { - id: true, - name: true, - environments: { - where: { - type: "production", - }, - select: { - id: true, - surveys: { - where: { - NOT: { - AND: [ - { status: "completed" }, - { - responses: { - none: { - createdAt: { - gte: expect.any(Date), - }, - }, - }, - }, - ], - }, - status: { - not: "draft", - }, - }, - select: { - id: true, - name: true, - questions: true, - status: true, - responses: { - where: { - createdAt: { - gte: expect.any(Date), - }, - }, - select: { - id: true, - createdAt: true, - updatedAt: true, - finished: true, - data: true, - }, - orderBy: { - createdAt: "desc", - }, - }, - displays: { - where: { - createdAt: { - gte: expect.any(Date), - }, - }, - select: { - id: true, - }, - }, - hiddenFields: true, - }, - }, - attributeKeys: { - select: { - id: true, - createdAt: true, - updatedAt: true, - name: true, - description: true, - type: true, - environmentId: true, - key: true, - isUnique: true, - }, - }, - }, - }, - organization: { - select: { - memberships: { - select: { - user: { - select: { - id: true, - email: true, - notificationSettings: true, - locale: true, - }, - }, - }, - }, - }, - }, - }, - }); - }); - }); -}); diff --git a/apps/web/app/api/cron/weekly-summary/lib/project.ts b/apps/web/app/api/cron/weekly-summary/lib/project.ts deleted file mode 100644 index d8c51561e0..0000000000 --- a/apps/web/app/api/cron/weekly-summary/lib/project.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { prisma } from "@formbricks/database"; -import { TWeeklySummaryProjectData } from "@formbricks/types/weekly-summary"; - -export const getProjectsByOrganizationId = async ( - organizationId: string -): Promise => { - const sevenDaysAgo = new Date(); - sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); - - return await prisma.project.findMany({ - where: { - organizationId: organizationId, - }, - select: { - id: true, - name: true, - environments: { - where: { - type: "production", - }, - select: { - id: true, - surveys: { - where: { - NOT: { - AND: [ - { status: "completed" }, - { - responses: { - none: { - createdAt: { - gte: sevenDaysAgo, - }, - }, - }, - }, - ], - }, - status: { - not: "draft", - }, - }, - select: { - id: true, - name: true, - questions: true, - status: true, - responses: { - where: { - createdAt: { - gte: sevenDaysAgo, - }, - }, - select: { - id: true, - createdAt: true, - updatedAt: true, - finished: true, - data: true, - }, - orderBy: { - createdAt: "desc", - }, - }, - displays: { - where: { - createdAt: { - gte: sevenDaysAgo, - }, - }, - select: { - id: true, - }, - }, - hiddenFields: true, - }, - }, - attributeKeys: { - select: { - id: true, - createdAt: true, - updatedAt: true, - name: true, - description: true, - type: true, - environmentId: true, - key: true, - isUnique: true, - }, - }, - }, - }, - organization: { - select: { - memberships: { - select: { - user: { - select: { - id: true, - email: true, - notificationSettings: true, - locale: true, - }, - }, - }, - }, - }, - }, - }, - }); -}; diff --git a/apps/web/app/api/cron/weekly-summary/route.ts b/apps/web/app/api/cron/weekly-summary/route.ts deleted file mode 100644 index 785db9ff8c..0000000000 --- a/apps/web/app/api/cron/weekly-summary/route.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { responses } from "@/app/lib/api/response"; -import { CRON_SECRET } from "@/lib/constants"; -import { hasUserEnvironmentAccess } from "@/lib/environment/auth"; -import { sendNoLiveSurveyNotificationEmail, sendWeeklySummaryNotificationEmail } from "@/modules/email"; -import { headers } from "next/headers"; -import { getNotificationResponse } from "./lib/notificationResponse"; -import { getOrganizationIds } from "./lib/organization"; -import { getProjectsByOrganizationId } from "./lib/project"; - -const BATCH_SIZE = 500; - -export const POST = async (): Promise => { - const headersList = await headers(); - // Check authentication - if (headersList.get("x-api-key") !== CRON_SECRET) { - return responses.notAuthenticatedResponse(); - } - - const emailSendingPromises: Promise[] = []; - - // Fetch all organization IDs - const organizationIds = await getOrganizationIds(); - - // Paginate through organizations - for (let i = 0; i < organizationIds.length; i += BATCH_SIZE) { - const batchedOrganizationIds = organizationIds.slice(i, i + BATCH_SIZE); - // Fetch projects for batched organizations asynchronously - const batchedProjectsPromises = batchedOrganizationIds.map((organizationId) => - getProjectsByOrganizationId(organizationId) - ); - - const batchedProjects = await Promise.all(batchedProjectsPromises); - for (const projects of batchedProjects) { - for (const project of projects) { - const organizationMembers = project.organization.memberships; - const organizationMembersWithNotificationEnabled = organizationMembers.filter( - (member) => - member.user.notificationSettings?.weeklySummary && - member.user.notificationSettings.weeklySummary[project.id] - ); - - if (organizationMembersWithNotificationEnabled.length === 0) continue; - - const notificationResponse = getNotificationResponse(project.environments[0], project.name); - - if (notificationResponse.insights.numLiveSurvey === 0) { - for (const organizationMember of organizationMembersWithNotificationEnabled) { - if (await hasUserEnvironmentAccess(organizationMember.user.id, project.environments[0].id)) { - emailSendingPromises.push( - sendNoLiveSurveyNotificationEmail(organizationMember.user.email, notificationResponse) - ); - } - } - continue; - } - - for (const organizationMember of organizationMembersWithNotificationEnabled) { - if (await hasUserEnvironmentAccess(organizationMember.user.id, project.environments[0].id)) { - emailSendingPromises.push( - sendWeeklySummaryNotificationEmail(organizationMember.user.email, notificationResponse) - ); - } - } - } - } - } - - await Promise.all(emailSendingPromises); - return responses.successResponse({}, true); -}; diff --git a/apps/web/app/page.test.tsx b/apps/web/app/page.test.tsx index a117a07ff3..796bb2f137 100644 --- a/apps/web/app/page.test.tsx +++ b/apps/web/app/page.test.tsx @@ -127,7 +127,6 @@ describe("Page", () => { objective: null, notificationSettings: { alert: {}, - weeklySummary: {}, unsubscribedOrganizationIds: [], }, locale: "en-US", @@ -171,7 +170,6 @@ describe("Page", () => { objective: null, notificationSettings: { alert: {}, - weeklySummary: {}, unsubscribedOrganizationIds: [], }, locale: "en-US", @@ -261,7 +259,6 @@ describe("Page", () => { objective: null, notificationSettings: { alert: {}, - weeklySummary: {}, unsubscribedOrganizationIds: [], }, locale: "en-US", @@ -351,7 +348,6 @@ describe("Page", () => { objective: null, notificationSettings: { alert: {}, - weeklySummary: {}, unsubscribedOrganizationIds: [], }, locale: "en-US", diff --git a/apps/web/lib/organization/service.test.ts b/apps/web/lib/organization/service.test.ts index d0129587f0..546253983a 100644 --- a/apps/web/lib/organization/service.test.ts +++ b/apps/web/lib/organization/service.test.ts @@ -273,7 +273,6 @@ describe("Organization Service", () => { id: "user-123", notificationSettings: { alert: { "existing-survey-id": true }, - weeklySummary: {}, unsubscribedOrganizationIds: [], // User is subscribed to all organizations }, } as any; @@ -296,7 +295,7 @@ describe("Organization Service", () => { "existing-survey-id": true, "survey-123": true, }, - weeklySummary: {}, + unsubscribedOrganizationIds: [], }, }); @@ -307,7 +306,6 @@ describe("Organization Service", () => { id: "user-123", notificationSettings: { alert: { "existing-survey-id": true }, - weeklySummary: {}, unsubscribedOrganizationIds: ["org-123"], // User has unsubscribed from this organization }, } as any; diff --git a/apps/web/lib/organization/service.ts b/apps/web/lib/organization/service.ts index 160a1d1317..0e2570a337 100644 --- a/apps/web/lib/organization/service.ts +++ b/apps/web/lib/organization/service.ts @@ -296,7 +296,7 @@ export const subscribeOrganizationMembersToSurveyResponses = async ( return; } - const defaultSettings = { alert: {}, weeklySummary: {} }; + const defaultSettings = { alert: {} }; const updatedNotificationSettings: TUserNotificationSettings = { ...defaultSettings, ...surveyCreator.notificationSettings, diff --git a/apps/web/lib/survey/__mock__/survey.mock.ts b/apps/web/lib/survey/__mock__/survey.mock.ts index 81347d0caa..316d7a53a4 100644 --- a/apps/web/lib/survey/__mock__/survey.mock.ts +++ b/apps/web/lib/survey/__mock__/survey.mock.ts @@ -130,7 +130,7 @@ export const mockUser: TUser = { objective: "improve_user_retention", notificationSettings: { alert: {}, - weeklySummary: {}, + unsubscribedOrganizationIds: [], }, role: "other", diff --git a/apps/web/lib/user/service.test.ts b/apps/web/lib/user/service.test.ts index d986497917..457f746db0 100644 --- a/apps/web/lib/user/service.test.ts +++ b/apps/web/lib/user/service.test.ts @@ -48,7 +48,7 @@ describe("User Service", () => { objective: Objective.increase_conversion, notificationSettings: { alert: {}, - weeklySummary: {}, + unsubscribedOrganizationIds: [], }, locale: "en-US" as TUserLocale, diff --git a/apps/web/modules/auth/invite/page.tsx b/apps/web/modules/auth/invite/page.tsx index 21bfe6ab31..91d6d5b282 100644 --- a/apps/web/modules/auth/invite/page.tsx +++ b/apps/web/modules/auth/invite/page.tsx @@ -107,7 +107,6 @@ export const InvitePage = async (props: InvitePageProps) => { notificationSettings: { ...user.notificationSettings, alert: user.notificationSettings.alert ?? {}, - weeklySummary: user.notificationSettings.weeklySummary ?? {}, unsubscribedOrganizationIds: Array.from( new Set([ ...(user.notificationSettings?.unsubscribedOrganizationIds || []), diff --git a/apps/web/modules/auth/lib/mock-data.ts b/apps/web/modules/auth/lib/mock-data.ts index 3dfd70c011..07227762c3 100644 --- a/apps/web/modules/auth/lib/mock-data.ts +++ b/apps/web/modules/auth/lib/mock-data.ts @@ -13,7 +13,7 @@ export const mockUser: TUser = { objective: "improve_user_retention", notificationSettings: { alert: {}, - weeklySummary: {}, + unsubscribedOrganizationIds: [], }, role: "other", diff --git a/apps/web/modules/auth/signup/actions.ts b/apps/web/modules/auth/signup/actions.ts index 8ed15a9ae9..d72bad55ba 100644 --- a/apps/web/modules/auth/signup/actions.ts +++ b/apps/web/modules/auth/signup/actions.ts @@ -124,7 +124,7 @@ async function handleInviteAcceptance( await updateUser(user.id, { notificationSettings: { alert: {}, - weeklySummary: {}, + unsubscribedOrganizationIds: [invite.organizationId], }, }); @@ -149,7 +149,7 @@ async function handleOrganizationCreation(ctx: ActionClientCtx, user: TCreatedUs notificationSettings: { ...user.notificationSettings, alert: { ...user.notificationSettings?.alert }, - weeklySummary: { ...user.notificationSettings?.weeklySummary }, + unsubscribedOrganizationIds: Array.from( new Set([...(user.notificationSettings?.unsubscribedOrganizationIds ?? []), organization.id]) ), diff --git a/apps/web/modules/ee/audit-logs/lib/utils.test.ts b/apps/web/modules/ee/audit-logs/lib/utils.test.ts index 6dffdd8b98..c68cd84a8a 100644 --- a/apps/web/modules/ee/audit-logs/lib/utils.test.ts +++ b/apps/web/modules/ee/audit-logs/lib/utils.test.ts @@ -127,7 +127,6 @@ describe("withAuditLogging", () => { objective: null, notificationSettings: { alert: {}, - weeklySummary: {}, }, }, organizationId: "org1", @@ -167,7 +166,6 @@ describe("withAuditLogging", () => { objective: null, notificationSettings: { alert: {}, - weeklySummary: {}, }, }, organizationId: "org1", diff --git a/apps/web/modules/ee/contacts/[contactId]/components/response-timeline.test.tsx b/apps/web/modules/ee/contacts/[contactId]/components/response-timeline.test.tsx index f40265db61..c938ee0c0e 100644 --- a/apps/web/modules/ee/contacts/[contactId]/components/response-timeline.test.tsx +++ b/apps/web/modules/ee/contacts/[contactId]/components/response-timeline.test.tsx @@ -38,7 +38,6 @@ describe("ResponseTimeline", () => { isActive: true, notificationSettings: { alert: {}, - weeklySummary: {}, }, locale: "en-US", lastLoginAt: new Date(), diff --git a/apps/web/modules/ee/sso/lib/sso-handlers.ts b/apps/web/modules/ee/sso/lib/sso-handlers.ts index ed12d14e65..895742a49f 100644 --- a/apps/web/modules/ee/sso/lib/sso-handlers.ts +++ b/apps/web/modules/ee/sso/lib/sso-handlers.ts @@ -216,9 +216,6 @@ export const handleSsoCallback = async ({ unsubscribedOrganizationIds: Array.from( new Set([...(userProfile.notificationSettings?.unsubscribedOrganizationIds || []), organization.id]) ), - weeklySummary: { - ...userProfile.notificationSettings?.weeklySummary, - }, }; await updateUser(userProfile.id, { diff --git a/apps/web/modules/ee/sso/lib/tests/__mock__/sso-handlers.mock.ts b/apps/web/modules/ee/sso/lib/tests/__mock__/sso-handlers.mock.ts index eb319298f7..2c02097ff5 100644 --- a/apps/web/modules/ee/sso/lib/tests/__mock__/sso-handlers.mock.ts +++ b/apps/web/modules/ee/sso/lib/tests/__mock__/sso-handlers.mock.ts @@ -9,7 +9,7 @@ export const mockUser: TUser = { name: "Test User", notificationSettings: { alert: {}, - weeklySummary: {}, + unsubscribedOrganizationIds: [], }, emailVerified: new Date(), diff --git a/apps/web/modules/email/emails/weekly-summary/create-reminder-notification-body.tsx b/apps/web/modules/email/emails/weekly-summary/create-reminder-notification-body.tsx deleted file mode 100644 index 3fbe96f2b0..0000000000 --- a/apps/web/modules/email/emails/weekly-summary/create-reminder-notification-body.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { WEBAPP_URL } from "@/lib/constants"; -import { getTranslate } from "@/tolgee/server"; -import { Container, Text } from "@react-email/components"; -import React from "react"; -import type { TWeeklySummaryNotificationResponse } from "@formbricks/types/weekly-summary"; -import { EmailButton } from "../../components/email-button"; -import { NotificationFooter } from "./notification-footer"; - -interface CreateReminderNotificationBodyProps { - notificationData: TWeeklySummaryNotificationResponse; -} - -export async function CreateReminderNotificationBody({ - notificationData, -}: CreateReminderNotificationBodyProps): Promise { - const t = await getTranslate(); - return ( - - - {t("emails.weekly_summary_create_reminder_notification_body_text", { - projectName: notificationData.projectName, - })} - - - {t("emails.weekly_summary_create_reminder_notification_body_dont_let_a_week_pass")} - - - - {t("emails.weekly_summary_create_reminder_notification_body_need_help")} - - {t("emails.weekly_summary_create_reminder_notification_body_cal_slot")} - - {t("emails.weekly_summary_create_reminder_notification_body_reply_email")} - - - - ); -} diff --git a/apps/web/modules/email/emails/weekly-summary/live-survey-notification.tsx b/apps/web/modules/email/emails/weekly-summary/live-survey-notification.tsx deleted file mode 100644 index 3f31811ef7..0000000000 --- a/apps/web/modules/email/emails/weekly-summary/live-survey-notification.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import { WEBAPP_URL } from "@/lib/constants"; -import { renderEmailResponseValue } from "@/modules/email/emails/lib/utils"; -import { getTranslate } from "@/tolgee/server"; -import { Container, Hr, Link, Tailwind, Text } from "@react-email/components"; -import { TFnType } from "@tolgee/react"; -import React, { type JSX } from "react"; -import type { TSurveyStatus } from "@formbricks/types/surveys/types"; -import type { - TWeeklySummaryNotificationDataSurvey, - TWeeklySummarySurveyResponseData, -} from "@formbricks/types/weekly-summary"; -import { EmailButton } from "../../components/email-button"; - -const getButtonLabel = (count: number, t: TFnType): string => { - if (count === 1) { - return t("emails.live_survey_notification_view_response"); - } - return t("emails.live_survey_notification_view_more_responses", { - responseCount: count > 2 ? (count - 1).toString() : "1", - }); -}; - -const convertSurveyStatus = (status: TSurveyStatus, t: TFnType): string => { - const statusMap = { - inProgress: t("emails.live_survey_notification_in_progress"), - paused: t("emails.live_survey_notification_paused"), - completed: t("emails.live_survey_notification_completed"), - draft: t("emails.live_survey_notification_draft"), - scheduled: t("emails.live_survey_notification_scheduled"), - }; - - return statusMap[status] || status; -}; - -interface LiveSurveyNotificationProps { - environmentId: string; - surveys: TWeeklySummaryNotificationDataSurvey[]; -} - -export async function LiveSurveyNotification({ - environmentId, - surveys, -}: LiveSurveyNotificationProps): Promise { - const t = await getTranslate(); - const createSurveyFields = ( - surveyResponses: TWeeklySummarySurveyResponseData[] - ): React.JSX.Element | React.JSX.Element[] => { - if (surveyResponses.length === 0) { - return ( - - - {t("emails.live_survey_notification_no_responses_yet")} - - - ); - } - const surveyFields: JSX.Element[] = []; - const responseCount = surveyResponses.length; - - surveyResponses.forEach((surveyResponse, index) => { - if (!surveyResponse.responseValue) { - return; - } - - surveyFields.push( - - {surveyResponse.headline} - {renderEmailResponseValue(surveyResponse.responseValue, surveyResponse.questionType, t)} - - ); - - // Add
only when there are 2 or more responses to display, and it's not the last response - if (responseCount >= 2 && index < responseCount - 1) { - surveyFields.push(
); - } - }); - - return surveyFields; - }; - - if (!surveys.length) return []; - - return surveys.map((survey) => { - const displayStatus = convertSurveyStatus(survey.status, t); - const isInProgress = displayStatus === "In Progress"; - const noResponseLastWeek = isInProgress && survey.responses.length === 0; - return ( - - - - - {survey.name} - - - - - {displayStatus} - - {noResponseLastWeek ? ( - {t("emails.live_survey_notification_no_new_response")} - ) : ( - createSurveyFields(survey.responses) - )} - {survey.responseCount > 0 && ( - - - - )} - - - ); - }); -} diff --git a/apps/web/modules/email/emails/weekly-summary/no-live-survey-notification-email.tsx b/apps/web/modules/email/emails/weekly-summary/no-live-survey-notification-email.tsx deleted file mode 100644 index 8f9d878855..0000000000 --- a/apps/web/modules/email/emails/weekly-summary/no-live-survey-notification-email.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React from "react"; -import type { TWeeklySummaryNotificationResponse } from "@formbricks/types/weekly-summary"; -import { CreateReminderNotificationBody } from "./create-reminder-notification-body"; -import { NotificationHeader } from "./notification-header"; - -interface NoLiveSurveyNotificationEmailProps { - notificationData: TWeeklySummaryNotificationResponse; - startDate: string; - endDate: string; - startYear: number; - endYear: number; -} - -export function NoLiveSurveyNotificationEmail({ - notificationData, - startDate, - endDate, - startYear, - endYear, -}: NoLiveSurveyNotificationEmailProps): React.JSX.Element { - return ( -
- - -
- ); -} diff --git a/apps/web/modules/email/emails/weekly-summary/notification-footer.tsx b/apps/web/modules/email/emails/weekly-summary/notification-footer.tsx deleted file mode 100644 index 19c8d417ef..0000000000 --- a/apps/web/modules/email/emails/weekly-summary/notification-footer.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { WEBAPP_URL } from "@/lib/constants"; -import { getTranslate } from "@/tolgee/server"; -import { Container, Link, Tailwind, Text } from "@react-email/components"; -import React from "react"; - -interface NotificatonFooterProps { - environmentId: string; -} -export async function NotificationFooter({ - environmentId, -}: NotificatonFooterProps): Promise { - const t = await getTranslate(); - return ( - - - {t("emails.notification_footer_all_the_best")} - {t("emails.notification_footer_the_formbricks_team")} - - - {t("emails.notification_footer_to_halt_weekly_updates")} - - {t("emails.notification_footer_please_turn_them_off")} - {" "} - {t("emails.notification_footer_in_your_settings")} 🙏 - - - - - ); -} diff --git a/apps/web/modules/email/emails/weekly-summary/notification-header.tsx b/apps/web/modules/email/emails/weekly-summary/notification-header.tsx deleted file mode 100644 index 542808d0a2..0000000000 --- a/apps/web/modules/email/emails/weekly-summary/notification-header.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { getTranslate } from "@/tolgee/server"; -import { Container, Heading, Text } from "@react-email/components"; -import React from "react"; - -interface NotificationHeaderProps { - projectName: string; - startDate: string; - endDate: string; - startYear: number; - endYear: number; -} - -export async function NotificationHeader({ - projectName, - startDate, - endDate, - startYear, - endYear, -}: NotificationHeaderProps): Promise { - const t = await getTranslate(); - const getNotificationHeaderTimePeriod = (): React.JSX.Element => { - if (startYear === endYear) { - return ( - - {startDate} - {endDate} {endYear} - - ); - } - - return ( - - {startDate} {startYear} - {endDate} {endYear} - - ); - }; - return ( - -
-
- {t("emails.notification_header_hey")} -
-
- - {t("emails.notification_header_weekly_report_for")} {projectName} - - {getNotificationHeaderTimePeriod()} -
-
-
- ); -} diff --git a/apps/web/modules/email/emails/weekly-summary/notification-insight.tsx b/apps/web/modules/email/emails/weekly-summary/notification-insight.tsx deleted file mode 100644 index f9bce12bf2..0000000000 --- a/apps/web/modules/email/emails/weekly-summary/notification-insight.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { getTranslate } from "@/tolgee/server"; -import { Column, Container, Row, Section, Text } from "@react-email/components"; -import React from "react"; -import type { TWeeklySummaryInsights } from "@formbricks/types/weekly-summary"; - -interface NotificationInsightProps { - insights: TWeeklySummaryInsights; -} - -export async function NotificationInsight({ - insights, -}: NotificationInsightProps): Promise { - const t = await getTranslate(); - return ( - -
- - - {t("emails.notification_insight_surveys")} - {insights.numLiveSurvey} - - - {t("emails.notification_insight_displays")} - {insights.totalDisplays} - - - {t("emails.notification_insight_responses")} - {insights.totalResponses} - - - {t("emails.notification_insight_completed")} - {insights.totalCompletedResponses} - - {insights.totalDisplays !== 0 ? ( - - {t("emails.notification_insight_completion_rate")} - {Math.round(insights.completionRate)}% - - ) : ( - "" - )} - -
-
- ); -} diff --git a/apps/web/modules/email/emails/weekly-summary/weekly-summary-notification-email.tsx b/apps/web/modules/email/emails/weekly-summary/weekly-summary-notification-email.tsx deleted file mode 100644 index df6b8348c6..0000000000 --- a/apps/web/modules/email/emails/weekly-summary/weekly-summary-notification-email.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { TFnType } from "@tolgee/react"; -import React from "react"; -import type { TWeeklySummaryNotificationResponse } from "@formbricks/types/weekly-summary"; -import { EmailTemplate } from "../../components/email-template"; -import { LiveSurveyNotification } from "./live-survey-notification"; -import { NotificationFooter } from "./notification-footer"; -import { NotificationHeader } from "./notification-header"; -import { NotificationInsight } from "./notification-insight"; - -interface WeeklySummaryNotificationEmailProps { - notificationData: TWeeklySummaryNotificationResponse; - startDate: string; - endDate: string; - startYear: number; - endYear: number; - t: TFnType; -} - -export function WeeklySummaryNotificationEmail({ - notificationData, - startDate, - endDate, - startYear, - endYear, - t, -}: WeeklySummaryNotificationEmailProps): React.JSX.Element { - return ( - - - - - - - ); -} diff --git a/apps/web/modules/email/index.tsx b/apps/web/modules/email/index.tsx index a75e673556..d8d1637b38 100644 --- a/apps/web/modules/email/index.tsx +++ b/apps/web/modules/email/index.tsx @@ -26,7 +26,6 @@ import { InvalidInputError } from "@formbricks/types/errors"; import type { TResponse } from "@formbricks/types/responses"; import type { TSurvey } from "@formbricks/types/surveys/types"; import { TUserEmail, TUserLocale } from "@formbricks/types/user"; -import type { TWeeklySummaryNotificationResponse } from "@formbricks/types/weekly-summary"; import { ForgotPasswordEmail } from "./emails/auth/forgot-password-email"; import { PasswordResetNotifyEmail } from "./emails/auth/password-reset-notify-email"; import { VerificationEmail } from "./emails/auth/verification-email"; @@ -35,8 +34,6 @@ import { InviteEmail } from "./emails/invite/invite-email"; import { EmbedSurveyPreviewEmail } from "./emails/survey/embed-survey-preview-email"; import { LinkSurveyEmail } from "./emails/survey/link-survey-email"; import { ResponseFinishedEmail } from "./emails/survey/response-finished-email"; -import { NoLiveSurveyNotificationEmail } from "./emails/weekly-summary/no-live-survey-notification-email"; -import { WeeklySummaryNotificationEmail } from "./emails/weekly-summary/weekly-summary-notification-email"; export const IS_SMTP_CONFIGURED = Boolean(SMTP_HOST && SMTP_PORT); @@ -290,70 +287,3 @@ export const sendLinkSurveyToVerifiedEmail = async (data: TLinkSurveyEmailData): html, }); }; - -export const sendWeeklySummaryNotificationEmail = async ( - email: string, - notificationData: TWeeklySummaryNotificationResponse -): Promise => { - const startDate = `${notificationData.lastWeekDate.getDate().toString()} ${notificationData.lastWeekDate.toLocaleString( - "default", - { month: "short" } - )}`; - const endDate = `${notificationData.currentDate.getDate().toString()} ${notificationData.currentDate.toLocaleString( - "default", - { month: "short" } - )}`; - const startYear = notificationData.lastWeekDate.getFullYear(); - const endYear = notificationData.currentDate.getFullYear(); - const t = await getTranslate(); - const html = await render( - WeeklySummaryNotificationEmail({ - notificationData, - startDate, - endDate, - startYear, - endYear, - t, - }) - ); - await sendEmail({ - to: email, - subject: t("emails.weekly_summary_email_subject", { - projectName: notificationData.projectName, - }), - html, - }); -}; - -export const sendNoLiveSurveyNotificationEmail = async ( - email: string, - notificationData: TWeeklySummaryNotificationResponse -): Promise => { - const startDate = `${notificationData.lastWeekDate.getDate().toString()} ${notificationData.lastWeekDate.toLocaleString( - "default", - { month: "short" } - )}`; - const endDate = `${notificationData.currentDate.getDate().toString()} ${notificationData.currentDate.toLocaleString( - "default", - { month: "short" } - )}`; - const startYear = notificationData.lastWeekDate.getFullYear(); - const endYear = notificationData.currentDate.getFullYear(); - const t = await getTranslate(); - const html = await render( - NoLiveSurveyNotificationEmail({ - notificationData, - startDate, - endDate, - startYear, - endYear, - }) - ); - await sendEmail({ - to: email, - subject: t("emails.weekly_summary_email_subject", { - projectName: notificationData.projectName, - }), - html, - }); -}; diff --git a/apps/web/modules/organization/actions.ts b/apps/web/modules/organization/actions.ts index dba70c0f9a..9f95190999 100644 --- a/apps/web/modules/organization/actions.ts +++ b/apps/web/modules/organization/actions.ts @@ -36,7 +36,7 @@ export const createOrganizationAction = authenticatedActionClient.schema(ZCreate accepted: true, }); - const project = await createProject(newOrganization.id, { + await createProject(newOrganization.id, { name: "My Project", }); @@ -45,10 +45,7 @@ export const createOrganizationAction = authenticatedActionClient.schema(ZCreate alert: { ...ctx.user.notificationSettings?.alert, }, - weeklySummary: { - ...ctx.user.notificationSettings?.weeklySummary, - [project.id]: true, - }, + unsubscribedOrganizationIds: Array.from( new Set([...(ctx.user.notificationSettings?.unsubscribedOrganizationIds || []), newOrganization.id]) // NOSONAR // We want to check for empty strings too ), diff --git a/apps/web/modules/setup/organization/create/components/removed-from-organization.test.tsx b/apps/web/modules/setup/organization/create/components/removed-from-organization.test.tsx index 9e8ff9c2d6..b5a51a6756 100644 --- a/apps/web/modules/setup/organization/create/components/removed-from-organization.test.tsx +++ b/apps/web/modules/setup/organization/create/components/removed-from-organization.test.tsx @@ -63,7 +63,6 @@ const mockUser = { updatedAt: new Date(), notificationSettings: { alert: {}, - weeklySummary: {}, }, role: "other", } as TUser; diff --git a/apps/web/modules/setup/organization/create/page.test.tsx b/apps/web/modules/setup/organization/create/page.test.tsx index 7124f5ed8a..785d9ebb39 100644 --- a/apps/web/modules/setup/organization/create/page.test.tsx +++ b/apps/web/modules/setup/organization/create/page.test.tsx @@ -160,16 +160,6 @@ describe("CreateOrganizationPage", () => { surveyUpdated: true, surveyCreated: true, }, - weeklySummary: { - surveyInvite: true, - surveyResponse: true, - surveyClosed: true, - surveyPaused: true, - surveyCompleted: true, - surveyDeleted: true, - surveyUpdated: true, - surveyCreated: true, - }, unsubscribedOrganizationIds: [], }, locale: "en-US" as const, diff --git a/docker/cronjobs b/docker/cronjobs index 919421c1d4..e118a434e7 100644 --- a/docker/cronjobs +++ b/docker/cronjobs @@ -1,3 +1,3 @@ 0 0 * * * curl $WEBAPP_URL/api/cron/survey-status -X POST -H 'content-type: application/json' -H 'x-api-key: '"$CRON_SECRET"'' -0 8 * * 1 curl $WEBAPP_URL/api/cron/weekly-summary -X POST -H 'content-type: application/json' -H 'x-api-key: '"$CRON_SECRET"'' + 0 9 * * * curl $WEBAPP_URL/api/cron/ping -X POST -H 'content-type: application/json' -H 'x-api-key: '"$CRON_SECRET"'' diff --git a/infra/formbricks-cloud-helm/values-staging.yaml.gotmpl b/infra/formbricks-cloud-helm/values-staging.yaml.gotmpl index def821965d..5d3b0b1aa4 100644 --- a/infra/formbricks-cloud-helm/values-staging.yaml.gotmpl +++ b/infra/formbricks-cloud-helm/values-staging.yaml.gotmpl @@ -48,29 +48,7 @@ cronJob: tag: latest schedule: 0 0 * * * successfulJobsHistoryLimit: 0 - weekly-summary: - args: - - /bin/sh - - -c - - 'curl -X POST -H "content-type: application/json" -H "x-api-key: $CRON_SECRET" - "$WEBAPP_URL/api/cron/weekly-summary"' - env: - CRON_SECRET: - valueFrom: - secretKeyRef: - key: CRON_SECRET - name: formbricks-stage-app-env - WEBAPP_URL: - valueFrom: - secretKeyRef: - key: WEBAPP_URL - name: formbricks-stage-app-env - image: - imagePullPolicy: IfNotPresent - repository: quay.io/curl/curl - tag: latest - schedule: 0 8 * * 1 - successfulJobsHistoryLimit: 0 + ## Deployment & Autoscaling deployment: diff --git a/infra/formbricks-cloud-helm/values.yaml.gotmpl b/infra/formbricks-cloud-helm/values.yaml.gotmpl index 514906e78e..0d05350022 100644 --- a/infra/formbricks-cloud-helm/values.yaml.gotmpl +++ b/infra/formbricks-cloud-helm/values.yaml.gotmpl @@ -47,29 +47,7 @@ cronJob: tag: latest schedule: 0 0 * * * successfulJobsHistoryLimit: 0 - weekely-summary: - args: - - /bin/sh - - -c - - 'curl -X POST -H "content-type: application/json" -H "x-api-key: $CRON_SECRET" - "$WEBAPP_URL/api/cron/weekly-summary"' - env: - CRON_SECRET: - valueFrom: - secretKeyRef: - key: CRON_SECRET - name: formbricks-app-env - WEBAPP_URL: - valueFrom: - secretKeyRef: - key: WEBAPP_URL - name: formbricks-app-env - image: - imagePullPolicy: IfNotPresent - repository: quay.io/curl/curl - tag: latest - schedule: 0 8 * * 1 - successfulJobsHistoryLimit: 0 + ## Deployment & Autoscaling deployment: diff --git a/packages/database/migration/20250722075349_sunset_weekly_summary/migration.ts b/packages/database/migration/20250722075349_sunset_weekly_summary/migration.ts new file mode 100644 index 0000000000..2ca949c30f --- /dev/null +++ b/packages/database/migration/20250722075349_sunset_weekly_summary/migration.ts @@ -0,0 +1,15 @@ +import type { MigrationScript } from "../../src/scripts/migration-runner"; + +export const sunsetWeeklySummary: MigrationScript = { + type: "data", + id: "v0ruecekavlfu8410htz4mly", + name: "20250722075349_sunset_weekly_summary", + run: async ({ tx }) => { + // Remove weeklySummary from notificationSettings for all users where it is set + await tx.$queryRaw` + UPDATE "User" + SET "notificationSettings" = "notificationSettings" - 'weeklySummary' + WHERE "notificationSettings"->>'weeklySummary' IS NOT NULL + `; + }, +}; diff --git a/packages/types/user.ts b/packages/types/user.ts index f591078dae..7892b3938e 100644 --- a/packages/types/user.ts +++ b/packages/types/user.ts @@ -18,7 +18,6 @@ export type TUserObjective = z.infer; export const ZUserNotificationSettings = z.object({ alert: z.record(z.boolean()), - weeklySummary: z.record(z.boolean()), unsubscribedOrganizationIds: z.array(z.string()).optional(), }); diff --git a/packages/types/weekly-summary.ts b/packages/types/weekly-summary.ts deleted file mode 100644 index f2c7d95312..0000000000 --- a/packages/types/weekly-summary.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { z } from "zod"; -import { ZContactAttributeKey } from "./contact-attribute-key"; -import { ZResponseData } from "./responses"; -import { ZSurveyHiddenFields, ZSurveyQuestion, ZSurveyQuestionType, ZSurveyStatus } from "./surveys/types"; -import { ZUserNotificationSettings } from "./user"; - -const ZWeeklySummaryInsights = z.object({ - totalCompletedResponses: z.number(), - totalDisplays: z.number(), - totalResponses: z.number(), - completionRate: z.number(), - numLiveSurvey: z.number(), -}); - -export type TWeeklySummaryInsights = z.infer; - -export const ZWeeklySummarySurveyResponseData = z.object({ - headline: z.string(), - responseValue: z.union([z.string(), z.array(z.string())]), - questionType: ZSurveyQuestionType, -}); - -export type TWeeklySummarySurveyResponseData = z.infer; - -export const ZWeeklySummaryNotificationDataSurvey = z.object({ - id: z.string(), - name: z.string(), - responses: z.array(ZWeeklySummarySurveyResponseData), - responseCount: z.number(), - status: ZSurveyStatus, -}); - -export type TWeeklySummaryNotificationDataSurvey = z.infer; - -export const ZWeeklySummaryNotificationResponse = z.object({ - environmentId: z.string(), - currentDate: z.date(), - lastWeekDate: z.date(), - projectName: z.string(), - surveys: z.array(ZWeeklySummaryNotificationDataSurvey), - insights: ZWeeklySummaryInsights, -}); - -export type TWeeklySummaryNotificationResponse = z.infer; - -export const ZWeeklyEmailResponseData = z.object({ - id: z.string(), - createdAt: z.date(), - updatedAt: z.date(), - finished: z.boolean(), - data: ZResponseData, -}); - -export type TWeeklyEmailResponseData = z.infer; - -export const ZWeeklySummarySurveyData = z.object({ - id: z.string(), - name: z.string(), - questions: z.array(ZSurveyQuestion), - status: ZSurveyStatus, - responses: z.array(ZWeeklyEmailResponseData), - displays: z.array(z.object({ id: z.string() })), - hiddenFields: ZSurveyHiddenFields, -}); - -export type TWeeklySummarySurveyData = z.infer; - -export const ZWeeklySummaryEnvironmentData = z.object({ - id: z.string(), - surveys: z.array(ZWeeklySummarySurveyData), - attributeKeys: z.array(ZContactAttributeKey), -}); - -export type TWeeklySummaryEnvironmentData = z.infer; - -export const ZWeeklySummaryUserData = z.object({ - id: z.string(), - email: z.string(), - notificationSettings: ZUserNotificationSettings, - locale: z.string(), -}); - -export type TWeeklySummaryUserData = z.infer; - -export const ZWeeklySummaryMembershipData = z.object({ - user: ZWeeklySummaryUserData, -}); - -export type TWeeklySummaryMembershipData = z.infer; - -export const ZWeeklyEmailOrganizationData = z.object({ - memberships: z.array(ZWeeklySummaryMembershipData), -}); - -export type TWeeklyEmailOrganizationData = z.infer; - -export const ZWeeklySummaryProjectData = z.object({ - id: z.string(), - name: z.string(), - environments: z.array(ZWeeklySummaryEnvironmentData), - organization: ZWeeklyEmailOrganizationData, -}); - -export type TWeeklySummaryProjectData = z.infer;