File

apps/recallassess/recallassess-api/src/api/shared/email/services/email-preferences.service.ts

Index

Properties
Methods

Constructor

constructor(prisma: BNestPrismaService)
Parameters :
Name Type Optional
prisma BNestPrismaService No

Methods

Private Async createDefaultPreferences
createDefaultPreferences(participantId: number)

Create default preferences for a participant

Parameters :
Name Type Optional
participantId number No
Returns : unknown
Async getParticipantPreferences
getParticipantPreferences(participantId: number)

Get email preferences for a participant

Parameters :
Name Type Optional
participantId number No
Async getPreferenceOptions
getPreferenceOptions(participantId: number)

Get preference options for smart unsubscribe (before full unsubscribe)

Parameters :
Name Type Optional
participantId number No
Returns : Promise<literal type>
Async isEmailTypeEnabled
isEmailTypeEnabled(participantId: number, emailType: string)

Check if email type is enabled for participant

Parameters :
Name Type Optional
participantId number No
emailType string No
Returns : Promise<boolean>
Async isSuperAdminUnsubscribed
isSuperAdminUnsubscribed(userId: number)

Check if a super admin is unsubscribed

Parameters :
Name Type Optional
userId number No
Returns : Promise<boolean>
Async isUnsubscribed
isUnsubscribed(participantId: number)

Check if a participant is unsubscribed

Parameters :
Name Type Optional
participantId number No
Returns : Promise<boolean>
Async resubscribeParticipant
resubscribeParticipant(participantId: number, source: string)

Re-subscribe a participant to email reminders

Parameters :
Name Type Optional
participantId number No
source string No
Returns : Promise<void>
Async resubscribeSuperAdmin
resubscribeSuperAdmin(userId: number, source: string)

Resubscribe a super admin to email reminders

Parameters :
Name Type Optional
userId number No
source string No
Returns : Promise<void>
Async shouldSendEmail
shouldSendEmail(participantId: number, emailType: string, scheduledTime?: Date)

Check if email should be sent based on preferences

Parameters :
Name Type Optional
participantId number No
emailType string No
scheduledTime Date Yes
Returns : Promise<boolean>
Async unsubscribeParticipant
unsubscribeParticipant(participantId: number, reason?: string)

Unsubscribe a participant from all email reminders

Parameters :
Name Type Optional
participantId number No
reason string Yes
Returns : Promise<void>
Async unsubscribeSuperAdmin
unsubscribeSuperAdmin(userId: number, reason?: string)

Unsubscribe a super admin from email reminders

Parameters :
Name Type Optional
userId number No
reason string Yes
Returns : Promise<void>
Async updateParticipantPreferences
updateParticipantPreferences(participantId: number, preferences: EmailPreferencesPatch)

Update email preferences for a participant

Parameters :
Name Type Optional
participantId number No
preferences EmailPreferencesPatch No
Async updatePreferencesWithoutUnsubscribe
updatePreferencesWithoutUnsubscribe(participantId: number, action: string, value?: string)

Update preferences without full unsubscribe (smart unsubscribe)

Parameters :
Name Type Optional
participantId number No
action string No
value string Yes

Properties

Private Readonly logger
Type : unknown
Default value : new Logger(EmailPreferencesService.name)
import { BNestPrismaService } from "@bish-nest/core/services";
import { Injectable, Logger, NotFoundException } from "@nestjs/common";

export interface EmailPreferenceDto {
  emailType: string;
  enabled: boolean;
  preferredTime?: string;
  preferredDay?: "weekday" | "weekend" | "any";
  timezone?: string;
}

export interface EmailPreferences {
  frequency: "immediate" | "daily" | "weekly" | "monthly" | "never";
  categories: {
    inactivityReminders: boolean;
    courseProgress: boolean;
    deadlines: boolean;
    assessments: boolean;
    achievements: boolean;
    weeklyDigest: boolean;
  };
  quietHours: {
    start: string; // HH:mm format
    end: string; // HH:mm format
  };
  timezone: string;
  preferredSendTime: string; // HH:mm format
}

