From 3ff6a96bc8903794305c65f0b638fd63437ccebd Mon Sep 17 00:00:00 2001 From: Scott Pouliot Date: Sat, 2 May 2026 20:09:49 -0400 Subject: [PATCH] Add SMS START/re-subscribe handling to Twilio webhook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Customers who replied STOP by mistake can now reply START, YES, or UNSTOP to automatically re-enable their SMS opt-in — no staff action needed. Adds SmsInboundStart notification type, HandleStartAsync in WebhooksController, and updates AI knowledge base and help docs. Co-Authored-By: Claude Sonnet 4.6 --- .../Enums/NotificationEnums.cs | 3 +- .../Controllers/WebhooksController.cs | 47 +++++++++++++++++++ .../Helpers/HelpKnowledgeBase.cs | 4 +- .../Views/Help/Settings.cshtml | 4 +- 4 files changed, 54 insertions(+), 4 deletions(-) diff --git a/src/PowderCoating.Core/Enums/NotificationEnums.cs b/src/PowderCoating.Core/Enums/NotificationEnums.cs index 50435d6..239013f 100644 --- a/src/PowderCoating.Core/Enums/NotificationEnums.cs +++ b/src/PowderCoating.Core/Enums/NotificationEnums.cs @@ -18,5 +18,6 @@ public enum NotificationType SubscriptionExpired = 11, SmsInboundStop = 12, SmsInboundHelp = 13, - AdminEmail = 14 + AdminEmail = 14, + SmsInboundStart = 15 } diff --git a/src/PowderCoating.Web/Controllers/WebhooksController.cs b/src/PowderCoating.Web/Controllers/WebhooksController.cs index 8b30d61..01db4cd 100644 --- a/src/PowderCoating.Web/Controllers/WebhooksController.cs +++ b/src/PowderCoating.Web/Controllers/WebhooksController.cs @@ -31,6 +31,12 @@ public class WebhooksController : ControllerBase "STOP", "STOPALL", "UNSUBSCRIBE", "CANCEL", "END", "QUIT" }; + // CTIA-standard opt-in keywords + private static readonly HashSet StartKeywords = new(StringComparer.OrdinalIgnoreCase) + { + "START", "YES", "UNSTOP" + }; + // CTIA-standard help keywords private static readonly HashSet HelpKeywords = new(StringComparer.OrdinalIgnoreCase) { @@ -77,6 +83,9 @@ public class WebhooksController : ControllerBase if (StopKeywords.Contains(body)) return await HandleStopAsync(payload.From); + if (StartKeywords.Contains(body)) + return await HandleStartAsync(payload.From); + if (HelpKeywords.Contains(body)) return await HandleHelpAsync(payload.From); @@ -123,6 +132,44 @@ public class WebhooksController : ControllerBase $"Reply START to re-subscribe."); } + // ── START ───────────────────────────────────────────────────────────────── + + /// + /// Processes a START keyword: re-enables SMS for the customer, clears SmsOptedOutAt, + /// logs to NotificationLog, and returns a TwiML confirmation message. + /// + private async Task HandleStartAsync(string from) + { + var (customer, digits10) = await FindCustomerByPhoneAsync(from); + + if (customer == null) + { + _logger.LogWarning("Twilio START from {From} — no matching customer found", from); + return TwimlMessage("You have been re-subscribed and will receive messages again."); + } + + var companyName = await GetCompanyNameAsync(customer.CompanyId); + + if (!customer.NotifyBySms) + { + customer.NotifyBySms = true; + customer.SmsOptedOutAt = null; + customer.UpdatedAt = DateTime.UtcNow; + await _context.SaveChangesAsync(); + _logger.LogInformation("Customer {CustomerId} re-subscribed to SMS via START reply", customer.Id); + } + + await WriteInboundLogAsync( + NotificationType.SmsInboundStart, + customer, + from, + "START"); + + return TwimlMessage( + $"{companyName}: You have been re-subscribed and will receive messages again. " + + $"Reply STOP to unsubscribe at any time."); + } + // ── HELP ────────────────────────────────────────────────────────────────── /// diff --git a/src/PowderCoating.Web/Helpers/HelpKnowledgeBase.cs b/src/PowderCoating.Web/Helpers/HelpKnowledgeBase.cs index 14360e5..9b2da45 100644 --- a/src/PowderCoating.Web/Helpers/HelpKnowledgeBase.cs +++ b/src/PowderCoating.Web/Helpers/HelpKnowledgeBase.cs @@ -1097,8 +1097,8 @@ public static class HelpKnowledgeBase **Send SMS button:** On any completed job's Details page, Company Admins and Managers see a **Send SMS** button that opens the same compose modal, allowing you to send a follow-up message at any time. - **Customer opt-out:** - If a customer replies STOP to any message, they are automatically opted out and will not receive further SMS messages. They can reply START to re-subscribe. + **Customer opt-out and opt back in:** + If a customer replies STOP (or STOPALL, CANCEL, END, QUIT, UNSUBSCRIBE) to any message, they are automatically opted out and will not receive further SMS messages. If they change their mind, they can reply START, YES, or UNSTOP to re-subscribe automatically — no action needed from your staff. You can also manually toggle SMS back on from their customer record. ### Platform Announcements Occasional platform-wide announcements from the Powder Coating Logix team are delivered directly to your notification bell — not as page banners. These may cover new features, scheduled maintenance, or policy updates. They appear as **Announcement** type items in the bell dropdown. diff --git a/src/PowderCoating.Web/Views/Help/Settings.cshtml b/src/PowderCoating.Web/Views/Help/Settings.cshtml index 594cc32..56cfee2 100644 --- a/src/PowderCoating.Web/Views/Help/Settings.cshtml +++ b/src/PowderCoating.Web/Views/Help/Settings.cshtml @@ -416,7 +416,9 @@
Every outbound SMS automatically includes opt-out instructions ("Reply STOP to opt out"). If a customer replies STOP, they are immediately opted out and will receive no further messages. - You can re-enable them on their customer record if they later ask to be re-subscribed. + If they change their mind, they can reply START, YES, or UNSTOP + to re-subscribe automatically — no action needed from your staff. You can also manually re-enable + SMS on their customer record.