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));
+ }
+ }
}