Fix prospect quote conversion losing the job; add reply-to in email footer
QuotesController — ConvertToCustomer POST was wrongly setting the quote status to 'Converted' (which means a job exists) and redirecting to the customer page with no job created. The quote then disappeared from the default list filter and the user had no way to create the job without hunting for it. Fix: leave the quote at 'Approved' after customer creation and redirect back to the quote details page with a toast prompting the next step. 'Converted' status is now set exclusively by CreateJobFromQuote when a job actually exists. NotificationService — add tenant reply-to email address as a visible line in the email footer so customers who ignore or whose mail client doesn't honour the Reply-To header still have a clear address to contact. Also adds Warning-level logging when no reply-to is configured for a company so future routing issues are diagnosable from app logs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -94,7 +94,7 @@ public class NotificationService : INotificationService
|
||||
quote.CompanyId, NotificationType.QuoteSent, values,
|
||||
$"Your Quote {quote.QuoteNumber} from {companyName}");
|
||||
|
||||
var fullHtml = AppendUnsubscribeFooterHtml(htmlBody, token: null, company, baseUrl);
|
||||
var fullHtml = AppendUnsubscribeFooterHtml(htmlBody, token: null, company, baseUrl, replyToEmail);
|
||||
var plainText = StripHtml(fullHtml);
|
||||
|
||||
var (success, error) = await _emailService.SendEmailAsync(
|
||||
@@ -137,7 +137,7 @@ public class NotificationService : INotificationService
|
||||
quote.CompanyId, NotificationType.QuoteSent, values,
|
||||
$"Your Quote {quote.QuoteNumber} from {companyName}");
|
||||
|
||||
var fullHtml = AppendUnsubscribeFooterHtml(htmlBody, customer.UnsubscribeToken, company, baseUrl);
|
||||
var fullHtml = AppendUnsubscribeFooterHtml(htmlBody, customer.UnsubscribeToken, company, baseUrl, replyToEmail);
|
||||
var plainText = StripHtml(fullHtml);
|
||||
|
||||
var (success, error, recipientsLog) = await SendToEmailListAsync(
|
||||
@@ -300,7 +300,7 @@ public class NotificationService : INotificationService
|
||||
quote.CompanyId, NotificationType.QuoteApproved, values,
|
||||
$"Quote {quote.QuoteNumber} Approved — {companyName}");
|
||||
|
||||
var fullHtml = AppendUnsubscribeFooterHtml(htmlBody, customer.UnsubscribeToken, company, await GetBaseUrlAsync());
|
||||
var fullHtml = AppendUnsubscribeFooterHtml(htmlBody, customer.UnsubscribeToken, company, await GetBaseUrlAsync(), replyToEmail);
|
||||
var plainText = StripHtml(fullHtml);
|
||||
|
||||
var (success, error, recipientsLog) = await SendToEmailListAsync(
|
||||
@@ -383,7 +383,7 @@ public class NotificationService : INotificationService
|
||||
var (subject, htmlBody) = await GetRenderedEmailAsync(
|
||||
job.CompanyId, notifType, values, defaultSubject);
|
||||
|
||||
var fullHtml = AppendUnsubscribeFooterHtml(htmlBody, customer.UnsubscribeToken, company, await GetBaseUrlAsync());
|
||||
var fullHtml = AppendUnsubscribeFooterHtml(htmlBody, customer.UnsubscribeToken, company, await GetBaseUrlAsync(), replyToEmail);
|
||||
var plainText = StripHtml(fullHtml);
|
||||
|
||||
var (success, error, recipientsLog) = await SendToEmailListAsync(
|
||||
@@ -451,7 +451,7 @@ public class NotificationService : INotificationService
|
||||
job.CompanyId, NotificationType.JobCompleted, values,
|
||||
$"Job {job.JobNumber} Complete — {companyName}");
|
||||
|
||||
var fullHtml = AppendUnsubscribeFooterHtml(htmlBody, customer.UnsubscribeToken, company, await GetBaseUrlAsync());
|
||||
var fullHtml = AppendUnsubscribeFooterHtml(htmlBody, customer.UnsubscribeToken, company, await GetBaseUrlAsync(), replyToEmail);
|
||||
var plainText = StripHtml(fullHtml);
|
||||
|
||||
var (success, error, recipientsLog) = await SendToEmailListAsync(
|
||||
@@ -674,7 +674,7 @@ public class NotificationService : INotificationService
|
||||
""";
|
||||
}
|
||||
|
||||
var fullHtml = AppendUnsubscribeFooterHtml(htmlBody, customer.UnsubscribeToken, company, await GetBaseUrlAsync());
|
||||
var fullHtml = AppendUnsubscribeFooterHtml(htmlBody, customer.UnsubscribeToken, company, await GetBaseUrlAsync(), replyToEmail);
|
||||
var plainText = !string.IsNullOrEmpty(paymentUrl)
|
||||
? StripHtml(htmlBody) + $"\r\n\r\nPay online: {paymentUrl}"
|
||||
: StripHtml(fullHtml);
|
||||
@@ -793,7 +793,7 @@ public class NotificationService : INotificationService
|
||||
invoice.CompanyId, NotificationType.PaymentReceived, values,
|
||||
$"Payment Received — Invoice {invoice.InvoiceNumber}");
|
||||
|
||||
var fullHtml = AppendUnsubscribeFooterHtml(htmlBody, customer.UnsubscribeToken, company, await GetBaseUrlAsync());
|
||||
var fullHtml = AppendUnsubscribeFooterHtml(htmlBody, customer.UnsubscribeToken, company, await GetBaseUrlAsync(), replyToEmail);
|
||||
var plainText = StripHtml(fullHtml);
|
||||
|
||||
var (success, error, recipientsLog) = await SendToEmailListAsync(
|
||||
@@ -867,7 +867,7 @@ public class NotificationService : INotificationService
|
||||
invoice.CompanyId, NotificationType.PaymentReminder, values,
|
||||
$"Payment Reminder — Invoice {invoice.InvoiceNumber} ({daysOverdue} days overdue)");
|
||||
|
||||
var fullHtml = AppendUnsubscribeFooterHtml(htmlBody, customer.UnsubscribeToken, company, await GetBaseUrlAsync());
|
||||
var fullHtml = AppendUnsubscribeFooterHtml(htmlBody, customer.UnsubscribeToken, company, await GetBaseUrlAsync(), replyToEmail);
|
||||
var plainText = StripHtml(fullHtml);
|
||||
|
||||
var (success, error, recipientsLog) = await SendToEmailListAsync(
|
||||
@@ -971,7 +971,7 @@ public class NotificationService : INotificationService
|
||||
var (subject, htmlBody) = await GetRenderedEmailAsync(
|
||||
quote.CompanyId, notificationType, values, defaultSubject);
|
||||
|
||||
var fullHtml = AppendUnsubscribeFooterHtml(htmlBody, token: null, company, await GetBaseUrlAsync());
|
||||
var fullHtml = AppendUnsubscribeFooterHtml(htmlBody, token: null, company, await GetBaseUrlAsync(), replyToEmail);
|
||||
var plainText = StripHtml(fullHtml);
|
||||
|
||||
var (success, error) = await _emailService.SendEmailAsync(
|
||||
@@ -1218,7 +1218,7 @@ public class NotificationService : INotificationService
|
||||
var (custSubject, custHtml) = await GetRenderedEmailAsync(
|
||||
appointment.CompanyId, NotificationType.AppointmentReminder, customerValues, defaultSubject);
|
||||
|
||||
var custFullHtml = AppendUnsubscribeFooterHtml(custHtml, customer.UnsubscribeToken, company, baseUrl);
|
||||
var custFullHtml = AppendUnsubscribeFooterHtml(custHtml, customer.UnsubscribeToken, company, baseUrl, replyToEmail);
|
||||
var custPlainText = StripHtml(custFullHtml);
|
||||
|
||||
var (custOk, custErr, custLog) = await SendToEmailListAsync(
|
||||
@@ -1388,17 +1388,25 @@ public class NotificationService : INotificationService
|
||||
/// <summary>
|
||||
/// Appends CAN-SPAM required footer as HTML.
|
||||
/// </summary>
|
||||
private static string AppendUnsubscribeFooterHtml(string htmlBody, string? token, Company? company = null, string? baseUrl = null)
|
||||
private static string AppendUnsubscribeFooterHtml(string htmlBody, string? token, Company? company = null, string? baseUrl = null, string? replyToEmail = null)
|
||||
{
|
||||
var hasUnsubscribeUrl = !string.IsNullOrEmpty(token) && !string.IsNullOrEmpty(baseUrl);
|
||||
var hasAddress = company != null && !string.IsNullOrWhiteSpace(company.Address);
|
||||
var hasAddress = company != null && !string.IsNullOrWhiteSpace(company.Address);
|
||||
var hasReplyTo = !string.IsNullOrWhiteSpace(replyToEmail);
|
||||
|
||||
if (!hasUnsubscribeUrl && !hasAddress)
|
||||
if (!hasUnsubscribeUrl && !hasAddress && !hasReplyTo)
|
||||
return htmlBody;
|
||||
|
||||
var footer = "<hr style=\"border: none; border-top: 1px solid #eee; margin: 24px 0;\">" +
|
||||
"<p style=\"font-size: 0.8em; color: #888; margin: 0;\">";
|
||||
|
||||
if (hasReplyTo)
|
||||
{
|
||||
var encodedEmail = WebUtility.HtmlEncode(replyToEmail!);
|
||||
footer += $"Questions? Reply to this email or contact us at <a href=\"mailto:{encodedEmail}\" style=\"color: #888;\">{encodedEmail}</a>";
|
||||
if (hasAddress || hasUnsubscribeUrl) footer += "<br>";
|
||||
}
|
||||
|
||||
if (hasAddress)
|
||||
{
|
||||
var addressLine = BuildAddressLine(company!);
|
||||
|
||||
Reference in New Issue
Block a user