174 lines
6.9 KiB
TypeScript
174 lines
6.9 KiB
TypeScript
import { NotificationChannel } from "../NotificationJobs/NotificationJobs.Interface";
|
|
import { CompanyNotificationPolicyAdapterMongoose } from "./CompanyNotificationPolicy.Adapter.Mongoose";
|
|
import { ClientNotificationPreferencesAdapterMongoose } from "./ClientNotificationPreferences.Adapter.Mongoose";
|
|
import { ClientCompanyNotificationOverrideAdapterMongoose } from "./ClientCompanyNotificationOverride.Adapter.Mongoose";
|
|
import { IClientNotificationPreferences } from "./ClientNotificationPreferences.Interface";
|
|
import { IClientCompanyNotificationOverride } from "./ClientCompanyNotificationOverride.Interface";
|
|
import { ICompanyNotificationPolicy } from "./CompanyNotificationPolicy.Interface";
|
|
import PlanSubscriptionsList from "../PlanSubscriptions/PlanSubscriptons";
|
|
import { PlanFeatures } from "../Plans/Plans.interface";
|
|
import ClientsList from "../Clients/Clients";
|
|
|
|
const DEFAULT_TIMEZONE = "America/Argentina/Buenos_Aires";
|
|
const SYSTEM_DEFAULT_CHANNELS: NotificationChannel[] = ["whatsapp", "email", "system"];
|
|
|
|
const PLAN_CHANNEL_MAP: Record<string, PlanFeatures> = {
|
|
whatsapp: PlanFeatures.WAP_NOTIFICATIONS,
|
|
email: PlanFeatures.MAIL_NOTIFICATIONS,
|
|
};
|
|
|
|
export interface ResolvedPolicy {
|
|
channels: NotificationChannel[];
|
|
timezone: string;
|
|
quietHours?: { from: string; to: string };
|
|
reminderRules?: { offset: number; enabled: boolean }[];
|
|
}
|
|
|
|
export class PolicyResolver {
|
|
private companyPolicyAdapter: CompanyNotificationPolicyAdapterMongoose;
|
|
private clientPreferencesAdapter: ClientNotificationPreferencesAdapterMongoose;
|
|
private clientCompanyOverrideAdapter: ClientCompanyNotificationOverrideAdapterMongoose;
|
|
|
|
constructor() {
|
|
this.companyPolicyAdapter = new CompanyNotificationPolicyAdapterMongoose();
|
|
this.clientPreferencesAdapter = new ClientNotificationPreferencesAdapterMongoose();
|
|
this.clientCompanyOverrideAdapter = new ClientCompanyNotificationOverrideAdapterMongoose();
|
|
}
|
|
|
|
async resolve(companyId: string, clientId: string): Promise<ResolvedPolicy> {
|
|
// Level 2: ClientCompanyNotificationOverride (per-client per-org)
|
|
const override = await this.clientCompanyOverrideAdapter.findOne({
|
|
clientId,
|
|
companyId,
|
|
});
|
|
|
|
const client = await ClientsList.clients.findOne({ _id: clientId });
|
|
|
|
// Level 3: ClientNotificationPreferences (user global)
|
|
const clientPrefs = client?.userId
|
|
? await this.clientPreferencesAdapter.findOne({ userId: String(client.userId) })
|
|
: null;
|
|
|
|
// Level 4: CompanyNotificationPolicy (org defaults)
|
|
const companyPolicy = await this.companyPolicyAdapter.findOne({ companyId });
|
|
|
|
// Determine channels from cascade
|
|
let channels: NotificationChannel[];
|
|
let timezone: string;
|
|
let quietHours: ResolvedPolicy["quietHours"];
|
|
let reminderRules: ResolvedPolicy["reminderRules"];
|
|
|
|
if (override) {
|
|
channels = [...override.preferredChannels];
|
|
} else if (clientPrefs) {
|
|
channels = [...clientPrefs.preferredChannels];
|
|
} else if (companyPolicy) {
|
|
channels = [...companyPolicy.defaultChannels];
|
|
} else {
|
|
// Level 5: System defaults
|
|
channels = [...SYSTEM_DEFAULT_CHANNELS];
|
|
}
|
|
|
|
// Apply muted channels from the winning level only — do NOT merge across levels
|
|
const mutedChannels = this.getMutedChannels(override, clientPrefs, companyPolicy);
|
|
channels = channels.filter((ch) => !mutedChannels.includes(ch));
|
|
|
|
// Timezone and quiet hours from company policy
|
|
if (companyPolicy) {
|
|
timezone = companyPolicy.timezone;
|
|
quietHours = companyPolicy.quietHours;
|
|
reminderRules = companyPolicy.reminderRules;
|
|
} else {
|
|
timezone = DEFAULT_TIMEZONE;
|
|
}
|
|
|
|
// Level 1: Plan limits — filter channels by plan features
|
|
channels = await this.applyPlanLimits(companyId, channels);
|
|
|
|
return {
|
|
channels,
|
|
timezone,
|
|
quietHours,
|
|
reminderRules,
|
|
};
|
|
}
|
|
|
|
async resolveAvailableChannels(
|
|
companyId: string,
|
|
channels: NotificationChannel[]
|
|
): Promise<NotificationChannel[]> {
|
|
const companyPolicy = await this.companyPolicyAdapter.findOne({ companyId });
|
|
const enabledChannels = companyPolicy
|
|
? companyPolicy.defaultChannels
|
|
: SYSTEM_DEFAULT_CHANNELS;
|
|
const companyEnabledChannels = channels.filter((channel) =>
|
|
enabledChannels.includes(channel)
|
|
);
|
|
|
|
return this.applyPlanLimits(companyId, companyEnabledChannels);
|
|
}
|
|
|
|
/**
|
|
* Returns muted channels from the winning cascade level only.
|
|
* Per spec, muted channels are NOT merged across levels.
|
|
* The level that determines preferredChannels also determines mutedChannels.
|
|
*/
|
|
private getMutedChannels(
|
|
override: IClientCompanyNotificationOverride | null,
|
|
clientPrefs: IClientNotificationPreferences | null,
|
|
companyPolicy: ICompanyNotificationPolicy | null
|
|
): NotificationChannel[] {
|
|
if (override) {
|
|
// Override wins — use only override's muted channels
|
|
return override.mutedChannels || [];
|
|
}
|
|
if (clientPrefs) {
|
|
// Client prefs wins — use only clientPrefs' muted channels
|
|
return clientPrefs.mutedChannels || [];
|
|
}
|
|
if (companyPolicy) {
|
|
// Company policy wins — use only companyPolicy' muted channels
|
|
return companyPolicy.mutedChannels || [];
|
|
}
|
|
// System defaults — no muted channels
|
|
return [];
|
|
}
|
|
|
|
private async applyPlanLimits(
|
|
companyId: string,
|
|
channels: NotificationChannel[]
|
|
): Promise<NotificationChannel[]> {
|
|
// Resolve the company owner's userId for plan feature checks
|
|
// Use lazy require to avoid circular dependency (PolicyResolver → CompaniesManager → Appointments → NotificationJobService → PolicyResolver)
|
|
const CompaniesManager = require("../Companies/Companies").default;
|
|
const company = await CompaniesManager.companies.findOne({ _id: companyId });
|
|
if (!company) {
|
|
// Company not found — return channels unchanged (fail-open)
|
|
return channels;
|
|
}
|
|
|
|
const ownerUserId = String(company.ownerId);
|
|
const filtered: NotificationChannel[] = [];
|
|
|
|
for (const channel of channels) {
|
|
const planFeature = PLAN_CHANNEL_MAP[channel];
|
|
if (!planFeature) {
|
|
// Channel has no plan restriction (e.g., system)
|
|
filtered.push(channel);
|
|
continue;
|
|
}
|
|
|
|
const canSend = await PlanSubscriptionsList.checkFeature({
|
|
userId: ownerUserId,
|
|
feature: planFeature,
|
|
});
|
|
|
|
if (canSend) {
|
|
filtered.push(channel);
|
|
}
|
|
}
|
|
|
|
return filtered;
|
|
}
|
|
}
|