feat(notificaciones): edit summary recipients in the UI
NOTIFICATION_ADMIN_EMAILS made "add Beto to the summaries" a redeploy — the wrong unit of work for a list that changes when office staff change. Adds `app_settings`, a key/value table for the configuration staff must be able to change without a deploy, and `SettingsService`, which resolves every key db -> env -> default and reports which of the three a value came from. That ladder is what makes the move safe: a deployment behaves exactly as before until somebody saves in the UI, and the screen can say "this is still coming from the deployment" rather than implying somebody chose it. - new ability `setting:manage` (ADMIN) — deliberately above `notification:send`, since redirecting the audit summaries is how someone would quietly stop them being read - GET/PUT /notifications/settings/admin-emails; read is open to any logged-in user so the UI can display the list, write is gated - resolved per job, not cached at boot, or we would reintroduce exactly the restart-to-apply behaviour being removed - a saved empty list means "nobody" and does NOT fall through to the env, or clearing the field would keep mailing the people just removed Credentials stay in env — see the model doc for where the line is drawn. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import { Injectable, Logger, ServiceUnavailableException } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import {
|
||||
Currency,
|
||||
EmailNotificationServicio,
|
||||
@@ -10,6 +9,7 @@ import {
|
||||
} from "@jorgecuadros/database";
|
||||
import { MailService } from "../mail/mail.service";
|
||||
import { PrismaService } from "../prisma/prisma.service";
|
||||
import { SettingsService } from "../settings/settings.service";
|
||||
import { NotificationLogService } from "./notification-log.service";
|
||||
import {
|
||||
SendAttempt,
|
||||
@@ -75,26 +75,12 @@ const PAYMENT_LOOKBACK_HOURS = 24;
|
||||
export class NotificationsService {
|
||||
private readonly logger = new Logger(NotificationsService.name);
|
||||
|
||||
/** Override addresses — comma-separated in env. Falls back to the
|
||||
* legacy defaults so a fresh deploy still has somewhere to send. */
|
||||
private readonly adminEmails: string[];
|
||||
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly mail: MailService,
|
||||
private readonly log: NotificationLogService,
|
||||
config: ConfigService,
|
||||
) {
|
||||
const csv = config.get<string>("NOTIFICATION_ADMIN_EMAILS");
|
||||
if (csv && csv.trim()) {
|
||||
this.adminEmails = csv
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
} else {
|
||||
this.adminEmails = ["rmancinas@freakma.net", "mpulido@freakma.net"];
|
||||
}
|
||||
}
|
||||
private readonly settings: SettingsService,
|
||||
) {}
|
||||
|
||||
/* ============================================================================
|
||||
* Public jobs — called by the controller and by future cron sweeps alike.
|
||||
@@ -958,7 +944,11 @@ export class NotificationsService {
|
||||
* response so it matches the legacy format verbatim. */
|
||||
private async adminSummary(subject: string, response: NotificationJobResponse) {
|
||||
const body = JSON.stringify(response);
|
||||
for (const to of this.adminEmails) {
|
||||
// Resolved per job, not cached at boot: the recipient list is edited in
|
||||
// the UI while the app is running (SettingsService), and a cached copy
|
||||
// would put us back to needing a restart for it to take effect.
|
||||
const { value: adminEmails } = await this.settings.notificationAdminEmails();
|
||||
for (const to of adminEmails) {
|
||||
try {
|
||||
const { messageId } = await this.mail.send({
|
||||
to,
|
||||
|
||||
Reference in New Issue
Block a user