From 487624a8af4419fac208c10bbe835101367c5e8a Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Tue, 21 Oct 2025 23:16:05 +1100 Subject: [PATCH] feat: implement immutable Lead entity and repository interface - Add Lead record with init properties for immutability - Use DateTimeOffset for UTC-safe timestamps - Implement domain validation methods (email, GUID, full name) - Add ILeadRepository with async methods and idempotency support - Follow Clean Architecture and SOLID principles --- src/LeadProcessor.Domain/Entities/Lead.cs | 118 ++++++++++++++++++ .../Repositories/ILeadRepository.cs | 35 ++++++ 2 files changed, 153 insertions(+) create mode 100644 src/LeadProcessor.Domain/Entities/Lead.cs create mode 100644 src/LeadProcessor.Domain/Repositories/ILeadRepository.cs diff --git a/src/LeadProcessor.Domain/Entities/Lead.cs b/src/LeadProcessor.Domain/Entities/Lead.cs new file mode 100644 index 0000000..d07dcd0 --- /dev/null +++ b/src/LeadProcessor.Domain/Entities/Lead.cs @@ -0,0 +1,118 @@ +namespace LeadProcessor.Domain.Entities; + +/// +/// Represents a lead entity captured from various sources. +/// +public record Lead +{ + /// + /// Gets the unique identifier for the lead. + /// + public int Id { get; init; } + + /// + /// Gets the tenant identifier for multi-tenancy support. + /// + public required string TenantId { get; init; } + + /// + /// Gets the correlation identifier for idempotency and message tracking. + /// + public required string CorrelationId { get; init; } + + /// + /// Gets the email address of the lead. + /// + public required string Email { get; init; } + + /// + /// Gets the first name of the lead. + /// + public string? FirstName { get; init; } + + /// + /// Gets the last name of the lead. + /// + public string? LastName { get; init; } + + /// + /// Gets the phone number of the lead. + /// + public string? Phone { get; init; } + + /// + /// Gets the company name of the lead. + /// + public string? Company { get; init; } + + /// + /// Gets the source from which the lead originated (e.g., website, mobile app, referral). + /// + public required string Source { get; init; } + + /// + /// Gets the metadata as a JSON string containing additional information about the lead. + /// + public string? Metadata { get; init; } + + /// + /// Gets the UTC date and time when the lead was created. + /// + public DateTimeOffset CreatedAt { get; init; } + + /// + /// Gets the UTC date and time when the lead was last updated. + /// + public DateTimeOffset UpdatedAt { get; init; } + + /// + /// Validates that the email format is correct. + /// + /// + /// True if the email is in a valid format according to RFC 5322, otherwise false. + /// Returns false if email is null, empty, or whitespace. + /// + public bool HasValidEmail() + { + if (string.IsNullOrWhiteSpace(Email)) + return false; + + try + { + var addr = new System.Net.Mail.MailAddress(Email); + return addr.Address == Email; + } + catch (FormatException) + { + return false; + } + } + + /// + /// Validates that the correlation ID is in GUID format. + /// + /// True if the correlation ID can be parsed as a GUID, otherwise false. + public bool IsCorrelationIdGuid() + { + return Guid.TryParse(CorrelationId, out _); + } + + /// + /// Gets the full name of the lead by combining first and last names. + /// + /// The full name, or null if both names are empty. + public string? GetFullName() + { + var hasFirst = !string.IsNullOrWhiteSpace(FirstName); + var hasLast = !string.IsNullOrWhiteSpace(LastName); + + return (hasFirst, hasLast) switch + { + (true, true) => $"{FirstName} {LastName}", + (true, false) => FirstName, + (false, true) => LastName, + _ => null + }; + } +} + diff --git a/src/LeadProcessor.Domain/Repositories/ILeadRepository.cs b/src/LeadProcessor.Domain/Repositories/ILeadRepository.cs new file mode 100644 index 0000000..731b2a8 --- /dev/null +++ b/src/LeadProcessor.Domain/Repositories/ILeadRepository.cs @@ -0,0 +1,35 @@ +using LeadProcessor.Domain.Entities; + +namespace LeadProcessor.Domain.Repositories; + +/// +/// Repository interface for managing lead persistence operations. +/// +public interface ILeadRepository +{ + /// + /// Saves a lead to the data store asynchronously. + /// + /// The lead entity to save. + /// Cancellation token to cancel the operation. + /// The saved lead entity with updated fields (e.g., Id, timestamps). + Task SaveLeadAsync(Lead lead, CancellationToken cancellationToken = default); + + /// + /// Checks if a lead with the specified correlation ID already exists. + /// This method supports idempotency by allowing duplicate message detection. + /// + /// The correlation ID to check for. + /// Cancellation token to cancel the operation. + /// True if a lead with the correlation ID exists, otherwise false. + Task ExistsByCorrelationIdAsync(string correlationId, CancellationToken cancellationToken = default); + + /// + /// Retrieves a lead by its correlation ID asynchronously. + /// + /// The correlation ID to search for. + /// Cancellation token to cancel the operation. + /// The lead entity if found, otherwise null. + Task GetByCorrelationIdAsync(string correlationId, CancellationToken cancellationToken = default); +} +