Add Quote Sent SMS template and fix consent confirmation wording

Adds a customizable QuoteSent SMS template to seed data and
DefaultTemplates so companies can edit the quote approval message
from Notification Templates. Wires NotifyQuoteSentSmsAsync to use
the template system instead of a hardcoded string. Updates
SmsConsentConfirmation wording to mention quote approvals alongside
job updates. Help docs and AI knowledge base updated to match.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-02 21:19:43 -04:00
parent a9048dea2e
commit 0b6a7a14c4
4 changed files with 41 additions and 9 deletions
@@ -860,7 +860,7 @@ New accounts walk through an 18-step setup wizard to configure company informati
}
/// <summary>
/// Returns the 8 canonical default notification templates for a company.
/// Returns the canonical default notification templates for a company.
/// Called by both SeedData and CompanySettingsController for auto-seeding.
/// </summary>
public static List<NotificationTemplate> BuildDefaultNotificationTemplates(int companyId)
@@ -934,12 +934,23 @@ New accounts walk through an 18-step setup wizard to configure company informati
CreatedAt = DateTime.UtcNow
},
new NotificationTemplate
{
NotificationType = NotificationType.QuoteSent,
Channel = NotificationChannel.Sms,
DisplayName = "Quote Sent (SMS)",
Subject = null,
Body = "{{companyName}}: Quote {{quoteNumber}} for {{quoteTotal}} is ready for your review. Approve or decline: {{approvalUrl}} Reply STOP to opt out.",
IsActive = true,
CompanyId = companyId,
CreatedAt = DateTime.UtcNow
},
new NotificationTemplate
{
NotificationType = NotificationType.SmsConsentConfirmation,
Channel = NotificationChannel.Sms,
DisplayName = "SMS Enrollment Confirmation",
Subject = null,
Body = "{{companyName}}: You're now enrolled for SMS job & pickup updates. Reply STOP at any time to opt out. Msg & data rates may apply.",
Body = "{{companyName}}: You're now enrolled for SMS job updates and quote approvals. Reply STOP at any time to opt out. Msg & data rates may apply.",
IsActive = true,
CompanyId = companyId,
CreatedAt = DateTime.UtcNow
@@ -224,7 +224,17 @@ public class NotificationService : INotificationService
}
}
var message = $"{companyName}: Quote {quote.QuoteNumber} for {quote.Total:C} is ready for your review. Approve or decline: {approvalUrl} Reply STOP to opt out.";
var smsValues = new Dictionary<string, string>
{
["companyName"] = companyName,
["quoteNumber"] = quote.QuoteNumber ?? string.Empty,
["quoteTotal"] = quote.Total.ToString("C"),
["approvalUrl"] = approvalUrl
};
var message = await GetRenderedSmsAsync(
quote.CompanyId, NotificationType.QuoteSent, smsValues,
$"{companyName}: Quote {quote.QuoteNumber} for {quote.Total:C} is ready for your review. Approve or decline: {approvalUrl} Reply STOP to opt out.");
var (success, error) = await _smsService.SendSmsAsync(smsPhone, message);
await WriteLog(new NotificationLog
@@ -945,7 +955,7 @@ public class NotificationService : INotificationService
var smsMessage = await GetRenderedSmsAsync(
customer.CompanyId, NotificationType.SmsConsentConfirmation, values,
$"{companyName}: You're now enrolled for SMS job & pickup updates. Reply STOP at any time to opt out. Msg & data rates may apply.");
$"{companyName}: You're now enrolled for SMS job updates and quote approvals. Reply STOP at any time to opt out. Msg & data rates may apply.");
var (success, error) = await _smsService.SendSmsAsync(smsPhone, smsMessage);
@@ -1103,13 +1113,17 @@ public class NotificationService : INotificationService
"Job {{jobNumber}} Complete — {{companyName}}",
"<p>Dear {{customerName}},</p><p>Your job <strong>{{jobNumber}}</strong> is complete. Final price: <strong>{{finalPrice}}</strong>. It is now ready for pickup.</p><p>Thank you for choosing {{companyName}}.</p>"
),
[(NotificationType.QuoteSent, NotificationChannel.Sms)] = (
null,
"{{companyName}}: Quote {{quoteNumber}} for {{quoteTotal}} is ready for your review. Approve or decline: {{approvalUrl}} Reply STOP to opt out."
),
[(NotificationType.JobCompleted, NotificationChannel.Sms)] = (
null,
"{{companyName}}: Job {{jobNumber}} is done and ready for pickup! Reply STOP to opt out."
),
[(NotificationType.SmsConsentConfirmation, NotificationChannel.Sms)] = (
null,
"{{companyName}}: You're now enrolled for SMS job & pickup updates. Reply STOP at any time to opt out. Msg & data rates may apply."
"{{companyName}}: You're now enrolled for SMS job updates and quote approvals. Reply STOP at any time to opt out. Msg & data rates may apply."
),
[(NotificationType.InvoiceSent, NotificationChannel.Email)] = (
"Invoice {{invoiceNumber}} from {{companyName}}",
@@ -1062,6 +1062,7 @@ public static class HelpKnowledgeBase
- Quote approved/rejected by customer
- Job status changes
- Job completed (email + SMS)
- Quote sent (SMS approval link)
- Invoice sent
- Payment received
- Overdue payment reminders
@@ -1082,8 +1083,10 @@ public static class HelpKnowledgeBase
On each customer's record (Edit Customer), check the **SMS Opt-In** box and enter a **Mobile Phone** number. A customer will not receive SMS messages unless both boxes are set. You are responsible for obtaining the customer's verbal or written consent before enabling this.
**What events send an SMS:**
- Quote sent sends the customer a link to review and approve or decline their quote.
- Job completed notifies the customer their job is done and ready for pickup.
- Quote approval request sends the customer a link to review and approve or decline their quote.
Both SMS templates are customizable at Company Settings Notification Templates. Companies can change the wording of both the quote SMS and the job-completed SMS to match their voice.
**Sending a quote approval link via SMS:**
On any quote's Details page, click **Send Quote via SMS**. The system texts the customer's mobile number a short message with the approval link. If no valid approval token exists yet, one is generated automatically. If an email was already sent for the same quote, the existing token is reused so both the email link and the SMS link remain valid simultaneously. The customer follows the link to the self-service approval portal and can approve or decline from their phone. Prospects (non-customers) receive the SMS at their ProspectPhone number without requiring an opt-in check; registered customers must have SMS Opt-In enabled.
@@ -392,14 +392,18 @@
</ul>
<h4 class="h6 fw-semibold mt-3 mb-2" style="font-size:.85rem;">What events send an SMS</h4>
<ul class="mb-3">
<li class="mb-1"><strong>Job Completed</strong> — notifies the customer their job is done and ready for pickup.</li>
<li class="mb-1">
<strong>Quote Approval Request</strong> — sends the customer a link to review and approve or decline
their quote directly from their phone. Click <strong>Send Quote via SMS</strong> on any quote's Details
<strong>Quote Sent</strong> — sends the customer a link to review and approve or decline their
quote directly from their phone. Click <strong>Send Quote via SMS</strong> on any quote's Details
page. If an email was already sent for the same quote, the existing approval link is reused so both
delivery methods work simultaneously.
</li>
<li class="mb-1"><strong>Job Completed</strong> — notifies the customer their job is done and ready for pickup.</li>
</ul>
<p class="small text-muted mb-3">
Both SMS message templates can be customised at
<strong>Company Settings &rsaquo; Notification Templates</strong>.
</p>
<h4 class="h6 fw-semibold mt-3 mb-2" style="font-size:.85rem;">Compose-before-send vs. auto-send</h4>
<p>
When a <strong>Company Admin or Manager</strong> marks a job complete, the system pre-fills a draft