Add SMS gating, TCPA terms agreement, and compose-before-send modal
- Three-tier SMS gate: platform kill-switch → admin force-disable → plan AllowSms → company opt-in - CompanySmsAgreement entity records admin acceptance of TCPA terms with IP, user agent, and terms version - SMS terms of service modal on Company Settings with versioned re-agreement (AppConstants.SmsTermsVersion) - Dev redirect: non-production SMS routed to Twilio:DevRedirectPhone to protect real customer numbers - Removed redundant Ready for Pickup SMS (Job Completed covers it) - Role-based compose modal on job completion: Admin/Manager reviews and edits before send; ShopFloor auto-sends - Send SMS button on job details for ad-hoc messages (Admin/Manager only) - SendJobSmsAsync auto-appends STOP opt-out language if missing - Migrations: AddSmsGating, AddCompanySmsAgreement Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PowderCoating.Application.Interfaces;
|
||||
using Twilio;
|
||||
@@ -10,16 +12,24 @@ namespace PowderCoating.Infrastructure.Services;
|
||||
public class SmsService : ISmsService
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly IWebHostEnvironment _env;
|
||||
private readonly ILogger<SmsService> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="SmsService"/>. Twilio credentials are read
|
||||
/// per call (not cached here) because they may be rotated without restarting the application,
|
||||
/// and Twilio's client initialization (<c>TwilioClient.Init</c>) is idempotent and cheap.
|
||||
/// <para>
|
||||
/// In non-production environments, all outbound messages are redirected to
|
||||
/// <c>Twilio:DevRedirectPhone</c> (if configured) so real customer numbers are never
|
||||
/// texted during development or staging. The original destination is prepended to the
|
||||
/// message body for traceability.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public SmsService(IConfiguration configuration, ILogger<SmsService> logger)
|
||||
public SmsService(IConfiguration configuration, IWebHostEnvironment env, ILogger<SmsService> logger)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_env = env;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -58,6 +68,21 @@ public class SmsService : ISmsService
|
||||
return (false, "Invalid phone number");
|
||||
}
|
||||
|
||||
// Non-production guard: redirect all messages to the dev phone so real customers
|
||||
// are never texted outside of production. Double-gated on environment name AND
|
||||
// the config value so a misconfigured prod deploy can't accidentally redirect.
|
||||
var devRedirect = _configuration["Twilio:DevRedirectPhone"];
|
||||
if (!_env.IsProduction() && !string.IsNullOrWhiteSpace(devRedirect))
|
||||
{
|
||||
var devPhone = NormalizePhone(devRedirect);
|
||||
if (!string.IsNullOrEmpty(devPhone))
|
||||
{
|
||||
_logger.LogWarning("Non-production environment: redirecting SMS from {Original} to dev number {Dev}", normalizedPhone, devPhone);
|
||||
message = $"[DEV → {normalizedPhone}] {message}";
|
||||
normalizedPhone = devPhone;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
TwilioClient.Init(accountSid, authToken);
|
||||
|
||||
Reference in New Issue
Block a user