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 = { 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 { // 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 { 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 { // 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; } }