export type EmailPreferencesPatch = Partial<Omit<EmailPreferences, "categories" | "quietHours">> & {
  categories?: Partial<EmailPreferences["categories"]>;
  quietHours?: Partial<EmailPreferences["quietHours"]>;
};

@Injectable()
export class EmailPreferencesService {
  private readonly logger = new Logger(EmailPreferencesService.name);

  constructor(private prisma: BNestPrismaService) {}

  /**
   * Get email preferences for a participant
   */
  async getParticipantPreferences(participantId: number): Promise<EmailPreferences> {
    const participant = await this.prisma.client.participant.findUnique({
      where: { id: participantId },
    });

    if (!participant) {
      throw new NotFoundException(`Participant not found: ${participantId}`);
    }

    // Query email_preferences table
    let prefs = await (this.prisma.client as any).emailPreference.findUnique({
      where: { participant_id: participantId },
    });

    // If not found, create defaults
    if (!prefs) {
      prefs = await this.createDefaultPreferences(participantId);
    }

    return {
      frequency: (prefs.frequency as "immediate" | "daily" | "weekly" | "monthly" | "never") || "immediate",
      categories: {
        inactivityReminders: prefs.inactivity_reminders,
        courseProgress: prefs.course_progress,
        deadlines: prefs.deadlines,
        assessments: prefs.assessments,
        achievements: prefs.achievements,
        weeklyDigest: prefs.weekly_digest,
      },
      quietHours: {
        start: prefs.quiet_hours_start || "22:00",
        end: prefs.quiet_hours_end || "08:00",
      },
      timezone: prefs.timezone || "UTC",
      preferredSendTime: prefs.preferred_send_time || "10:00",
    };
  }

  /**
   * Create default preferences for a participant
   */
  private async createDefaultPreferences(participantId: number) {
    return await (this.prisma.client as any).emailPreference.create({
      data: {
        participant_id: participantId,
        frequency: "immediate",
        inactivity_reminders: true,
        course_progress: true,
        deadlines: true,
        assessments: true,
        achievements: true,
        weekly_digest: true,
        quiet_hours_start: "22:00",
        quiet_hours_end: "08:00",
        timezone: "UTC",
        preferred_send_time: "10:00",
        all_reminders_enabled: true,
        resubscribe_count: 0,
      },
    });
  }

  /**
   * Update email preferences for a participant
   */
  async updateParticipantPreferences(
    participantId: number,
    preferences: EmailPreferencesPatch,
  ): Promise<EmailPreferences> {
    const participant = await this.prisma.client.participant.findUnique({
      where: { id: participantId },
    });

    if (!participant) {
      throw new NotFoundException(`Participant not found: ${participantId}`);
    }

    // Get or create preferences
    let prefs = await (this.prisma.client as any).emailPreference.findUnique({
      where: { participant_id: participantId },
    });

    if (!prefs) {
      prefs = await this.createDefaultPreferences(participantId);
    }

    // Update preferences
    const updated = await (this.prisma.client as any).emailPreference.update({
      where: { id: prefs.id },
      data: {
        frequency: preferences.frequency || prefs.frequency,
        inactivity_reminders: preferences.categories?.inactivityReminders ?? prefs.inactivity_reminders,
        course_progress: preferences.categories?.courseProgress ?? prefs.course_progress,
        deadlines: preferences.categories?.deadlines ?? prefs.deadlines,
        assessments: preferences.categories?.assessments ?? prefs.assessments,
        achievements: preferences.categories?.achievements ?? prefs.achievements,
        weekly_digest: preferences.categories?.weeklyDigest ?? prefs.weekly_digest,
        quiet_hours_start: preferences.quietHours?.start || prefs.quiet_hours_start,
        quiet_hours_end: preferences.quietHours?.end || prefs.quiet_hours_end,
        timezone: preferences.timezone || prefs.timezone,
        preferred_send_time: preferences.preferredSendTime || prefs.preferred_send_time,
      },
    });

    this.logger.log(`Updated preferences for participant ${participantId}`);

    return this.getParticipantPreferences(participantId);
  }

