apps/recallassess/recallassess-api/src/api/client/email/email-preferences.controller.ts
api/client/email/preferences
Methods |
|
| Async adminResubscribe | |||||||||
adminResubscribe(auth: CLAuthData, participantId: number)
|
|||||||||
Decorators :
@Put('participant/:participantId/resubscribe')
|
|||||||||
|
Admin: Resubscribe a participant in the same company
Parameters :
Returns :
unknown
|
| Async adminResubscribeAlias | |||||||||
adminResubscribeAlias(auth: CLAuthData, participantId: number)
|
|||||||||
Decorators :
@Put('participant/:participantId/email/resubscribe')
|
|||||||||
|
Alias route for resubscribe (compat)
Parameters :
Returns :
unknown
|
| Async adminUnsubscribe | ||||||||||||
adminUnsubscribe(auth: CLAuthData, participantId: number, dto: UnsubscribeReasonDto)
|
||||||||||||
Decorators :
@Put('participant/:participantId/unsubscribe')
|
||||||||||||
|
Admin: Unsubscribe a participant in the same company
Parameters :
Returns :
unknown
|
| Async adminUnsubscribeAlias | ||||||||||||
adminUnsubscribeAlias(auth: CLAuthData, participantId: number, dto: UnsubscribeReasonDto)
|
||||||||||||
Decorators :
@Put('participant/:participantId/email/unsubscribe')
|
||||||||||||
|
Alias route for unsubscribe (compat)
Parameters :
Returns :
unknown
|
| Private assertParticipantAdmin | ||||||
assertParticipantAdmin(auth: CLAuthData)
|
||||||
|
Parameters :
Returns :
void
|
| Async getMyPreferences | ||||||
getMyPreferences(auth: CLAuthData)
|
||||||
Decorators :
@Get()
|
||||||
|
Parameters :
Returns :
unknown
|
| Async getMyStatus | ||||||
getMyStatus(auth: CLAuthData)
|
||||||
Decorators :
@Get('status')
|
||||||
|
Parameters :
Returns :
unknown
|
| Async getParticipantsStatus | ||||||
getParticipantsStatus(auth: CLAuthData)
|
||||||
Decorators :
@Get('participants/status')
|
||||||
|
Get email subscription statuses for all participants in the company For participant admins to see who has unsubscribed
Parameters :
Returns :
unknown
|
| Async resubscribe | ||||||
resubscribe(auth: CLAuthData)
|
||||||
Decorators :
@Put('resubscribe')
|
||||||
|
Parameters :
Returns :
unknown
|
| Async unsubscribe | |||||||||
unsubscribe(auth: CLAuthData, dto: UnsubscribeReasonDto)
|
|||||||||
Decorators :
@Put('unsubscribe')
|
|||||||||
|
Parameters :
Returns :
unknown
|
| Async updateMyPreferences | |||||||||
updateMyPreferences(auth: CLAuthData, preferences: Partial<unknown>)
|
|||||||||
Decorators :
@Put()
|
|||||||||
|
Parameters :
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);
}
}