Files
PowderCoatingLogix/src/PowderCoating.Infrastructure/Services/AdminNotificationService.cs
T
2026-04-23 21:38:24 -04:00

310 lines
15 KiB
C#

using Microsoft.Extensions.Logging;
using PowderCoating.Application.Interfaces;
using PowderCoating.Core.Entities;
namespace PowderCoating.Infrastructure.Services;
/// <summary>
/// Platform-level email notification service that alerts SuperAdmin recipients about
/// key lifecycle events: new company registrations, bug reports, subscription expirations,
/// and grace-period transitions.
/// </summary>
/// <remarks>
/// Admin email addresses are read from the <c>AdminNotificationEmail</c> platform setting
/// (a comma-separated list). If that setting is absent or empty, all notification methods
/// silently no-op rather than throwing, so a mis-configured setting never breaks the
/// action that triggered the notification.
///
/// This service sends platform-level alerts only — it does NOT send company-facing
/// emails (invoices, quote approvals, etc.), which are handled by <c>IEmailService</c>
/// directly from the relevant controllers.
/// </remarks>
public class AdminNotificationService : IAdminNotificationService
{
private readonly IEmailService _emailService;
private readonly IPlatformSettingsService _platformSettings;
private readonly ILogger<AdminNotificationService> _logger;
public AdminNotificationService(
IEmailService emailService,
IPlatformSettingsService platformSettings,
ILogger<AdminNotificationService> logger)
{
_emailService = emailService;
_platformSettings = platformSettings;
_logger = logger;
}
/// <summary>
/// Sends an email notification to all configured SuperAdmin addresses when a new tenant
/// company completes registration, including company ID, plan name, and primary contact.
/// </summary>
/// <param name="companyId">Database ID of the newly created company.</param>
/// <param name="companyName">Display name of the new company.</param>
/// <param name="planName">Subscription plan the company signed up for.</param>
/// <param name="contactName">Full name of the registration contact.</param>
/// <param name="contactEmail">Email address of the registration contact.</param>
public async Task NotifyNewCompanyRegisteredAsync(
int companyId, string companyName, string planName,
string contactName, string contactEmail)
{
var adminEmails = await GetAdminEmailsAsync();
if (adminEmails == null) return;
var subject = $"[New Signup] {companyName}";
var html = $"""
<h2>New Company Registered</h2>
<table cellpadding="6" style="border-collapse:collapse;">
<tr><td><strong>Company:</strong></td><td>{Encode(companyName)} (ID: {companyId})</td></tr>
<tr><td><strong>Plan:</strong></td><td>{Encode(planName)}</td></tr>
<tr><td><strong>Contact Name:</strong></td><td>{Encode(contactName)}</td></tr>
<tr><td><strong>Contact Email:</strong></td><td>{Encode(contactEmail)}</td></tr>
<tr><td><strong>Registered:</strong></td><td>{DateTime.UtcNow:MM/dd/yyyy h:mm tt} UTC</td></tr>
</table>
""";
var plain = $"New Company Registered\n\n" +
$"Company: {companyName} (ID: {companyId})\n" +
$"Plan: {planName}\n" +
$"Contact: {contactName} <{contactEmail}>\n" +
$"Registered: {DateTime.UtcNow:MM/dd/yyyy h:mm tt} UTC";
await SendAsync(adminEmails, subject, plain, html);
}
/// <summary>
/// Sends an email notification to all configured SuperAdmin addresses when a user submits
/// a bug report, including the report ID, title, priority, submitting user, and full
/// description body so admins can triage without logging in.
/// </summary>
/// <param name="bugReportId">Database ID of the new bug report.</param>
/// <param name="title">Short title of the bug report.</param>
/// <param name="description">Full description text entered by the user.</param>
/// <param name="priority">Priority label (e.g. Low, Medium, High, Critical).</param>
/// <param name="submittedByName">Display name of the user who submitted the report.</param>
/// <param name="companyName">Name of the company the submitting user belongs to.</param>
public async Task NotifyBugReportSubmittedAsync(
int bugReportId, string title, string description,
string priority, string submittedByName, string companyName)
{
var adminEmails = await GetAdminEmailsAsync();
if (adminEmails == null) return;
var subject = $"[Bug Report] {title}";
var html = $"""
<h2>New Bug Report Submitted</h2>
<table cellpadding="6" style="border-collapse:collapse;">
<tr><td><strong>Report #:</strong></td><td>{bugReportId}</td></tr>
<tr><td><strong>Title:</strong></td><td>{Encode(title)}</td></tr>
<tr><td><strong>Priority:</strong></td><td>{Encode(priority)}</td></tr>
<tr><td><strong>Submitted By:</strong></td><td>{Encode(submittedByName)}</td></tr>
<tr><td><strong>Company:</strong></td><td>{Encode(companyName)}</td></tr>
<tr><td><strong>Submitted:</strong></td><td>{DateTime.UtcNow:MM/dd/yyyy h:mm tt} UTC</td></tr>
</table>
<h3>Description</h3>
<p style="white-space:pre-wrap;">{Encode(description)}</p>
""";
var plain = $"New Bug Report #{bugReportId}\n\n" +
$"Title: {title}\n" +
$"Priority: {priority}\n" +
$"Submitted By: {submittedByName}\n" +
$"Company: {companyName}\n" +
$"Submitted: {DateTime.UtcNow:MM/dd/yyyy h:mm tt} UTC\n\n" +
$"Description:\n{description}";
await SendAsync(adminEmails, subject, plain, html);
}
/// <summary>
/// Sends an email notification to all configured SuperAdmin addresses when a company's
/// subscription has expired and its account has been deactivated.
/// </summary>
/// <param name="companyId">Database ID of the expired company.</param>
/// <param name="companyName">Display name of the company.</param>
/// <param name="contactEmail">Primary contact email for the company.</param>
/// <param name="expiredOn">The UTC date on which the subscription expired.</param>
public async Task NotifyCompanyExpiredAsync(
int companyId, string companyName, string contactEmail, DateTime expiredOn)
{
var adminEmails = await GetAdminEmailsAsync();
if (adminEmails == null) return;
var subject = $"[Subscription Expired] {companyName}";
var html = $"""
<h2>Company Subscription Expired</h2>
<table cellpadding="6" style="border-collapse:collapse;">
<tr><td><strong>Company:</strong></td><td>{Encode(companyName)} (ID: {companyId})</td></tr>
<tr><td><strong>Contact Email:</strong></td><td>{Encode(contactEmail)}</td></tr>
<tr><td><strong>Expired On:</strong></td><td>{expiredOn:MMMM d, yyyy}</td></tr>
</table>
<p>This company has been deactivated and is no longer able to log in.</p>
""";
var plain = $"Company Subscription Expired\n\n" +
$"Company: {companyName} (ID: {companyId})\n" +
$"Contact Email: {contactEmail}\n" +
$"Expired On: {expiredOn:MMMM d, yyyy}\n\n" +
$"This company has been deactivated.";
await SendAsync(adminEmails, subject, plain, html);
}
/// <summary>
/// Sends an email notification to all configured SuperAdmin addresses when a company
/// enters a subscription grace period, including the grace-period end date so admins
/// know when the account will auto-deactivate if not renewed.
/// </summary>
/// <param name="companyId">Database ID of the company entering grace period.</param>
/// <param name="companyName">Display name of the company.</param>
/// <param name="contactEmail">Primary contact email for the company.</param>
/// <param name="gracePeriodEndsOn">UTC date on which the grace period expires.</param>
public async Task NotifyCompanyGracePeriodAsync(
int companyId, string companyName, string contactEmail, DateTime gracePeriodEndsOn)
{
var adminEmails = await GetAdminEmailsAsync();
if (adminEmails == null) return;
var subject = $"[Grace Period Started] {companyName}";
var html = $"""
<h2>Company Entered Grace Period</h2>
<table cellpadding="6" style="border-collapse:collapse;">
<tr><td><strong>Company:</strong></td><td>{Encode(companyName)} (ID: {companyId})</td></tr>
<tr><td><strong>Contact Email:</strong></td><td>{Encode(contactEmail)}</td></tr>
<tr><td><strong>Grace Period Ends:</strong></td><td>{gracePeriodEndsOn:MMMM d, yyyy}</td></tr>
</table>
<p>If not renewed by the grace period end date, this company will be automatically deactivated.</p>
""";
var plain = $"Company Entered Grace Period\n\n" +
$"Company: {companyName} (ID: {companyId})\n" +
$"Contact Email: {contactEmail}\n" +
$"Grace Period Ends: {gracePeriodEndsOn:MMMM d, yyyy}\n\n" +
$"If not renewed, this company will be automatically deactivated.";
await SendAsync(adminEmails, subject, plain, html);
}
/// <summary>
/// Sends an email to all configured SuperAdmin addresses when a user submits the
/// Contact Us form, including the sender's name, email, company, category, subject,
/// and full message so admins can respond directly from their email client.
/// </summary>
public async Task NotifyContactFormSubmittedAsync(
string senderName, string senderEmail, string companyName,
string category, string subject, string message)
{
var adminEmails = await GetAdminEmailsAsync();
if (adminEmails == null) return;
var emailSubject = $"[Contact Us] {category} — {subject}";
var html = $"""
<h2>Contact Us Form Submission</h2>
<table cellpadding="6" style="border-collapse:collapse;">
<tr><td><strong>Name:</strong></td><td>{Encode(senderName)}</td></tr>
<tr><td><strong>Email:</strong></td><td><a href="mailto:{Encode(senderEmail)}">{Encode(senderEmail)}</a></td></tr>
<tr><td><strong>Company:</strong></td><td>{Encode(companyName)}</td></tr>
<tr><td><strong>Category:</strong></td><td>{Encode(category)}</td></tr>
<tr><td><strong>Subject:</strong></td><td>{Encode(subject)}</td></tr>
<tr><td><strong>Submitted:</strong></td><td>{DateTime.UtcNow:MM/dd/yyyy h:mm tt} UTC</td></tr>
</table>
<h3>Message</h3>
<p style="white-space:pre-wrap;">{Encode(message)}</p>
""";
var plain = $"Contact Us Form Submission\n\n" +
$"Name: {senderName}\n" +
$"Email: {senderEmail}\n" +
$"Company: {companyName}\n" +
$"Category: {category}\n" +
$"Subject: {subject}\n" +
$"Submitted: {DateTime.UtcNow:MM/dd/yyyy h:mm tt} UTC\n\n" +
$"Message:\n{message}";
await SendAsync(adminEmails, emailSubject, plain, html, replyToEmail: senderEmail, replyToName: senderName);
}
// ─── Helpers ─────────────────────────────────────────────────────────────
/// <summary>
/// Reads the <c>AdminNotificationEmail</c> platform setting and returns a list of valid
/// email addresses, or <c>null</c> if the setting is absent, blank, or contains no
/// parseable addresses.
/// </summary>
/// <remarks>
/// Returning <c>null</c> (rather than an empty list) is intentional: callers use a
/// null-check early-return pattern (<c>if (adminEmails == null) return;</c>) which is
/// cleaner than checking <c>Count == 0</c> in each notification method. A DEBUG-level
/// log entry is written so that missing configuration is detectable without generating
/// noise at WARNING or above in normal operation.
/// </remarks>
/// <returns>Non-empty list of email strings, or <c>null</c> if none are configured.</returns>
private async Task<List<string>?> GetAdminEmailsAsync()
{
var raw = await _platformSettings.GetAsync(PlatformSettingKeys.AdminNotificationEmail);
if (string.IsNullOrWhiteSpace(raw))
{
_logger.LogDebug("PlatformSetting '{Key}' is not configured — skipping admin notification.",
PlatformSettingKeys.AdminNotificationEmail);
return null;
}
var emails = raw
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Where(e => e.Contains('@'))
.ToList();
if (emails.Count == 0)
{
_logger.LogDebug("PlatformSetting '{Key}' contained no valid addresses — skipping.",
PlatformSettingKeys.AdminNotificationEmail);
return null;
}
return emails;
}
/// <summary>
/// Sends the notification email to each address in <paramref name="adminEmails"/> via
/// <see cref="IEmailService"/>, logging a warning for any individual send failure without
/// re-throwing so that a bad address does not prevent delivery to the remaining recipients.
/// </summary>
/// <param name="adminEmails">Validated list of recipient email addresses.</param>
/// <param name="subject">Email subject line.</param>
/// <param name="plain">Plain-text fallback body.</param>
/// <param name="html">HTML-formatted body shown by modern email clients.</param>
private async Task SendAsync(List<string> adminEmails, string subject, string plain, string html,
string? replyToEmail = null, string? replyToName = null)
{
foreach (var email in adminEmails)
{
var (success, error) = await _emailService.SendEmailAsync(
toEmail: email,
toName: "PCL Admin",
subject: subject,
plainTextBody: plain,
htmlBody: html,
replyToEmail: replyToEmail,
replyToName: replyToName);
if (!success)
_logger.LogWarning("Admin notification email failed for {Email} ({Subject}): {Error}", email, subject, error);
else
_logger.LogInformation("Admin notification sent to {Email}: {Subject}", email, subject);
}
}
/// <summary>
/// HTML-encodes a string for safe embedding in email HTML bodies, guarding against
/// XSS if user-supplied values (company names, titles, descriptions) contain angle
/// brackets or other special characters. Null input is treated as empty string.
/// </summary>
private static string Encode(string? value) =>
System.Net.WebUtility.HtmlEncode(value ?? string.Empty);
}