using Microsoft.Extensions.Logging; using PowderCoating.Application.Interfaces; using PowderCoating.Core.Entities; namespace PowderCoating.Infrastructure.Services; /// /// Platform-level email notification service that alerts SuperAdmin recipients about /// key lifecycle events: new company registrations, bug reports, subscription expirations, /// and grace-period transitions. /// /// /// Admin email addresses are read from the AdminNotificationEmail 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 IEmailService /// directly from the relevant controllers. /// public class AdminNotificationService : IAdminNotificationService { private readonly IEmailService _emailService; private readonly IPlatformSettingsService _platformSettings; private readonly ILogger _logger; public AdminNotificationService( IEmailService emailService, IPlatformSettingsService platformSettings, ILogger logger) { _emailService = emailService; _platformSettings = platformSettings; _logger = logger; } /// /// Sends an email notification to all configured SuperAdmin addresses when a new tenant /// company completes registration, including company ID, plan name, and primary contact. /// /// Database ID of the newly created company. /// Display name of the new company. /// Subscription plan the company signed up for. /// Full name of the registration contact. /// Email address of the registration contact. 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 = $"""

New Company Registered

Company:{Encode(companyName)} (ID: {companyId})
Plan:{Encode(planName)}
Contact Name:{Encode(contactName)}
Contact Email:{Encode(contactEmail)}
Registered:{DateTime.UtcNow:MM/dd/yyyy h:mm tt} UTC
"""; 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); } /// /// 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. /// /// Database ID of the new bug report. /// Short title of the bug report. /// Full description text entered by the user. /// Priority label (e.g. Low, Medium, High, Critical). /// Display name of the user who submitted the report. /// Name of the company the submitting user belongs to. 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 = $"""

New Bug Report Submitted

Report #:{bugReportId}
Title:{Encode(title)}
Priority:{Encode(priority)}
Submitted By:{Encode(submittedByName)}
Company:{Encode(companyName)}
Submitted:{DateTime.UtcNow:MM/dd/yyyy h:mm tt} UTC

Description

{Encode(description)}

"""; 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); } /// /// Sends an email notification to all configured SuperAdmin addresses when a company's /// subscription has expired and its account has been deactivated. /// /// Database ID of the expired company. /// Display name of the company. /// Primary contact email for the company. /// The UTC date on which the subscription expired. 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 = $"""

Company Subscription Expired

Company:{Encode(companyName)} (ID: {companyId})
Contact Email:{Encode(contactEmail)}
Expired On:{expiredOn:MMMM d, yyyy}

This company has been deactivated and is no longer able to log in.

"""; 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); } /// /// 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. /// /// Database ID of the company entering grace period. /// Display name of the company. /// Primary contact email for the company. /// UTC date on which the grace period expires. 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 = $"""

Company Entered Grace Period

Company:{Encode(companyName)} (ID: {companyId})
Contact Email:{Encode(contactEmail)}
Grace Period Ends:{gracePeriodEndsOn:MMMM d, yyyy}

If not renewed by the grace period end date, this company will be automatically deactivated.

"""; 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); } /// /// 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. /// 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 = $"""

Contact Us Form Submission

Name:{Encode(senderName)}
Email:{Encode(senderEmail)}
Company:{Encode(companyName)}
Category:{Encode(category)}
Subject:{Encode(subject)}
Submitted:{DateTime.UtcNow:MM/dd/yyyy h:mm tt} UTC

Message

{Encode(message)}

"""; 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 ───────────────────────────────────────────────────────────── /// /// Reads the AdminNotificationEmail platform setting and returns a list of valid /// email addresses, or null if the setting is absent, blank, or contains no /// parseable addresses. /// /// /// Returning null (rather than an empty list) is intentional: callers use a /// null-check early-return pattern (if (adminEmails == null) return;) which is /// cleaner than checking Count == 0 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. /// /// Non-empty list of email strings, or null if none are configured. private async Task?> 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; } /// /// Sends the notification email to each address in via /// , logging a warning for any individual send failure without /// re-throwing so that a bad address does not prevent delivery to the remaining recipients. /// /// Validated list of recipient email addresses. /// Email subject line. /// Plain-text fallback body. /// HTML-formatted body shown by modern email clients. private async Task SendAsync(List 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); } } /// /// 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. /// private static string Encode(string? value) => System.Net.WebUtility.HtmlEncode(value ?? string.Empty); }