310 lines
15 KiB
C#
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);
|
|
}
|