Add TCPA-compliant SMS consent tracking for prospect quotes

- Quote entity: ProspectSmsConsent (bool) + ProspectSmsConsentedAt (DateTime?) fields
- QuoteDtos: consent fields on Create/Update/Convert DTOs with TCPA guidance text
- Quote Create/Edit views: SMS consent checkbox shown when mobile number is entered
- Quote ConvertToCustomer view: staff must re-confirm consent carries over to customer record
- QuoteApproval: consent state exposed in ViewModel and ApprovalPage for transparency
- Consent timestamp cleared when prospect quote is linked to an existing customer
- Migration: AddProspectSmsConsent

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-08 20:47:04 -04:00
parent fb979bc88d
commit f40d58ac2e
10 changed files with 9776 additions and 3 deletions
@@ -59,6 +59,8 @@ public class QuoteDto
public string? ProspectCity { get; set; }
public string? ProspectState { get; set; }
public string? ProspectZipCode { get; set; }
public bool ProspectSmsConsent { get; set; }
public DateTime? ProspectSmsConsentedAt { get; set; }
public string? PreparedById { get; set; }
public string? PreparedByName { get; set; }
@@ -186,6 +188,9 @@ public class CreateQuoteDto
[StringLength(10)]
public string? ProspectZipCode { get; set; }
[Display(Name = "SMS Consent")]
public bool ProspectSmsConsent { get; set; } = false;
// Oven Selection
[Display(Name = "Oven")]
public int? OvenCostId { get; set; }
@@ -322,6 +327,9 @@ public class UpdateQuoteDto
[StringLength(10)]
public string? ProspectZipCode { get; set; }
[Display(Name = "SMS Consent")]
public bool ProspectSmsConsent { get; set; } = false;
// Oven Selection
[Display(Name = "Oven")]
public int? OvenCostId { get; set; }
@@ -685,6 +693,16 @@ public class ConvertQuoteToCustomerDto
[Display(Name = "Notes")]
[DataType(DataType.MultilineText)]
public string? Notes { get; set; }
/// <summary>
/// Staff must explicitly confirm verbal SMS consent before it carries over to the new customer record.
/// Pre-checked when ProspectSmsConsent was true on the source quote.
/// </summary>
[Display(Name = "Customer has given verbal consent to receive SMS notifications")]
public bool SmsConsent { get; set; }
/// <summary>Timestamp from the source quote — preserved so the consent record reflects when consent was originally given.</summary>
public DateTime? ProspectSmsConsentedAt { get; set; }
}
// ============================================================================
@@ -745,6 +763,16 @@ public class CreateQuoteItemCoatDto
/// When true, the additional layer labor charge is not applied even if this is not the first coat.
/// </summary>
public bool NoExtraLayerCharge { get; set; }
/// <summary>Platform powder catalog item ID selected via the Custom tab lookup.</summary>
public int? CatalogItemId { get; set; }
/// <summary>
/// When true (and CatalogItemId is set), the server creates a 0-balance IsIncoming inventory
/// item from the catalog entry so QR codes can be printed while the powder is in transit.
/// The coat is then linked to that new inventory record.
/// </summary>
public bool AddAsIncoming { get; set; }
}
/// <summary>