fb979bc88d
- Customer entity + DTO: new BillingEmail field (accounting/invoicing address) - Email fields now accept comma-separated lists; DTO validates each address individually - NotificationService: SendToEmailListAsync helper fans out to all addresses in a list; NotifyQuoteSentAsync accepts optional overrideEmail so staff can send to an ad-hoc address - Migration: AddCustomerBillingEmail - Customer Create/Edit/Details views updated to show Billing Email field - customer-billing-email.js: client-side helpers for billing email input Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
224 lines
7.5 KiB
C#
224 lines
7.5 KiB
C#
using System.ComponentModel.DataAnnotations;
|
|
|
|
namespace PowderCoating.Application.DTOs.Customer;
|
|
|
|
public class CustomerDto
|
|
{
|
|
public int Id { get; set; }
|
|
public string? CompanyName { get; set; }
|
|
public string? ContactFirstName { get; set; }
|
|
public string? ContactLastName { get; set; }
|
|
public string? Email { get; set; }
|
|
public string? BillingEmail { get; set; }
|
|
public string? Phone { get; set; }
|
|
public string? MobilePhone { get; set; }
|
|
public string? Address { get; set; }
|
|
public string? City { get; set; }
|
|
public string? State { get; set; }
|
|
public string? ZipCode { get; set; }
|
|
public string? Country { get; set; }
|
|
public bool IsCommercial { get; set; }
|
|
public string? TaxId { get; set; }
|
|
public decimal CreditLimit { get; set; }
|
|
public decimal CurrentBalance { get; set; }
|
|
public decimal CreditBalance { get; set; }
|
|
public string? PaymentTerms { get; set; }
|
|
public int? PricingTierId { get; set; }
|
|
public string? PricingTierName { get; set; }
|
|
public bool IsTaxExempt { get; set; }
|
|
public bool HasTaxExemptCertificate { get; set; }
|
|
public string? TaxExemptCertificateFileName { get; set; }
|
|
public string? GeneralNotes { get; set; }
|
|
public bool IsActive { get; set; }
|
|
public DateTime? LastContactDate { get; set; }
|
|
public DateTime CreatedAt { get; set; }
|
|
public bool NotifyByEmail { get; set; }
|
|
public bool NotifyBySms { get; set; }
|
|
public DateTime? SmsConsentedAt { get; set; }
|
|
public string? SmsConsentMethod { get; set; }
|
|
}
|
|
|
|
public class CreateCustomerDto : IValidatableObject
|
|
{
|
|
[Display(Name = "Company Name")]
|
|
[StringLength(200)]
|
|
public string? CompanyName { get; set; }
|
|
|
|
[Display(Name = "First Name")]
|
|
[StringLength(100)]
|
|
public string? ContactFirstName { get; set; }
|
|
|
|
[Display(Name = "Last Name")]
|
|
[StringLength(100)]
|
|
public string? ContactLastName { get; set; }
|
|
|
|
[Display(Name = "Email")]
|
|
[StringLength(1000)]
|
|
public string? Email { get; set; }
|
|
|
|
[Display(Name = "Billing / Accounting Email")]
|
|
[StringLength(1000)]
|
|
public string? BillingEmail { get; set; }
|
|
|
|
[Display(Name = "Phone")]
|
|
[Phone(ErrorMessage = "Please enter a valid phone number")]
|
|
[StringLength(20)]
|
|
public string? Phone { get; set; }
|
|
|
|
[Display(Name = "Mobile Phone")]
|
|
[Phone(ErrorMessage = "Please enter a valid mobile phone number")]
|
|
[StringLength(20)]
|
|
public string? MobilePhone { get; set; }
|
|
|
|
[Display(Name = "Street Address")]
|
|
[StringLength(500)]
|
|
public string? Address { get; set; }
|
|
|
|
[Display(Name = "City")]
|
|
[StringLength(100)]
|
|
public string? City { get; set; }
|
|
|
|
[Display(Name = "State")]
|
|
[StringLength(50)]
|
|
public string? State { get; set; }
|
|
|
|
[Display(Name = "Zip Code")]
|
|
[StringLength(20)]
|
|
public string? ZipCode { get; set; }
|
|
|
|
[Display(Name = "Country")]
|
|
[StringLength(100)]
|
|
public string? Country { get; set; } = "USA";
|
|
|
|
[Display(Name = "Customer Type")]
|
|
public bool IsCommercial { get; set; }
|
|
|
|
[Display(Name = "Tax ID / EIN")]
|
|
[StringLength(50)]
|
|
public string? TaxId { get; set; }
|
|
|
|
[Display(Name = "Credit Limit")]
|
|
[Range(0, 9999999.99)]
|
|
public decimal CreditLimit { get; set; }
|
|
|
|
[Display(Name = "Payment Terms")]
|
|
[StringLength(50)]
|
|
public string? PaymentTerms { get; set; }
|
|
|
|
[Display(Name = "Pricing Tier")]
|
|
public int? PricingTierId { get; set; }
|
|
|
|
[Display(Name = "Tax Exempt")]
|
|
public bool IsTaxExempt { get; set; }
|
|
|
|
[Display(Name = "General Notes")]
|
|
[StringLength(2000)]
|
|
public string? GeneralNotes { get; set; }
|
|
|
|
[Display(Name = "Notify by Email")]
|
|
public bool NotifyByEmail { get; set; } = true;
|
|
|
|
[Display(Name = "Notify by SMS")]
|
|
public bool NotifyBySms { get; set; } = false;
|
|
|
|
/// <summary>
|
|
/// Form-only flag: staff checks this to record that the customer gave verbal consent to receive SMS.
|
|
/// Not mapped to any entity field — the controller handles setting SmsConsentedAt and SmsConsentMethod.
|
|
/// </summary>
|
|
[Display(Name = "Customer has verbally consented to receive SMS notifications")]
|
|
public bool SmsConsentGranted { get; set; } = false;
|
|
|
|
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
|
{
|
|
// At least one name field is required (Company Name OR Contact First/Last Name)
|
|
if (string.IsNullOrWhiteSpace(CompanyName) &&
|
|
string.IsNullOrWhiteSpace(ContactFirstName) &&
|
|
string.IsNullOrWhiteSpace(ContactLastName))
|
|
{
|
|
yield return new ValidationResult(
|
|
"Please provide either a Company Name or a Contact Name (First and/or Last Name)",
|
|
new[] { nameof(CompanyName), nameof(ContactFirstName), nameof(ContactLastName) });
|
|
}
|
|
|
|
// At least one contact method is required (Email OR Phone)
|
|
if (string.IsNullOrWhiteSpace(Email) && string.IsNullOrWhiteSpace(Phone))
|
|
{
|
|
yield return new ValidationResult(
|
|
"Please provide at least one contact method (Email or Phone)",
|
|
new[] { nameof(Email), nameof(Phone) });
|
|
}
|
|
|
|
// Validate each address in comma-separated email fields
|
|
foreach (var addr in SplitEmails(Email))
|
|
{
|
|
if (!IsValidEmail(addr))
|
|
yield return new ValidationResult(
|
|
$"'{addr}' is not a valid email address.",
|
|
new[] { nameof(Email) });
|
|
}
|
|
foreach (var addr in SplitEmails(BillingEmail))
|
|
{
|
|
if (!IsValidEmail(addr))
|
|
yield return new ValidationResult(
|
|
$"'{addr}' is not a valid email address.",
|
|
new[] { nameof(BillingEmail) });
|
|
}
|
|
}
|
|
|
|
private static IEnumerable<string> SplitEmails(string? value) =>
|
|
string.IsNullOrWhiteSpace(value)
|
|
? []
|
|
: value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
|
|
|
private static bool IsValidEmail(string email)
|
|
{
|
|
try { _ = new System.Net.Mail.MailAddress(email); return true; }
|
|
catch { return false; }
|
|
}
|
|
}
|
|
|
|
public class UpdateCustomerDto : CreateCustomerDto
|
|
{
|
|
public int Id { get; set; }
|
|
public bool IsActive { get; set; }
|
|
public bool HasTaxExemptCertificate { get; set; }
|
|
public string? TaxExemptCertificateFileName { get; set; }
|
|
// Read from entity so the Edit view can display consent status
|
|
public DateTime? SmsConsentedAt { get; set; }
|
|
public string? SmsConsentMethod { get; set; }
|
|
}
|
|
|
|
public class CustomerListDto
|
|
{
|
|
public int Id { get; set; }
|
|
public string? CompanyName { get; set; }
|
|
public string ContactName { get; set; } = string.Empty;
|
|
public string? Phone { get; set; }
|
|
public string? Email { get; set; }
|
|
public bool IsCommercial { get; set; }
|
|
public decimal CurrentBalance { get; set; }
|
|
public bool IsActive { get; set; }
|
|
public DateTime? LastContactDate { get; set; }
|
|
public bool NotifyByEmail { get; set; }
|
|
public bool NotifyBySms { get; set; }
|
|
}
|
|
|
|
public class AddCreditDto
|
|
{
|
|
[Required]
|
|
[Range(0.01, 99999.99, ErrorMessage = "Amount must be between $0.01 and $99,999.99")]
|
|
public decimal Amount { get; set; }
|
|
|
|
[Required]
|
|
[StringLength(200, ErrorMessage = "Reason cannot exceed 200 characters")]
|
|
[Display(Name = "Reason")]
|
|
public string Reason { get; set; } = string.Empty;
|
|
|
|
[StringLength(1000)]
|
|
[Display(Name = "Notes")]
|
|
public string? Notes { get; set; }
|
|
|
|
[Display(Name = "Expiry Date (optional)")]
|
|
public DateTime? ExpiryDate { get; set; }
|
|
}
|