File

apps/recallassess/recallassess-api/src/api/client/email/email-preferences.controller.ts

Prefix

api/client/email/preferences

Index

Methods

Methods

Async adminResubscribe
adminResubscribe(auth: CLAuthData, participantId: number)
Decorators :
@Put('participant/:participantId/resubscribe')
@HttpCode(HttpStatus.OK)
@ApiOperation({summary: 'Resubscribe a participant (admin within company)'})
@ApiResponse({status: 200, description: 'Participant resubscribed successfully'})

Admin: Resubscribe a participant in the same company

Parameters :
Name Type Optional
auth CLAuthData No
participantId number No
Returns : unknown
Async adminResubscribeAlias
adminResubscribeAlias(auth: CLAuthData, participantId: number)
Decorators :
@Put('participant/:participantId/email/resubscribe')
@HttpCode(HttpStatus.OK)
@ApiOperation({summary: 'Resubscribe a participant (alias path)'})

Alias route for resubscribe (compat)

Parameters :
Name Type Optional
auth CLAuthData No
participantId number No
Returns : unknown
Async adminUnsubscribe
adminUnsubscribe(auth: CLAuthData, participantId: number, dto: UnsubscribeReasonDto)
Decorators :
@Put('participant/:participantId/unsubscribe')
@HttpCode(HttpStatus.OK)
@ApiOperation({summary: 'Unsubscribe a participant (admin within company)'})
@ApiResponse({status: 200, description: 'Participant unsubscribed successfully'})

Admin: Unsubscribe a participant in the same company

Parameters :
Name Type Optional
auth CLAuthData No
participantId number No
dto UnsubscribeReasonDto No
Returns : unknown
Async adminUnsubscribeAlias
adminUnsubscribeAlias(auth: CLAuthData, participantId: number, dto: UnsubscribeReasonDto)
Decorators :
@Put('participant/:participantId/email/unsubscribe')
@HttpCode(HttpStatus.OK)
@ApiOperation({summary: 'Unsubscribe a participant (alias path)'})

Alias route for unsubscribe (compat)

