diff --git a/LiteCharms.Features/Email/Models/EmailEnquiry.cs b/LiteCharms.Features/Email/Models/EmailEnquiryModel.cs similarity index 94% rename from LiteCharms.Features/Email/Models/EmailEnquiry.cs rename to LiteCharms.Features/Email/Models/EmailEnquiryModel.cs index 97c2fe3..b13e3a7 100644 --- a/LiteCharms.Features/Email/Models/EmailEnquiry.cs +++ b/LiteCharms.Features/Email/Models/EmailEnquiryModel.cs @@ -2,7 +2,7 @@ namespace LiteCharms.Features.Email.Models; -public sealed class EmailEnquiry +public sealed class EmailEnquiryModel { [Required] [MinLength(2)] diff --git a/LiteCharms.Features/Extensions/EntityModeMappers.cs b/LiteCharms.Features/Extensions/EntityModeMappers.cs index d73a028..9dce6e8 100644 --- a/LiteCharms.Features/Extensions/EntityModeMappers.cs +++ b/LiteCharms.Features/Extensions/EntityModeMappers.cs @@ -176,13 +176,15 @@ public static class EntityModeMappers public static Product ToModel(this Features.Shop.Products.Entities.Product entity) => new() { - Id = entity.Id, + Id = entity.Id, + CreatedAt = entity.CreatedAt, Name = entity.Name, Description = entity.Description, Active = entity.Active, Summary = entity.Summary, ImageUrl = entity.ImageUrl, - Thumbnails = entity.Thumbnails + Thumbnails = entity.Thumbnails, + Metadata = entity.Metadata, }; public static ProductPrice ToModel(this Features.Shop.Products.Entities.ProductPrice entity) => diff --git a/LiteCharms.Features/Shop/CartPackages/PackageService.cs b/LiteCharms.Features/Shop/CartPackages/PackageService.cs index 3479998..f2aeda5 100644 --- a/LiteCharms.Features/Shop/CartPackages/PackageService.cs +++ b/LiteCharms.Features/Shop/CartPackages/PackageService.cs @@ -2,7 +2,6 @@ using LiteCharms.Features.Models; using LiteCharms.Features.Shop.CartPackages.Models; using LiteCharms.Features.Shop.Postgres; -using static LiteCharms.Features.Extensions.Timezones; namespace LiteCharms.Features.Shop.CartPackages; diff --git a/LiteCharms.Features/Shop/Postgres/Migrations/20260520191059_AddedProductMetadata.Designer.cs b/LiteCharms.Features/Shop/Postgres/Migrations/20260520191059_AddedProductMetadata.Designer.cs new file mode 100644 index 0000000..7ac8666 --- /dev/null +++ b/LiteCharms.Features/Shop/Postgres/Migrations/20260520191059_AddedProductMetadata.Designer.cs @@ -0,0 +1,788 @@ +// +using System; +using LiteCharms.Features.Shop.Postgres; +using LiteCharms.Features.Shop.Products.Models; +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.Features.Shop.Postgres.Migrations +{ + [DbContext(typeof(ShopDbContext))] + [Migration("20260520191059_AddedProductMetadata")] + partial class AddedProductMetadata + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.Package", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("ImageUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Package", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.PackageItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("PackageId") + .HasColumnType("uuid"); + + b.Property("ProductPriceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("PackageId"); + + b.HasIndex("ProductPriceId"); + + b.ToTable("PackageItem", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Customers.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") + .HasDefaultValueSql("now()"); + + 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") + .HasColumnType("timestamp with time zone"); + + b.Property("Website") + .HasColumnType("text"); + + b.Property("Whatsapp") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Customers", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Leads.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") + .HasDefaultValueSql("now()"); + + 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") + .HasColumnType("timestamp with time zone"); + + b.Property("WebClickId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Leads", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Notifications.Entities.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CorrelationId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CorrelationIdType") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Direction") + .HasColumnType("integer"); + + b.PrimitiveCollection("Errors") + .HasColumnType("jsonb"); + + b.Property("HasError") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsHtml") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsInternal") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("Platform") + .HasColumnType("integer"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("Processed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("RecipientAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("RecipientName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SenderAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("SenderName") + .HasColumnType("text"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("InvoiceUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.PrimitiveCollection("Notes") + .HasColumnType("jsonb"); + + b.PrimitiveCollection("Requirements") + .HasColumnType("jsonb"); + + b.Property("Status") + .HasColumnType("integer"); + + b.PrimitiveCollection("Terms") + .HasColumnType("jsonb"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Orders", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Orders.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") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderRefunds", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("ImageUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Metadata") + .HasColumnType("jsonb"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.PrimitiveCollection("Thumbnails") + .HasColumnType("jsonb"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Products", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.ProductPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + 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") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductPrices", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Quotes.Entities.Quote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("InvoiceUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("ShoppingCartId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.HasIndex("ShoppingCartId") + .IsUnique(); + + b.ToTable("Quotes", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.ToTable("ShoppingCarts", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("ProductPriceId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1); + + b.Property("ShoppingCartId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProductPriceId"); + + b.HasIndex("ShoppingCartId"); + + b.ToTable("ShoppingCartItems", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartPackage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("PackageId") + .HasColumnType("uuid"); + + b.Property("ShoppingCartId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("PackageId"); + + b.HasIndex("ShoppingCartId"); + + b.ToTable("ShoppingCartPackages", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.PackageItem", b => + { + b.HasOne("LiteCharms.Features.Shop.CartPackages.Entities.Package", "Package") + .WithMany("PackageItems") + .HasForeignKey("PackageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.Shop.Products.Entities.ProductPrice", "ProductPrice") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Package"); + + b.Navigation("ProductPrice"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Leads.Entities.Lead", b => + { + b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer") + .WithMany("Leads") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.Order", b => + { + b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.OrderRefund", b => + { + b.HasOne("LiteCharms.Features.Shop.Orders.Entities.Order", "Order") + .WithMany("Refunds") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.ProductPrice", b => + { + b.HasOne("LiteCharms.Features.Shop.Products.Entities.Product", "Product") + .WithMany("ProductPrices") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Quotes.Entities.Quote", b => + { + b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer") + .WithMany("Quotes") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.Shop.Orders.Entities.Order", "Order") + .WithOne("Quote") + .HasForeignKey("LiteCharms.Features.Shop.Quotes.Entities.Quote", "OrderId"); + + b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "ShoppingCart") + .WithOne("Quote") + .HasForeignKey("LiteCharms.Features.Shop.Quotes.Entities.Quote", "ShoppingCartId"); + + b.Navigation("Customer"); + + b.Navigation("Order"); + + b.Navigation("ShoppingCart"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", b => + { + b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer") + .WithMany("ShoppingCarts") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.Shop.Orders.Entities.Order", "Order") + .WithOne("ShoppingCart") + .HasForeignKey("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "OrderId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Customer"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartItem", b => + { + b.HasOne("LiteCharms.Features.Shop.Products.Entities.ProductPrice", "ProductPrice") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "ShoppingCart") + .WithMany("ShoppingCartItems") + .HasForeignKey("ShoppingCartId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ProductPrice"); + + b.Navigation("ShoppingCart"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartPackage", b => + { + b.HasOne("LiteCharms.Features.Shop.CartPackages.Entities.Package", "Package") + .WithMany() + .HasForeignKey("PackageId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "ShoppingCart") + .WithMany("ShoppingCartPackages") + .HasForeignKey("ShoppingCartId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Package"); + + b.Navigation("ShoppingCart"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.Package", b => + { + b.Navigation("PackageItems"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Customers.Entities.Customer", b => + { + b.Navigation("Leads"); + + b.Navigation("Orders"); + + b.Navigation("Quotes"); + + b.Navigation("ShoppingCarts"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.Order", b => + { + b.Navigation("Quote"); + + b.Navigation("Refunds"); + + b.Navigation("ShoppingCart"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.Product", b => + { + b.Navigation("ProductPrices"); + }); + + modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", b => + { + b.Navigation("Quote"); + + b.Navigation("ShoppingCartItems"); + + b.Navigation("ShoppingCartPackages"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LiteCharms.Features/Shop/Postgres/Migrations/20260520191059_AddedProductMetadata.cs b/LiteCharms.Features/Shop/Postgres/Migrations/20260520191059_AddedProductMetadata.cs new file mode 100644 index 0000000..142f394 --- /dev/null +++ b/LiteCharms.Features/Shop/Postgres/Migrations/20260520191059_AddedProductMetadata.cs @@ -0,0 +1,71 @@ +using System; +using LiteCharms.Features.Shop.Products.Models; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LiteCharms.Features.Shop.Postgres.Migrations +{ + /// + public partial class AddedProductMetadata : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Active", + table: "Products", + type: "boolean", + nullable: false, + defaultValue: false, + oldClrType: typeof(bool), + oldType: "boolean", + oldDefaultValue: true); + + migrationBuilder.AddColumn( + name: "CreatedAt", + table: "Products", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()"); + + migrationBuilder.AddColumn( + name: "Metadata", + table: "Products", + type: "jsonb", + nullable: true); + + migrationBuilder.AddColumn( + name: "UpdatedAt", + table: "Products", + type: "timestamp with time zone", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "CreatedAt", + table: "Products"); + + migrationBuilder.DropColumn( + name: "Metadata", + table: "Products"); + + migrationBuilder.DropColumn( + name: "UpdatedAt", + table: "Products"); + + migrationBuilder.AlterColumn( + name: "Active", + table: "Products", + type: "boolean", + nullable: false, + defaultValue: true, + oldClrType: typeof(bool), + oldType: "boolean", + oldDefaultValue: false); + } + } +} diff --git a/LiteCharms.Features/Shop/Postgres/Migrations/ShopDbContextModelSnapshot.cs b/LiteCharms.Features/Shop/Postgres/Migrations/ShopDbContextModelSnapshot.cs index 955c903..55c8c90 100644 --- a/LiteCharms.Features/Shop/Postgres/Migrations/ShopDbContextModelSnapshot.cs +++ b/LiteCharms.Features/Shop/Postgres/Migrations/ShopDbContextModelSnapshot.cs @@ -1,6 +1,7 @@ // using System; using LiteCharms.Features.Shop.Postgres; +using LiteCharms.Features.Shop.Products.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; @@ -386,7 +387,12 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations b.Property("Active") .ValueGeneratedOnAdd() .HasColumnType("boolean") - .HasDefaultValue(true); + .HasDefaultValue(false); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); b.Property("Description") .IsRequired() @@ -397,6 +403,9 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations .HasMaxLength(2048) .HasColumnType("character varying(2048)"); + b.Property("Metadata") + .HasColumnType("jsonb"); + b.Property("Name") .IsRequired() .HasColumnType("text"); @@ -409,6 +418,9 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations b.PrimitiveCollection("Thumbnails") .HasColumnType("jsonb"); + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + b.HasKey("Id"); b.ToTable("Products", (string)null); diff --git a/LiteCharms.Features/Shop/Products/Entities/ProductConfiguration.cs b/LiteCharms.Features/Shop/Products/Entities/ProductConfiguration.cs index 4307e62..6449acb 100644 --- a/LiteCharms.Features/Shop/Products/Entities/ProductConfiguration.cs +++ b/LiteCharms.Features/Shop/Products/Entities/ProductConfiguration.cs @@ -8,10 +8,13 @@ public class ProductConfiguration : IEntityTypeConfiguration builder.HasKey(f => f.Id); builder.Property(f => f.Name).IsRequired(); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.UpdatedAt).IsRequired(false); builder.Property(f => f.Summary).IsRequired().HasMaxLength(512); builder.Property(f => f.Description).IsRequired().HasMaxLength(2048); builder.Property(f => f.ImageUrl).IsRequired(false).HasMaxLength(2048); builder.Property(f => f.Thumbnails).HasColumnType("jsonb").IsRequired(false); - builder.Property(f => f.Active).HasDefaultValue(true); + builder.Property(f => f.Active).HasDefaultValue(false); + builder.Property(f => f.Metadata).HasColumnType("jsonb").IsRequired(false); } } diff --git a/LiteCharms.Features/Shop/Products/Entities/ProductPriceConfiguration.cs b/LiteCharms.Features/Shop/Products/Entities/ProductPriceConfiguration.cs index beab2ca..265dc6a 100644 --- a/LiteCharms.Features/Shop/Products/Entities/ProductPriceConfiguration.cs +++ b/LiteCharms.Features/Shop/Products/Entities/ProductPriceConfiguration.cs @@ -7,7 +7,7 @@ public class ProductPriceConfiguration : IEntityTypeConfiguration builder.ToTable("ProductPrices"); builder.HasKey(f => f.Id); - builder.Property(f => f.CreatedAt).ValueGeneratedOnAdd().HasDefaultValueSql("now()"); + builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); builder.Property(f => f.UpdatedAt).IsRequired(false); builder.Property(f => f.ProductId).IsRequired(); builder.Property(f => f.Price).IsRequired().HasPrecision(18, 2); diff --git a/LiteCharms.Features/Shop/Products/Models/CreateProductModel.cs b/LiteCharms.Features/Shop/Products/Models/CreateProductModel.cs new file mode 100644 index 0000000..355347e --- /dev/null +++ b/LiteCharms.Features/Shop/Products/Models/CreateProductModel.cs @@ -0,0 +1,42 @@ +using System.ComponentModel.DataAnnotations; + +namespace LiteCharms.Features.Shop.Products.Models; + +public class CreateProductModel +{ + [MaxLength(128)] + [Required(ErrorMessage = "Product name is required.")] + public string? Name { get; set; } + + [MaxLength(512)] + [Required(ErrorMessage = "Summary is required.")] + public string? Summary { get; set; } + + [MaxLength(2048)] + [Required(ErrorMessage = "Description is required.")] + public string? Description { get; set; } + + [Range(0.01, double.MaxValue, ErrorMessage = "Price must be greater than zero.")] + public decimal Price { get; set; } + + [MaxLength(128)] + [Required(ErrorMessage = "Author metadata is required.")] + public string? Author { get; set; } + + [Required(ErrorMessage = "Publication Date is required.")] + public DateTime PublishDate { get; set; } = DateTime.Today; + + [MaxLength(255)] + [Required(ErrorMessage = "Copyright Information field is required.")] + public string? CopyrightInfo { get; set; } + + [MaxLength(128)] + [Required(ErrorMessage = "ISBN code is required.")] + [RegularExpression(@"^(?=(?:\D*\d){10}(?:(?:\D*\d){3})?$)[\d-]+$", ErrorMessage = "Please enter a valid ISBN-10 or ISBN-13 string.")] + public string? Isbn { get; set; } + + [Required(ErrorMessage = "Primary image is required.")] + public string? ImageUrl { get; set; } + + public List Thumbnails { get; set; } = []; +} diff --git a/LiteCharms.Features/Shop/Products/Models/Product.cs b/LiteCharms.Features/Shop/Products/Models/Product.cs index 8fdc778..e4e10d2 100644 --- a/LiteCharms.Features/Shop/Products/Models/Product.cs +++ b/LiteCharms.Features/Shop/Products/Models/Product.cs @@ -4,6 +4,10 @@ public class Product { public Guid Id { get; set; } + public DateTime CreatedAt { get; set; } + + public DateTime? UpdatedAt { get; set; } + public string? Name { get; set; } public string? Summary { get; set; } @@ -15,4 +19,6 @@ public class Product public string[]? Thumbnails { get; set; } public bool Active { get; set; } + + public ProductMetadata? Metadata { get; set; } } diff --git a/LiteCharms.Features/Shop/Products/Models/ProductMetadata.cs b/LiteCharms.Features/Shop/Products/Models/ProductMetadata.cs new file mode 100644 index 0000000..da56f1a --- /dev/null +++ b/LiteCharms.Features/Shop/Products/Models/ProductMetadata.cs @@ -0,0 +1,12 @@ +namespace LiteCharms.Features.Shop.Products.Models; + +public class ProductMetadata +{ + public string? Manufacturer { get; set; } + + public string? ManufactureDate { get; set; } + + public string? CopyrightInfo { get; set; } + + public string? SerialNumber { get; set; } +} diff --git a/LiteCharms.Features/Shop/Products/Models/Records.cs b/LiteCharms.Features/Shop/Products/Models/Records.cs index b5579b8..8027e18 100644 --- a/LiteCharms.Features/Shop/Products/Models/Records.cs +++ b/LiteCharms.Features/Shop/Products/Models/Records.cs @@ -11,4 +11,6 @@ public record CreateProduct public required string ImageUrl { get; set; } public string[]? Thumbnails { get; set; } + + public ProductMetadata? Metadata { get; set; } } diff --git a/LiteCharms.Features/Shop/Products/ProductService.cs b/LiteCharms.Features/Shop/Products/ProductService.cs index 9016da5..685fb5c 100644 --- a/LiteCharms.Features/Shop/Products/ProductService.cs +++ b/LiteCharms.Features/Shop/Products/ProductService.cs @@ -67,10 +67,11 @@ public class ProductService(IDbContextFactory contextFactory) var newProduct = context.Products.Add(new Entities.Product { Name = request.Name, - Summary = request.Summary, + Summary = request.Summary, Description = request.Description, ImageUrl = request.ImageUrl, - Thumbnails = request.Thumbnails + Thumbnails = request.Thumbnails, + Metadata = request.Metadata }); return await context.SaveChangesAsync(cancellationToken) > 0 @@ -89,12 +90,12 @@ public class ProductService(IDbContextFactory contextFactory) try { using var context = await contextFactory.CreateDbContextAsync(cancellationToken); - + var newProductPrice = context.ProductPrices.Add(new Entities.ProductPrice { Price = price, Discount = discount, - ProductId = productId + ProductId = productId }); return await context.SaveChangesAsync(cancellationToken) > 0 @@ -206,7 +207,7 @@ public class ProductService(IDbContextFactory contextFactory) var result = await CreateProductPriceAsync(existingPrice.ProductId, price, discount, cancellationToken); - if(result.IsFailed) + if (result.IsFailed) { var deactivatedPrice = await context.ProductPrices.FirstOrDefaultAsync(p => p.Id == productPriceId, cancellationToken); @@ -225,4 +226,52 @@ public class ProductService(IDbContextFactory contextFactory) return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } + + public async ValueTask SetProductPriceStatusAsync(Guid productPriceId, bool active, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var productPrice = await context.ProductPrices.FirstOrDefaultAsync(p => p.Id == productPriceId, cancellationToken); + + if (productPrice is null) + return Result.Fail($"Could not find product price with ID {productPriceId}"); + + productPrice.Active = active; + productPrice.UpdatedAt = DateTime.UtcNow; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail($"Failed to change product price status by ID {productPriceId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask UpdateProductMetadataAsync(Guid productId, ProductMetadata metadata, CancellationToken cancellationToken = default) + { + try + { + using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var product = await context.Products.FirstOrDefaultAsync(p => p.Id == productId, cancellationToken); + + if (product is null) + return Result.Fail($"Could not find product with ID {productId}"); + + product.Metadata = metadata; + product.UpdatedAt = DateTime.UtcNow; + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail($"Failed to update product metadata by ID {productId}"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } }