From e40c958066811111708d7458f41c54201a4e03d3 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sat, 30 May 2026 14:22:00 +0200 Subject: [PATCH 1/2] Implemented category feature --- .../CategoryServiceFeatureTests.cs | 70 ++ .../Categories/CategoryService.cs | 132 +++ .../Categories/Entities/Category.cs | 4 + .../Entities/CategoryConfiguration.cs | 14 + .../Categories/Models/Category.cs | 12 + .../Extensions/Mappers.cs | 15 +- .../LiteCharms.Features.MidrandBooks.csproj | 10 +- .../Postgres/MidrandBooksDbContext.cs | 3 + ...20260530104851_AddedCategories.Designer.cs | 966 ++++++++++++++++++ .../20260530104851_AddedCategories.cs | 37 + .../MidrandBooksDbContextModelSnapshot.cs | 28 + 11 files changed, 1284 insertions(+), 7 deletions(-) create mode 100644 LiteCharms.Features.MidrandBooks.Tests/CategoryServiceFeatureTests.cs create mode 100644 LiteCharms.Features.MidrandBooks/Categories/CategoryService.cs create mode 100644 LiteCharms.Features.MidrandBooks/Categories/Entities/Category.cs create mode 100644 LiteCharms.Features.MidrandBooks/Categories/Entities/CategoryConfiguration.cs create mode 100644 LiteCharms.Features.MidrandBooks/Categories/Models/Category.cs create mode 100644 LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260530104851_AddedCategories.Designer.cs create mode 100644 LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260530104851_AddedCategories.cs diff --git a/LiteCharms.Features.MidrandBooks.Tests/CategoryServiceFeatureTests.cs b/LiteCharms.Features.MidrandBooks.Tests/CategoryServiceFeatureTests.cs new file mode 100644 index 0000000..7c8fbea --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Tests/CategoryServiceFeatureTests.cs @@ -0,0 +1,70 @@ +using LiteCharms.Features.MidrandBooks.Categories; +using LiteCharms.Features.MidrandBooks.Tests.Common; + +namespace LiteCharms.Features.MidrandBooks.Tests; + +public class CategoryServiceFeatureTests(Fixture fixture) : IClassFixture +{ + private readonly CategoryService categoryService = fixture.Services.GetRequiredService(); + + [IntegrationFact] + public async Task UpdateCategoryStatusAsync_ShouldReturn_ResultWithSuccess() + { + var result = await categoryService.UpdateCategoryStatusAsync(3, false, false, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task GetCategoryAsync_ShouldReturn_ResultWithCategory() + { + var result = await categoryService.GetCategoryAsync(3, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotNull(result.Value); + } + + [IntegrationFact] + public async Task GetCategoriesAsync_ShouldReturn_All_ResultWithCategoryList() + { + var result = await categoryService.GetCategoriesAsync(isMain: null,fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotEmpty(result.Value); + } + + [IntegrationFact] + public async Task GetCategoriesAsync_ShouldReturn_MainCategory_ResultWithCategoryList() + { + var result = await categoryService.GetCategoriesAsync(true, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotEmpty(result.Value); + } + + [IntegrationFact] + public async Task GetCategoriesAsync_ShouldReturn_SubMainCategory_ResultWithCategoryList() + { + var result = await categoryService.GetCategoriesAsync(false, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotEmpty(result.Value); + } + + [IntegrationFact] + public async Task CreateCategoriesAsync_ShouldReturn_ResultWithSuccess() + { + var result = await categoryService.CreateCategoriesAsync(fixture.CancellationToken, "Test", "Test 1", "Test 2"); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task CreateCategoryAsync_ShouldReturn_ResultWithCategoryId() + { + var result = await categoryService.CreateCategoryAsync("Test", false, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.True(result.Value > 0); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Categories/CategoryService.cs b/LiteCharms.Features.MidrandBooks/Categories/CategoryService.cs new file mode 100644 index 0000000..dd6c557 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Categories/CategoryService.cs @@ -0,0 +1,132 @@ +using LiteCharms.Features.MidrandBooks.Abstractions; +using LiteCharms.Features.MidrandBooks.Categories.Models; +using LiteCharms.Features.MidrandBooks.Extensions; +using LiteCharms.Features.MidrandBooks.Postgres; + +namespace LiteCharms.Features.MidrandBooks.Categories; + +public sealed class CategoryService(IDbContextFactory contextFactory) : IService +{ + public async ValueTask UpdateCategoryStatusAsync(long categoryId, bool enabled, bool isMain, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var rowsUpdated = await context.Categories + .Where(c => c.Id == categoryId && c.Enabled) + .ExecuteUpdateAsync(setters => setters + .SetProperty(c => c.Enabled, enabled) + .SetProperty(c => c.IsMain, isMain), cancellationToken); + + return rowsUpdated > 0 + ? Result.Ok() + : Result.Fail(new Error($"Failed to update category")); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetCategoryAsync(long categoryId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var category = await context.Categories.AsNoTracking().FirstOrDefaultAsync(c => c.Id == categoryId, cancellationToken); + + return category is not null + ? Result.Ok(category.ToModel()) + : Result.Fail("Failed to create new category"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetCategoriesAsync(bool? isMain = null, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var query = context.Categories.AsNoTracking() + .OrderByDescending(o => o.IsMain) + .ThenByDescending(o => o.Id) + .ThenBy(o => o.IsMain) + .AsQueryable(); + + query = isMain is null + ? query.Where(c => c.Enabled).AsQueryable() + : query.Where(c => c.Enabled && c.IsMain == isMain.Value); + + var categories = await query.ToListAsync(cancellationToken); + + return categories?.Count > 0 + ? Result.Ok(categories.Select(c => c.ToModel()).ToArray()) + : Result.Fail("No categories found"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask CreateCategoriesAsync(CancellationToken cancellationToken = default, params string[] categories) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + foreach (var category in categories) + { + if (await context.Categories.AnyAsync(c => EF.Functions.ILike(c.Name!, category!), cancellationToken)) + continue; + + context.Categories.Add(new Entities.Category + { + Name = category.Humanize(LetterCasing.Title), + IsMain = false, + Enabled = true, + }); + } + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail("Failed to add any category in the list"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> CreateCategoryAsync(string category, bool isMain, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (await context.Categories.AnyAsync(c => EF.Functions.ILike(c.Name!, category!), cancellationToken)) + return Result.Fail($"Category '{category}' already exists"); + + var newCategory = context.Categories.Add(new Entities.Category + { + Name = StringHumanizeExtensions.Humanize(category, LetterCasing.Title), + IsMain = isMain, + Enabled = true, + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok(newCategory.Entity.Id) + : Result.Fail("Failed to create new category"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Categories/Entities/Category.cs b/LiteCharms.Features.MidrandBooks/Categories/Entities/Category.cs new file mode 100644 index 0000000..5aac931 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Categories/Entities/Category.cs @@ -0,0 +1,4 @@ +namespace LiteCharms.Features.MidrandBooks.Categories.Entities; + +[EntityTypeConfiguration] +public sealed class Category : Models.Category; diff --git a/LiteCharms.Features.MidrandBooks/Categories/Entities/CategoryConfiguration.cs b/LiteCharms.Features.MidrandBooks/Categories/Entities/CategoryConfiguration.cs new file mode 100644 index 0000000..bc5a7bc --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Categories/Entities/CategoryConfiguration.cs @@ -0,0 +1,14 @@ +namespace LiteCharms.Features.MidrandBooks.Categories.Entities; + +public sealed class CategoryConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Categories"); + + builder.HasKey(c => c.Id); + builder.Property(c => c.Name).IsRequired().HasMaxLength(15); + builder.Property(c => c.IsMain).HasDefaultValue(false); + builder.Property(c => c.Enabled).HasDefaultValue(true); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Categories/Models/Category.cs b/LiteCharms.Features.MidrandBooks/Categories/Models/Category.cs new file mode 100644 index 0000000..b793204 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Categories/Models/Category.cs @@ -0,0 +1,12 @@ +namespace LiteCharms.Features.MidrandBooks.Categories.Models; + +public class Category +{ + public long Id { get; set; } + + public string? Name { get; set; } + + public bool IsMain { get; set; } + + public bool Enabled { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs b/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs index 753bcc7..3c3790d 100644 --- a/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs +++ b/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs @@ -1,5 +1,6 @@ using LiteCharms.Features.MidrandBooks.AuthorBooks.Models; using LiteCharms.Features.MidrandBooks.Authors.Models; +using LiteCharms.Features.MidrandBooks.Categories.Models; using LiteCharms.Features.MidrandBooks.Customers.Models; using LiteCharms.Features.MidrandBooks.Orders.Models; using LiteCharms.Features.MidrandBooks.Pages.Models; @@ -9,6 +10,14 @@ namespace LiteCharms.Features.MidrandBooks.Extensions; public static class Mappers { + public static Category ToModel(this Categories.Entities.Category entity) => new() + { + Id = entity.Id, + Name = entity.Name, + IsMain = entity.IsMain, + Enabled = entity.Enabled, + }; + public static ShippingProvider ToModel(this Orders.Entities.ShippingProvider entity) => new() { Id = entity.Id, @@ -65,7 +74,7 @@ public static class Mappers SocialMedia = entiry.SocialMedia, UpdatedAt = entiry.UpdatedAt, VatNumber = entiry.VatNumber, - Website = entiry.Website + Website = entiry.Website }; public static Address ToModel(this Customers.Entities.Address entity) => new() @@ -83,7 +92,7 @@ public static class Mappers Street = entity.Street, City = entity.City, State = entity.State, - Country = entity.Country + Country = entity.Country }; public static Contact ToModel(this Customers.Entities.Contact entity) => new() @@ -112,7 +121,7 @@ public static class Mappers Enabled = entity.Enabled, Notes = entity.Notes, References = entity.References, - Type = entity.Type + Type = entity.Type }; public static AuthorBook ToModel(this AuthorBooks.Entities.AuthorBook entity) => new() diff --git a/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj b/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj index 17db6dd..e2e58d3 100644 --- a/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj +++ b/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj @@ -31,7 +31,8 @@ - + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -104,7 +105,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + @@ -115,8 +116,8 @@ - - + + @@ -147,6 +148,7 @@ + diff --git a/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs b/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs index a70e1cf..30af0d7 100644 --- a/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs +++ b/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs @@ -1,5 +1,6 @@ using LiteCharms.Features.MidrandBooks.AuthorBooks.Entities; using LiteCharms.Features.MidrandBooks.Authors.Entities; +using LiteCharms.Features.MidrandBooks.Categories.Entities; using LiteCharms.Features.MidrandBooks.Customers.Entities; using LiteCharms.Features.MidrandBooks.Orders.Entities; using LiteCharms.Features.MidrandBooks.Pages.Entities; @@ -35,4 +36,6 @@ public sealed class MidrandBooksDbContext(DbContextOptions Shippings => Set(); public DbSet ShippingProviders => Set(); + + public DbSet Categories => Set(); } diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260530104851_AddedCategories.Designer.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260530104851_AddedCategories.Designer.cs new file mode 100644 index 0000000..e007a80 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260530104851_AddedCategories.Designer.cs @@ -0,0 +1,966 @@ +// +using System; +using LiteCharms.Features.MidrandBooks.Postgres; +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.MidrandBooks.Postgres.Migrations +{ + [DbContext(typeof(MidrandBooksDbContext))] + [Migration("20260530104851_AddedCategories")] + partial class AddedCategories + { + /// + 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.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("Ranking") + .HasColumnType("integer"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("ProductId"); + + b.ToTable("Books"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Biography") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("ImageUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("PublisherType") + .HasColumnType("integer"); + + b.Property("ThumbnailImageUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VatNumber") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Website") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.HasKey("Id"); + + b.ToTable("Authors", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Categories.Entities.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsMain") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasMaxLength(15) + .HasColumnType("character varying(15)"); + + b.HasKey("Id"); + + b.ToTable("Categories", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BuildingType") + .HasColumnType("integer"); + + b.Property("City") + .IsRequired() + .HasColumnType("text"); + + b.Property("Country") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .IsRequired() + .HasColumnType("text"); + + b.Property("Street") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Addresses", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Contacts", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VatNumber") + .HasColumnType("text"); + + b.Property("Website") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Customers", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("InvoiceUrl") + .HasColumnType("text"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.ToTable("Orders", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorBookId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("ProductPriceId") + .HasColumnType("bigint"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AuthorBookId"); + + b.HasIndex("OrderId"); + + b.HasIndex("ProductPriceId"); + + b.ToTable("OrderItems", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AddressId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("ShippingProviderId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TrackingNumber") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("AddressId"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.HasIndex("ShippingProviderId"); + + b.ToTable("Shippings", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("TrackingUrl") + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("ShippingProviders"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorBookId") + .HasColumnType("bigint"); + + b.Property("Content") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("ContentType") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.PrimitiveCollection("Notes") + .HasColumnType("text[]"); + + b.Property("Number") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("AuthorBookId"); + + b.ToTable("BookPages", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + 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("bigint"); + + b.Property("Reason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Refunds", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.PrimitiveCollection("Categories") + .HasColumnType("text[]"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ImageUrl") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.PrimitiveCollection("ThumbnailUrls") + .HasColumnType("text[]"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.ToTable("Products", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Discount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("Prices", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", "Author") + .WithMany("Books") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 => + { + b1.Property("AuthorId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("ImageUrl"); + + b1.Property("Name"); + + b1.Property("Type"); + + b1.Property("Url"); + + b1.HasKey("AuthorId", "__synthesizedOrdinal"); + + b1.ToTable("Authors"); + + b1 + .ToJson("SocialMedia") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("AuthorId"); + }); + + b.Navigation("SocialMedia"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany("Addresses") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany("Contacts") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 => + { + b1.Property("CustomerId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("ImageUrl"); + + b1.Property("Name"); + + b1.Property("Type"); + + b1.Property("Url"); + + b1.HasKey("CustomerId", "__synthesizedOrdinal"); + + b1.ToTable("Customers"); + + b1 + .ToJson("SocialMedia") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("CustomerId"); + }); + + b.Navigation("SocialMedia"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "AuthorBook") + .WithMany() + .HasForeignKey("AuthorBookId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany("OrderItems") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", "ProductPrice") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AuthorBook"); + + b.Navigation("Order"); + + b.Navigation("ProductPrice"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", "Address") + .WithMany() + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithOne("Shipping") + .HasForeignKey("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", "OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", "ShippingProvider") + .WithMany("Shippings") + .HasForeignKey("ShippingProviderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Address"); + + b.Navigation("Order"); + + b.Navigation("ShippingProvider"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "Book") + .WithMany("Pages") + .HasForeignKey("AuthorBookId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.OwnsMany("LiteCharms.Features.Models.PageReference", "References", b1 => + { + b1.Property("BookPageId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("Description"); + + b1.Property("Tag"); + + b1.Property("Url"); + + b1.HasKey("BookPageId", "__synthesizedOrdinal"); + + b1.ToTable("BookPages"); + + b1 + .ToJson("References") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("BookPageId"); + }); + + b.Navigation("Book"); + + b.Navigation("References"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany("Refunds") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.OwnsOne("LiteCharms.Features.Models.ProductMetadata", "Metadata", b1 => + { + b1.Property("ProductId"); + + b1.Property("CopyrightInfo"); + + b1.Property("ManufactureDate"); + + b1.Property("Manufacturer"); + + b1.Property("SerialNumber"); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("Metadata") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.Navigation("Metadata"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany("Prices") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.Navigation("Pages"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.Navigation("Books"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.Navigation("Addresses"); + + b.Navigation("Contacts"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b => + { + b.Navigation("OrderItems"); + + b.Navigation("Refunds"); + + b.Navigation("Shipping"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b => + { + b.Navigation("Shippings"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.Navigation("Prices"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260530104851_AddedCategories.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260530104851_AddedCategories.cs new file mode 100644 index 0000000..96d30de --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260530104851_AddedCategories.cs @@ -0,0 +1,37 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations +{ + /// + public partial class AddedCategories : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Categories", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "character varying(15)", maxLength: 15, nullable: false), + IsMain = table.Column(type: "boolean", nullable: false, defaultValue: false), + Enabled = table.Column(type: "boolean", nullable: false, defaultValue: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Categories", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Categories"); + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs index 6045ed4..ec18d1c 100644 --- a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs @@ -130,6 +130,34 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b.ToTable("Authors", (string)null); }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Categories.Entities.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsMain") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasMaxLength(15) + .HasColumnType("character varying(15)"); + + b.HasKey("Id"); + + b.ToTable("Categories", (string)null); + }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => { b.Property("Id") From 18d1640808a4542e3204b9ca9b70c86189932b59 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sat, 30 May 2026 15:35:35 +0200 Subject: [PATCH 2/2] Added product categories --- .../CategoryServiceFeatureTests.cs | 2 +- .../ProductServiceFeatureTests.cs | 45 +- .../Categories/CategoryService.cs | 4 +- .../Extensions/Mappers.cs | 32 +- .../Postgres/MidrandBooksDbContext.cs | 2 + ...0125323_AddedProductCategories.Designer.cs | 1007 +++++++++++++++++ .../20260530125323_AddedProductCategories.cs | 68 ++ .../MidrandBooksDbContextModelSnapshot.cs | 47 +- .../Products/Entities/Product.cs | 2 + .../Products/Entities/ProductCategory.cs | 11 + .../Entities/ProductCategoryConfiguration.cs | 23 + .../Products/Entities/ProductConfiguration.cs | 1 - .../Products/Models/Product.cs | 2 - .../Products/Models/ProductCategory.cs | 10 + .../Products/ProductService.cs | 99 +- LiteCharms.Features/Models/ProductFilter.cs | 2 - 16 files changed, 1318 insertions(+), 39 deletions(-) create mode 100644 LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260530125323_AddedProductCategories.Designer.cs create mode 100644 LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260530125323_AddedProductCategories.cs create mode 100644 LiteCharms.Features.MidrandBooks/Products/Entities/ProductCategory.cs create mode 100644 LiteCharms.Features.MidrandBooks/Products/Entities/ProductCategoryConfiguration.cs create mode 100644 LiteCharms.Features.MidrandBooks/Products/Models/ProductCategory.cs diff --git a/LiteCharms.Features.MidrandBooks.Tests/CategoryServiceFeatureTests.cs b/LiteCharms.Features.MidrandBooks.Tests/CategoryServiceFeatureTests.cs index 7c8fbea..c1f103c 100644 --- a/LiteCharms.Features.MidrandBooks.Tests/CategoryServiceFeatureTests.cs +++ b/LiteCharms.Features.MidrandBooks.Tests/CategoryServiceFeatureTests.cs @@ -62,7 +62,7 @@ public class CategoryServiceFeatureTests(Fixture fixture) : IClassFixture 0); diff --git a/LiteCharms.Features.MidrandBooks.Tests/ProductServiceFeatureTests.cs b/LiteCharms.Features.MidrandBooks.Tests/ProductServiceFeatureTests.cs index 6dc7e88..d4e6e4c 100644 --- a/LiteCharms.Features.MidrandBooks.Tests/ProductServiceFeatureTests.cs +++ b/LiteCharms.Features.MidrandBooks.Tests/ProductServiceFeatureTests.cs @@ -10,7 +10,40 @@ public class ProductServiceFeatureTests(Fixture fixture, ITestOutputHelper outpu private readonly ProductService productService = fixture.Services.GetRequiredService(); [IntegrationFact] - public async Task GetProductPriceAsync_ShouldReturn_RetultOneProductPrice() + public async Task AddProductCategoryAsync_ShouldReturn_ResultWithId() + { + var result = await productService.AddProductCategoryAsync(1, 2, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task GetProductCategoriesAsync_ShouldReturn_ResultWithCategoryList() + { + var result = await productService.GetProductCategoriesAsync(1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotEmpty(result.Value); + } + + [IntegrationFact] + public async Task DeleteProductCategoryAsync_ShouldReturn_ResultWithSuccess() + { + var result = await productService.DeleteProductCategoryAsync(1, 1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task DeleteAllProductCategoriesAsync_ShouldReturn_ResultWithSuccess() + { + var result = await productService.DeleteAllProductCategoriesAsync(1, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task GetProductPriceAsync_ShouldReturn_ResultOneProductPrice() { var result = await productService.GetProductPriceAsync(2, fixture.CancellationToken); @@ -21,7 +54,7 @@ public class ProductServiceFeatureTests(Fixture fixture, ITestOutputHelper outpu } [IntegrationFact] - public async Task GetProductPricesAsync_ShouldReturn_RetultProductPriceList() + public async Task GetProductPricesAsync_ShouldReturn_ResultProductPriceList() { var result = await productService.GetProductPricesAsync(2, fixture.CancellationToken); @@ -32,7 +65,7 @@ public class ProductServiceFeatureTests(Fixture fixture, ITestOutputHelper outpu } [IntegrationFact] - public async Task SearchProductsAsync_ShouldReturn_RetultMatchingProducts() + public async Task SearchProductsAsync_ShouldReturn_ResultMatchingProducts() { var filter = new ProductFilter { @@ -52,7 +85,7 @@ public class ProductServiceFeatureTests(Fixture fixture, ITestOutputHelper outpu } [IntegrationFact] - public async Task GetProductAsync_ShouldReturn_RetultOneProduct() + public async Task GetProductAsync_ShouldReturn_ResultOneProduct() { var result = await productService.GetProductAsync(2, fixture.CancellationToken); @@ -63,7 +96,7 @@ public class ProductServiceFeatureTests(Fixture fixture, ITestOutputHelper outpu } [IntegrationFact] - public async Task GetProductsAsync_ShouldReturn_RetultProducts() + public async Task GetProductsAsync_ShouldReturn_ResultProducts() { var range = new DateRange { @@ -81,7 +114,7 @@ public class ProductServiceFeatureTests(Fixture fixture, ITestOutputHelper outpu } [IntegrationFact] - public async Task UpdateProductStatusAsync_ShouldReturn_ResultTrue() + public async Task UpdateProductStatusAsync_ShouldResurn_ResultTrue() { var result = await productService.UpdateProductStatusAsync(2, true, fixture.CancellationToken); diff --git a/LiteCharms.Features.MidrandBooks/Categories/CategoryService.cs b/LiteCharms.Features.MidrandBooks/Categories/CategoryService.cs index dd6c557..2a5b9f4 100644 --- a/LiteCharms.Features.MidrandBooks/Categories/CategoryService.cs +++ b/LiteCharms.Features.MidrandBooks/Categories/CategoryService.cs @@ -55,8 +55,8 @@ public sealed class CategoryService(IDbContextFactory con var query = context.Categories.AsNoTracking() .OrderByDescending(o => o.IsMain) - .ThenByDescending(o => o.Id) - .ThenBy(o => o.IsMain) + .ThenByDescending(o => o.Id) + .ThenBy(o => o.Name) .AsQueryable(); query = isMain is null diff --git a/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs b/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs index 3c3790d..1a81b74 100644 --- a/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs +++ b/LiteCharms.Features.MidrandBooks/Extensions/Mappers.cs @@ -147,25 +147,21 @@ public static class Mappers Enabled = entity.Enabled }; - public static Product ToModel(this Products.Entities.Product entity) + public static Product ToModel(this Products.Entities.Product entity) => new Product { - return new Product - { - Id = entity.Id, - CreatedAt = entity.CreatedAt, - UpdatedAt = entity.UpdatedAt, - Name = entity.Name, - Summary = entity.Summary, - Description = entity.Description, - Type = entity.Type, - ImageUrl = entity.ImageUrl, - ThumbnailUrls = entity.ThumbnailUrls, - Metadata = entity.Metadata, - Categories = entity.Categories, - Enabled = entity.Enabled, - Price = entity.Prices?.FirstOrDefault(p => p.Enabled)?.ToModel() ?? null, - }; - } + Id = entity.Id, + CreatedAt = entity.CreatedAt, + UpdatedAt = entity.UpdatedAt, + Name = entity.Name, + Summary = entity.Summary, + Description = entity.Description, + Type = entity.Type, + ImageUrl = entity.ImageUrl, + ThumbnailUrls = entity.ThumbnailUrls, + Metadata = entity.Metadata, + Enabled = entity.Enabled, + Price = entity.Prices?.FirstOrDefault(p => p.Enabled)?.ToModel() ?? null, + }; public static Author ToModel(this Authors.Entities.Author entity) => new() { diff --git a/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs b/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs index 30af0d7..782bb1f 100644 --- a/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs +++ b/LiteCharms.Features.MidrandBooks/Postgres/MidrandBooksDbContext.cs @@ -38,4 +38,6 @@ public sealed class MidrandBooksDbContext(DbContextOptions ShippingProviders => Set(); public DbSet Categories => Set(); + + public DbSet ProductCategories => Set(); } diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260530125323_AddedProductCategories.Designer.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260530125323_AddedProductCategories.Designer.cs new file mode 100644 index 0000000..af132c2 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260530125323_AddedProductCategories.Designer.cs @@ -0,0 +1,1007 @@ +// +using System; +using LiteCharms.Features.MidrandBooks.Postgres; +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.MidrandBooks.Postgres.Migrations +{ + [DbContext(typeof(MidrandBooksDbContext))] + [Migration("20260530125323_AddedProductCategories")] + partial class AddedProductCategories + { + /// + 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.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("Ranking") + .HasColumnType("integer"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("ProductId"); + + b.ToTable("Books"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Biography") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("ImageUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("PublisherType") + .HasColumnType("integer"); + + b.Property("ThumbnailImageUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VatNumber") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Website") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.HasKey("Id"); + + b.ToTable("Authors", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Categories.Entities.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsMain") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasMaxLength(15) + .HasColumnType("character varying(15)"); + + b.HasKey("Id"); + + b.ToTable("Categories", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BuildingType") + .HasColumnType("integer"); + + b.Property("City") + .IsRequired() + .HasColumnType("text"); + + b.Property("Country") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .IsRequired() + .HasColumnType("text"); + + b.Property("Street") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Addresses", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Contacts", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Company") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VatNumber") + .HasColumnType("text"); + + b.Property("Website") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Customers", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("InvoiceUrl") + .HasColumnType("text"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Total") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.ToTable("Orders", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorBookId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("ProductPriceId") + .HasColumnType("bigint"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AuthorBookId"); + + b.HasIndex("OrderId"); + + b.HasIndex("ProductPriceId"); + + b.ToTable("OrderItems", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AddressId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("OrderId") + .HasColumnType("bigint"); + + b.Property("ShippingProviderId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TrackingNumber") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("AddressId"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.HasIndex("ShippingProviderId"); + + b.ToTable("Shippings", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("TrackingUrl") + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("ShippingProviders"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorBookId") + .HasColumnType("bigint"); + + b.Property("Content") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("ContentType") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.PrimitiveCollection("Notes") + .HasColumnType("text[]"); + + b.Property("Number") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("AuthorBookId"); + + b.ToTable("BookPages", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + 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("bigint"); + + b.Property("Reason") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("Refunds", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ImageUrl") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.PrimitiveCollection("ThumbnailUrls") + .HasColumnType("text[]"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.ToTable("Products", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("bigint"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductCategories", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Discount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("Prices", (string)null); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", "Author") + .WithMany("Books") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 => + { + b1.Property("AuthorId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("ImageUrl"); + + b1.Property("Name"); + + b1.Property("Type"); + + b1.Property("Url"); + + b1.HasKey("AuthorId", "__synthesizedOrdinal"); + + b1.ToTable("Authors"); + + b1 + .ToJson("SocialMedia") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("AuthorId"); + }); + + b.Navigation("SocialMedia"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany("Addresses") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") + .WithMany("Contacts") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 => + { + b1.Property("CustomerId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("ImageUrl"); + + b1.Property("Name"); + + b1.Property("Type"); + + b1.Property("Url"); + + b1.HasKey("CustomerId", "__synthesizedOrdinal"); + + b1.ToTable("Customers"); + + b1 + .ToJson("SocialMedia") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("CustomerId"); + }); + + b.Navigation("SocialMedia"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "AuthorBook") + .WithMany() + .HasForeignKey("AuthorBookId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany("OrderItems") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", "ProductPrice") + .WithMany() + .HasForeignKey("ProductPriceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AuthorBook"); + + b.Navigation("Order"); + + b.Navigation("ProductPrice"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", "Address") + .WithMany() + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithOne("Shipping") + .HasForeignKey("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", "OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", "ShippingProvider") + .WithMany("Shippings") + .HasForeignKey("ShippingProviderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Address"); + + b.Navigation("Order"); + + b.Navigation("ShippingProvider"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "Book") + .WithMany("Pages") + .HasForeignKey("AuthorBookId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.OwnsMany("LiteCharms.Features.Models.PageReference", "References", b1 => + { + b1.Property("BookPageId"); + + b1.Property("__synthesizedOrdinal") + .ValueGeneratedOnAdd(); + + b1.Property("Description"); + + b1.Property("Tag"); + + b1.Property("Url"); + + b1.HasKey("BookPageId", "__synthesizedOrdinal"); + + b1.ToTable("BookPages"); + + b1 + .ToJson("References") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("BookPageId"); + }); + + b.Navigation("Book"); + + b.Navigation("References"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") + .WithMany("Refunds") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.OwnsOne("LiteCharms.Features.Models.ProductMetadata", "Metadata", b1 => + { + b1.Property("ProductId"); + + b1.Property("CopyrightInfo"); + + b1.Property("ManufactureDate"); + + b1.Property("Manufacturer"); + + b1.Property("SerialNumber"); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("Metadata") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.Navigation("Metadata"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductCategory", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Categories.Entities.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany("Categories") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany("Prices") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => + { + b.Navigation("Pages"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b => + { + b.Navigation("Books"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b => + { + b.Navigation("Addresses"); + + b.Navigation("Contacts"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b => + { + b.Navigation("OrderItems"); + + b.Navigation("Refunds"); + + b.Navigation("Shipping"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b => + { + b.Navigation("Shippings"); + }); + + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => + { + b.Navigation("Categories"); + + b.Navigation("Prices"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260530125323_AddedProductCategories.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260530125323_AddedProductCategories.cs new file mode 100644 index 0000000..4f35ac7 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260530125323_AddedProductCategories.cs @@ -0,0 +1,68 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations +{ + /// + public partial class AddedProductCategories : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Categories", + table: "Products"); + + migrationBuilder.CreateTable( + name: "ProductCategories", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ProductId = table.Column(type: "bigint", nullable: false), + CategoryId = table.Column(type: "bigint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ProductCategories", x => x.Id); + table.ForeignKey( + name: "FK_ProductCategories_Categories_CategoryId", + column: x => x.CategoryId, + principalTable: "Categories", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ProductCategories_Products_ProductId", + column: x => x.ProductId, + principalTable: "Products", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_ProductCategories_CategoryId", + table: "ProductCategories", + column: "CategoryId"); + + migrationBuilder.CreateIndex( + name: "IX_ProductCategories_ProductId", + table: "ProductCategories", + column: "ProductId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ProductCategories"); + + migrationBuilder.AddColumn( + name: "Categories", + table: "Products", + type: "text[]", + nullable: true); + } + } +} diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs index ec18d1c..8af6b80 100644 --- a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs @@ -586,9 +586,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - b.PrimitiveCollection("Categories") - .HasColumnType("text[]"); - b.Property("CreatedAt") .ValueGeneratedOnAdd() .HasColumnType("timestamp with time zone") @@ -633,6 +630,29 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b.ToTable("Products", (string)null); }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("bigint"); + + b.Property("ProductId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("ProductId"); + + b.ToTable("ProductCategories", (string)null); + }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => { b.Property("Id") @@ -911,6 +931,25 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b.Navigation("Metadata"); }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductCategory", b => + { + b.HasOne("LiteCharms.Features.MidrandBooks.Categories.Entities.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") + .WithMany("Categories") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Product"); + }); + modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => { b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") @@ -955,6 +994,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => { + b.Navigation("Categories"); + b.Navigation("Prices"); }); #pragma warning restore 612, 618 diff --git a/LiteCharms.Features.MidrandBooks/Products/Entities/Product.cs b/LiteCharms.Features.MidrandBooks/Products/Entities/Product.cs index e7e82c6..265f787 100644 --- a/LiteCharms.Features.MidrandBooks/Products/Entities/Product.cs +++ b/LiteCharms.Features.MidrandBooks/Products/Entities/Product.cs @@ -3,5 +3,7 @@ [EntityTypeConfiguration] public class Product : Models.Product { + public virtual ICollection Categories { get; set; } = []; + public virtual ICollection Prices { get; set; } = []; } diff --git a/LiteCharms.Features.MidrandBooks/Products/Entities/ProductCategory.cs b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductCategory.cs new file mode 100644 index 0000000..6b84876 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductCategory.cs @@ -0,0 +1,11 @@ +using LiteCharms.Features.MidrandBooks.Categories.Entities; + +namespace LiteCharms.Features.MidrandBooks.Products.Entities; + +[EntityTypeConfiguration] +public class ProductCategory : Models.ProductCategory +{ + public virtual Product? Product { get; set; } + + public virtual Category? Category { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Products/Entities/ProductCategoryConfiguration.cs b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductCategoryConfiguration.cs new file mode 100644 index 0000000..768926d --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductCategoryConfiguration.cs @@ -0,0 +1,23 @@ +namespace LiteCharms.Features.MidrandBooks.Products.Entities; + +public sealed class ProductCategoryConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("ProductCategories"); + + builder.HasKey(p => p.Id); + builder.Property(p => p.ProductId).IsRequired(); + builder.Property(p => p.CategoryId).IsRequired(); + + builder.HasOne(p => p.Product) + .WithMany(p => p.Categories) + .HasForeignKey(p => p.ProductId) + .OnDelete(DeleteBehavior.Cascade); + + builder.HasOne(c => c.Category) + .WithMany() + .HasForeignKey(c => c.CategoryId) + .OnDelete(DeleteBehavior.Cascade); + } +} diff --git a/LiteCharms.Features.MidrandBooks/Products/Entities/ProductConfiguration.cs b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductConfiguration.cs index 795097d..daf7f19 100644 --- a/LiteCharms.Features.MidrandBooks/Products/Entities/ProductConfiguration.cs +++ b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductConfiguration.cs @@ -17,7 +17,6 @@ public sealed class ProductConfiguration : IEntityTypeConfiguration builder.Property(f => f.Description).HasMaxLength(1024); builder.Property(f => f.ImageUrl).HasMaxLength(1024); builder.Property(f => f.Enabled).HasDefaultValue(false); - builder.Property(f => f.Categories).IsRequired(false); builder.Property(f => f.ThumbnailUrls).IsRequired(false); builder.OwnsOne(f => f.Metadata, b => { b.ToJson(); }); diff --git a/LiteCharms.Features.MidrandBooks/Products/Models/Product.cs b/LiteCharms.Features.MidrandBooks/Products/Models/Product.cs index 1207c34..bf425e4 100644 --- a/LiteCharms.Features.MidrandBooks/Products/Models/Product.cs +++ b/LiteCharms.Features.MidrandBooks/Products/Models/Product.cs @@ -22,8 +22,6 @@ public class Product public string[]? ThumbnailUrls { get; set; } - public string[]? Categories { get; set; } - public ProductMetadata? Metadata { get; set; } public ProductPrice? Price { get; set; } diff --git a/LiteCharms.Features.MidrandBooks/Products/Models/ProductCategory.cs b/LiteCharms.Features.MidrandBooks/Products/Models/ProductCategory.cs new file mode 100644 index 0000000..38e70de --- /dev/null +++ b/LiteCharms.Features.MidrandBooks/Products/Models/ProductCategory.cs @@ -0,0 +1,10 @@ +namespace LiteCharms.Features.MidrandBooks.Products.Models; + +public class ProductCategory +{ + public long Id { get; set; } + + public long ProductId { get; set; } + + public long CategoryId { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Products/ProductService.cs b/LiteCharms.Features.MidrandBooks/Products/ProductService.cs index 4d1b0ab..a7b47ce 100644 --- a/LiteCharms.Features.MidrandBooks/Products/ProductService.cs +++ b/LiteCharms.Features.MidrandBooks/Products/ProductService.cs @@ -1,4 +1,5 @@ using LiteCharms.Features.MidrandBooks.Abstractions; +using LiteCharms.Features.MidrandBooks.Categories.Models; using LiteCharms.Features.MidrandBooks.Extensions; using LiteCharms.Features.MidrandBooks.Postgres; using LiteCharms.Features.MidrandBooks.Products.Models; @@ -8,6 +9,100 @@ namespace LiteCharms.Features.MidrandBooks.Products; public sealed class ProductService(IDbContextFactory contextFactory) : IService { + public async ValueTask AddProductCategoryAsync(long productId, long categoryId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Products.AnyAsync(p => p.Id == productId && p.Enabled, cancellationToken)) + return Result.Fail("Product does not exist"); + + if (!await context.Categories.AnyAsync(c => c.Id == categoryId && c.Enabled, cancellationToken)) + return Result.Fail("Category does not exist"); + + if (await context.ProductCategories.AnyAsync(c => c.ProductId == productId && c.CategoryId == categoryId, cancellationToken)) + return Result.Fail("Category already assigned to product"); + + context.ProductCategories.Add(new Entities.ProductCategory + { + ProductId = productId, + CategoryId = categoryId, + }); + + return await context.SaveChangesAsync(cancellationToken) > 0 + ? Result.Ok() + : Result.Fail("Could not add category to product"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask> GetProductCategoriesAsync(long productId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var categories = await context.ProductCategories.AsNoTracking() + .Where(p => p.ProductId == productId) + .OrderByDescending(o => o.Id) + .Select(p => p.Category) + .ToArrayAsync(cancellationToken); + + return categories?.Length > 0 + ? Result.Ok(categories.Select(c => c!.ToModel()).ToArray()) + : Result.Fail("Failed to get product categories"); + + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask DeleteProductCategoryAsync(long productId, long categoryId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var rowsDeleted = await context.ProductCategories + .Where(p => p.ProductId == productId && p.CategoryId == categoryId) + .ExecuteDeleteAsync(cancellationToken); + + return rowsDeleted > 0 + ? Result.Ok() + : Result.Fail("No product categories were deleted"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + + public async ValueTask DeleteAllProductCategoriesAsync(long productId, CancellationToken cancellationToken = default) + { + try + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var rowsDeleted = await context.ProductCategories + .Where(p => p.ProductId == productId) + .ExecuteDeleteAsync(cancellationToken); + + return rowsDeleted > 0 + ? Result.Ok() + : Result.Fail("No product categories were deleted"); + } + catch (Exception ex) + { + return Result.Fail(new Error(ex.Message).CausedBy(ex)); + } + } + public async ValueTask UpdateProductPriceStatusAsync(long productPriceId, bool isEnabled, CancellationToken cancellationToken = default) { try @@ -68,9 +163,6 @@ public sealed class ProductService(IDbContextFactory cont if (!string.IsNullOrWhiteSpace(filter.Title)) query = query.Where(p => EF.Functions.ILike(p.Name!, $"%{filter.Title}%")); - if (!string.IsNullOrWhiteSpace(filter.Category)) - query = query.Where(p => p.Categories.Contains(filter.Category)); - if (!string.IsNullOrWhiteSpace(filter.Manufacturer)) query = query.Where(p => EF.Functions.ILike(p.Metadata!.Manufacturer!, $"%{filter.Manufacturer}%")); @@ -152,7 +244,6 @@ public sealed class ProductService(IDbContextFactory cont ImageUrl = request.ImageUrl, ThumbnailUrls = request.ThumbnailUrls, Metadata = request.Metadata, - Categories = request.Categories, Enabled = true }); diff --git a/LiteCharms.Features/Models/ProductFilter.cs b/LiteCharms.Features/Models/ProductFilter.cs index bfed022..78f6d90 100644 --- a/LiteCharms.Features/Models/ProductFilter.cs +++ b/LiteCharms.Features/Models/ProductFilter.cs @@ -6,8 +6,6 @@ public sealed class ProductFilter public string? Title { get; set; } - public string? Category { get; set; } - public string? Manufacturer { get; set; } public string? SerialNumber { get; set; }