From 2a0b34c73013a0908ffca825a882f8f77ac5fed8 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Thu, 28 May 2026 09:05:49 +0200 Subject: [PATCH] Refactored database registration to allow postgres to use internal representations for afster performance --- .../Fixture.cs | 7 +- .../IntegrationFactAttribute.cs | 10 ++ ...eCharms.Features.MidrandBooks.Tests.csproj | 1 + .../ProductServiceFeatureTest.cs | 90 +++++++++++ .../appsettings.json | 6 - .../Authors/Entities/AuthorConfiguration.cs | 3 +- .../Entities/CustomerConfiguration.cs | 3 +- .../Extensions/Postgres.cs | 12 +- .../Extensions/Shop.cs | 12 +- .../LiteCharms.Features.MidrandBooks.csproj | 3 + .../Orders/Entities/OrderConfiguration.cs | 2 +- .../Pages/Entities/BookPageConfiguration.cs | 7 +- ...ner.cs => 20260528052014_Init.Designer.cs} | 150 +++++++++++++++--- ...7070840_Init.cs => 20260528052014_Init.cs} | 71 ++++----- .../MidrandBooksDbContextModelSnapshot.cs | 148 ++++++++++++++--- .../Products/Entities/ProductConfiguration.cs | 7 +- .../Products/Entities/ProductPrice.cs | 2 +- .../Products/Models/CreateProductPrice.cs | 10 -- .../Products/Models/Records.cs | 7 + .../Products/ProductService.cs | 6 +- 20 files changed, 441 insertions(+), 116 deletions(-) create mode 100644 LiteCharms.Features.MidrandBooks.Tests/IntegrationFactAttribute.cs create mode 100644 LiteCharms.Features.MidrandBooks.Tests/ProductServiceFeatureTest.cs rename LiteCharms.Features.MidrandBooks/Postgres/Migrations/{20260527070840_Init.Designer.cs => 20260528052014_Init.Designer.cs} (87%) rename LiteCharms.Features.MidrandBooks/Postgres/Migrations/{20260527070840_Init.cs => 20260528052014_Init.cs} (91%) delete mode 100644 LiteCharms.Features.MidrandBooks/Products/Models/CreateProductPrice.cs diff --git a/LiteCharms.Features.MidrandBooks.Tests/Fixture.cs b/LiteCharms.Features.MidrandBooks.Tests/Fixture.cs index 38c8bd7..d99ea23 100644 --- a/LiteCharms.Features.MidrandBooks.Tests/Fixture.cs +++ b/LiteCharms.Features.MidrandBooks.Tests/Fixture.cs @@ -1,4 +1,5 @@ using LiteCharms.Features.Extensions; +using LiteCharms.Features.MidrandBooks.Abstractions; using LiteCharms.Features.MidrandBooks.Extensions; namespace LiteCharms.Features.MidrandBooks.Tests; @@ -11,6 +12,10 @@ public class Fixture : IDisposable public IMediator Mediator { get; set; } + private readonly CancellationTokenSource cancellationTokenSource = new(); + + public CancellationToken CancellationToken => cancellationTokenSource.Token; + public Fixture() { Configuration = new ConfigurationBuilder() @@ -23,12 +28,12 @@ public class Fixture : IDisposable Services = new ServiceCollection() .AddMediator() .AddLogging() - //.AddMidrandShopServices() .AddEmailServiceBus() .AddGarageS3(Configuration) .AddMidrandShopDatabase(Configuration) .AddEmailServices(Configuration) .AddSingleton(Configuration) + .AddShopServices() .BuildServiceProvider(); Mediator = Services.GetRequiredService(); diff --git a/LiteCharms.Features.MidrandBooks.Tests/IntegrationFactAttribute.cs b/LiteCharms.Features.MidrandBooks.Tests/IntegrationFactAttribute.cs new file mode 100644 index 0000000..24194e6 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Tests/IntegrationFactAttribute.cs @@ -0,0 +1,10 @@ +namespace LiteCharms.Features.MidrandBooks.Tests; + +public class IntegrationFactAttribute : FactAttribute +{ + public IntegrationFactAttribute() + { + if(!Debugger.IsAttached) + Skip = "This test requires the debugger to be attached."; + } +} diff --git a/LiteCharms.Features.MidrandBooks.Tests/LiteCharms.Features.MidrandBooks.Tests.csproj b/LiteCharms.Features.MidrandBooks.Tests/LiteCharms.Features.MidrandBooks.Tests.csproj index 824c4ff..9eba9bd 100644 --- a/LiteCharms.Features.MidrandBooks.Tests/LiteCharms.Features.MidrandBooks.Tests.csproj +++ b/LiteCharms.Features.MidrandBooks.Tests/LiteCharms.Features.MidrandBooks.Tests.csproj @@ -39,6 +39,7 @@ + diff --git a/LiteCharms.Features.MidrandBooks.Tests/ProductServiceFeatureTest.cs b/LiteCharms.Features.MidrandBooks.Tests/ProductServiceFeatureTest.cs new file mode 100644 index 0000000..3c41d55 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Tests/ProductServiceFeatureTest.cs @@ -0,0 +1,90 @@ +using LiteCharms.Features.MidrandBooks.Products; +using LiteCharms.Features.MidrandBooks.Products.Models; +using LiteCharms.Features.Models; +using System.Text.Json; + +namespace LiteCharms.Features.MidrandBooks.Tests; + +public class ProductServiceFeatureTest(Fixture fixture, ITestOutputHelper output) : IClassFixture +{ + private readonly ProductService productService = fixture.Services.GetRequiredService(); + + [IntegrationFact] + public async Task GetProductsAsync_ShouldReturn_RetultProducts() + { + var range = new DateRange + { + From = DateOnly.FromDateTime(DateTime.UtcNow.AddDays(-7)), + To = DateOnly.FromDateTime(DateTime.UtcNow), + MaxRecords = 1000 + }; + + var result = await productService.GetProductsAsync(0, range, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.NotEmpty(result.Value); + + output.WriteLine(JsonSerializer.Serialize(result.Value)); + } + + [IntegrationFact] + public async Task UpdateProductStatusAsync_ShouldReturn_ResultTrue() + { + var result = await productService.UpdateProductStatusAsync(2, true, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task UpdateProductPriceStatusAsync_ShouldReturn_ResultTrue() + { + var result = await productService.UpdateProductPriceStatusAsync(2, true, fixture.CancellationToken); + + Assert.True(result.IsSuccess); + } + + [IntegrationFact] + public async Task CreateProductPriceAsync_Should_Return_NewProductPriceId() + { + var request = new CreateProductPrice + { + Amount = 29.99m, + Discount = 0.00m + }; + + var result = await productService.CreateProductPriceAsync(2, request, fixture.CancellationToken); + + Assert.True(result.IsSuccess, "Product price creation should be successful."); + Assert.True(result.Value > 0, "New ProductPriceId should be greater than 0."); + + output.WriteLine($"Created ProductPriceId: {result.Value}"); + } + + [IntegrationFact] + public async Task CreateProductAsync_Result_Returns_ProductId() + { + var request = new CreateProduct + { + Name = "Systems Rewired", + Description = "[Design], , AND /CHAORS/ IN ***SYNC***", + Summary = "A comprehensive guide to systems thinking and design.", + ImageUrl = "https://bookshop.cdn.khongisa.co.za/design/2bf1f9a2-7b25-4fcf-9aa7-08941ea21e6c_1764838499686.webp", + Type = ProductTypes.Book, + Categories = ["Systems Thinking", "Design", "Programming"], + Metadata = new ProductMetadata + { + CopyrightInfo = "© 2024 John Doe. All rights reserved.", + ManufactureDate = "2024-06-01", + Manufacturer = "TechWave Publishing", + SerialNumber = "SR-2024-0001" + } + }; + + var result = await productService.CreateProductAsync(request, fixture.CancellationToken); + + Assert.True(result.IsSuccess, "Product creation should be successful."); + Assert.True(result.Value > 0, "ProductId should be greater than 0."); + + output.WriteLine($"Created ProductId: {result.Value}"); + } +} diff --git a/LiteCharms.Features.MidrandBooks.Tests/appsettings.json b/LiteCharms.Features.MidrandBooks.Tests/appsettings.json index 1066af9..7f9a6b8 100644 --- a/LiteCharms.Features.MidrandBooks.Tests/appsettings.json +++ b/LiteCharms.Features.MidrandBooks.Tests/appsettings.json @@ -5,12 +5,6 @@ "BucketName": "bookshop", "CdnBaseUrl": "https://bookshop.cdn.khongisa.co.za" }, - "BookshopQuotesS3Settings": { - "ServiceUrl": "http://192.168.1.177:30900", - "Region": "garage", - "BucketName": "bookshop.quotes", - "CdnBaseUrl": "https://bookshop.quotes.cdn.khongisa.co.za" - }, "Email": { "Credentials": { "Username": "shop@litecharms.co.za" diff --git a/LiteCharms.Features.MidrandBooks/Authors/Entities/AuthorConfiguration.cs b/LiteCharms.Features.MidrandBooks/Authors/Entities/AuthorConfiguration.cs index 0c0af10..8a7de14 100644 --- a/LiteCharms.Features.MidrandBooks/Authors/Entities/AuthorConfiguration.cs +++ b/LiteCharms.Features.MidrandBooks/Authors/Entities/AuthorConfiguration.cs @@ -18,7 +18,8 @@ public sealed class AuthorConfiguration : IEntityTypeConfiguration builder.Property(f => f.Website).IsRequired(false).HasMaxLength(1024); builder.Property(f => f.ImageUrl).IsRequired().HasMaxLength(2048); builder.Property(f => f.ThumbnailImageUrl).IsRequired(false).HasMaxLength(2048); - builder.Property(f => f.SocialMedia).IsRequired(false).HasColumnType("jsonb"); builder.Property(f => f.Enabled).HasDefaultValue(true); + + builder.OwnsMany(f => f.SocialMedia, b => { b.ToJson(); }); } } diff --git a/LiteCharms.Features.MidrandBooks/Customers/Entities/CustomerConfiguration.cs b/LiteCharms.Features.MidrandBooks/Customers/Entities/CustomerConfiguration.cs index f5015a7..b68c086 100644 --- a/LiteCharms.Features.MidrandBooks/Customers/Entities/CustomerConfiguration.cs +++ b/LiteCharms.Features.MidrandBooks/Customers/Entities/CustomerConfiguration.cs @@ -14,7 +14,8 @@ public sealed class CustomerConfiguration : IEntityTypeConfiguration builder.Property(c => c.Email).IsRequired(); builder.Property(c => c.Phone).IsRequired(); builder.Property(c => c.Website).IsRequired(); - builder.Property(c => c.SocialMedia).IsRequired(false).HasColumnType("jsonb"); builder.Property(c => c.Enabled).HasDefaultValue(true); + + builder.OwnsMany(f => f.SocialMedia, b => { b.ToJson(); }); } } diff --git a/LiteCharms.Features.MidrandBooks/Extensions/Postgres.cs b/LiteCharms.Features.MidrandBooks/Extensions/Postgres.cs index 228634d..54307dc 100644 --- a/LiteCharms.Features.MidrandBooks/Extensions/Postgres.cs +++ b/LiteCharms.Features.MidrandBooks/Extensions/Postgres.cs @@ -8,8 +8,18 @@ public static class Postgres public static IServiceCollection AddMidrandShopDatabase(this IServiceCollection services, IConfiguration configuration) { + var connectionString = configuration.GetConnectionString(MidrandBooksDbConfigName); + + var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString); + + dataSourceBuilder.ConfigureTypeLoading(options => { options.EnableTypeLoading(false); }); + + var dataSource = dataSourceBuilder.Build(); + + services.AddSingleton(dataSource); + services.AddPooledDbContextFactory(options => - options.UseNpgsql(configuration.GetConnectionString(MidrandBooksDbConfigName))); + options.UseNpgsql(dataSource)); return services; } diff --git a/LiteCharms.Features.MidrandBooks/Extensions/Shop.cs b/LiteCharms.Features.MidrandBooks/Extensions/Shop.cs index 43e407e..f407c5b 100644 --- a/LiteCharms.Features.MidrandBooks/Extensions/Shop.cs +++ b/LiteCharms.Features.MidrandBooks/Extensions/Shop.cs @@ -4,19 +4,15 @@ namespace LiteCharms.Features.MidrandBooks.Extensions; public static class Shop { - public static IServiceCollection AddShopServices(this IServiceCollection services, Assembly assembly, ServiceLifetime serviceLifetime) + public static IServiceCollection AddShopServices(this IServiceCollection services) { var serviceType = typeof(IService); - var implementations = assembly.GetTypes() + var implementations = Assembly.GetExecutingAssembly().GetTypes() .Where(t => serviceType.IsAssignableFrom(t) && t.IsClass && !t.IsAbstract); - foreach (var implementation in implementations) - { - var descriptor = new ServiceDescriptor(serviceType, implementation, serviceLifetime); - - services.Add(descriptor); - } + foreach (var implementation in implementations) + services.AddScoped(implementation); return services; } diff --git a/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj b/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj index 17db6dd..82d3a1b 100644 --- a/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj +++ b/LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj @@ -162,5 +162,8 @@ + + + diff --git a/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderConfiguration.cs b/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderConfiguration.cs index c7ae590..629b030 100644 --- a/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderConfiguration.cs +++ b/LiteCharms.Features.MidrandBooks/Orders/Entities/OrderConfiguration.cs @@ -11,7 +11,7 @@ public sealed class OrderConfiguration : IEntityTypeConfiguration builder.Property(o => o.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); builder.Property(o => o.UpdatedAt).HasDefaultValueSql("now()"); builder.Property(o => o.Status).IsRequired(); - builder.Property(o => o.Total).IsRequired().HasColumnType("decimal(18,2)"); + builder.Property(o => o.Total).IsRequired().HasPrecision(18, 2); builder.Property(o => o.Notes).HasMaxLength(1000); } } diff --git a/LiteCharms.Features.MidrandBooks/Pages/Entities/BookPageConfiguration.cs b/LiteCharms.Features.MidrandBooks/Pages/Entities/BookPageConfiguration.cs index d6c9fed..eb33f55 100644 --- a/LiteCharms.Features.MidrandBooks/Pages/Entities/BookPageConfiguration.cs +++ b/LiteCharms.Features.MidrandBooks/Pages/Entities/BookPageConfiguration.cs @@ -13,10 +13,11 @@ public sealed class BookPageConfiguration : IEntityTypeConfiguration builder.Property(bp => bp.AuthorBookId).IsRequired(); builder.Property(bp => bp.Content).IsRequired(); builder.Property(bp => bp.Type).IsRequired(); - builder.Property(bp => bp.ContentType).IsRequired(); - builder.Property(bp => bp.Notes).IsRequired(false).HasColumnType("jsonb"); - builder.Property(bp => bp.References).IsRequired(false).HasColumnType("jsonb"); + builder.Property(bp => bp.ContentType).IsRequired(); builder.Property(bp => bp.Enabled).HasDefaultValue(true); + builder.Property(bp => bp.Notes).IsRequired(false); + + builder.OwnsMany(f => f.References, b => { b.ToJson(); }); builder.HasOne(f =>f.Book) .WithMany(b => b.Pages) diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260527070840_Init.Designer.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260528052014_Init.Designer.cs similarity index 87% rename from LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260527070840_Init.Designer.cs rename to LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260528052014_Init.Designer.cs index 856fd68..ed24e21 100644 --- a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260527070840_Init.Designer.cs +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260528052014_Init.Designer.cs @@ -1,7 +1,6 @@ // using System; using LiteCharms.Features.MidrandBooks.Postgres; -using LiteCharms.Features.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -13,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations { [DbContext(typeof(MidrandBooksDbContext))] - [Migration("20260527070840_Init")] + [Migration("20260528052014_Init")] partial class Init { /// @@ -112,9 +111,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b.Property("PublisherType") .HasColumnType("integer"); - b.Property("SocialMedia") - .HasColumnType("jsonb"); - b.Property("ThumbnailImageUrl") .HasMaxLength(2048) .HasColumnType("character varying(2048)"); @@ -291,9 +287,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations .IsRequired() .HasColumnType("text"); - b.Property("SocialMedia") - .HasColumnType("jsonb"); - b.Property("UpdatedAt") .ValueGeneratedOnAdd() .HasColumnType("timestamp with time zone") @@ -338,7 +331,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations .HasColumnType("integer"); b.Property("Total") - .HasColumnType("decimal(18,2)"); + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); b.Property("UpdatedAt") .ValueGeneratedOnAdd() @@ -494,17 +488,14 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations .HasColumnType("boolean") .HasDefaultValue(true); - b.PrimitiveCollection("Notes") - .HasColumnType("jsonb"); + b.PrimitiveCollection("Notes") + .HasColumnType("text[]"); b.Property("Number") .ValueGeneratedOnAdd() .HasColumnType("integer") .HasDefaultValue(0); - b.Property("References") - .HasColumnType("jsonb"); - b.Property("Type") .HasColumnType("integer"); @@ -570,8 +561,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - b.PrimitiveCollection("Categories") - .HasColumnType("jsonb"); + b.PrimitiveCollection("Categories") + .HasColumnType("text[]"); b.Property("CreatedAt") .ValueGeneratedOnAdd() @@ -591,9 +582,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations .HasMaxLength(1024) .HasColumnType("character varying(1024)"); - b.Property("Metadata") - .HasColumnType("jsonb"); - b.Property("Name") .IsRequired() .HasMaxLength(255) @@ -604,8 +592,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations .HasMaxLength(512) .HasColumnType("character varying(512)"); - b.PrimitiveCollection("ThumbnailUrls") - .HasColumnType("jsonb"); + b.PrimitiveCollection("ThumbnailUrls") + .HasColumnType("text[]"); b.Property("Type") .HasColumnType("integer"); @@ -714,6 +702,38 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations 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") @@ -736,6 +756,38 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations 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") @@ -798,7 +850,34 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations .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 => @@ -812,6 +891,35 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations 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") diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260527070840_Init.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260528052014_Init.cs similarity index 91% rename from LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260527070840_Init.cs rename to LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260528052014_Init.cs index 774c77a..1c0cb96 100644 --- a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260527070840_Init.cs +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/20260528052014_Init.cs @@ -1,5 +1,4 @@ using System; -using LiteCharms.Features.Models; using Microsoft.EntityFrameworkCore.Migrations; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; @@ -31,8 +30,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations Website = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: true), ImageUrl = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: false), ThumbnailImageUrl = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: true), - SocialMedia = table.Column(type: "jsonb", nullable: true), - Enabled = table.Column(type: "boolean", nullable: false, defaultValue: true) + Enabled = table.Column(type: "boolean", nullable: false, defaultValue: true), + SocialMedia = table.Column(type: "jsonb", nullable: true) }, constraints: table => { @@ -52,8 +51,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations Email = table.Column(type: "text", nullable: false), Website = table.Column(type: "text", nullable: false), Phone = table.Column(type: "text", nullable: false), - SocialMedia = table.Column(type: "jsonb", nullable: true), - Enabled = table.Column(type: "boolean", nullable: false, defaultValue: true) + Enabled = table.Column(type: "boolean", nullable: false, defaultValue: true), + SocialMedia = table.Column(type: "jsonb", nullable: true) }, constraints: table => { @@ -70,7 +69,7 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, defaultValueSql: "now()"), CustomerId = table.Column(type: "bigint", nullable: false), Status = table.Column(type: "integer", nullable: false), - Total = table.Column(type: "numeric(18,2)", nullable: false), + Total = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), Notes = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: true), InvoiceUrl = table.Column(type: "text", nullable: true) }, @@ -92,10 +91,10 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations Summary = table.Column(type: "character varying(512)", maxLength: 512, nullable: false), Description = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: true), ImageUrl = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: true), - ThumbnailUrls = table.Column(type: "jsonb", nullable: true), - Categories = table.Column(type: "jsonb", nullable: true), - Metadata = table.Column(type: "jsonb", nullable: true), - Enabled = table.Column(type: "boolean", nullable: false, defaultValue: false) + ThumbnailUrls = table.Column(type: "text[]", nullable: true), + Categories = table.Column(type: "text[]", nullable: true), + Enabled = table.Column(type: "boolean", nullable: false, defaultValue: false), + Metadata = table.Column(type: "jsonb", nullable: true) }, constraints: table => { @@ -260,29 +259,29 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations onDelete: ReferentialAction.Restrict); }); - migrationBuilder.CreateTable( - name: "ProductPrice", - columns: table => new - { - Id = table.Column(type: "bigint", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), - ProductId = table.Column(type: "bigint", nullable: false), - Amount = table.Column(type: "numeric", nullable: false), - Discount = table.Column(type: "numeric", nullable: false), - Enabled = table.Column(type: "boolean", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ProductPrice", x => x.Id); - table.ForeignKey( - name: "FK_ProductPrice_Products_ProductId", - column: x => x.ProductId, - principalTable: "Products", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); + //migrationBuilder.CreateTable( + // name: "ProductPrice", + // columns: table => new + // { + // Id = table.Column(type: "bigint", nullable: false) + // .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + // CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + // UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + // ProductId = table.Column(type: "bigint", nullable: false), + // Amount = table.Column(type: "numeric", nullable: false), + // Discount = table.Column(type: "numeric", nullable: false), + // Enabled = table.Column(type: "boolean", nullable: false) + // }, + // constraints: table => + // { + // table.PrimaryKey("PK_ProductPrice", x => x.Id); + // table.ForeignKey( + // name: "FK_ProductPrice_Products_ProductId", + // column: x => x.ProductId, + // principalTable: "Products", + // principalColumn: "Id", + // onDelete: ReferentialAction.Cascade); + // }); migrationBuilder.CreateTable( name: "Shippings", @@ -334,9 +333,9 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations ContentType = table.Column(type: "integer", nullable: false), Number = table.Column(type: "integer", nullable: false, defaultValue: 0), Content = table.Column(type: "bytea", nullable: false), - Notes = table.Column(type: "jsonb", nullable: true), - References = table.Column(type: "jsonb", nullable: true), - Enabled = table.Column(type: "boolean", nullable: false, defaultValue: true) + Notes = table.Column(type: "text[]", nullable: true), + Enabled = table.Column(type: "boolean", nullable: false, defaultValue: true), + References = table.Column(type: "jsonb", nullable: true) }, constraints: table => { diff --git a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs index 04c9235..6340386 100644 --- a/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs +++ b/LiteCharms.Features.MidrandBooks/Postgres/Migrations/MidrandBooksDbContextModelSnapshot.cs @@ -1,7 +1,6 @@ // using System; using LiteCharms.Features.MidrandBooks.Postgres; -using LiteCharms.Features.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; @@ -109,9 +108,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations b.Property("PublisherType") .HasColumnType("integer"); - b.Property("SocialMedia") - .HasColumnType("jsonb"); - b.Property("ThumbnailImageUrl") .HasMaxLength(2048) .HasColumnType("character varying(2048)"); @@ -288,9 +284,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations .IsRequired() .HasColumnType("text"); - b.Property("SocialMedia") - .HasColumnType("jsonb"); - b.Property("UpdatedAt") .ValueGeneratedOnAdd() .HasColumnType("timestamp with time zone") @@ -335,7 +328,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations .HasColumnType("integer"); b.Property("Total") - .HasColumnType("decimal(18,2)"); + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); b.Property("UpdatedAt") .ValueGeneratedOnAdd() @@ -491,17 +485,14 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations .HasColumnType("boolean") .HasDefaultValue(true); - b.PrimitiveCollection("Notes") - .HasColumnType("jsonb"); + b.PrimitiveCollection("Notes") + .HasColumnType("text[]"); b.Property("Number") .ValueGeneratedOnAdd() .HasColumnType("integer") .HasDefaultValue(0); - b.Property("References") - .HasColumnType("jsonb"); - b.Property("Type") .HasColumnType("integer"); @@ -567,8 +558,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - b.PrimitiveCollection("Categories") - .HasColumnType("jsonb"); + b.PrimitiveCollection("Categories") + .HasColumnType("text[]"); b.Property("CreatedAt") .ValueGeneratedOnAdd() @@ -588,9 +579,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations .HasMaxLength(1024) .HasColumnType("character varying(1024)"); - b.Property("Metadata") - .HasColumnType("jsonb"); - b.Property("Name") .IsRequired() .HasMaxLength(255) @@ -601,8 +589,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations .HasMaxLength(512) .HasColumnType("character varying(512)"); - b.PrimitiveCollection("ThumbnailUrls") - .HasColumnType("jsonb"); + b.PrimitiveCollection("ThumbnailUrls") + .HasColumnType("text[]"); b.Property("Type") .HasColumnType("integer"); @@ -711,6 +699,38 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations 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") @@ -733,6 +753,38 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations 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") @@ -795,7 +847,34 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations .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 => @@ -809,6 +888,35 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations 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") diff --git a/LiteCharms.Features.MidrandBooks/Products/Entities/ProductConfiguration.cs b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductConfiguration.cs index e360385..db18548 100644 --- a/LiteCharms.Features.MidrandBooks/Products/Entities/ProductConfiguration.cs +++ b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductConfiguration.cs @@ -14,9 +14,10 @@ public sealed class ProductConfiguration : IEntityTypeConfiguration builder.Property(f => f.Summary).IsRequired().HasMaxLength(512); builder.Property(f => f.Description).HasMaxLength(1024); builder.Property(f => f.ImageUrl).HasMaxLength(1024); - builder.Property(f => f.ThumbnailUrls).IsRequired(false).HasColumnType("jsonb"); - builder.Property(f => f.Metadata).IsRequired(false).HasColumnType("jsonb"); - builder.Property(f => f.Categories).IsRequired(false).HasColumnType("jsonb"); 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/Entities/ProductPrice.cs b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductPrice.cs index e78c42b..ba6a593 100644 --- a/LiteCharms.Features.MidrandBooks/Products/Entities/ProductPrice.cs +++ b/LiteCharms.Features.MidrandBooks/Products/Entities/ProductPrice.cs @@ -3,5 +3,5 @@ [EntityTypeConfiguration] public class ProductPrice : Models.ProductPrice { - public virtual Product Product { get; set; } = new(); + public virtual Product? Product { get; set; } } diff --git a/LiteCharms.Features.MidrandBooks/Products/Models/CreateProductPrice.cs b/LiteCharms.Features.MidrandBooks/Products/Models/CreateProductPrice.cs deleted file mode 100644 index b237f96..0000000 --- a/LiteCharms.Features.MidrandBooks/Products/Models/CreateProductPrice.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace LiteCharms.Features.MidrandBooks.Products.Models; - -public sealed class CreateProductPrice -{ - public long ProductId { get; set; } - - public decimal Amount { get; set; } - - public decimal Discount { get; set; } -} diff --git a/LiteCharms.Features.MidrandBooks/Products/Models/Records.cs b/LiteCharms.Features.MidrandBooks/Products/Models/Records.cs index 44786dd..9c09551 100644 --- a/LiteCharms.Features.MidrandBooks/Products/Models/Records.cs +++ b/LiteCharms.Features.MidrandBooks/Products/Models/Records.cs @@ -20,3 +20,10 @@ public sealed record CreateProduct public ProductMetadata? Metadata { get; set; } } + +public sealed class CreateProductPrice +{ + public decimal Amount { get; set; } + + public decimal Discount { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks/Products/ProductService.cs b/LiteCharms.Features.MidrandBooks/Products/ProductService.cs index 99f1c45..a43e489 100644 --- a/LiteCharms.Features.MidrandBooks/Products/ProductService.cs +++ b/LiteCharms.Features.MidrandBooks/Products/ProductService.cs @@ -228,8 +228,8 @@ public sealed class ProductService(IDbContextFactory cont { try { - var fromDate = range.From.ToDateTime(TimeOnly.MinValue); - var toDate = range.To.ToDateTime(TimeOnly.MaxValue); + var fromDate = range.From.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc); + var toDate = range.To.ToDateTime(TimeOnly.MaxValue, DateTimeKind.Utc); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); @@ -244,7 +244,7 @@ public sealed class ProductService(IDbContextFactory cont .AsSplitQuery() .ToArrayAsync(cancellationToken); - return await context.SaveChangesAsync(cancellationToken) > 0 + return products?.Length > 0 ? Result.Ok(products.Select(p => p.ToModel()).ToArray()) : Result.Fail(new Error("Failed to retrieve products.")); }