  /**
   * Check if email type is enabled for participant
   */
  async isEmailTypeEnabled(participantId: number, emailType: string): Promise<boolean> {
    const preferences = await this.getParticipantPreferences(participantId);

    // Map email types to preference categories
    const categoryMap: Record<string, keyof EmailPreferences["categories"]> = {
      inactivity: "inactivityReminders",
      course_progress: "courseProgress",
      deadline: "deadlines",
      assessment: "assessments",
      achievement: "achievements",
      weekly_digest: "weeklyDigest",
      winback: "inactivityReminders", // Win-back emails are treated as inactivity reminders
    };

    const category = categoryMap[emailType.toLowerCase()];
    if (category) {
      return preferences.categories[category];
    }

    // Default to enabled if not found
    return true;
  }

  /**
   * Check if email should be sent based on preferences
   */
  async shouldSendEmail(participantId: number, emailType: string, scheduledTime?: Date): Promise<boolean> {
    // First check if participant is unsubscribed
    if (await this.isUnsubscribed(participantId)) {
      return false;
    }

    const prefs = await this.getParticipantPreferences(participantId);

    // Check frequency preference
    if (prefs.frequency === "never") {
      return false;
    }

    // Check category preference
    const categoryEnabled = await this.isEmailTypeEnabled(participantId, emailType);
    if (!categoryEnabled) {
      return false;
    }

    // Check quiet hours
    if (scheduledTime) {
      const hour = scheduledTime.getHours();
      const minute = scheduledTime.getMinutes();
      const [startHour, startMinute] = prefs.quietHours.start.split(":").map(Number);
      const [endHour, endMinute] = prefs.quietHours.end.split(":").map(Number);

      const currentTime = hour * 60 + minute;
      const startTime = startHour * 60 + startMinute;
      const endTime = endHour * 60 + endMinute;

      // Handle quiet hours that span midnight
      if (startTime > endTime) {
        if (currentTime >= startTime || currentTime < endTime) {
          return false; // In quiet hours
        }
      } else {
        if (currentTime >= startTime && currentTime < endTime) {
          return false; // In quiet hours
        }
      }
    }

    return true;
  }

  /**
   * Unsubscribe a participant from all email reminders
   */
  async unsubscribeParticipant(participantId: number, reason?: string): Promise<void> {
    const participant = await this.prisma.client.participant.findUnique({
      where: { id: participantId },
    });

    if (!participant) {
      throw new NotFoundException(`Participant not found: ${participantId}`);
    }

    // Get or create preferences
    let prefs = await (this.prisma.client as any).emailPreference.findUnique({
      where: { participant_id: participantId },
    });

    if (!prefs) {
      prefs = await this.createDefaultPreferences(participantId);
    }

    // Update to unsubscribe
    await (this.prisma.client as any).emailPreference.update({
      where: { id: prefs.id },
      data: {
        all_reminders_enabled: false,
        unsubscribed_at: new Date(),
        unsubscribed_reason: reason || null,
      },
    });

    this.logger.log(`Unsubscribed participant ${participantId}${reason ? ` with reason: ${reason}` : ""}`);
  }

