diff --git a/LiteCharms.Entities/Configuration/LeadConfiguration.cs b/LiteCharms.Entities/Configuration/LeadConfiguration.cs index c2ec1b6..8c8d004 100644 --- a/LiteCharms.Entities/Configuration/LeadConfiguration.cs +++ b/LiteCharms.Entities/Configuration/LeadConfiguration.cs @@ -10,7 +10,8 @@ public class LeadConfiguration : IEntityTypeConfiguration builder.Property(f => f.CreatedAt).ValueGeneratedOnAdd(); builder.Property(f => f.UpdatedAt).IsRequired(false).ValueGeneratedOnUpdate(); builder.Property(f => f.CustomerId).IsRequired(false); - builder.Property(f => f.GoogleClickId); + 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); diff --git a/LiteCharms.Extensions/EntityModeMappers.cs b/LiteCharms.Extensions/EntityModeMappers.cs index 2632526..217dd4a 100644 --- a/LiteCharms.Extensions/EntityModeMappers.cs +++ b/LiteCharms.Extensions/EntityModeMappers.cs @@ -43,7 +43,8 @@ public static class EntityModeMappers ClickLocation = entity.ClickLocation, CustomerId = entity.CustomerId, FeedItemId = entity.FeedItemId, - GoogleClickId = entity.GoogleClickId, + Source = entity.Source, + ClickId = entity.ClickId, TargetId = entity.TargetId, WebClickId = entity.WebClickId, Status = entity.Status diff --git a/LiteCharms.Features/Leads/Commands/CreateLeadCommand.cs b/LiteCharms.Features/Leads/Commands/CreateLeadCommand.cs index e9f3ecb..5b120df 100644 --- a/LiteCharms.Features/Leads/Commands/CreateLeadCommand.cs +++ b/LiteCharms.Features/Leads/Commands/CreateLeadCommand.cs @@ -4,7 +4,9 @@ public class CreateLeadCommand : IRequest> { public Guid? CustomerId { get; set; } - public string? GoogleClickId { get; set; } + public string? Source { get; set; } + + public string? ClickId { get; set; } public string? WebClickId { get; set; } @@ -24,10 +26,11 @@ public class CreateLeadCommand : IRequest> public string? AttribusionHash { get; set; } - private CreateLeadCommand(Guid? customerId, string googleClickId, string webClickId, string appClickId, long? campaignId, long? adGroupId, long? adName, long? targetId, long? feedItemId, string? clickLocation, string? attribusionHash) + private CreateLeadCommand(Guid? customerId, string source, string clickId, string webClickId, string appClickId, long? campaignId, long? adGroupId, long? adName, long? targetId, long? feedItemId, string? clickLocation, string? attribusionHash) { CustomerId = customerId; - GoogleClickId = googleClickId; + Source = source; + ClickId = clickId; WebClickId = webClickId; AppClickId = appClickId; CampaignId = campaignId; @@ -39,11 +42,14 @@ public class CreateLeadCommand : IRequest> AttribusionHash = attribusionHash; } - public static CreateLeadCommand Create(Guid? customerId, string googleClickId, string webClickId, string appClickId, long? campaignId, long? adGroupId, long? adName, long? targetId, long? feedItemId, string? clickLocation, string? attribusionHash) + public static CreateLeadCommand Create(Guid? customerId, string source, string clickId, string webClickId, string appClickId, long? campaignId, long? adGroupId, long? adName, long? targetId, long? feedItemId, string? clickLocation, string? attribusionHash) { - if (string.IsNullOrWhiteSpace(googleClickId) || string.IsNullOrWhiteSpace(appClickId) || string.IsNullOrWhiteSpace(webClickId)) - throw new ArgumentException("Google ClickId, App ClickId and Web ClickId are required to create a lead."); + if(string.IsNullOrWhiteSpace(source)) + throw new ArgumentNullException("Lead source is required to create a lead.", nameof(source)); - return new(customerId, googleClickId, webClickId, appClickId, campaignId, adGroupId, adName, targetId, feedItemId, clickLocation, attribusionHash); + if (string.IsNullOrWhiteSpace(clickId) || string.IsNullOrWhiteSpace(appClickId) || string.IsNullOrWhiteSpace(webClickId)) + throw new ArgumentException("ClickId, App ClickId and Web ClickId are required to create a lead."); + + return new(customerId, source, clickId, webClickId, appClickId, campaignId, adGroupId, adName, targetId, feedItemId, clickLocation, attribusionHash); } } diff --git a/LiteCharms.Features/Leads/Commands/Handlers/CreateLeadCommandHandler.cs b/LiteCharms.Features/Leads/Commands/Handlers/CreateLeadCommandHandler.cs index 0d93755..bd99665 100644 --- a/LiteCharms.Features/Leads/Commands/Handlers/CreateLeadCommandHandler.cs +++ b/LiteCharms.Features/Leads/Commands/Handlers/CreateLeadCommandHandler.cs @@ -9,11 +9,11 @@ public class CreateLeadCommandHandler(IDbContextFactory { try { - var hashCommand = ComputeHashCommand.Create($"{request.GoogleClickId}{request.AppClickId}{request.WebClickId}"); + var hashCommand = ComputeHashCommand.Create($"{request.ClickId}{request.AppClickId}{request.WebClickId}"); var hashResult = await mediator.Send(hashCommand, cancellationToken); if(hashResult.IsFailed) - return Result.Fail(new Error($"Failed to compute hash for lead -> Google ClickId: {request.GoogleClickId}, App ClickId: {request.AppClickId}, Web ClickId: {request.WebClickId}") + return Result.Fail(new Error($"Failed to compute hash for lead -> Google ClickId: {request.ClickId}, App ClickId: {request.AppClickId}, Web ClickId: {request.WebClickId}") .CausedBy(hashResult.Errors)); using var context = await contextFactory.CreateDbContextAsync(cancellationToken); @@ -22,7 +22,8 @@ public class CreateLeadCommandHandler(IDbContextFactory { WebClickId = request.WebClickId, AppClickId = request.AppClickId, - GoogleClickId = request.GoogleClickId, + Source = request.Source, + ClickId = request.ClickId, AdGroupId = request.AdGroupId, AdName = request.AdName, CampaignId = request.CampaignId, @@ -36,7 +37,7 @@ public class CreateLeadCommandHandler(IDbContextFactory return await context.SaveChangesAsync(cancellationToken) > 0 ? Result.Ok(newLead.Entity.Id) - : Result.Fail(new Error($"Failed to create lead -> Google ClickId: {request.GoogleClickId}, App ClickId: {request.AppClickId}, Web ClickId: {request.WebClickId}")); + : Result.Fail(new Error($"Failed to create lead -> Google ClickId: {request.ClickId}, App ClickId: {request.AppClickId}, Web ClickId: {request.WebClickId}")); } catch (Exception ex) { diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260505120450_GeneralisedLead.Designer.cs b/LiteCharms.Infrastructure/Database/Migrations/20260505120450_GeneralisedLead.Designer.cs new file mode 100644 index 0000000..6f22981 --- /dev/null +++ b/LiteCharms.Infrastructure/Database/Migrations/20260505120450_GeneralisedLead.Designer.cs @@ -0,0 +1,360 @@ +// +using System; +using LiteCharms.Infrastructure.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LiteCharms.Infrastructure.Database.Migrations +{ + [DbContext(typeof(LeadGeneratorDbContext))] + [Migration("20260505120450_GeneralisedLead")] + partial class GeneralisedLead + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LiteCharms.Entities.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("Country") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone"); + + b.Property("Discord") + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LinkedIn") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("PostalCode") + .HasColumnType("text"); + + b.Property("Region") + .HasColumnType("text"); + + b.Property("Slack") + .HasColumnType("text"); + + b.Property("Tax") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnUpdate() + .HasColumnType("timestamp with time zone"); + + b.Property("Website") + .HasColumnType("text"); + + b.Property("Whatsapp") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Customer", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Entities.Lead", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AdGroupId") + .HasColumnType("bigint"); + + b.Property("AdName") + .HasColumnType("bigint"); + + b.Property("AppClickId") + .HasColumnType("text"); + + b.Property("AttributionHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("CampaignId") + .HasColumnType("bigint"); + + b.Property("ClickId") + .HasColumnType("text"); + + b.Property("ClickLocation") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("FeedItemId") + .HasColumnType("bigint"); + + b.Property("Source") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TargetId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .ValueGeneratedOnUpdate() + .HasColumnType("timestamp with time zone"); + + b.Property("WebClickId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Lead", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Entities.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.PrimitiveCollection("Notes") + .HasColumnType("jsonb"); + + b.Property("ProductPriceId") + .HasColumnType("uuid"); + + b.Property("RefundId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnUpdate() + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("ProductPriceId"); + + b.ToTable("Order", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Entities.OrderRefund", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.ToTable("OrderRefund", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Product", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Entities.ProductPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone"); + + b.Property("Discount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .ValueGeneratedOnUpdate() + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductPrice", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Entities.Lead", b => + { + b.HasOne("LiteCharms.Entities.Customer", "Customer") + .WithMany("Leads") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Entities.Order", b => + { + b.HasOne("LiteCharms.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Entities.ProductPrice", "ProductPrice") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Customer"); + + b.Navigation("ProductPrice"); + }); + + modelBuilder.Entity("LiteCharms.Entities.OrderRefund", b => + { + b.HasOne("LiteCharms.Entities.Order", "Order") + .WithOne("Refund") + .HasForeignKey("LiteCharms.Entities.OrderRefund", "OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Entities.ProductPrice", b => + { + b.HasOne("LiteCharms.Entities.Product", "Product") + .WithMany("ProductPrices") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Entities.Customer", b => + { + b.Navigation("Leads"); + + b.Navigation("Orders"); + }); + + modelBuilder.Entity("LiteCharms.Entities.Order", b => + { + b.Navigation("Refund"); + }); + + modelBuilder.Entity("LiteCharms.Entities.Product", b => + { + b.Navigation("ProductPrices"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LiteCharms.Infrastructure/Database/Migrations/20260505120450_GeneralisedLead.cs b/LiteCharms.Infrastructure/Database/Migrations/20260505120450_GeneralisedLead.cs new file mode 100644 index 0000000..6176058 --- /dev/null +++ b/LiteCharms.Infrastructure/Database/Migrations/20260505120450_GeneralisedLead.cs @@ -0,0 +1,38 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LiteCharms.Infrastructure.Database.Migrations +{ + /// + public partial class GeneralisedLead : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "GoogleClickId", + table: "Lead", + newName: "Source"); + + migrationBuilder.AddColumn( + name: "ClickId", + table: "Lead", + type: "text", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ClickId", + table: "Lead"); + + migrationBuilder.RenameColumn( + name: "Source", + table: "Lead", + newName: "GoogleClickId"); + } + } +} diff --git a/LiteCharms.Infrastructure/Database/Migrations/LeadGeneratorDbContextModelSnapshot.cs b/LiteCharms.Infrastructure/Database/Migrations/LeadGeneratorDbContextModelSnapshot.cs index 20a5632..e2ad938 100644 --- a/LiteCharms.Infrastructure/Database/Migrations/LeadGeneratorDbContextModelSnapshot.cs +++ b/LiteCharms.Infrastructure/Database/Migrations/LeadGeneratorDbContextModelSnapshot.cs @@ -119,6 +119,9 @@ namespace LiteCharms.Infrastructure.Migrations b.Property("CampaignId") .HasColumnType("bigint"); + b.Property("ClickId") + .HasColumnType("text"); + b.Property("ClickLocation") .HasColumnType("text"); @@ -132,7 +135,7 @@ namespace LiteCharms.Infrastructure.Migrations b.Property("FeedItemId") .HasColumnType("bigint"); - b.Property("GoogleClickId") + b.Property("Source") .HasColumnType("text"); b.Property("Status") diff --git a/LiteCharms.Models/EmailEnquiry.cs b/LiteCharms.Models/EmailEnquiry.cs new file mode 100644 index 0000000..58a0598 --- /dev/null +++ b/LiteCharms.Models/EmailEnquiry.cs @@ -0,0 +1,25 @@ +namespace LiteCharms.Models; + +public sealed class EmailEnquiry +{ + [Required] + [MinLength(2)] + [MaxLength(255)] + public string? FullName { get; set; } + + [Required] + [EmailAddress] + [MinLength(20)] + [MaxLength(255)] + public string? EmailAddress { get; set; } + + [Required] + [MinLength(2)] + [MaxLength(255)] + public string? EmailSubject { get; set; } + + [Required] + [MinLength(2)] + [MaxLength(2000)] + public string? Message { get; set; } +} diff --git a/LiteCharms.Models/Lead.cs b/LiteCharms.Models/Lead.cs index a221561..5222114 100644 --- a/LiteCharms.Models/Lead.cs +++ b/LiteCharms.Models/Lead.cs @@ -10,7 +10,9 @@ public class Lead public Guid? CustomerId { get; set; } - public string? GoogleClickId { get; set; } + public string? Source { get; set; } + + public string? ClickId { get; set; } public string? WebClickId { get; set; } diff --git a/LiteCharms.Models/LiteCharms.Models.csproj b/LiteCharms.Models/LiteCharms.Models.csproj index c4cace3..94d83ad 100644 --- a/LiteCharms.Models/LiteCharms.Models.csproj +++ b/LiteCharms.Models/LiteCharms.Models.csproj @@ -23,6 +23,11 @@ icon.png + + + + +