Add BillingEmail field for commercial customers; support comma-separated multi-email
- 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>
This commit is contained in:
@@ -9,6 +9,7 @@ public class CustomerDto
|
||||
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; }
|
||||
@@ -52,10 +53,13 @@ public class CreateCustomerDto : IValidatableObject
|
||||
public string? ContactLastName { get; set; }
|
||||
|
||||
[Display(Name = "Email")]
|
||||
[EmailAddress(ErrorMessage = "Please enter a valid email address")]
|
||||
[StringLength(200)]
|
||||
[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)]
|
||||
@@ -143,6 +147,33 @@ public class CreateCustomerDto : IValidatableObject
|
||||
"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; }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ public interface INotificationService
|
||||
/// Notify when a quote is created/sent. Handles both registered customers and prospects.
|
||||
/// Optionally attaches the quote PDF to the email.
|
||||
/// </summary>
|
||||
Task NotifyQuoteSentAsync(Quote quote, byte[]? pdfAttachment = null, string? pdfFilename = null);
|
||||
Task NotifyQuoteSentAsync(Quote quote, byte[]? pdfAttachment = null, string? pdfFilename = null, string? overrideEmail = null);
|
||||
|
||||
/// <summary>
|
||||
/// Sends the quote approval link to the customer via SMS.
|
||||
@@ -58,7 +58,7 @@ public interface INotificationService
|
||||
/// Notify customer when an invoice has been sent.
|
||||
/// Optionally includes an online payment link in the email body.
|
||||
/// </summary>
|
||||
Task NotifyInvoiceSentAsync(Invoice invoice, byte[]? pdfAttachment = null, string? pdfFilename = null, string? paymentUrl = null);
|
||||
Task NotifyInvoiceSentAsync(Invoice invoice, byte[]? pdfAttachment = null, string? pdfFilename = null, string? paymentUrl = null, string? overrideEmail = null);
|
||||
|
||||
/// <summary>
|
||||
/// Notify customer (internal) when a payment has been recorded on an invoice.
|
||||
|
||||
Reference in New Issue
Block a user