Parameters :
Name Type Optional
auth CLAuthData No
participantId number No
dto UnsubscribeReasonDto No
Returns : unknown
Private assertParticipantAdmin
assertParticipantAdmin(auth: CLAuthData)
Parameters :
Name Type Optional
auth CLAuthData No
Returns : void
Async getMyPreferences
getMyPreferences(auth: CLAuthData)
Decorators :
@Get()
@HttpCode(HttpStatus.OK)
@ApiOperation({summary: 'Get current participant's email preferences'})
@ApiResponse({status: 200, description: 'Returns participant email preferences'})
Parameters :
Name Type Optional
auth CLAuthData No
Returns : unknown
Async getMyStatus
getMyStatus(auth: CLAuthData)
Decorators :
@Get('status')
@HttpCode(HttpStatus.OK)
@ApiOperation({summary: 'Get current participant's email subscription status'})
@ApiResponse({status: 200, description: 'Returns email subscription status'})
Parameters :
Name Type Optional
auth CLAuthData No
Returns : unknown
Async getParticipantsStatus
getParticipantsStatus(auth: CLAuthData)
Decorators :
@Get('participants/status')
@HttpCode(HttpStatus.OK)
@ApiOperation({summary: 'Get email subscription statuses for all participants in company'})
@ApiResponse({status: 200, description: 'Returns email subscription statuses for all participants'})

Get email subscription statuses for all participants in the company For participant admins to see who has unsubscribed

Parameters :
Name Type Optional
auth CLAuthData No
Returns : unknown
Async resubscribe
resubscribe(auth: CLAuthData)
Decorators :
@Put('resubscribe')
@HttpCode(HttpStatus.OK)
@ApiOperation({summary: 'Re-subscribe current participant to all email reminders'})
@ApiResponse({status: 200, description: 'Participant re-subscribed successfully'})
Parameters :
Name Type Optional
auth CLAuthData No
Returns : unknown
Async unsubscribe
unsubscribe(auth: CLAuthData, dto: UnsubscribeReasonDto)
Decorators :
@Put('unsubscribe')
@HttpCode(HttpStatus.OK)
@ApiOperation({summary: 'Unsubscribe current participant from all email reminders'})
@ApiResponse({status: 200, description: 'Participant unsubscribed successfully'})
Parameters :
Name Type Optional
auth CLAuthData No
dto UnsubscribeReasonDto No
Returns : unknown
Async updateMyPreferences
updateMyPreferences(auth: CLAuthData, preferences: Partial<unknown>)
Decorators :
@Put()
@HttpCode(HttpStatus.OK)
@ApiOperation({summary: 'Update current participant's email preferences'})
@ApiResponse({status: 200, description: 'Updates participant email preferences'})
Parameters :
Name Type Optional
auth CLAuthData No
preferences Partial<unknown> No
Returns : unknown
import { BNestEmailSenderService, requireEnv } from "@bish-nest/core";
import { BNestPrismaService } from "@bish-nest/core/services";
import {
  Body,
  Controller,
  ForbiddenException,
  Get,
  HttpCode,
  HttpStatus,
  Logger,
  NotFoundException,
  Param,
  ParseIntPipe,
  Put,
} from "@nestjs/common";
import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
import { CLAuthData, ClientAuth } from "../../shared/decorators/client-auth.decorator";
import { EmailPreferencesService } from "../../shared/email/services/email-preferences.service";
import { UnsubscribeReasonDto } from "./dto/unsubscribe-reason.dto";

@ApiTags("Client - Email Preferences")
@Controller("api/client/email/preferences")
export class CLEmailPreferencesController {
  private readonly logger = new Logger(CLEmailPreferencesController.name);

  constructor(
    private readonly emailPreferencesService: EmailPreferencesService,
    private readonly prisma: BNestPrismaService,
    private readonly emailSender: BNestEmailSenderService,
  ) {}

  private assertParticipantAdmin(auth: CLAuthData) {
    // Allow only participant admins to manage other participants
    if (auth.role !== "PARTICIPANT_ADMIN") {
      throw new ForbiddenException("Only participant admins can perform this action");
    }
  }

  @Get()
  @HttpCode(HttpStatus.OK)
  @ApiOperation({ summary: "Get current participant's email preferences" })
  @ApiResponse({
    status: 200,
    description: "Returns participant email preferences",
  })
  async getMyPreferences(@ClientAuth() auth: CLAuthData) {
    return this.emailPreferencesService.getParticipantPreferences(auth.participantId);
  }

  @Put()
  @HttpCode(HttpStatus.OK)
  @ApiOperation({ summary: "Update current participant's email preferences" })
  @ApiResponse({
    status: 200,
    description: "Updates participant email preferences",
  })
  async updateMyPreferences(
    @ClientAuth() auth: CLAuthData,
    @Body() preferences: Partial<import("../../shared/email/services/email-preferences.service").EmailPreferences>,
  ) {
    return this.emailPreferencesService.updateParticipantPreferences(auth.participantId, preferences);
  }

  @Put("unsubscribe")
  @HttpCode(HttpStatus.OK)
  @ApiOperation({ summary: "Unsubscribe current participant from all email reminders" })
  @ApiResponse({
    status: 200,
    description: "Participant unsubscribed successfully",
  })
  async unsubscribe(@ClientAuth() auth: CLAuthData, @Body() dto: UnsubscribeReasonDto) {
    await this.emailPreferencesService.unsubscribeParticipant(auth.participantId, dto.reason);

    // Fire-and-forget confirmation email (do not block unsubscribe)
    const frontendUrl = requireEnv("FRONTEND_URL");
    const managePreferencesUrl = `${frontendUrl}/portal/settings/notifications`;
    const resubscribeUrl = `${frontendUrl}/portal/settings/notifications`;

    void this.emailSender
      .sendTemplatedEmail({
        to: auth.email,
        templateKey: "subscription.unsubscribe.confirmation",
        variables: {
          "user.name": auth.fullName || auth.email,
          "system.resubscribeUrl": resubscribeUrl,
          "system.managePreferencesUrl": managePreferencesUrl,
        },
        metadata: {
          participant_id: auth.participantId,
          company_id: auth.companyId,
          unsubscribe_reason: dto?.reason,
          initiated_by: "self",
        },
      })
      .catch((error: unknown) => {
        this.logger.error(`Failed to send unsubscribe confirmation email to ${auth.email}:`, error);
      });

    return { success: true, message: "Unsubscribed successfully" };
  }

  @Put("resubscribe")
  @HttpCode(HttpStatus.OK)
  @ApiOperation({ summary: "Re-subscribe current participant to all email reminders" })
  @ApiResponse({
    status: 200,
    description: "Participant re-subscribed successfully",
  })
  async resubscribe(@ClientAuth() auth: CLAuthData) {
    await this.emailPreferencesService.resubscribeParticipant(auth.participantId, "settings");

    const frontendUrl = requireEnv("FRONTEND_URL");
    const managePreferencesUrl = `${frontendUrl}/portal/settings/notifications`;

    void this.emailSender
      .sendTemplatedEmail({
        to: auth.email,
        templateKey: "subscription.resubscribe.welcome",
        variables: {
          "user.name": auth.fullName || auth.email,
          "system.managePreferencesUrl": managePreferencesUrl,
        },
        metadata: {
          participant_id: auth.participantId,
          company_id: auth.companyId,
          initiated_by: "self",
        },
      })
      .catch((error: unknown) => {
        this.logger.error(`Failed to send resubscribe welcome email to ${auth.email}:`, error);
      });

    return { success: true, message: "Resubscribed successfully" };
  }

  @Get("status")
  @HttpCode(HttpStatus.OK)
  @ApiOperation({ summary: "Get current participant's email subscription status" })
  @ApiResponse({
    status: 200,
    description: "Returns email subscription status",
  })
  async getMyStatus(@ClientAuth() auth: CLAuthData) {
    const isUnsubscribed = await this.emailPreferencesService.isUnsubscribed(auth.participantId);
    const prefs = await (this.prisma.client as any).emailPreference.findUnique({
      where: { participant_id: auth.participantId },
    });

    return {
      isUnsubscribed,
      allRemindersEnabled: prefs?.all_reminders_enabled ?? true,
      unsubscribedAt: prefs?.unsubscribed_at ?? null,
      unsubscribedReason: prefs?.unsubscribed_reason ?? null,
      resubscribedAt: prefs?.resubscribed_at ?? null,
      resubscribeCount: prefs?.resubscribe_count ?? 0,
    };
  }

  /**
   * Get email subscription statuses for all participants in the company
   * For participant admins to see who has unsubscribed
   */
  @Get("participants/status")
  @HttpCode(HttpStatus.OK)
  @ApiOperation({ summary: "Get email subscription statuses for all participants in company" })
  @ApiResponse({
    status: 200,
    description: "Returns email subscription statuses for all participants",
  })
  async getParticipantsStatus(@ClientAuth() auth: CLAuthData) {
    // Get all participants in the company
    const participants = await this.prisma.client.participant.findMany({
      where: { company_id: auth.companyId },
      select: { id: true },
    });

    const participantIds = participants.map((p) => p.id);

    // Get email preferences for all participants
    const emailPreferences = await (this.prisma.client as any).emailPreference.findMany({
      where: {
        participant_id: { in: participantIds },
      },
      select: {
        participant_id: true,
        all_reminders_enabled: true,
        unsubscribed_at: true,
        unsubscribed_reason: true,
        resubscribed_at: true,
        resubscribe_count: true,
      },
    });

    // Create a map for quick lookup
    const statusMap = new Map();
    emailPreferences.forEach((pref: any) => {
      statusMap.set(pref.participant_id, {
        isUnsubscribed: !pref.all_reminders_enabled,
        unsubscribedAt: pref.unsubscribed_at,
        unsubscribedReason: pref.unsubscribed_reason,
        resubscribedAt: pref.resubscribed_at,
        resubscribeCount: pref.resubscribe_count || 0,
      });
    });

    // Return status for all participants (including those without preferences)
    return participantIds.map((participantId) => {
      const status = statusMap.get(participantId);
      return {
        participantId,
        isUnsubscribed: status?.isUnsubscribed ?? false,
        unsubscribedAt: status?.unsubscribedAt ?? null,
        unsubscribedReason: status?.unsubscribedReason ?? null,
        resubscribedAt: status?.resubscribedAt ?? null,
        resubscribeCount: status?.resubscribeCount ?? 0,
      };
    });
  }

  /**
   * Admin: Unsubscribe a participant in the same company
   */
  @Put("participant/:participantId/unsubscribe")
  @HttpCode(HttpStatus.OK)
  @ApiOperation({ summary: "Unsubscribe a participant (admin within company)" })
  @ApiResponse({ status: 200, description: "Participant unsubscribed successfully" })
  async adminUnsubscribe(
    @ClientAuth() auth: CLAuthData,
    @Param("participantId", ParseIntPipe) participantId: number,
    @Body() dto: UnsubscribeReasonDto,
  ) {
    this.assertParticipantAdmin(auth);
    const participant = await (this.prisma.client as any).participant.findUnique({
      where: { id: participantId },
      select: {
        company_id: true,
        email: true,
        first_name: true,
        last_name: true,
      },
    });

    if (!participant) {
      throw new NotFoundException("Participant not found");
    }

    if (participant.company_id !== auth.companyId) {
      throw new ForbiddenException("You are not allowed to modify this participant");
    }

    await this.emailPreferencesService.unsubscribeParticipant(participantId, dto?.reason ?? "admin-panel");

    // Send confirmation email to participant
    const frontendUrl = requireEnv("FRONTEND_URL");
    const managePreferencesUrl = `${frontendUrl}/portal/settings/notifications`;
    const resubscribeUrl = `${frontendUrl}/portal/settings/notifications`;

    void this.emailSender
      .sendTemplatedEmail({
        to: participant.email,
        templateKey: "subscription.unsubscribe.confirmation",
        variables: {
          "user.name": participant.first_name
            ? `${participant.first_name} ${participant.last_name}`
            : participant.email,
          "system.resubscribeUrl": resubscribeUrl,
          "system.managePreferencesUrl": managePreferencesUrl,
        },
        metadata: {
          participant_id: participantId,
          company_id: participant.company_id,
          unsubscribe_reason: dto?.reason || "admin-panel",
          initiated_by: "admin",
          admin_id: auth.participantId,
        },
      })
      .catch((error) => {
        this.logger.error(`Failed to send unsubscribe confirmation email to ${participant.email}:`, error);
        // Don't fail the unsubscribe if confirmation email fails
      });

    return { success: true, message: "Unsubscribed successfully" };
  }

  /**
   * Alias route for unsubscribe (compat)
   */
  @Put("participant/:participantId/email/unsubscribe")
  @HttpCode(HttpStatus.OK)
  @ApiOperation({ summary: "Unsubscribe a participant (alias path)" })
  async adminUnsubscribeAlias(
    @ClientAuth() auth: CLAuthData,
    @Param("participantId", ParseIntPipe) participantId: number,
    @Body() dto: UnsubscribeReasonDto,
  ) {
    return this.adminUnsubscribe(auth, participantId, dto);
  }

  /**
   * Admin: Resubscribe a participant in the same company
   */
  @Put("participant/:participantId/resubscribe")
  @HttpCode(HttpStatus.OK)
  @ApiOperation({ summary: "Resubscribe a participant (admin within company)" })
  @ApiResponse({ status: 200, description: "Participant resubscribed successfully" })
  async adminResubscribe(
    @ClientAuth() auth: CLAuthData,
    @Param("participantId", ParseIntPipe) participantId: number,
  ) {
    this.assertParticipantAdmin(auth);
    const participant = await (this.prisma.client as any).participant.findUnique({
      where: { id: participantId },
      select: {
        company_id: true,
        email: true,
        first_name: true,
        last_name: true,
      },
    });

    if (!participant) {
      throw new NotFoundException("Participant not found");
    }

    if (participant.company_id !== auth.companyId) {
      throw new ForbiddenException("You are not allowed to modify this participant");
    }

    await this.emailPreferencesService.resubscribeParticipant(participantId, "admin-panel");

    // Send welcome back email to participant
    const frontendUrl = requireEnv("FRONTEND_URL");
    const managePreferencesUrl = `${frontendUrl}/portal/settings/notifications`;

    void this.emailSender
      .sendTemplatedEmail({
        to: participant.email,
        templateKey: "subscription.resubscribe.welcome",
        variables: {
          "user.name": participant.first_name
            ? `${participant.first_name} ${participant.last_name}`
            : participant.email,
          "system.managePreferencesUrl": managePreferencesUrl,
        },
        metadata: {
          participant_id: participantId,
          company_id: participant.company_id,
          initiated_by: "admin",
          admin_id: auth.participantId,
        },
      })
      .catch((error) => {
        this.logger.error(`Failed to send resubscribe welcome email to ${participant.email}:`, error);
        // Don't fail the resubscribe if welcome email fails
      });

    return { success: true, message: "Resubscribed successfully" };
  }

  /**
   * Alias route for resubscribe (compat)
   */
  @Put("participant/:participantId/email/resubscribe")
  @HttpCode(HttpStatus.OK)
  @ApiOperation({ summary: "Resubscribe a participant (alias path)" })
  async adminResubscribeAlias(
    @ClientAuth() auth: CLAuthData,
    @Param("participantId", ParseIntPipe) participantId: number,
  ) {
    return this.adminResubscribe(auth, participantId);
  }
}

results matching ""

    No results matching ""