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:
@@ -1079,6 +1079,45 @@
|
||||
</a>
|
||||
</h5>
|
||||
<p class="text-muted">Control which events trigger email notifications and alert thresholds.</p>
|
||||
@if (ViewBag.SmsEnabled == true)
|
||||
{
|
||||
<div class="card border mb-4">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title fw-semibold"><i class="bi bi-phone me-1"></i> SMS Notifications</h6>
|
||||
@if (Model.SmsDisabledByAdmin)
|
||||
{
|
||||
<div class="alert alert-danger alert-permanent mb-3 py-2">
|
||||
<i class="bi bi-slash-circle me-1"></i>
|
||||
<strong>SMS has been disabled by an administrator.</strong> Contact support to re-enable.
|
||||
</div>
|
||||
}
|
||||
else if (!Model.AllowSms)
|
||||
{
|
||||
<div class="alert alert-info alert-permanent mb-3 py-2">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
SMS notifications are not included in your current plan. Upgrade to Pro or Enterprise to enable customer SMS alerts.
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="text-muted small mb-3">When enabled, customers who have given SMS consent will receive text alerts for job status changes (e.g. ready for pickup).</p>
|
||||
<div class="form-check form-switch" id="smsToggleWrap">
|
||||
<input class="form-check-input" type="checkbox" id="smsEnabledToggle" @(Model.SmsEnabled ? "checked" : "")
|
||||
data-has-agreement="@(Model.HasCurrentSmsAgreement ? "true" : "false")"
|
||||
data-terms-version="@Model.SmsTermsVersion">
|
||||
<label class="form-check-label fw-medium" for="smsEnabledToggle">Enable SMS Notifications</label>
|
||||
</div>
|
||||
@if (!Model.HasCurrentSmsAgreement && !Model.SmsEnabled)
|
||||
{
|
||||
<div class="form-text text-warning mt-1">
|
||||
<i class="bi bi-info-circle me-1"></i>You'll need to accept the SMS terms of service the first time you enable this.
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<form id="notificationsForm">
|
||||
<h6 class="border-bottom pb-2 mb-3">Email Sender</h6>
|
||||
<div class="row mb-3">
|
||||
@@ -1995,6 +2034,54 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SMS Terms of Service Agreement Modal -->
|
||||
<div class="modal fade" id="smsTermsModal" tabindex="-1" aria-labelledby="smsTermsModalLabel" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-primary text-white">
|
||||
<h5 class="modal-title" id="smsTermsModalLabel">
|
||||
<i class="bi bi-phone me-2"></i>SMS Notifications — Terms of Service
|
||||
</h5>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="alert alert-warning alert-permanent mb-3">
|
||||
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
||||
<strong>Read carefully before enabling.</strong> Sending unsolicited text messages carries significant legal risk. By enabling SMS you are personally accepting responsibility for your company's compliance.
|
||||
</div>
|
||||
|
||||
<h6 class="fw-bold">1. Prior Express Written Consent Required</h6>
|
||||
<p class="text-muted small">You <strong>must obtain clear, documented consent</strong> from each customer before sending them any SMS message. This means each customer must have explicitly agreed — in writing or through a recorded digital interaction — that they wish to receive text messages from your business. Enabling this feature is not consent on their behalf. You must collect and record their authorization individually, before enabling SMS for their account in this system.</p>
|
||||
|
||||
<h6 class="fw-bold">2. Federal Law Governs SMS — Fines Are Real</h6>
|
||||
<p class="text-muted small">The <strong>Telephone Consumer Protection Act (TCPA)</strong>, enforced by the Federal Communications Commission (FCC), imposes fines of <strong>$500 to $1,500 per individual message</strong> sent without proper authorization. These fines apply per text, not per customer. A single campaign to 100 unconsented recipients could result in exposure of $50,000 to $150,000. The FCC and private plaintiffs both actively pursue TCPA violations.</p>
|
||||
|
||||
<h6 class="fw-bold">3. Opt-Out Requests Must Be Honored Immediately</h6>
|
||||
<p class="text-muted small">Any customer who replies <strong>STOP, UNSUBSCRIBE, CANCEL, END, or QUIT</strong> must be removed from all future SMS immediately. This system will process inbound opt-out replies automatically, but you must also honor any opt-out communicated by phone, email, or in person. Continuing to text a customer after an opt-out is a TCPA violation.</p>
|
||||
|
||||
<h6 class="fw-bold">4. Message Rates & Content Restrictions</h6>
|
||||
<p class="text-muted small">Every message sent must include your business name and an opt-out reminder (e.g., "Reply STOP to opt out"). Messages must be directly relevant to the service the customer consented to receive and must not contain solicitations, promotions, or third-party offers unless the customer has separately consented to those.</p>
|
||||
|
||||
<h6 class="fw-bold">5. Your Responsibility — Not Ours</h6>
|
||||
<p class="text-muted small">Powder Coating Logix provides this feature as a communication tool only. <strong>We are not responsible for how you use it.</strong> You agree that your company is solely responsible for obtaining proper consent, maintaining records of that consent, honoring opt-outs, and ensuring all outbound messages comply with the TCPA, FCC regulations, and any applicable state laws. You agree to indemnify and hold Powder Coating Logix harmless from any claims, fines, or damages arising from your company's use of SMS.</p>
|
||||
|
||||
<hr />
|
||||
<div class="form-check mt-3">
|
||||
<input class="form-check-input" type="checkbox" id="smsTermsAgreementCheck">
|
||||
<label class="form-check-label fw-semibold" for="smsTermsAgreementCheck">
|
||||
I have read and understood the above terms. I confirm that my company will obtain proper customer consent before enabling SMS for any individual customer, and I accept full responsibility for our compliance with all applicable laws.
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" id="smsTermsDeclineBtn">Cancel — Keep SMS Disabled</button>
|
||||
<button type="button" class="btn btn-primary" id="smsTermsAcceptBtn" disabled>
|
||||
<i class="bi bi-check-circle me-1"></i>I Agree & Enable SMS
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
@@ -2415,6 +2502,83 @@
|
||||
};
|
||||
}, 'Save Retention Policy');
|
||||
|
||||
// SMS toggle — shows terms modal on first enable (or after terms version change)
|
||||
(function () {
|
||||
const toggle = document.getElementById('smsEnabledToggle');
|
||||
if (!toggle) return;
|
||||
|
||||
const smsTermsModal = new bootstrap.Modal(document.getElementById('smsTermsModal'));
|
||||
const acceptBtn = document.getElementById('smsTermsAcceptBtn');
|
||||
const declineBtn = document.getElementById('smsTermsDeclineBtn');
|
||||
const agreeCheck = document.getElementById('smsTermsAgreementCheck');
|
||||
|
||||
// Unlock the accept button only when checkbox is ticked
|
||||
agreeCheck.addEventListener('change', function () {
|
||||
acceptBtn.disabled = !this.checked;
|
||||
});
|
||||
|
||||
function postSmsPreference(enabled, agreedToTerms, termsVersion) {
|
||||
$.ajax({
|
||||
url: '@Url.Action("UpdateSmsPreferences", "CompanySettings")',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({ SmsEnabled: enabled, AgreedToTerms: agreedToTerms, TermsVersion: termsVersion }),
|
||||
headers: { 'RequestVerificationToken': $('input[name="__RequestVerificationToken"]').first().val() },
|
||||
success: function (res) {
|
||||
if (res.success) {
|
||||
toggle.dataset.hasAgreement = 'true';
|
||||
showToast('success', res.message);
|
||||
} else {
|
||||
// Revert toggle on failure
|
||||
toggle.checked = !enabled;
|
||||
showToast('error', res.message || 'Failed to save SMS preference.');
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
toggle.checked = !enabled;
|
||||
showToast('error', 'Failed to save SMS preference.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toggle.addEventListener('change', function () {
|
||||
const enabled = this.checked;
|
||||
const hasAgreement = this.dataset.hasAgreement === 'true';
|
||||
const termsVersion = this.dataset.termsVersion;
|
||||
|
||||
if (!enabled) {
|
||||
// Disabling: no agreement needed
|
||||
postSmsPreference(false, false, null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasAgreement) {
|
||||
// Re-enabling: already agreed to this version
|
||||
postSmsPreference(true, false, null);
|
||||
return;
|
||||
}
|
||||
|
||||
// First enable (or terms version changed): show the modal
|
||||
agreeCheck.checked = false;
|
||||
acceptBtn.disabled = true;
|
||||
smsTermsModal.show();
|
||||
|
||||
// Revert toggle until they explicitly agree
|
||||
toggle.checked = false;
|
||||
|
||||
acceptBtn.onclick = function () {
|
||||
smsTermsModal.hide();
|
||||
toggle.checked = true;
|
||||
postSmsPreference(true, true, termsVersion);
|
||||
};
|
||||
|
||||
declineBtn.onclick = function () {
|
||||
smsTermsModal.hide();
|
||||
toggle.checked = false;
|
||||
};
|
||||
});
|
||||
})();
|
||||
|
||||
// Toast helper function (exposed globally for lookup management)
|
||||
window.showToast = function(type, message) {
|
||||
if (type === 'success') {
|
||||
|
||||
Reference in New Issue
Block a user