apps/recallassess/recallassess-api/src/api/shared/email/controllers/email-preferences.controller.ts
api/admin/email/preferences
Super-admin / recallassess-admin PWA endpoints under the same base path as CLEmailPreferencesController. Duplicate routes (unsubscribe/resubscribe/status list) live only in the client controller to avoid Nest route conflicts.
Methods |
|
| Async bulkResubscribe | ||||||
bulkResubscribe(dto: literal type)
|
||||||
Decorators :
@Post('bulk-resubscribe')
|
||||||
|
Parameters :
Returns :
unknown
|
| Async bulkUnsubscribe | ||||||
bulkUnsubscribe(dto: literal type)
|
||||||
Decorators :
@Post('bulk-unsubscribe')
|
||||||
|
Parameters :
Returns :
unknown
|
| Async getCompanyEmailHistory |
getCompanyEmailHistory(companyId: number, limit?: number)
|
Decorators :
@Get('company/:companyId/history')
|
|
Returns :
unknown
|
| Async getEmailHistory |
getEmailHistory(participantId: number, limit?: number)
|
Decorators :
@Get('participant/:participantId/history')
|
|
Returns :
unknown
|
| Async getEmailStatus | ||||||
getEmailStatus(participantId: number)
|
||||||
Decorators :
@Get('participant/:participantId/status')
|
||||||
|
Parameters :
Returns :
unknown
|
| Async getParticipantPreferences | ||||||
getParticipantPreferences(participantId: number)
|
||||||
Decorators :
@Get('participant/:participantId')
|
||||||
|
Parameters :
Returns :
unknown
|
| Async updateParticipantPreferences | |||||||||
updateParticipantPreferences(participantId: number, preferences: Partial<unknown>)
|
|||||||||
Decorators :
@Put('participant/:participantId')
|
|||||||||
|
Parameters :
Returns :
unknown
|
import { BNestEmailSenderService, requireEnv } from "@bish-nest/core";
import { BNestPrismaService } from "@bish-nest/core/services";
import {
Body,
Controller,
Get,
HttpCode,
HttpStatus,
Logger,
Param,
ParseIntPipe,
Post,
Put,
Query,
} from "@nestjs/common";
import { EmailPreferencesService } from "../services/email-preferences.service";
/**
* Super-admin / recallassess-admin PWA endpoints under the same base path as
* {@link CLEmailPreferencesController}. Duplicate routes (unsubscribe/resubscribe/status list)
* live only in the client controller to avoid Nest route conflicts.
*/
@Controller("api/admin/email/preferences")
export class EmailPreferencesController {
private readonly logger = new Logger(EmailPreferencesController.name);
constructor(
private readonly emailPreferencesService: EmailPreferencesService,
private readonly prisma: BNestPrismaService,
private readonly emailSender: BNestEmailSenderService,
) {}
@Get("participant/:participantId")
@HttpCode(HttpStatus.OK)
async getParticipantPreferences(@Param("participantId", ParseIntPipe) participantId: number) {
return this.emailPreferencesService.getParticipantPreferences(participantId);
}
@Put("participant/:participantId")
@HttpCode(HttpStatus.OK)
async updateParticipantPreferences(
@Param("participantId", ParseIntPipe) participantId: number,
@Body() preferences: Partial<import("../services/email-preferences.service").EmailPreferences>,
) {
return this.emailPreferencesService.updateParticipantPreferences(participantId, preferences);
}
@Get("participant/:participantId/status")
@HttpCode(HttpStatus.OK)
async getEmailStatus(@Param("participantId", ParseIntPipe) participantId: number) {
const isUnsubscribed = await this.emailPreferencesService.isUnsubscribed(participantId);
const preferences = await this.emailPreferencesService.getParticipantPreferences(participantId);
// Get email preference details
const prefs = await (this.prisma.client as any).emailPreference.findUnique({
where: { participant_id: 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,
preferences,
};
}
@Get("participant/:participantId/history")
@HttpCode(HttpStatus.OK)
async getEmailHistory(
@Param("participantId", ParseIntPipe) participantId: number,
@Query("limit", new ParseIntPipe({ optional: true })) limit?: number,
) {
const participant = await this.prisma.client.participant.findUnique({
where: { id: participantId },
select: { email: true },
});
if (!participant) {
return [];
}
const take = limit || 10;
const emailTrimmed = participant.email?.trim();
// Many rows only set recipient_email or learning_group_participant_id, not participant_id.
const emails = await this.prisma.client.emailLog.findMany({
where: {
OR: [
{ participant_id: participantId },
{
learningGroupParticipant: {
participant_id: participantId,
},
},
...(emailTrimmed
? [
{
recipient_email: {
equals: emailTrimmed,
mode: "insensitive" as const,
},
},
]
: []),
],
},
orderBy: {
created_at: "desc",
},
take,
select: {
id: true,
subject: true,
status: true,
sent_date: true,
created_at: true,
error_message: true,
bounce_reason: true,
complaint_reason: true,
emailTemplate: {
select: {
template_key: true,
subject: true,
description: true,
},
},
},
});
return emails;
}
@Get("company/:companyId/history")
@HttpCode(HttpStatus.OK)
async getCompanyEmailHistory(
@Param("companyId", ParseIntPipe) companyId: number,
@Query("limit", new ParseIntPipe({ optional: true })) limit?: number,
) {
const emails = await this.prisma.client.emailLog.findMany({
where: {
company_id: companyId,
},
orderBy: {
created_at: "desc",
},
take: limit || 10,
select: {
id: true,
recipient_email: true,
subject: true,
status: true,
sent_date: true,
created_at: true,
error_message: true,
bounce_reason: true,
complaint_reason: true,
emailTemplate: {
select: {
template_key: true,
subject: true,
description: true,
},
},
},
});
return emails;
}
@Post("bulk-unsubscribe")
@HttpCode(HttpStatus.OK)
async bulkUnsubscribe(@Body() dto: { participantIds: number[]; reason?: string; adminNotes?: string }) {
const results = {
total: dto.participantIds.length,
successful: 0,
failed: 0,
errors: [] as Array<{ participantId: number; error: string }>,
};
const frontendUrl = requireEnv("FRONTEND_URL");
const managePreferencesUrl = `${frontendUrl}/portal/settings/notifications`;
const resubscribeUrl = `${frontendUrl}/portal/settings/notifications`;
for (const participantId of dto.participantIds) {
try {
// Get participant info
const participant = await this.prisma.client.participant.findUnique({
where: { id: participantId },
select: {
email: true,
first_name: true,
last_name: true,
company_id: true,
},
});
if (!participant) {
throw new Error(`Participant not found: ${participantId}`);
}
// Perform unsubscribe
await this.emailPreferencesService.unsubscribeParticipant(participantId, dto.reason);
results.successful++;
// Send confirmation email to participant (fire-and-forget)
void this.emailSender
.sendTemplatedEmail({
to: participant.email,
templateKey: "subscription.unsubscribe.confirmation",
variables: {
"user.name":
participant.first_name && participant.last_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-bulk",
initiated_by: "super_admin",
},
})
.catch((error) => {
this.logger.error(`Failed to send unsubscribe confirmation email to ${participant.email}:`, error);
// Don't fail the unsubscribe if confirmation email fails
});
} catch (error: any) {
results.failed++;
results.errors.push({
participantId,
error: error.message || "Unknown error",
});
}
}
return results;
}
@Post("bulk-resubscribe")
@HttpCode(HttpStatus.OK)
async bulkResubscribe(@Body() dto: { participantIds: number[] }) {
const results = {
total: dto.participantIds.length,
successful: 0,
failed: 0,
errors: [] as Array<{ participantId: number; error: string }>,
};
const frontendUrlR = requireEnv("FRONTEND_URL");
const managePreferencesUrl = `${frontendUrlR}/portal/settings/notifications`;
for (const participantId of dto.participantIds) {
try {
// Get participant info
const participant = await this.prisma.client.participant.findUnique({
where: { id: participantId },
select: {
email: true,
first_name: true,
last_name: true,
company_id: true,
},
});
if (!participant) {
throw new Error(`Participant not found: ${participantId}`);
}
// Perform resubscribe
await this.emailPreferencesService.resubscribeParticipant(participantId, "admin_panel_bulk");
results.successful++;
// Send welcome back email to participant (fire-and-forget)
void this.emailSender
.sendTemplatedEmail({
to: participant.email,
templateKey: "subscription.resubscribe.welcome",
variables: {
"user.name":
participant.first_name && participant.last_name
? `${participant.first_name} ${participant.last_name}`
: participant.email,
"system.managePreferencesUrl": managePreferencesUrl,
},
metadata: {
participant_id: participantId,
company_id: participant.company_id,
initiated_by: "super_admin",
},
})
.catch((error) => {
this.logger.error(`Failed to send resubscribe welcome email to ${participant.email}:`, error);
// Don't fail the resubscribe if welcome email fails
});
} catch (error: any) {
results.failed++;
results.errors.push({
participantId,
error: error.message || "Unknown error",
});
}
}
return results;
}
}