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:
@@ -88,6 +88,19 @@ public class Company : BaseEntity
|
||||
/// </summary>
|
||||
public bool? AccountingOverride { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Company admin opt-in for SMS notifications. Defaults to false — company admin must
|
||||
/// explicitly accept the SMS terms of service before enabling. Has no effect if the plan
|
||||
/// does not allow SMS or if SmsDisabledByAdmin is true.
|
||||
/// </summary>
|
||||
public bool SmsEnabled { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// SuperAdmin force-disable for this company's SMS. When true, no SMS is sent regardless
|
||||
/// of plan or company settings. Use when a company is abusing SMS or requests a full opt-out.
|
||||
/// </summary>
|
||||
public bool SmsDisabledByAdmin { get; set; } = false;
|
||||
|
||||
// Email marketing opt-out (CAN-SPAM compliance for platform broadcast emails)
|
||||
public bool MarketingEmailOptOut { get; set; } = false;
|
||||
public string MarketingUnsubscribeToken { get; set; } = Guid.NewGuid().ToString("N");
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
namespace PowderCoating.Core.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Immutable audit record of a company admin accepting the SMS terms of service.
|
||||
/// One record is written each time a user accepts (including re-accepts after a terms update).
|
||||
/// The most recent record whose <see cref="TermsVersion"/> matches
|
||||
/// <c>AppConstants.SmsTermsVersion</c> is the authoritative acceptance for that company.
|
||||
/// Never soft-deleted — this is a legal audit trail.
|
||||
/// </summary>
|
||||
public class CompanySmsAgreement : BaseEntity
|
||||
{
|
||||
/// <summary>The Identity user ID of the admin who clicked "I Agree".</summary>
|
||||
public string AgreedByUserId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Display name snapshot of the user at the time of agreement (for audit readability after user changes).</summary>
|
||||
public string AgreedByUserName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>UTC timestamp of acceptance.</summary>
|
||||
public DateTime AgreedAt { get; set; }
|
||||
|
||||
/// <summary>Client IP address at the time of acceptance. Stored for legal/fraud purposes.</summary>
|
||||
public string? IpAddress { get; set; }
|
||||
|
||||
/// <summary>HTTP User-Agent header at the time of acceptance.</summary>
|
||||
public string? UserAgent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The version of the SMS terms that was accepted (matches <c>AppConstants.SmsTermsVersion</c>
|
||||
/// at the moment of acceptance). When the platform bumps this version, existing records become
|
||||
/// stale and the company must re-accept.
|
||||
/// </summary>
|
||||
public string TermsVersion { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -49,6 +49,9 @@ public class SubscriptionPlanConfig : BaseEntity
|
||||
/// <summary>When true, companies on this plan can run the AI Catalog Price Check (Enterprise only).</summary>
|
||||
public bool AllowAiCatalogPriceCheck { get; set; } = false;
|
||||
|
||||
/// <summary>When true, companies on this plan can send SMS notifications to customers (subject to platform kill-switch and per-company opt-in).</summary>
|
||||
public bool AllowSms { get; set; } = false;
|
||||
|
||||
public bool IsActive { get; set; } = true;
|
||||
public int SortOrder { get; set; }
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ public interface IUnitOfWork : IDisposable
|
||||
IRepository<Company> Companies { get; }
|
||||
IRepository<CompanyOperatingCosts> CompanyOperatingCosts { get; }
|
||||
IRepository<CompanyPreferences> CompanyPreferences { get; }
|
||||
IRepository<CompanySmsAgreement> CompanySmsAgreements { get; }
|
||||
|
||||
// AI Predictions
|
||||
IRepository<AiItemPrediction> AiItemPredictions { get; }
|
||||
|
||||
Reference in New Issue
Block a user