feat: implement comprehensive notification engine with policies, preferences, and automated dispatching services
This commit is contained in:
@@ -0,0 +1,312 @@
|
||||
import { NotificationChannel } from "../../NotificationJobs/NotificationJobs.Interface";
|
||||
|
||||
// Mock all external dependencies before importing the module under test
|
||||
jest.mock("../../PlanSubscriptions/PlanSubscriptons", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
checkFeature: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../CompanyNotificationPolicy.Adapter.Mongoose", () => ({
|
||||
CompanyNotificationPolicyAdapterMongoose: jest.fn().mockImplementation(() => ({
|
||||
findOne: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock("../ClientNotificationPreferences.Adapter.Mongoose", () => ({
|
||||
ClientNotificationPreferencesAdapterMongoose: jest.fn().mockImplementation(() => ({
|
||||
findOne: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock("../ClientCompanyNotificationOverride.Adapter.Mongoose", () => ({
|
||||
ClientCompanyNotificationOverrideAdapterMongoose: jest.fn().mockImplementation(() => ({
|
||||
findOne: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock("../../Companies/Companies", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
companies: {
|
||||
findOne: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../Clients/Clients", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
clients: {
|
||||
findOne: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
import { PolicyResolver } from "../PolicyResolver";
|
||||
import PlanSubscriptionsList from "../../PlanSubscriptions/PlanSubscriptons";
|
||||
import CompaniesManager from "../../Companies/Companies";
|
||||
import ClientsList from "../../Clients/Clients";
|
||||
import { PlanFeatures } from "../../Plans/Plans.interface";
|
||||
|
||||
describe("PolicyResolver", () => {
|
||||
let resolver: PolicyResolver;
|
||||
let mockCompanyPolicyFindOne: jest.Mock;
|
||||
let mockClientPreferencesFindOne: jest.Mock;
|
||||
let mockOverrideFindOne: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
resolver = new PolicyResolver();
|
||||
|
||||
// Access the mocked adapter instances
|
||||
mockCompanyPolicyFindOne = (
|
||||
resolver as any
|
||||
).companyPolicyAdapter.findOne as jest.Mock;
|
||||
mockClientPreferencesFindOne = (
|
||||
resolver as any
|
||||
).clientPreferencesAdapter.findOne as jest.Mock;
|
||||
mockOverrideFindOne = (
|
||||
resolver as any
|
||||
).clientCompanyOverrideAdapter.findOne as jest.Mock;
|
||||
|
||||
// Mock CompaniesManager to return a company with an ownerId
|
||||
(CompaniesManager.companies.findOne as jest.Mock).mockResolvedValue({
|
||||
_id: "company-1",
|
||||
ownerId: "owner-user-1",
|
||||
});
|
||||
(ClientsList.clients.findOne as jest.Mock).mockResolvedValue({
|
||||
_id: "client-1",
|
||||
userId: "client-user-1",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns system defaults when no policies exist", async () => {
|
||||
mockCompanyPolicyFindOne.mockResolvedValue(null);
|
||||
mockClientPreferencesFindOne.mockResolvedValue(null);
|
||||
mockOverrideFindOne.mockResolvedValue(null);
|
||||
(PlanSubscriptionsList.checkFeature as jest.Mock).mockResolvedValue(true);
|
||||
|
||||
const result = await resolver.resolve("company-1", "client-1");
|
||||
|
||||
expect(result.channels).toEqual(
|
||||
expect.arrayContaining(["whatsapp", "email", "system"])
|
||||
);
|
||||
expect(result.channels).toHaveLength(3);
|
||||
});
|
||||
|
||||
it("uses company policy defaults when no client override exists", async () => {
|
||||
mockCompanyPolicyFindOne.mockResolvedValue({
|
||||
companyId: "company-1",
|
||||
defaultChannels: ["email" as NotificationChannel],
|
||||
timezone: "America/Argentina/Buenos_Aires",
|
||||
});
|
||||
mockClientPreferencesFindOne.mockResolvedValue(null);
|
||||
mockOverrideFindOne.mockResolvedValue(null);
|
||||
(PlanSubscriptionsList.checkFeature as jest.Mock).mockResolvedValue(true);
|
||||
|
||||
const result = await resolver.resolve("company-1", "client-1");
|
||||
|
||||
expect(result.channels).toEqual(["email"]);
|
||||
});
|
||||
|
||||
it("client override wins over company default", async () => {
|
||||
mockCompanyPolicyFindOne.mockResolvedValue({
|
||||
companyId: "company-1",
|
||||
defaultChannels: ["email" as NotificationChannel],
|
||||
timezone: "America/Argentina/Buenos_Aires",
|
||||
});
|
||||
mockClientPreferencesFindOne.mockResolvedValue(null);
|
||||
mockOverrideFindOne.mockResolvedValue({
|
||||
clientId: "client-1",
|
||||
companyId: "company-1",
|
||||
preferredChannels: ["whatsapp" as NotificationChannel],
|
||||
mutedChannels: [],
|
||||
});
|
||||
(PlanSubscriptionsList.checkFeature as jest.Mock).mockResolvedValue(true);
|
||||
|
||||
const result = await resolver.resolve("company-1", "client-1");
|
||||
|
||||
expect(result.channels).toEqual(["whatsapp"]);
|
||||
});
|
||||
|
||||
it("client global preferences used when no override exists", async () => {
|
||||
mockCompanyPolicyFindOne.mockResolvedValue({
|
||||
companyId: "company-1",
|
||||
defaultChannels: ["whatsapp" as NotificationChannel, "email" as NotificationChannel],
|
||||
timezone: "America/Argentina/Buenos_Aires",
|
||||
});
|
||||
mockClientPreferencesFindOne.mockResolvedValue({
|
||||
userId: "client-user-1",
|
||||
preferredChannels: ["email" as NotificationChannel],
|
||||
mutedChannels: [],
|
||||
});
|
||||
mockOverrideFindOne.mockResolvedValue(null);
|
||||
(PlanSubscriptionsList.checkFeature as jest.Mock).mockResolvedValue(true);
|
||||
|
||||
const result = await resolver.resolve("company-1", "client-1");
|
||||
|
||||
expect(result.channels).toEqual(["email"]);
|
||||
});
|
||||
|
||||
it("excludes muted channels from override", async () => {
|
||||
mockCompanyPolicyFindOne.mockResolvedValue(null);
|
||||
mockClientPreferencesFindOne.mockResolvedValue(null);
|
||||
mockOverrideFindOne.mockResolvedValue({
|
||||
clientId: "client-1",
|
||||
companyId: "company-1",
|
||||
preferredChannels: [
|
||||
"whatsapp" as NotificationChannel,
|
||||
"email" as NotificationChannel,
|
||||
],
|
||||
mutedChannels: ["whatsapp" as NotificationChannel],
|
||||
});
|
||||
(PlanSubscriptionsList.checkFeature as jest.Mock).mockResolvedValue(true);
|
||||
|
||||
const result = await resolver.resolve("company-1", "client-1");
|
||||
|
||||
expect(result.channels).toEqual(["email"]);
|
||||
expect(result.channels).not.toContain("whatsapp");
|
||||
});
|
||||
|
||||
it("excludes muted channels from client preferences", async () => {
|
||||
mockCompanyPolicyFindOne.mockResolvedValue(null);
|
||||
mockClientPreferencesFindOne.mockResolvedValue({
|
||||
userId: "client-user-1",
|
||||
preferredChannels: [
|
||||
"whatsapp" as NotificationChannel,
|
||||
"email" as NotificationChannel,
|
||||
"system" as NotificationChannel,
|
||||
],
|
||||
mutedChannels: ["email" as NotificationChannel],
|
||||
});
|
||||
mockOverrideFindOne.mockResolvedValue(null);
|
||||
(PlanSubscriptionsList.checkFeature as jest.Mock).mockResolvedValue(true);
|
||||
|
||||
const result = await resolver.resolve("company-1", "client-1");
|
||||
|
||||
expect(result.channels).toEqual(
|
||||
expect.arrayContaining(["whatsapp", "system"])
|
||||
);
|
||||
expect(result.channels).not.toContain("email");
|
||||
});
|
||||
|
||||
it("loads global preferences by the appointment client's userId", async () => {
|
||||
mockCompanyPolicyFindOne.mockResolvedValue(null);
|
||||
mockClientPreferencesFindOne.mockResolvedValue({
|
||||
userId: "client-user-1",
|
||||
preferredChannels: ["system" as NotificationChannel],
|
||||
mutedChannels: [],
|
||||
});
|
||||
mockOverrideFindOne.mockResolvedValue(null);
|
||||
(PlanSubscriptionsList.checkFeature as jest.Mock).mockResolvedValue(true);
|
||||
|
||||
const result = await resolver.resolve("company-1", "client-1");
|
||||
|
||||
expect(ClientsList.clients.findOne).toHaveBeenCalledWith({ _id: "client-1" });
|
||||
expect(mockClientPreferencesFindOne).toHaveBeenCalledWith({ userId: "client-user-1" });
|
||||
expect(result.channels).toEqual(["system"]);
|
||||
});
|
||||
|
||||
it("plan blocks WhatsApp when feature not available", async () => {
|
||||
mockCompanyPolicyFindOne.mockResolvedValue({
|
||||
companyId: "company-1",
|
||||
defaultChannels: [
|
||||
"whatsapp" as NotificationChannel,
|
||||
"email" as NotificationChannel,
|
||||
],
|
||||
timezone: "America/Argentina/Buenos_Aires",
|
||||
});
|
||||
mockClientPreferencesFindOne.mockResolvedValue(null);
|
||||
mockOverrideFindOne.mockResolvedValue(null);
|
||||
(PlanSubscriptionsList.checkFeature as jest.Mock).mockImplementation(
|
||||
async (params: { feature: string }) => {
|
||||
if (params.feature === PlanFeatures.WAP_NOTIFICATIONS) return false;
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
const result = await resolver.resolve("company-1", "client-1");
|
||||
|
||||
expect(result.channels).not.toContain("whatsapp");
|
||||
expect(result.channels).toContain("email");
|
||||
});
|
||||
|
||||
it("plan blocks email when feature not available", async () => {
|
||||
mockCompanyPolicyFindOne.mockResolvedValue({
|
||||
companyId: "company-1",
|
||||
defaultChannels: [
|
||||
"whatsapp" as NotificationChannel,
|
||||
"email" as NotificationChannel,
|
||||
],
|
||||
timezone: "America/Argentina/Buenos_Aires",
|
||||
});
|
||||
mockClientPreferencesFindOne.mockResolvedValue(null);
|
||||
mockOverrideFindOne.mockResolvedValue(null);
|
||||
(PlanSubscriptionsList.checkFeature as jest.Mock).mockImplementation(
|
||||
async (params: { feature: string }) => {
|
||||
if (params.feature === PlanFeatures.MAIL_NOTIFICATIONS) return false;
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
const result = await resolver.resolve("company-1", "client-1");
|
||||
|
||||
expect(result.channels).not.toContain("email");
|
||||
expect(result.channels).toContain("whatsapp");
|
||||
});
|
||||
|
||||
it("returns timezone from company policy", async () => {
|
||||
mockCompanyPolicyFindOne.mockResolvedValue({
|
||||
companyId: "company-1",
|
||||
defaultChannels: ["system" as NotificationChannel],
|
||||
timezone: "America/New_York",
|
||||
quietHours: { from: "22:00", to: "07:00" },
|
||||
});
|
||||
mockClientPreferencesFindOne.mockResolvedValue(null);
|
||||
mockOverrideFindOne.mockResolvedValue(null);
|
||||
(PlanSubscriptionsList.checkFeature as jest.Mock).mockResolvedValue(true);
|
||||
|
||||
const result = await resolver.resolve("company-1", "client-1");
|
||||
|
||||
expect(result.timezone).toBe("America/New_York");
|
||||
expect(result.quietHours).toEqual({ from: "22:00", to: "07:00" });
|
||||
});
|
||||
|
||||
it("returns default timezone when no company policy", async () => {
|
||||
mockCompanyPolicyFindOne.mockResolvedValue(null);
|
||||
mockClientPreferencesFindOne.mockResolvedValue(null);
|
||||
mockOverrideFindOne.mockResolvedValue(null);
|
||||
(PlanSubscriptionsList.checkFeature as jest.Mock).mockResolvedValue(true);
|
||||
|
||||
const result = await resolver.resolve("company-1", "client-1");
|
||||
|
||||
expect(result.timezone).toBe("America/Argentina/Buenos_Aires");
|
||||
});
|
||||
|
||||
it("returns empty channels when all are muted via override", async () => {
|
||||
mockCompanyPolicyFindOne.mockResolvedValue(null);
|
||||
mockClientPreferencesFindOne.mockResolvedValue(null);
|
||||
mockOverrideFindOne.mockResolvedValue({
|
||||
clientId: "client-1",
|
||||
companyId: "company-1",
|
||||
preferredChannels: [
|
||||
"whatsapp" as NotificationChannel,
|
||||
"email" as NotificationChannel,
|
||||
"system" as NotificationChannel,
|
||||
],
|
||||
mutedChannels: [
|
||||
"whatsapp" as NotificationChannel,
|
||||
"email" as NotificationChannel,
|
||||
"system" as NotificationChannel,
|
||||
],
|
||||
});
|
||||
(PlanSubscriptionsList.checkFeature as jest.Mock).mockResolvedValue(true);
|
||||
|
||||
const result = await resolver.resolve("company-1", "client-1");
|
||||
|
||||
expect(result.channels).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user