Skip to content

Commit 487624a

Browse files
committed
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
1 parent 3d8ed73 commit 487624a

File tree

2 files changed

+153
-0
lines changed

2 files changed

+153
-0
lines changed
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
namespace LeadProcessor.Domain.Entities;
2+
3+
/// <summary>
4+
/// Represents a lead entity captured from various sources.
5+
/// </summary>
6+
public record Lead
7+
{
8+
/// <summary>
9+
/// Gets the unique identifier for the lead.
10+
/// </summary>
11+
public int Id { get; init; }
12+
13+
/// <summary>
14+
/// Gets the tenant identifier for multi-tenancy support.
15+
/// </summary>
16+
public required string TenantId { get; init; }
17+
18+
/// <summary>
19+
/// Gets the correlation identifier for idempotency and message tracking.
20+
/// </summary>
21+
public required string CorrelationId { get; init; }
22+
23+
/// <summary>
24+
/// Gets the email address of the lead.
25+
/// </summary>
26+
public required string Email { get; init; }
27+
28+
/// <summary>
29+
/// Gets the first name of the lead.
30+
/// </summary>
31+
public string? FirstName { get; init; }
32+
33+
/// <summary>
34+
/// Gets the last name of the lead.
35+
/// </summary>
36+
public string? LastName { get; init; }
37+
38+
/// <summary>
39+
/// Gets the phone number of the lead.
40+
/// </summary>
41+
public string? Phone { get; init; }
42+
43+
/// <summary>
44+
/// Gets the company name of the lead.
45+
/// </summary>
46+
public string? Company { get; init; }
47+
48+
/// <summary>
49+
/// Gets the source from which the lead originated (e.g., website, mobile app, referral).
50+
/// </summary>
51+
public required string Source { get; init; }
52+
53+
/// <summary>
54+
/// Gets the metadata as a JSON string containing additional information about the lead.
55+
/// </summary>
56+
public string? Metadata { get; init; }
57+
58+
/// <summary>
59+
/// Gets the UTC date and time when the lead was created.
60+
/// </summary>
61+
public DateTimeOffset CreatedAt { get; init; }
62+
63+
/// <summary>
64+
/// Gets the UTC date and time when the lead was last updated.
65+
/// </summary>
66+
public DateTimeOffset UpdatedAt { get; init; }
67+
68+
/// <summary>
69+
/// Validates that the email format is correct.
70+
/// </summary>
71+
/// <returns>
72+
/// True if the email is in a valid format according to RFC 5322, otherwise false.
73+
/// Returns false if email is null, empty, or whitespace.
74+
/// </returns>
75+
public bool HasValidEmail()
76+
{
77+
if (string.IsNullOrWhiteSpace(Email))
78+
return false;
79+
80+
try
81+
{
82+
var addr = new System.Net.Mail.MailAddress(Email);
83+
return addr.Address == Email;
84+
}
85+
catch (FormatException)
86+
{
87+
return false;
88+
}
89+
}
90+
91+
/// <summary>
92+
/// Validates that the correlation ID is in GUID format.
93+
/// </summary>
94+
/// <returns>True if the correlation ID can be parsed as a GUID, otherwise false.</returns>
95+
public bool IsCorrelationIdGuid()
96+
{
97+
return Guid.TryParse(CorrelationId, out _);
98+
}
99+
100+
/// <summary>
101+
/// Gets the full name of the lead by combining first and last names.
102+
/// </summary>
103+
/// <returns>The full name, or null if both names are empty.</returns>
104+
public string? GetFullName()
105+
{
106+
var hasFirst = !string.IsNullOrWhiteSpace(FirstName);
107+
var hasLast = !string.IsNullOrWhiteSpace(LastName);
108+
109+
return (hasFirst, hasLast) switch
110+
{
111+
(true, true) => $"{FirstName} {LastName}",
112+
(true, false) => FirstName,
113+
(false, true) => LastName,
114+
_ => null
115+
};
116+
}
117+
}
118+
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using LeadProcessor.Domain.Entities;
2+
3+
namespace LeadProcessor.Domain.Repositories;
4+
5+
/// <summary>
6+
/// Repository interface for managing lead persistence operations.
7+
/// </summary>
8+
public interface ILeadRepository
9+
{
10+
/// <summary>
11+
/// Saves a lead to the data store asynchronously.
12+
/// </summary>
13+
/// <param name="lead">The lead entity to save.</param>
14+
/// <param name="cancellationToken">Cancellation token to cancel the operation.</param>
15+
/// <returns>The saved lead entity with updated fields (e.g., Id, timestamps).</returns>
16+
Task<Lead> SaveLeadAsync(Lead lead, CancellationToken cancellationToken = default);
17+
18+
/// <summary>
19+
/// Checks if a lead with the specified correlation ID already exists.
20+
/// This method supports idempotency by allowing duplicate message detection.
21+
/// </summary>
22+
/// <param name="correlationId">The correlation ID to check for.</param>
23+
/// <param name="cancellationToken">Cancellation token to cancel the operation.</param>
24+
/// <returns>True if a lead with the correlation ID exists, otherwise false.</returns>
25+
Task<bool> ExistsByCorrelationIdAsync(string correlationId, CancellationToken cancellationToken = default);
26+
27+
/// <summary>
28+
/// Retrieves a lead by its correlation ID asynchronously.
29+
/// </summary>
30+
/// <param name="correlationId">The correlation ID to search for.</param>
31+
/// <param name="cancellationToken">Cancellation token to cancel the operation.</param>
32+
/// <returns>The lead entity if found, otherwise null.</returns>
33+
Task<Lead?> GetByCorrelationIdAsync(string correlationId, CancellationToken cancellationToken = default);
34+
}
35+

0 commit comments

Comments
 (0)