Split Features to create space for more projects
continuous-integration/drone/pr Build is passing

This commit is contained in:
Khwezi Mngoma
2026-05-24 13:19:09 +02:00
parent 032b9e1818
commit 70c6e0bfbc
95 changed files with 621 additions and 314 deletions
@@ -0,0 +1,9 @@
using LiteCharms.Features.TechShop.Customers.Entities;
namespace LiteCharms.Features.TechShop.Leads.Entities;
[EntityTypeConfiguration<LeadConfiguration, Lead>]
public class Lead : Models.Lead
{
public virtual Customer? Customer { get; set; }
}
@@ -0,0 +1,31 @@
namespace LiteCharms.Features.TechShop.Leads.Entities;
public class LeadConfiguration : IEntityTypeConfiguration<Lead>
{
public void Configure(EntityTypeBuilder<Lead> builder)
{
builder.ToTable("Leads");
builder.HasKey(f => f.Id);
builder.Property(f => f.CreatedAt).ValueGeneratedOnAdd().HasDefaultValueSql("now()");
builder.Property(f => f.UpdatedAt).IsRequired(false).HasDefaultValueSql(null);
builder.Property(f => f.CustomerId);
builder.Property(f => f.Source);
builder.Property(f => f.ClickId);
builder.Property(f => f.WebClickId);
builder.Property(f => f.AppClickId);
builder.Property(f => f.CampaignId);
builder.Property(f => f.AdGroupId);
builder.Property(f => f.AdName);
builder.Property(f => f.TargetId);
builder.Property(f => f.FeedItemId);
builder.Property(f => f.ClickLocation);
builder.Property(f => f.Status).IsRequired();
builder.Property(f => f.AttributionHash).IsRequired(true);
builder.HasOne(f => f.Customer)
.WithMany(f => f.Leads)
.HasForeignKey(f => f.CustomerId)
.OnDelete(DeleteBehavior.Restrict);
}
}
@@ -0,0 +1,118 @@
using LiteCharms.Features.Extensions;
using LiteCharms.Features.Models;
using LiteCharms.Features.TechShop.Extensions;
using LiteCharms.Features.TechShop.Leads.Models;
using LiteCharms.Features.TechShop.Postgres;
using static LiteCharms.Features.Extensions.Hash;
namespace LiteCharms.Features.TechShop.Leads;
public class LeadService(IDbContextFactory<ShopDbContext> contextFactory)
{
public async ValueTask<Result<Guid>> CreateLeadAsync(CreateLead request, CancellationToken cancellationToken = default)
{
try
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var newLead = context.Leads.Add(new Entities.Lead
{
WebClickId = request.WebClickId,
AppClickId = request.AppClickId,
Source = request.Source,
ClickId = request.ClickId,
AdGroupId = request.AdGroupId,
AdName = request.AdName,
CampaignId = request.CampaignId,
ClickLocation = request.ClickLocation,
CustomerId = request.CustomerId,
FeedItemId = request.FeedItemId,
Status = LeadStatus.New,
TargetId = request.TargetId,
AttributionHash = StringToSha256Hash.Invoke($"{request.ClickId}{request.AppClickId}{request.WebClickId}")
});
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok(newLead.Entity.Id)
: Result.Fail<Guid>(new Error($"Failed to create lead -> Google ClickId: {request.ClickId}, App ClickId: {request.AppClickId}, Web ClickId: {request.WebClickId}"));
}
catch (Exception ex)
{
return Result.Fail<Guid>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Lead[]>> GetCustomerLeadsAsync(Guid customerId, DateRange range, CancellationToken cancellationToken = default)
{
try
{
var fromDate = range.From.ToDateTime(TimeOnly.MinValue);
var toDate = range.To.ToDateTime(TimeOnly.MaxValue);
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var leads = await context.Leads.AsNoTracking()
.OrderByDescending(o => o.CreatedAt)
.Where(lead => lead.CustomerId == customerId)
.Where(lead => lead.CreatedAt.Date >= fromDate && lead.CreatedAt.Date <= toDate)
.ToArrayAsync(cancellationToken);
return leads?.Length > 0
? Result.Ok(leads.Select(l => l.ToModel()).ToArray())
: Result.Fail(new Error($"No customer {customerId} leads found for the specified date range {range.From} to {range.To}."));
}
catch (Exception ex)
{
return Result.Fail<Lead[]>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Lead[]>> GetLeadsAsync(DateRange range, CancellationToken cancellationToken = default)
{
try
{
var fromDate = range.From.ToDateTime(TimeOnly.MinValue);
var toDate = range.To.ToDateTime(TimeOnly.MaxValue);
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var leads = await context.Leads.AsNoTracking()
.OrderByDescending(o => o.CreatedAt)
.Where(l => l.CreatedAt.Date >= fromDate && l.CreatedAt.Date <= toDate)
.Take(range.MaxRecords)
.ToArrayAsync(cancellationToken);
return leads?.Length > 0
? Result.Ok(leads.Select(l => l.ToModel()).ToArray())
: Result.Fail(new Error($"No leads found for the specified date range {range.From} to {range.To}."));
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> UpdateLeadAsync(Guid leadId, LeadStatus status, CancellationToken cancellationToken = default)
{
try
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var lead = await context.Leads.FirstOrDefaultAsync(l => l.Id == leadId, cancellationToken);
if (lead is null)
return Result.Fail(new Error($"Lead with ID {leadId} not found."));
lead.Status = status;
lead.UpdatedAt = DateTime.UtcNow;
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok()
: Result.Fail(new Error($"Failed to update the lead {leadId}."));
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
}
@@ -0,0 +1,36 @@
namespace LiteCharms.Features.TechShop.Leads.Models;
public class Lead
{
public Guid Id { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public Guid? CustomerId { get; set; }
public string? Source { get; set; }
public string? ClickId { get; set; }
public string? WebClickId { get; set; }
public string? AppClickId { get; set; }
public long? CampaignId { get; set; }
public long? AdGroupId { get; set; }
public long? AdName { get; set; }
public long? TargetId { get; set; }
public long? FeedItemId { get; set; }
public string? ClickLocation { get; set; }
public string? AttributionHash { get; set; }
public LeadStatus Status { get; set; }
}
@@ -0,0 +1,28 @@
namespace LiteCharms.Features.TechShop.Leads.Models;
public record CreateLead
{
public Guid? CustomerId { get; set; }
public required string Source { get; set; }
public required string ClickId { get; set; }
public required string WebClickId { get; set; }
public required string AppClickId { get; set; }
public long? CampaignId { get; set; }
public long? AdGroupId { get; set; }
public long? AdName { get; set; }
public long? TargetId { get; set; }
public long? FeedItemId { get; set; }
public string? ClickLocation { get; set; }
public string? AttribusionHash { get; set; }
}