  /**
   * Re-subscribe a participant to email reminders
   */
  async resubscribeParticipant(participantId: number, source: string): Promise<void> {
    const participant = await this.prisma.client.participant.findUnique({
      where: { id: participantId },
    });

    if (!participant) {
      throw new NotFoundException(`Participant not found: ${participantId}`);
    }

    // Get or create preferences
    let prefs = await (this.prisma.client as any).emailPreference.findUnique({
      where: { participant_id: participantId },
    });

    if (!prefs) {
      prefs = await this.createDefaultPreferences(participantId);
    }

    // Get current resubscribe count
    const currentResubscribeCount = prefs.resubscribe_count || 0;

    // Update to resubscribe
    await (this.prisma.client as any).emailPreference.update({
      where: { id: prefs.id },
      data: {
        all_reminders_enabled: true,
        resubscribed_at: new Date(),
        resubscribe_count: currentResubscribeCount + 1,
        unsubscribed_reason: null, // Clear unsubscribe reason
        unsubscribed_at: null, // Clear unsubscribed timestamp
      },
    });

    this.logger.log(`Resubscribed participant ${participantId} from source: ${source}`);
  }

  /**
   * Check if a participant is unsubscribed
   */
  async isUnsubscribed(participantId: number): Promise<boolean> {
    const participant = await this.prisma.client.participant.findUnique({
      where: { id: participantId },
    });

    if (!participant) {
      return false; // If participant doesn't exist, default to not unsubscribed
    }

    // Query email_preferences table
    const prefs = await (this.prisma.client as any).emailPreference.findUnique({
      where: { participant_id: participantId },
    });

    // If no preferences exist, participant is not unsubscribed
    if (!prefs) {
      return false;
    }

    // Check if all_reminders_enabled is false or if unsubscribed_at is set
    return prefs.all_reminders_enabled === false || prefs.unsubscribed_at !== null;
  }

  /**
   * Get preference options for smart unsubscribe (before full unsubscribe)
   */
  async getPreferenceOptions(participantId: number): Promise<{
    currentFrequency: string;
    currentPreferences: EmailPreferences;
    options: Array<{
      type: string;
      label: string;
      description: string;
      action: string;
    }>;
  }> {
    const preferences = await this.getParticipantPreferences(participantId);
    const isUnsubscribed = await this.isUnsubscribed(participantId);

    const options = [
      {
        type: "reduce_frequency",
        label: "Send me weekly digest instead",
        description: "Receive one weekly email with all updates instead of multiple emails",
        action: "update_frequency",
        value: "weekly",
      },
      {
        type: "disable_specific",
        label: "Disable inactivity reminders only",
        description: "Keep important emails like deadlines and assessments",
        action: "disable_category",
        value: "inactivityReminders",
      },
      {
        type: "disable_progress",
        label: "Disable course progress updates",
        description: "Keep deadlines and assessments, but skip progress notifications",
        action: "disable_category",
        value: "courseProgress",
      },
      {
        type: "full_unsubscribe",
        label: "Unsubscribe from all email reminders",
        description: "You'll still receive essential account emails (password resets, security alerts)",
        action: "full_unsubscribe",
      },
    ];

    return {
      currentFrequency: preferences.frequency,
      currentPreferences: preferences,
      options,
    };
  }

  /**
   * Update preferences without full unsubscribe (smart unsubscribe)
   */
  async updatePreferencesWithoutUnsubscribe(
    participantId: number,
    action: string,
    value?: string,
  ): Promise<EmailPreferences> {
    const preferences = await this.getParticipantPreferences(participantId);

    if (action === "update_frequency" && value) {
      // Change frequency to weekly digest
      return await this.updateParticipantPreferences(participantId, {
        frequency: value as "weekly",
        categories: preferences.categories, // Keep all categories enabled
      });
    } else if (action === "disable_category" && value) {
      // Disable specific category
      const categoryMap: Record<string, keyof EmailPreferences["categories"]> = {
        inactivityReminders: "inactivityReminders",
        courseProgress: "courseProgress",
        deadlines: "deadlines",
        assessments: "assessments",
        achievements: "achievements",
        weeklyDigest: "weeklyDigest",
      };

      const category = categoryMap[value];
      if (category) {
        const updatedCategories = {
          ...preferences.categories,
          [category]: false,
        };
        return await this.updateParticipantPreferences(participantId, {
          categories: updatedCategories,
        });
      }
    }

    // If no valid action, return current preferences
    return preferences;
  }

