Add BillingEmail field for commercial customers; support comma-separated multi-email
- Customer entity + DTO: new BillingEmail field (accounting/invoicing address) - Email fields now accept comma-separated lists; DTO validates each address individually - NotificationService: SendToEmailListAsync helper fans out to all addresses in a list; NotifyQuoteSentAsync accepts optional overrideEmail so staff can send to an ad-hoc address - Migration: AddCustomerBillingEmail - Customer Create/Edit/Details views updated to show Billing Email field - customer-billing-email.js: client-side helpers for billing email input Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -70,7 +70,7 @@ public class NotificationService : INotificationService
|
||||
/// - Customer: respects NotifyByEmail; still writes a Skipped log if opted out.
|
||||
/// Always writes a NotificationLog row so the Notifications Sent tab shows delivery history.
|
||||
/// </summary>
|
||||
public async Task NotifyQuoteSentAsync(Quote quote, byte[]? pdfAttachment = null, string? pdfFilename = null)
|
||||
public async Task NotifyQuoteSentAsync(Quote quote, byte[]? pdfAttachment = null, string? pdfFilename = null, string? overrideEmail = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -80,7 +80,8 @@ public class NotificationService : INotificationService
|
||||
// Prospect quote (no customer record yet)
|
||||
if (quote.CustomerId == null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(quote.ProspectEmail))
|
||||
var prospectEmail = !string.IsNullOrWhiteSpace(overrideEmail) ? overrideEmail : quote.ProspectEmail;
|
||||
if (string.IsNullOrWhiteSpace(prospectEmail))
|
||||
return;
|
||||
|
||||
var prospectName = !string.IsNullOrWhiteSpace(quote.ProspectContactName)
|
||||
@@ -97,7 +98,7 @@ public class NotificationService : INotificationService
|
||||
var plainText = StripHtml(fullHtml);
|
||||
|
||||
var (success, error) = await _emailService.SendEmailAsync(
|
||||
quote.ProspectEmail, prospectName, subject, plainText, fullHtml,
|
||||
prospectEmail, prospectName, subject, plainText, fullHtml,
|
||||
pdfAttachment, pdfFilename, "application/pdf",
|
||||
replyToEmail, replyToName);
|
||||
|
||||
@@ -107,7 +108,7 @@ public class NotificationService : INotificationService
|
||||
NotificationType = NotificationType.QuoteSent,
|
||||
Status = success ? NotificationStatus.Sent : NotificationStatus.Failed,
|
||||
RecipientName = prospectName,
|
||||
Recipient = quote.ProspectEmail,
|
||||
Recipient = prospectEmail,
|
||||
Subject = subject,
|
||||
Message = plainText,
|
||||
ErrorMessage = error,
|
||||
@@ -124,7 +125,11 @@ public class NotificationService : INotificationService
|
||||
|
||||
var customerName = GetCustomerDisplayName(customer);
|
||||
|
||||
if (customer.NotifyByEmail && !string.IsNullOrWhiteSpace(customer.Email))
|
||||
// Override address (ad-hoc staff entry) takes priority over customer record.
|
||||
var emailToUse = !string.IsNullOrWhiteSpace(overrideEmail) ? overrideEmail : customer.Email;
|
||||
var quoteEmails = ParseEmailList(emailToUse);
|
||||
// Bypass NotifyByEmail preference when staff explicitly supplies an override address.
|
||||
if ((customer.NotifyByEmail || !string.IsNullOrWhiteSpace(overrideEmail)) && quoteEmails.Count > 0)
|
||||
{
|
||||
var baseUrl = await GetBaseUrlAsync();
|
||||
var values = BuildQuoteSentValues(companyName, customerName, quote, baseUrl);
|
||||
@@ -135,8 +140,8 @@ public class NotificationService : INotificationService
|
||||
var fullHtml = AppendUnsubscribeFooterHtml(htmlBody, customer.UnsubscribeToken, company, baseUrl);
|
||||
var plainText = StripHtml(fullHtml);
|
||||
|
||||
var (success, error) = await _emailService.SendEmailAsync(
|
||||
customer.Email, customerName, subject, plainText, fullHtml,
|
||||
var (success, error, recipientsLog) = await SendToEmailListAsync(
|
||||
emailToUse, customerName, subject, plainText, fullHtml,
|
||||
pdfAttachment, pdfFilename, "application/pdf",
|
||||
replyToEmail, replyToName);
|
||||
|
||||
@@ -146,7 +151,7 @@ public class NotificationService : INotificationService
|
||||
NotificationType = NotificationType.QuoteSent,
|
||||
Status = success ? NotificationStatus.Sent : NotificationStatus.Failed,
|
||||
RecipientName = customerName,
|
||||
Recipient = customer.Email,
|
||||
Recipient = recipientsLog,
|
||||
Subject = subject,
|
||||
Message = plainText,
|
||||
ErrorMessage = error,
|
||||
@@ -156,10 +161,10 @@ public class NotificationService : INotificationService
|
||||
CompanyId = quote.CompanyId
|
||||
});
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(customer.Email))
|
||||
else if (quoteEmails.Count > 0)
|
||||
{
|
||||
await WriteLog(SkippedLog(NotificationChannel.Email, NotificationType.QuoteSent,
|
||||
customerName, customer.Email, quote.CompanyId, customerId: customer.Id, quoteId: quote.Id));
|
||||
customerName, string.Join(", ", quoteEmails), quote.CompanyId, customerId: customer.Id, quoteId: quote.Id));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -195,7 +200,9 @@ public class NotificationService : INotificationService
|
||||
|
||||
if (quote.CustomerId == null)
|
||||
{
|
||||
// Prospect — use ProspectPhone; no opt-in check (they explicitly provided a phone)
|
||||
// Prospect — requires explicit staff-recorded consent (TCPA compliance)
|
||||
if (!quote.ProspectSmsConsent)
|
||||
return (false, "SMS consent has not been recorded for this prospect. Edit the quote to record verbal consent before sending via SMS.");
|
||||
smsPhone = quote.ProspectPhone;
|
||||
if (string.IsNullOrWhiteSpace(smsPhone))
|
||||
return (false, "No phone number on file for this prospect.");
|
||||
@@ -279,7 +286,8 @@ public class NotificationService : INotificationService
|
||||
var (replyToEmail, replyToName) = await GetEmailFromAsync(quote.CompanyId);
|
||||
var customerName = GetCustomerDisplayName(customer);
|
||||
|
||||
if (customer.NotifyByEmail && !string.IsNullOrWhiteSpace(customer.Email))
|
||||
var approvedEmails = ParseEmailList(customer.Email);
|
||||
if (customer.NotifyByEmail && approvedEmails.Count > 0)
|
||||
{
|
||||
var values = new Dictionary<string, string>
|
||||
{
|
||||
@@ -295,7 +303,7 @@ public class NotificationService : INotificationService
|
||||
var fullHtml = AppendUnsubscribeFooterHtml(htmlBody, customer.UnsubscribeToken, company, await GetBaseUrlAsync());
|
||||
var plainText = StripHtml(fullHtml);
|
||||
|
||||
var (success, error) = await _emailService.SendEmailAsync(
|
||||
var (success, error, recipientsLog) = await SendToEmailListAsync(
|
||||
customer.Email, customerName, subject, plainText, fullHtml,
|
||||
replyToEmail: replyToEmail, replyToName: replyToName);
|
||||
|
||||
@@ -305,7 +313,7 @@ public class NotificationService : INotificationService
|
||||
NotificationType = NotificationType.QuoteApproved,
|
||||
Status = success ? NotificationStatus.Sent : NotificationStatus.Failed,
|
||||
RecipientName = customerName,
|
||||
Recipient = customer.Email,
|
||||
Recipient = recipientsLog,
|
||||
Subject = subject,
|
||||
Message = plainText,
|
||||
ErrorMessage = error,
|
||||
@@ -315,10 +323,10 @@ public class NotificationService : INotificationService
|
||||
CompanyId = quote.CompanyId
|
||||
});
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(customer.Email))
|
||||
else if (approvedEmails.Count > 0)
|
||||
{
|
||||
await WriteLog(SkippedLog(NotificationChannel.Email, NotificationType.QuoteApproved,
|
||||
customerName, customer.Email, quote.CompanyId, customerId: customer.Id, quoteId: quote.Id));
|
||||
customerName, string.Join(", ", approvedEmails), quote.CompanyId, customerId: customer.Id, quoteId: quote.Id));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -349,7 +357,8 @@ public class NotificationService : INotificationService
|
||||
: NotificationType.JobStatusChanged;
|
||||
|
||||
// Email for all status changes
|
||||
if (customer.NotifyByEmail && !string.IsNullOrWhiteSpace(customer.Email))
|
||||
var statusEmails = ParseEmailList(customer.Email);
|
||||
if (customer.NotifyByEmail && statusEmails.Count > 0)
|
||||
{
|
||||
// ScheduledDate is when the shop plans to complete the work;
|
||||
// DueDate is the customer deadline — fall back to it only when no scheduled date is set.
|
||||
@@ -377,7 +386,7 @@ public class NotificationService : INotificationService
|
||||
var fullHtml = AppendUnsubscribeFooterHtml(htmlBody, customer.UnsubscribeToken, company, await GetBaseUrlAsync());
|
||||
var plainText = StripHtml(fullHtml);
|
||||
|
||||
var (success, error) = await _emailService.SendEmailAsync(
|
||||
var (success, error, recipientsLog) = await SendToEmailListAsync(
|
||||
customer.Email, customerName, subject, plainText, fullHtml,
|
||||
replyToEmail: replyToEmail, replyToName: replyToName);
|
||||
|
||||
@@ -387,7 +396,7 @@ public class NotificationService : INotificationService
|
||||
NotificationType = notifType,
|
||||
Status = success ? NotificationStatus.Sent : NotificationStatus.Failed,
|
||||
RecipientName = customerName,
|
||||
Recipient = customer.Email,
|
||||
Recipient = recipientsLog,
|
||||
Subject = subject,
|
||||
Message = plainText,
|
||||
ErrorMessage = error,
|
||||
@@ -397,10 +406,10 @@ public class NotificationService : INotificationService
|
||||
CompanyId = job.CompanyId
|
||||
});
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(customer.Email))
|
||||
else if (statusEmails.Count > 0)
|
||||
{
|
||||
await WriteLog(SkippedLog(NotificationChannel.Email, NotificationType.JobStatusChanged,
|
||||
customerName, customer.Email, job.CompanyId, customerId: customer.Id, jobId: job.Id));
|
||||
customerName, string.Join(", ", statusEmails), job.CompanyId, customerId: customer.Id, jobId: job.Id));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -427,7 +436,8 @@ public class NotificationService : INotificationService
|
||||
var customerName = GetCustomerDisplayName(customer);
|
||||
|
||||
// Email
|
||||
if (customer.NotifyByEmail && !string.IsNullOrWhiteSpace(customer.Email))
|
||||
var completedEmails = ParseEmailList(customer.Email);
|
||||
if (customer.NotifyByEmail && completedEmails.Count > 0)
|
||||
{
|
||||
var values = new Dictionary<string, string>
|
||||
{
|
||||
@@ -444,7 +454,7 @@ public class NotificationService : INotificationService
|
||||
var fullHtml = AppendUnsubscribeFooterHtml(htmlBody, customer.UnsubscribeToken, company, await GetBaseUrlAsync());
|
||||
var plainText = StripHtml(fullHtml);
|
||||
|
||||
var (success, error) = await _emailService.SendEmailAsync(
|
||||
var (success, error, recipientsLog) = await SendToEmailListAsync(
|
||||
customer.Email, customerName, subject, plainText, fullHtml,
|
||||
replyToEmail: replyToEmail, replyToName: replyToName);
|
||||
|
||||
@@ -454,7 +464,7 @@ public class NotificationService : INotificationService
|
||||
NotificationType = NotificationType.JobCompleted,
|
||||
Status = success ? NotificationStatus.Sent : NotificationStatus.Failed,
|
||||
RecipientName = customerName,
|
||||
Recipient = customer.Email,
|
||||
Recipient = recipientsLog,
|
||||
Subject = subject,
|
||||
Message = plainText,
|
||||
ErrorMessage = error,
|
||||
@@ -464,10 +474,10 @@ public class NotificationService : INotificationService
|
||||
CompanyId = job.CompanyId
|
||||
});
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(customer.Email))
|
||||
else if (completedEmails.Count > 0)
|
||||
{
|
||||
await WriteLog(SkippedLog(NotificationChannel.Email, NotificationType.JobCompleted,
|
||||
customerName, customer.Email, job.CompanyId, customerId: customer.Id, jobId: job.Id));
|
||||
customerName, string.Join(", ", completedEmails), job.CompanyId, customerId: customer.Id, jobId: job.Id));
|
||||
}
|
||||
|
||||
// SMS — skip when the caller (Admin/Manager) will handle it via the compose modal
|
||||
@@ -611,7 +621,7 @@ public class NotificationService : INotificationService
|
||||
/// (the <paramref name="paymentUrl"/> parameter). Without a payment URL the email is a
|
||||
/// standard "here is your invoice" message with no payment CTA.
|
||||
/// </summary>
|
||||
public async Task NotifyInvoiceSentAsync(Invoice invoice, byte[]? pdfAttachment = null, string? pdfFilename = null, string? paymentUrl = null)
|
||||
public async Task NotifyInvoiceSentAsync(Invoice invoice, byte[]? pdfAttachment = null, string? pdfFilename = null, string? paymentUrl = null, string? overrideEmail = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -622,7 +632,15 @@ public class NotificationService : INotificationService
|
||||
var (replyToEmail, replyToName) = await GetEmailFromAsync(invoice.CompanyId);
|
||||
var customerName = GetCustomerDisplayName(customer);
|
||||
|
||||
if (customer.NotifyByEmail && !string.IsNullOrWhiteSpace(customer.Email))
|
||||
// Override email (staff-provided ad-hoc address) takes priority over customer record.
|
||||
// Use BillingEmail when set (commercial accounting dept); fall back to primary Email.
|
||||
var invoiceEmail = !string.IsNullOrWhiteSpace(overrideEmail)
|
||||
? overrideEmail
|
||||
: (!string.IsNullOrWhiteSpace(customer.BillingEmail) ? customer.BillingEmail : customer.Email);
|
||||
|
||||
var invoiceEmails = ParseEmailList(invoiceEmail);
|
||||
// Bypass NotifyByEmail preference when staff explicitly supplies an override address.
|
||||
if ((customer.NotifyByEmail || !string.IsNullOrWhiteSpace(overrideEmail)) && invoiceEmails.Count > 0)
|
||||
{
|
||||
var dueText = invoice.DueDate.HasValue
|
||||
? $" Payment is due by {invoice.DueDate.Value:MMMM d, yyyy}."
|
||||
@@ -661,8 +679,8 @@ public class NotificationService : INotificationService
|
||||
? StripHtml(htmlBody) + $"\r\n\r\nPay online: {paymentUrl}"
|
||||
: StripHtml(fullHtml);
|
||||
|
||||
var (success, error) = await _emailService.SendEmailAsync(
|
||||
customer.Email, customerName, subject, plainText, fullHtml,
|
||||
var (success, error, recipientsLog) = await SendToEmailListAsync(
|
||||
invoiceEmail, customerName, subject, plainText, fullHtml,
|
||||
pdfAttachment, pdfFilename, "application/pdf",
|
||||
replyToEmail, replyToName);
|
||||
|
||||
@@ -672,7 +690,7 @@ public class NotificationService : INotificationService
|
||||
NotificationType = NotificationType.InvoiceSent,
|
||||
Status = success ? NotificationStatus.Sent : NotificationStatus.Failed,
|
||||
RecipientName = customerName,
|
||||
Recipient = customer.Email,
|
||||
Recipient = recipientsLog,
|
||||
Subject = subject,
|
||||
Message = plainText,
|
||||
ErrorMessage = error,
|
||||
@@ -682,10 +700,10 @@ public class NotificationService : INotificationService
|
||||
CompanyId = invoice.CompanyId
|
||||
});
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(customer.Email))
|
||||
else if (invoiceEmails.Count > 0)
|
||||
{
|
||||
await WriteLog(SkippedLog(NotificationChannel.Email, NotificationType.InvoiceSent,
|
||||
customerName, customer.Email, invoice.CompanyId, customerId: customer.Id, invoiceId: invoice.Id));
|
||||
customerName, string.Join(", ", invoiceEmails), invoice.CompanyId, customerId: customer.Id, invoiceId: invoice.Id));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -710,7 +728,8 @@ public class NotificationService : INotificationService
|
||||
var (replyToEmail, replyToName) = await GetEmailFromAsync(invoice.CompanyId);
|
||||
var customerName = GetCustomerDisplayName(customer);
|
||||
|
||||
if (customer.NotifyByEmail && !string.IsNullOrWhiteSpace(customer.Email))
|
||||
var paymentEmails = ParseEmailList(customer.Email);
|
||||
if (customer.NotifyByEmail && paymentEmails.Count > 0)
|
||||
{
|
||||
var balanceText = invoice.BalanceDue > 0
|
||||
? $" Remaining balance: {invoice.BalanceDue:C}."
|
||||
@@ -733,7 +752,7 @@ public class NotificationService : INotificationService
|
||||
var fullHtml = AppendUnsubscribeFooterHtml(htmlBody, customer.UnsubscribeToken, company, await GetBaseUrlAsync());
|
||||
var plainText = StripHtml(fullHtml);
|
||||
|
||||
var (success, error) = await _emailService.SendEmailAsync(
|
||||
var (success, error, recipientsLog) = await SendToEmailListAsync(
|
||||
customer.Email, customerName, subject, plainText, fullHtml,
|
||||
replyToEmail: replyToEmail, replyToName: replyToName);
|
||||
|
||||
@@ -743,7 +762,7 @@ public class NotificationService : INotificationService
|
||||
NotificationType = NotificationType.PaymentReceived,
|
||||
Status = success ? NotificationStatus.Sent : NotificationStatus.Failed,
|
||||
RecipientName = customerName,
|
||||
Recipient = customer.Email,
|
||||
Recipient = recipientsLog,
|
||||
Subject = subject,
|
||||
Message = plainText,
|
||||
ErrorMessage = error,
|
||||
@@ -753,10 +772,10 @@ public class NotificationService : INotificationService
|
||||
CompanyId = invoice.CompanyId
|
||||
});
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(customer.Email))
|
||||
else if (paymentEmails.Count > 0)
|
||||
{
|
||||
await WriteLog(SkippedLog(NotificationChannel.Email, NotificationType.PaymentReceived,
|
||||
customerName, customer.Email, invoice.CompanyId, customerId: customer.Id, invoiceId: invoice.Id));
|
||||
customerName, string.Join(", ", paymentEmails), invoice.CompanyId, customerId: customer.Id, invoiceId: invoice.Id));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -782,7 +801,8 @@ public class NotificationService : INotificationService
|
||||
var (replyToEmail, replyToName) = await GetEmailFromAsync(invoice.CompanyId);
|
||||
var customerName = GetCustomerDisplayName(customer);
|
||||
|
||||
if (customer.NotifyByEmail && !string.IsNullOrWhiteSpace(customer.Email))
|
||||
var reminderEmails = ParseEmailList(customer.Email);
|
||||
if (customer.NotifyByEmail && reminderEmails.Count > 0)
|
||||
{
|
||||
var dueDate = invoice.DueDate.HasValue
|
||||
? invoice.DueDate.Value.ToString("MMMM d, yyyy")
|
||||
@@ -806,7 +826,7 @@ public class NotificationService : INotificationService
|
||||
var fullHtml = AppendUnsubscribeFooterHtml(htmlBody, customer.UnsubscribeToken, company, await GetBaseUrlAsync());
|
||||
var plainText = StripHtml(fullHtml);
|
||||
|
||||
var (success, error) = await _emailService.SendEmailAsync(
|
||||
var (success, error, recipientsLog) = await SendToEmailListAsync(
|
||||
customer.Email, customerName, subject, plainText, fullHtml,
|
||||
replyToEmail: replyToEmail, replyToName: replyToName);
|
||||
|
||||
@@ -816,7 +836,7 @@ public class NotificationService : INotificationService
|
||||
NotificationType = NotificationType.PaymentReminder,
|
||||
Status = success ? NotificationStatus.Sent : NotificationStatus.Failed,
|
||||
RecipientName = customerName,
|
||||
Recipient = customer.Email,
|
||||
Recipient = recipientsLog,
|
||||
Subject = subject,
|
||||
Message = plainText,
|
||||
ErrorMessage = error,
|
||||
@@ -826,10 +846,10 @@ public class NotificationService : INotificationService
|
||||
CompanyId = invoice.CompanyId
|
||||
});
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(customer.Email))
|
||||
else if (reminderEmails.Count > 0)
|
||||
{
|
||||
await WriteLog(SkippedLog(NotificationChannel.Email, NotificationType.PaymentReminder,
|
||||
customerName, customer.Email, invoice.CompanyId, customerId: customer.Id, invoiceId: invoice.Id));
|
||||
customerName, string.Join(", ", reminderEmails), invoice.CompanyId, customerId: customer.Id, invoiceId: invoice.Id));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -1324,7 +1344,7 @@ public class NotificationService : INotificationService
|
||||
{
|
||||
var customer = invoice.Customer
|
||||
?? await _context.Customers.AsNoTracking().FirstOrDefaultAsync(c => c.Id == invoice.CustomerId);
|
||||
if (customer == null || string.IsNullOrWhiteSpace(customer.Email)) return;
|
||||
if (customer == null || ParseEmailList(customer.Email).Count == 0) return;
|
||||
|
||||
var customerName = GetCustomerDisplayName(customer);
|
||||
var company = await _context.Companies.AsNoTracking()
|
||||
@@ -1374,7 +1394,7 @@ Thank you for your business,
|
||||
<p style='color:#6b7280;font-size:14px;'>Thank you for your business,<br/><strong>{System.Net.WebUtility.HtmlEncode(companyName)}</strong></p>
|
||||
</div>";
|
||||
|
||||
await _emailService.SendEmailAsync(customer.Email, customerName, subject, plain, html,
|
||||
await SendToEmailListAsync(customer.Email, customerName, subject, plain, html,
|
||||
replyToEmail: replyToEmail, replyToName: replyToName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -1389,7 +1409,7 @@ Thank you for your business,
|
||||
{
|
||||
// Determine recipient — linked customer or prospect contact
|
||||
string? toEmail = quote.Customer?.Email ?? quote.ProspectEmail;
|
||||
if (string.IsNullOrWhiteSpace(toEmail)) return;
|
||||
if (ParseEmailList(toEmail).Count == 0) return;
|
||||
|
||||
string customerName;
|
||||
if (quote.Customer != null)
|
||||
@@ -1448,7 +1468,7 @@ Thank you for your business,
|
||||
<p style='color:#6b7280;font-size:14px;'>Thank you for your business,<br/><strong>{System.Net.WebUtility.HtmlEncode(companyName)}</strong></p>
|
||||
</div>";
|
||||
|
||||
await _emailService.SendEmailAsync(toEmail, customerName, subject, plain, html,
|
||||
await SendToEmailListAsync(toEmail, customerName, subject, plain, html,
|
||||
replyToEmail: replyToEmail, replyToName: replyToName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -1500,6 +1520,52 @@ Log in to your Stripe Dashboard to respond to this dispute. You typically have 7
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a comma-separated email field into individual, trimmed addresses.
|
||||
/// Silently ignores blank entries; does not validate format beyond requiring '@'.
|
||||
/// </summary>
|
||||
private static List<string> ParseEmailList(string? emailField)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(emailField)) return [];
|
||||
return emailField
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Where(e => e.Contains('@'))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends the same email to every address in a comma-separated <paramref name="emailList"/>.
|
||||
/// Returns (anySuccess, lastError, comma-joined recipient string for logging).
|
||||
/// </summary>
|
||||
private async Task<(bool Success, string? ErrorMessage, string RecipientsLog)> SendToEmailListAsync(
|
||||
string? emailList,
|
||||
string toName,
|
||||
string subject,
|
||||
string plainText,
|
||||
string? htmlBody = null,
|
||||
byte[]? attachmentData = null,
|
||||
string? attachmentFilename = null,
|
||||
string? attachmentContentType = null,
|
||||
string? replyToEmail = null,
|
||||
string? replyToName = null)
|
||||
{
|
||||
var emails = ParseEmailList(emailList);
|
||||
if (emails.Count == 0) return (false, "No valid email addresses", string.Empty);
|
||||
|
||||
bool anySuccess = false;
|
||||
string? lastError = null;
|
||||
foreach (var email in emails)
|
||||
{
|
||||
var (ok, err) = await _emailService.SendEmailAsync(
|
||||
email, toName, subject, plainText, htmlBody,
|
||||
attachmentData, attachmentFilename, attachmentContentType,
|
||||
replyToEmail, replyToName);
|
||||
if (ok) anySuccess = true;
|
||||
else lastError = err;
|
||||
}
|
||||
return (anySuccess, anySuccess ? null : lastError, string.Join(", ", emails));
|
||||
}
|
||||
|
||||
private static string GetCustomerDisplayName(Customer customer)
|
||||
{
|
||||
if (!customer.IsCommercial)
|
||||
|
||||
Reference in New Issue
Block a user