mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-31 00:50:34 -06:00
chore: sunset weekly summary (#6282)
This commit is contained in:
@@ -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, {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -49,7 +49,6 @@ const mockUser = {
|
||||
email: "test@example.com",
|
||||
notificationSettings: {
|
||||
alert: {},
|
||||
weeklySummary: {},
|
||||
unsubscribedOrganizationIds: [],
|
||||
},
|
||||
role: "project_manager",
|
||||
|
||||
@@ -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: () => <div data-testid="users-icon" />,
|
||||
}));
|
||||
|
||||
vi.mock("next/link", () => ({
|
||||
default: ({ children, href }: { children: React.ReactNode; href: string }) => (
|
||||
<a href={href} data-testid="link">
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}));
|
||||
|
||||
const mockNotificationSwitch = vi.fn();
|
||||
vi.mock("./NotificationSwitch", () => ({
|
||||
NotificationSwitch: (props: any) => {
|
||||
mockNotificationSwitch(props);
|
||||
return (
|
||||
<div data-testid={`notification-switch-${props.surveyOrProjectOrOrganizationId}`}>
|
||||
NotificationSwitch
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
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(<EditWeeklySummary memberships={mockMemberships} user={mockUser} environmentId={environmentId} />);
|
||||
|
||||
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(<EditWeeklySummary memberships={[]} user={mockUser} environmentId={environmentId} />);
|
||||
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(
|
||||
<EditWeeklySummary
|
||||
memberships={membershipsWithNoProjects}
|
||||
user={mockUser}
|
||||
environmentId={environmentId}
|
||||
/>
|
||||
);
|
||||
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
|
||||
});
|
||||
});
|
||||
@@ -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) => (
|
||||
<div key={membership.organization.id}>
|
||||
<div className="mb-5 flex items-center space-x-3 text-sm font-medium">
|
||||
<UsersIcon className="h-6 w-7 text-slate-600" />
|
||||
|
||||
<p className="text-slate-800">{membership.organization.name}</p>
|
||||
</div>
|
||||
<div className="mb-6 rounded-lg border border-slate-200">
|
||||
<div className="grid h-12 grid-cols-3 content-center rounded-t-lg bg-slate-100 px-4 text-left text-sm font-semibold text-slate-900">
|
||||
<div className="col-span-2">{t("common.project")}</div>
|
||||
<div className="col-span-1 text-center">{t("common.weekly_summary")}</div>
|
||||
</div>
|
||||
<div className="space-y-1 p-2">
|
||||
{membership.organization.projects.map((project) => (
|
||||
<div
|
||||
className="grid h-auto w-full cursor-pointer grid-cols-3 place-content-center justify-center rounded-lg px-2 py-2 text-left text-sm text-slate-900 hover:bg-slate-50"
|
||||
key={project.id}>
|
||||
<div className="col-span-2">{project?.name}</div>
|
||||
<div className="col-span-1 flex items-center justify-center">
|
||||
<NotificationSwitch
|
||||
surveyOrProjectOrOrganizationId={project.id}
|
||||
notificationSettings={user.notificationSettings!}
|
||||
notificationType={"weeklySummary"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="pb-3 pl-4 text-xs text-slate-400">
|
||||
{t("environments.settings.notifications.want_to_loop_in_organization_mates")}?{" "}
|
||||
<Link className="font-semibold" href={`/environments/${environmentId}/settings/general`}>
|
||||
{t("common.invite_them")}
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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(() => <div>EditAlertsComponent</div>),
|
||||
}));
|
||||
vi.mock("./components/EditWeeklySummary", () => ({
|
||||
EditWeeklySummary: vi.fn(() => <div>EditWeeklySummaryComponent</div>),
|
||||
}));
|
||||
|
||||
vi.mock("./components/IntegrationsTip", () => ({
|
||||
IntegrationsTip: () => <div>IntegrationsTipComponent</div>,
|
||||
}));
|
||||
@@ -71,7 +68,6 @@ const mockUser: Partial<TUser> = {
|
||||
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: [],
|
||||
};
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
/>
|
||||
</SettingsCard>
|
||||
<IntegrationsTip environmentId={params.environmentId} />
|
||||
<SettingsCard
|
||||
title={t("environments.settings.notifications.weekly_summary_projects")}
|
||||
description={t("environments.settings.notifications.stay_up_to_date_with_a_Weekly_every_Monday")}>
|
||||
<EditWeeklySummary memberships={memberships} user={user} environmentId={params.environmentId} />
|
||||
</SettingsCard>
|
||||
</PageContentWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -20,7 +20,7 @@ const mockUser = {
|
||||
email: "test@example.com",
|
||||
notificationSettings: {
|
||||
alert: {},
|
||||
weeklySummary: {},
|
||||
|
||||
unsubscribedOrganizationIds: [],
|
||||
},
|
||||
twoFactorEnabled: false,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -13,7 +13,7 @@ const mockUser = {
|
||||
locale: "en-US",
|
||||
notificationSettings: {
|
||||
alert: {},
|
||||
weeklySummary: {},
|
||||
|
||||
unsubscribedOrganizationIds: [],
|
||||
},
|
||||
twoFactorEnabled: false,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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[] = [
|
||||
|
||||
@@ -305,12 +305,8 @@ const mockUser: TUser = {
|
||||
isActive: true,
|
||||
notificationSettings: {
|
||||
alert: {
|
||||
weeklySummary: true,
|
||||
responseFinished: true,
|
||||
},
|
||||
weeklySummary: {
|
||||
test: true,
|
||||
},
|
||||
unsubscribedOrganizationIds: [],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -253,7 +253,6 @@ const mockUser: TUser = {
|
||||
isActive: true,
|
||||
notificationSettings: {
|
||||
alert: {},
|
||||
weeklySummary: {},
|
||||
unsubscribedOrganizationIds: [],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
import { prisma } from "@formbricks/database";
|
||||
|
||||
export const getOrganizationIds = async (): Promise<string[]> => {
|
||||
const organizations = await prisma.organization.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
return organizations.map((organization) => organization.id);
|
||||
};
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,111 +0,0 @@
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { TWeeklySummaryProjectData } from "@formbricks/types/weekly-summary";
|
||||
|
||||
export const getProjectsByOrganizationId = async (
|
||||
organizationId: string
|
||||
): Promise<TWeeklySummaryProjectData[]> => {
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -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<Response> => {
|
||||
const headersList = await headers();
|
||||
// Check authentication
|
||||
if (headersList.get("x-api-key") !== CRON_SECRET) {
|
||||
return responses.notAuthenticatedResponse();
|
||||
}
|
||||
|
||||
const emailSendingPromises: Promise<void>[] = [];
|
||||
|
||||
// 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);
|
||||
};
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -296,7 +296,7 @@ export const subscribeOrganizationMembersToSurveyResponses = async (
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultSettings = { alert: {}, weeklySummary: {} };
|
||||
const defaultSettings = { alert: {} };
|
||||
const updatedNotificationSettings: TUserNotificationSettings = {
|
||||
...defaultSettings,
|
||||
...surveyCreator.notificationSettings,
|
||||
|
||||
@@ -130,7 +130,7 @@ export const mockUser: TUser = {
|
||||
objective: "improve_user_retention",
|
||||
notificationSettings: {
|
||||
alert: {},
|
||||
weeklySummary: {},
|
||||
|
||||
unsubscribedOrganizationIds: [],
|
||||
},
|
||||
role: "other",
|
||||
|
||||
@@ -48,7 +48,7 @@ describe("User Service", () => {
|
||||
objective: Objective.increase_conversion,
|
||||
notificationSettings: {
|
||||
alert: {},
|
||||
weeklySummary: {},
|
||||
|
||||
unsubscribedOrganizationIds: [],
|
||||
},
|
||||
locale: "en-US" as TUserLocale,
|
||||
|
||||
@@ -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 || []),
|
||||
|
||||
@@ -13,7 +13,7 @@ export const mockUser: TUser = {
|
||||
objective: "improve_user_retention",
|
||||
notificationSettings: {
|
||||
alert: {},
|
||||
weeklySummary: {},
|
||||
|
||||
unsubscribedOrganizationIds: [],
|
||||
},
|
||||
role: "other",
|
||||
|
||||
@@ -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])
|
||||
),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -38,7 +38,6 @@ describe("ResponseTimeline", () => {
|
||||
isActive: true,
|
||||
notificationSettings: {
|
||||
alert: {},
|
||||
weeklySummary: {},
|
||||
},
|
||||
locale: "en-US",
|
||||
lastLoginAt: new Date(),
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -9,7 +9,7 @@ export const mockUser: TUser = {
|
||||
name: "Test User",
|
||||
notificationSettings: {
|
||||
alert: {},
|
||||
weeklySummary: {},
|
||||
|
||||
unsubscribedOrganizationIds: [],
|
||||
},
|
||||
emailVerified: new Date(),
|
||||
|
||||
@@ -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<React.JSX.Element> {
|
||||
const t = await getTranslate();
|
||||
return (
|
||||
<Container>
|
||||
<Text className="text-sm">
|
||||
{t("emails.weekly_summary_create_reminder_notification_body_text", {
|
||||
projectName: notificationData.projectName,
|
||||
})}
|
||||
</Text>
|
||||
<Text className="pt-4 text-sm font-medium">
|
||||
{t("emails.weekly_summary_create_reminder_notification_body_dont_let_a_week_pass")}
|
||||
</Text>
|
||||
<EmailButton
|
||||
href={`${WEBAPP_URL}/environments/${notificationData.environmentId}/surveys?utm_source=weekly&utm_medium=email&utm_content=SetupANewSurveyCTA`}
|
||||
label={t("emails.weekly_summary_create_reminder_notification_body_setup_a_new_survey")}
|
||||
/>
|
||||
<Text className="pt-4 text-sm">
|
||||
{t("emails.weekly_summary_create_reminder_notification_body_need_help")}
|
||||
<a href="https://cal.com/johannes/15">
|
||||
{t("emails.weekly_summary_create_reminder_notification_body_cal_slot")}
|
||||
</a>
|
||||
{t("emails.weekly_summary_create_reminder_notification_body_reply_email")}
|
||||
</Text>
|
||||
<NotificationFooter environmentId={notificationData.environmentId} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -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<React.JSX.Element[]> {
|
||||
const t = await getTranslate();
|
||||
const createSurveyFields = (
|
||||
surveyResponses: TWeeklySummarySurveyResponseData[]
|
||||
): React.JSX.Element | React.JSX.Element[] => {
|
||||
if (surveyResponses.length === 0) {
|
||||
return (
|
||||
<Container className="mt-4">
|
||||
<Text className="m-0 text-sm font-medium">
|
||||
{t("emails.live_survey_notification_no_responses_yet")}
|
||||
</Text>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
const surveyFields: JSX.Element[] = [];
|
||||
const responseCount = surveyResponses.length;
|
||||
|
||||
surveyResponses.forEach((surveyResponse, index) => {
|
||||
if (!surveyResponse.responseValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
surveyFields.push(
|
||||
<Container className="mt-4" key={`${index.toString()}-${surveyResponse.headline}`}>
|
||||
<Text className="m-0 text-sm">{surveyResponse.headline}</Text>
|
||||
{renderEmailResponseValue(surveyResponse.responseValue, surveyResponse.questionType, t)}
|
||||
</Container>
|
||||
);
|
||||
|
||||
// Add <hr/> 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(<Hr key={`hr-${index.toString()}`} />);
|
||||
}
|
||||
});
|
||||
|
||||
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 (
|
||||
<Tailwind key={survey.id}>
|
||||
<Container className="mt-12">
|
||||
<Text className="mb-0 inline">
|
||||
<Link
|
||||
className="text-sm text-black underline"
|
||||
href={`${WEBAPP_URL}/environments/${environmentId}/surveys/${survey.id}/responses?utm_source=weekly&utm_medium=email&utm_content=ViewResponsesCTA`}>
|
||||
{survey.name}
|
||||
</Link>
|
||||
</Text>
|
||||
|
||||
<Text
|
||||
className={`ml-2 inline ${isInProgress ? "bg-green-400 text-slate-100" : "bg-slate-300 text-blue-800"} rounded-full px-2 py-1 text-sm`}>
|
||||
{displayStatus}
|
||||
</Text>
|
||||
{noResponseLastWeek ? (
|
||||
<Text className="text-sm">{t("emails.live_survey_notification_no_new_response")}</Text>
|
||||
) : (
|
||||
createSurveyFields(survey.responses)
|
||||
)}
|
||||
{survey.responseCount > 0 && (
|
||||
<Container className="mt-4 block text-sm">
|
||||
<EmailButton
|
||||
href={`${WEBAPP_URL}/environments/${environmentId}/surveys/${survey.id}/responses?utm_source=weekly&utm_medium=email&utm_content=ViewResponsesCTA`}
|
||||
label={
|
||||
noResponseLastWeek
|
||||
? t("emails.live_survey_notification_view_previous_responses")
|
||||
: getButtonLabel(survey.responseCount, t)
|
||||
}
|
||||
/>
|
||||
</Container>
|
||||
)}
|
||||
</Container>
|
||||
</Tailwind>
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -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 (
|
||||
<div>
|
||||
<NotificationHeader
|
||||
endDate={endDate}
|
||||
endYear={endYear}
|
||||
projectName={notificationData.projectName}
|
||||
startDate={startDate}
|
||||
startYear={startYear}
|
||||
/>
|
||||
<CreateReminderNotificationBody notificationData={notificationData} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<React.JSX.Element> {
|
||||
const t = await getTranslate();
|
||||
return (
|
||||
<Tailwind>
|
||||
<Container className="w-full">
|
||||
<Text className="mb-0 pt-4 text-sm font-medium">{t("emails.notification_footer_all_the_best")}</Text>
|
||||
<Text className="mt-0 text-sm">{t("emails.notification_footer_the_formbricks_team")}</Text>
|
||||
<Container
|
||||
className="mt-0 w-full rounded-md bg-slate-100 px-4 text-center text-xs leading-5"
|
||||
style={{ fontStyle: "italic" }}>
|
||||
<Text className="text-sm">
|
||||
{t("emails.notification_footer_to_halt_weekly_updates")}
|
||||
<Link
|
||||
className="text-sm text-black underline"
|
||||
href={`${WEBAPP_URL}/environments/${environmentId}/settings/notifications`}>
|
||||
{t("emails.notification_footer_please_turn_them_off")}
|
||||
</Link>{" "}
|
||||
{t("emails.notification_footer_in_your_settings")} 🙏
|
||||
</Text>
|
||||
</Container>
|
||||
</Container>
|
||||
</Tailwind>
|
||||
);
|
||||
}
|
||||
@@ -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<React.JSX.Element> {
|
||||
const t = await getTranslate();
|
||||
const getNotificationHeaderTimePeriod = (): React.JSX.Element => {
|
||||
if (startYear === endYear) {
|
||||
return (
|
||||
<Text className="m-0 text-right text-sm">
|
||||
{startDate} - {endDate} {endYear}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Text className="m-0 text-right text-sm">
|
||||
{startDate} {startYear} - {endDate} {endYear}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<Container>
|
||||
<div className="block px-0 py-4">
|
||||
<div className="float-left mt-2">
|
||||
<Heading className="m-0">{t("emails.notification_header_hey")}</Heading>
|
||||
</div>
|
||||
<div className="float-right">
|
||||
<Text className="m-0 text-right text-sm font-medium">
|
||||
{t("emails.notification_header_weekly_report_for")} {projectName}
|
||||
</Text>
|
||||
{getNotificationHeaderTimePeriod()}
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -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<React.JSX.Element> {
|
||||
const t = await getTranslate();
|
||||
return (
|
||||
<Container>
|
||||
<Section className="my-4 rounded-md bg-slate-100">
|
||||
<Row>
|
||||
<Column className="text-center">
|
||||
<Text className="text-sm">{t("emails.notification_insight_surveys")}</Text>
|
||||
<Text className="text-sm font-medium">{insights.numLiveSurvey}</Text>
|
||||
</Column>
|
||||
<Column className="text-center">
|
||||
<Text className="text-sm">{t("emails.notification_insight_displays")}</Text>
|
||||
<Text className="text-sm font-medium">{insights.totalDisplays}</Text>
|
||||
</Column>
|
||||
<Column className="text-center">
|
||||
<Text className="text-sm">{t("emails.notification_insight_responses")}</Text>
|
||||
<Text className="text-sm font-medium">{insights.totalResponses}</Text>
|
||||
</Column>
|
||||
<Column className="text-center">
|
||||
<Text className="text-sm">{t("emails.notification_insight_completed")}</Text>
|
||||
<Text className="text-sm font-medium">{insights.totalCompletedResponses}</Text>
|
||||
</Column>
|
||||
{insights.totalDisplays !== 0 ? (
|
||||
<Column className="text-center">
|
||||
<Text className="text-sm">{t("emails.notification_insight_completion_rate")}</Text>
|
||||
<Text className="text-sm font-medium">{Math.round(insights.completionRate)}%</Text>
|
||||
</Column>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Row>
|
||||
</Section>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<EmailTemplate t={t}>
|
||||
<NotificationHeader
|
||||
endDate={endDate}
|
||||
endYear={endYear}
|
||||
projectName={notificationData.projectName}
|
||||
startDate={startDate}
|
||||
startYear={startYear}
|
||||
/>
|
||||
<NotificationInsight insights={notificationData.insights} />
|
||||
<LiveSurveyNotification
|
||||
environmentId={notificationData.environmentId}
|
||||
surveys={notificationData.surveys}
|
||||
/>
|
||||
<NotificationFooter environmentId={notificationData.environmentId} />
|
||||
</EmailTemplate>
|
||||
);
|
||||
}
|
||||
@@ -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<void> => {
|
||||
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<void> => {
|
||||
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,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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
|
||||
),
|
||||
|
||||
@@ -63,7 +63,6 @@ const mockUser = {
|
||||
updatedAt: new Date(),
|
||||
notificationSettings: {
|
||||
alert: {},
|
||||
weeklySummary: {},
|
||||
},
|
||||
role: "other",
|
||||
} as TUser;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"''
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
`;
|
||||
},
|
||||
};
|
||||
@@ -18,7 +18,6 @@ export type TUserObjective = z.infer<typeof ZUserObjective>;
|
||||
|
||||
export const ZUserNotificationSettings = z.object({
|
||||
alert: z.record(z.boolean()),
|
||||
weeklySummary: z.record(z.boolean()),
|
||||
unsubscribedOrganizationIds: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
|
||||
@@ -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<typeof ZWeeklySummaryInsights>;
|
||||
|
||||
export const ZWeeklySummarySurveyResponseData = z.object({
|
||||
headline: z.string(),
|
||||
responseValue: z.union([z.string(), z.array(z.string())]),
|
||||
questionType: ZSurveyQuestionType,
|
||||
});
|
||||
|
||||
export type TWeeklySummarySurveyResponseData = z.infer<typeof ZWeeklySummarySurveyResponseData>;
|
||||
|
||||
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<typeof ZWeeklySummaryNotificationDataSurvey>;
|
||||
|
||||
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<typeof ZWeeklySummaryNotificationResponse>;
|
||||
|
||||
export const ZWeeklyEmailResponseData = z.object({
|
||||
id: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
finished: z.boolean(),
|
||||
data: ZResponseData,
|
||||
});
|
||||
|
||||
export type TWeeklyEmailResponseData = z.infer<typeof ZWeeklyEmailResponseData>;
|
||||
|
||||
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<typeof ZWeeklySummarySurveyData>;
|
||||
|
||||
export const ZWeeklySummaryEnvironmentData = z.object({
|
||||
id: z.string(),
|
||||
surveys: z.array(ZWeeklySummarySurveyData),
|
||||
attributeKeys: z.array(ZContactAttributeKey),
|
||||
});
|
||||
|
||||
export type TWeeklySummaryEnvironmentData = z.infer<typeof ZWeeklySummaryEnvironmentData>;
|
||||
|
||||
export const ZWeeklySummaryUserData = z.object({
|
||||
id: z.string(),
|
||||
email: z.string(),
|
||||
notificationSettings: ZUserNotificationSettings,
|
||||
locale: z.string(),
|
||||
});
|
||||
|
||||
export type TWeeklySummaryUserData = z.infer<typeof ZWeeklySummaryUserData>;
|
||||
|
||||
export const ZWeeklySummaryMembershipData = z.object({
|
||||
user: ZWeeklySummaryUserData,
|
||||
});
|
||||
|
||||
export type TWeeklySummaryMembershipData = z.infer<typeof ZWeeklySummaryMembershipData>;
|
||||
|
||||
export const ZWeeklyEmailOrganizationData = z.object({
|
||||
memberships: z.array(ZWeeklySummaryMembershipData),
|
||||
});
|
||||
|
||||
export type TWeeklyEmailOrganizationData = z.infer<typeof ZWeeklyEmailOrganizationData>;
|
||||
|
||||
export const ZWeeklySummaryProjectData = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
environments: z.array(ZWeeklySummaryEnvironmentData),
|
||||
organization: ZWeeklyEmailOrganizationData,
|
||||
});
|
||||
|
||||
export type TWeeklySummaryProjectData = z.infer<typeof ZWeeklySummaryProjectData>;
|
||||
Reference in New Issue
Block a user