  /**
   * Unsubscribe a super admin from email reminders
   */
  async unsubscribeSuperAdmin(userId: number, reason?: string): Promise<void> {
    this.logger.log(
      `Unsubscribing super admin ${userId} from email reminders${reason ? ` - Reason: ${reason}` : ""}`,
    );

    // Store unsubscribe status in user metadata or system settings
    // For now, we'll use a simple approach: store in SystemSetting or User metadata
    // Since we don't have a UserEmailPreference table, we'll use a simple flag approach
    try {
      // Check if SystemSetting exists for user email preferences
      const existingSetting = await this.prisma.client.systemSetting.findFirst({
        where: {
          key: `user_email_preference_${userId}`,
        },
      });

      const unsubscribeData = {
        unsubscribed: true,
        unsubscribed_at: new Date().toISOString(),
        unsubscribed_reason: reason || null,
        user_id: userId,
      };

      if (existingSetting) {
        await this.prisma.client.systemSetting.update({
          where: { id: existingSetting.id },
          data: {
            value: unsubscribeData as any,
            updated_at: new Date(),
          },
        });
      } else {
        await this.prisma.client.systemSetting.create({
          data: {
            key: `user_email_preference_${userId}`,
            value: unsubscribeData as any,
            description: `Email preference for super admin user ${userId}`,
          } as any,
        });
      }

      this.logger.log(`Super admin ${userId} unsubscribed from email reminders`);
    } catch (error) {
      this.logger.error(`Failed to unsubscribe super admin ${userId}:`, error);
      throw error;
    }
  }

  /**
   * Resubscribe a super admin to email reminders
   */
  async resubscribeSuperAdmin(userId: number, source: string): Promise<void> {
    this.logger.log(`Resubscribing super admin ${userId} to email reminders (source: ${source})`);

    try {
      const existingSetting = await this.prisma.client.systemSetting.findFirst({
        where: {
          key: `user_email_preference_${userId}`,
        },
      });

      if (existingSetting) {
        const currentValue = existingSetting.value as any;
        const resubscribeData = {
          ...currentValue,
          unsubscribed: false,
          unsubscribed_at: null,
          unsubscribed_reason: null,
          resubscribed_at: new Date().toISOString(),
          resubscribe_source: source,
          resubscribe_count: ((currentValue?.resubscribe_count || 0) as number) + 1,
        };

        await this.prisma.client.systemSetting.update({
          where: { id: existingSetting.id },
          data: {
            value: resubscribeData as any,
            updated_at: new Date(),
          },
        });
      } else {
        // If no setting exists, create one with resubscribed status
        await this.prisma.client.systemSetting.create({
          data: {
            key: `user_email_preference_${userId}`,
            value: {
              unsubscribed: false,
              resubscribed_at: new Date().toISOString(),
              resubscribe_source: source,
              resubscribe_count: 1,
            } as any,
            description: `Email preference for super admin user ${userId}`,
          } as any,
        });
      }

      this.logger.log(`Super admin ${userId} resubscribed to email reminders`);
    } catch (error) {
      this.logger.error(`Failed to resubscribe super admin ${userId}:`, error);
      throw error;
    }
  }

  /**
   * Check if a super admin is unsubscribed
   */
  async isSuperAdminUnsubscribed(userId: number): Promise<boolean> {
    try {
      const setting = await this.prisma.client.systemSetting.findFirst({
        where: {
          key: `user_email_preference_${userId}`,
        },
      });

      if (!setting) {
        return false; // Default to not unsubscribed
      }

      const value = setting.value as any;
      return value?.unsubscribed === true;
    } catch (error) {
      this.logger.warn(`Failed to check super admin unsubscribe status for user ${userId}:`, error);
      return false; // Default to not unsubscribed on error
    }
  }
}

results matching ""

    No results matching ""