Refactored database registration to allow postgres to use internal representations for afster performance

This commit is contained in:
Khwezi Mngoma
2026-05-28 09:05:49 +02:00
parent 902942eee6
commit 2a0b34c730
20 changed files with 441 additions and 116 deletions
@@ -1,4 +1,5 @@
using LiteCharms.Features.Extensions; using LiteCharms.Features.Extensions;
using LiteCharms.Features.MidrandBooks.Abstractions;
using LiteCharms.Features.MidrandBooks.Extensions; using LiteCharms.Features.MidrandBooks.Extensions;
namespace LiteCharms.Features.MidrandBooks.Tests; namespace LiteCharms.Features.MidrandBooks.Tests;
@@ -11,6 +12,10 @@ public class Fixture : IDisposable
public IMediator Mediator { get; set; } public IMediator Mediator { get; set; }
private readonly CancellationTokenSource cancellationTokenSource = new();
public CancellationToken CancellationToken => cancellationTokenSource.Token;
public Fixture() public Fixture()
{ {
Configuration = new ConfigurationBuilder() Configuration = new ConfigurationBuilder()
@@ -23,12 +28,12 @@ public class Fixture : IDisposable
Services = new ServiceCollection() Services = new ServiceCollection()
.AddMediator() .AddMediator()
.AddLogging() .AddLogging()
//.AddMidrandShopServices()
.AddEmailServiceBus() .AddEmailServiceBus()
.AddGarageS3(Configuration) .AddGarageS3(Configuration)
.AddMidrandShopDatabase(Configuration) .AddMidrandShopDatabase(Configuration)
.AddEmailServices(Configuration) .AddEmailServices(Configuration)
.AddSingleton(Configuration) .AddSingleton(Configuration)
.AddShopServices()
.BuildServiceProvider(); .BuildServiceProvider();
Mediator = Services.GetRequiredService<IMediator>(); Mediator = Services.GetRequiredService<IMediator>();
@@ -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.";
}
}
@@ -39,6 +39,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Using Include="System.Diagnostics" />
<Using Include="Xunit" /> <Using Include="Xunit" />
</ItemGroup> </ItemGroup>
@@ -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<Fixture>
{
private readonly ProductService productService = fixture.Services.GetRequiredService<ProductService>();
[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], <Code>, 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}");
}
}
@@ -5,12 +5,6 @@
"BucketName": "bookshop", "BucketName": "bookshop",
"CdnBaseUrl": "https://bookshop.cdn.khongisa.co.za" "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": { "Email": {
"Credentials": { "Credentials": {
"Username": "shop@litecharms.co.za" "Username": "shop@litecharms.co.za"
@@ -18,7 +18,8 @@ public sealed class AuthorConfiguration : IEntityTypeConfiguration<Author>
builder.Property(f => f.Website).IsRequired(false).HasMaxLength(1024); builder.Property(f => f.Website).IsRequired(false).HasMaxLength(1024);
builder.Property(f => f.ImageUrl).IsRequired().HasMaxLength(2048); builder.Property(f => f.ImageUrl).IsRequired().HasMaxLength(2048);
builder.Property(f => f.ThumbnailImageUrl).IsRequired(false).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.Property(f => f.Enabled).HasDefaultValue(true);
builder.OwnsMany(f => f.SocialMedia, b => { b.ToJson(); });
} }
} }
@@ -14,7 +14,8 @@ public sealed class CustomerConfiguration : IEntityTypeConfiguration<Customer>
builder.Property(c => c.Email).IsRequired(); builder.Property(c => c.Email).IsRequired();
builder.Property(c => c.Phone).IsRequired(); builder.Property(c => c.Phone).IsRequired();
builder.Property(c => c.Website).IsRequired(); builder.Property(c => c.Website).IsRequired();
builder.Property(c => c.SocialMedia).IsRequired(false).HasColumnType("jsonb");
builder.Property(c => c.Enabled).HasDefaultValue(true); builder.Property(c => c.Enabled).HasDefaultValue(true);
builder.OwnsMany(f => f.SocialMedia, b => { b.ToJson(); });
} }
} }
@@ -8,8 +8,18 @@ public static class Postgres
public static IServiceCollection AddMidrandShopDatabase(this IServiceCollection services, IConfiguration configuration) 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<MidrandBooksDbContext>(options => services.AddPooledDbContextFactory<MidrandBooksDbContext>(options =>
options.UseNpgsql(configuration.GetConnectionString(MidrandBooksDbConfigName))); options.UseNpgsql(dataSource));
return services; return services;
} }
@@ -4,19 +4,15 @@ namespace LiteCharms.Features.MidrandBooks.Extensions;
public static class Shop 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 serviceType = typeof(IService);
var implementations = assembly.GetTypes() var implementations = Assembly.GetExecutingAssembly().GetTypes()
.Where(t => serviceType.IsAssignableFrom(t) && t.IsClass && !t.IsAbstract); .Where(t => serviceType.IsAssignableFrom(t) && t.IsClass && !t.IsAbstract);
foreach (var implementation in implementations) foreach (var implementation in implementations)
{ services.AddScoped(implementation);
var descriptor = new ServiceDescriptor(serviceType, implementation, serviceLifetime);
services.Add(descriptor);
}
return services; return services;
} }
@@ -162,5 +162,8 @@
<Using Include="Microsoft.Extensions.Options" /> <Using Include="Microsoft.Extensions.Options" />
<Using Include="Microsoft.Extensions.Logging" /> <Using Include="Microsoft.Extensions.Logging" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Postgres\Migrations\" />
</ItemGroup>
</Project> </Project>
@@ -11,7 +11,7 @@ public sealed class OrderConfiguration : IEntityTypeConfiguration<Order>
builder.Property(o => o.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); builder.Property(o => o.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()");
builder.Property(o => o.UpdatedAt).HasDefaultValueSql("now()"); builder.Property(o => o.UpdatedAt).HasDefaultValueSql("now()");
builder.Property(o => o.Status).IsRequired(); 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); builder.Property(o => o.Notes).HasMaxLength(1000);
} }
} }
@@ -13,10 +13,11 @@ public sealed class BookPageConfiguration : IEntityTypeConfiguration<BookPage>
builder.Property(bp => bp.AuthorBookId).IsRequired(); builder.Property(bp => bp.AuthorBookId).IsRequired();
builder.Property(bp => bp.Content).IsRequired(); builder.Property(bp => bp.Content).IsRequired();
builder.Property(bp => bp.Type).IsRequired(); builder.Property(bp => bp.Type).IsRequired();
builder.Property(bp => bp.ContentType).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.Enabled).HasDefaultValue(true); 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) builder.HasOne(f =>f.Book)
.WithMany(b => b.Pages) .WithMany(b => b.Pages)
@@ -1,7 +1,6 @@
// <auto-generated /> // <auto-generated />
using System; using System;
using LiteCharms.Features.MidrandBooks.Postgres; using LiteCharms.Features.MidrandBooks.Postgres;
using LiteCharms.Features.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
@@ -13,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
{ {
[DbContext(typeof(MidrandBooksDbContext))] [DbContext(typeof(MidrandBooksDbContext))]
[Migration("20260527070840_Init")] [Migration("20260528052014_Init")]
partial class Init partial class Init
{ {
/// <inheritdoc /> /// <inheritdoc />
@@ -112,9 +111,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
b.Property<int>("PublisherType") b.Property<int>("PublisherType")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<SocialMedia[]>("SocialMedia")
.HasColumnType("jsonb");
b.Property<string>("ThumbnailImageUrl") b.Property<string>("ThumbnailImageUrl")
.HasMaxLength(2048) .HasMaxLength(2048)
.HasColumnType("character varying(2048)"); .HasColumnType("character varying(2048)");
@@ -291,9 +287,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
.IsRequired() .IsRequired()
.HasColumnType("text"); .HasColumnType("text");
b.Property<SocialMedia[]>("SocialMedia")
.HasColumnType("jsonb");
b.Property<DateTime?>("UpdatedAt") b.Property<DateTime?>("UpdatedAt")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
@@ -338,7 +331,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<decimal>("Total") b.Property<decimal>("Total")
.HasColumnType("decimal(18,2)"); .HasPrecision(18, 2)
.HasColumnType("numeric(18,2)");
b.Property<DateTime?>("UpdatedAt") b.Property<DateTime?>("UpdatedAt")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
@@ -494,17 +488,14 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
.HasColumnType("boolean") .HasColumnType("boolean")
.HasDefaultValue(true); .HasDefaultValue(true);
b.PrimitiveCollection<string>("Notes") b.PrimitiveCollection<string[]>("Notes")
.HasColumnType("jsonb"); .HasColumnType("text[]");
b.Property<int>("Number") b.Property<int>("Number")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("integer") .HasColumnType("integer")
.HasDefaultValue(0); .HasDefaultValue(0);
b.Property<PageReference[]>("References")
.HasColumnType("jsonb");
b.Property<int>("Type") b.Property<int>("Type")
.HasColumnType("integer"); .HasColumnType("integer");
@@ -570,8 +561,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id")); NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.PrimitiveCollection<string>("Categories") b.PrimitiveCollection<string[]>("Categories")
.HasColumnType("jsonb"); .HasColumnType("text[]");
b.Property<DateTime>("CreatedAt") b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
@@ -591,9 +582,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
.HasMaxLength(1024) .HasMaxLength(1024)
.HasColumnType("character varying(1024)"); .HasColumnType("character varying(1024)");
b.Property<ProductMetadata>("Metadata")
.HasColumnType("jsonb");
b.Property<string>("Name") b.Property<string>("Name")
.IsRequired() .IsRequired()
.HasMaxLength(255) .HasMaxLength(255)
@@ -604,8 +592,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
.HasMaxLength(512) .HasMaxLength(512)
.HasColumnType("character varying(512)"); .HasColumnType("character varying(512)");
b.PrimitiveCollection<string>("ThumbnailUrls") b.PrimitiveCollection<string[]>("ThumbnailUrls")
.HasColumnType("jsonb"); .HasColumnType("text[]");
b.Property<int>("Type") b.Property<int>("Type")
.HasColumnType("integer"); .HasColumnType("integer");
@@ -714,6 +702,38 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
b.Navigation("Product"); b.Navigation("Product");
}); });
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b =>
{
b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 =>
{
b1.Property<long>("AuthorId");
b1.Property<int>("__synthesizedOrdinal")
.ValueGeneratedOnAdd();
b1.Property<string>("ImageUrl");
b1.Property<string>("Name");
b1.Property<int>("Type");
b1.Property<string>("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 => modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b =>
{ {
b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer")
@@ -736,6 +756,38 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
b.Navigation("Customer"); b.Navigation("Customer");
}); });
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b =>
{
b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 =>
{
b1.Property<long>("CustomerId");
b1.Property<int>("__synthesizedOrdinal")
.ValueGeneratedOnAdd();
b1.Property<string>("ImageUrl");
b1.Property<string>("Name");
b1.Property<int>("Type");
b1.Property<string>("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 => modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b =>
{ {
b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "AuthorBook") b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "AuthorBook")
@@ -798,7 +850,34 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
.OnDelete(DeleteBehavior.NoAction) .OnDelete(DeleteBehavior.NoAction)
.IsRequired(); .IsRequired();
b.OwnsMany("LiteCharms.Features.Models.PageReference", "References", b1 =>
{
b1.Property<long>("BookPageId");
b1.Property<int>("__synthesizedOrdinal")
.ValueGeneratedOnAdd();
b1.Property<string>("Description");
b1.Property<string>("Tag");
b1.Property<string>("Url");
b1.HasKey("BookPageId", "__synthesizedOrdinal");
b1.ToTable("BookPages");
b1
.ToJson("References")
.HasColumnType("jsonb");
b1.WithOwner()
.HasForeignKey("BookPageId");
});
b.Navigation("Book"); b.Navigation("Book");
b.Navigation("References");
}); });
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b =>
@@ -812,6 +891,35 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
b.Navigation("Order"); b.Navigation("Order");
}); });
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b =>
{
b.OwnsOne("LiteCharms.Features.Models.ProductMetadata", "Metadata", b1 =>
{
b1.Property<long>("ProductId");
b1.Property<string>("CopyrightInfo");
b1.Property<string>("ManufactureDate");
b1.Property<string>("Manufacturer");
b1.Property<string>("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 => modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b =>
{ {
b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product")
@@ -1,5 +1,4 @@
using System; using System;
using LiteCharms.Features.Models;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
@@ -31,8 +30,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
Website = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true), Website = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
ImageUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false), ImageUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
ThumbnailImageUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true), ThumbnailImageUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
SocialMedia = table.Column<SocialMedia[]>(type: "jsonb", nullable: true), Enabled = table.Column<bool>(type: "boolean", nullable: false, defaultValue: true),
Enabled = table.Column<bool>(type: "boolean", nullable: false, defaultValue: true) SocialMedia = table.Column<string>(type: "jsonb", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
@@ -52,8 +51,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
Email = table.Column<string>(type: "text", nullable: false), Email = table.Column<string>(type: "text", nullable: false),
Website = table.Column<string>(type: "text", nullable: false), Website = table.Column<string>(type: "text", nullable: false),
Phone = table.Column<string>(type: "text", nullable: false), Phone = table.Column<string>(type: "text", nullable: false),
SocialMedia = table.Column<SocialMedia[]>(type: "jsonb", nullable: true), Enabled = table.Column<bool>(type: "boolean", nullable: false, defaultValue: true),
Enabled = table.Column<bool>(type: "boolean", nullable: false, defaultValue: true) SocialMedia = table.Column<string>(type: "jsonb", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
@@ -70,7 +69,7 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, defaultValueSql: "now()"), UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, defaultValueSql: "now()"),
CustomerId = table.Column<long>(type: "bigint", nullable: false), CustomerId = table.Column<long>(type: "bigint", nullable: false),
Status = table.Column<int>(type: "integer", nullable: false), Status = table.Column<int>(type: "integer", nullable: false),
Total = table.Column<decimal>(type: "numeric(18,2)", nullable: false), Total = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
Notes = table.Column<string>(type: "character varying(1000)", maxLength: 1000, nullable: true), Notes = table.Column<string>(type: "character varying(1000)", maxLength: 1000, nullable: true),
InvoiceUrl = table.Column<string>(type: "text", nullable: true) InvoiceUrl = table.Column<string>(type: "text", nullable: true)
}, },
@@ -92,10 +91,10 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
Summary = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false), Summary = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
Description = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true), Description = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
ImageUrl = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true), ImageUrl = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
ThumbnailUrls = table.Column<string>(type: "jsonb", nullable: true), ThumbnailUrls = table.Column<string[]>(type: "text[]", nullable: true),
Categories = table.Column<string>(type: "jsonb", nullable: true), Categories = table.Column<string[]>(type: "text[]", nullable: true),
Metadata = table.Column<ProductMetadata>(type: "jsonb", nullable: true), Enabled = table.Column<bool>(type: "boolean", nullable: false, defaultValue: false),
Enabled = table.Column<bool>(type: "boolean", nullable: false, defaultValue: false) Metadata = table.Column<string>(type: "jsonb", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
@@ -260,29 +259,29 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
onDelete: ReferentialAction.Restrict); onDelete: ReferentialAction.Restrict);
}); });
migrationBuilder.CreateTable( //migrationBuilder.CreateTable(
name: "ProductPrice", // name: "ProductPrice",
columns: table => new // columns: table => new
{ // {
Id = table.Column<long>(type: "bigint", nullable: false) // Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), // .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false), // CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true), // UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
ProductId = table.Column<long>(type: "bigint", nullable: false), // ProductId = table.Column<long>(type: "bigint", nullable: false),
Amount = table.Column<decimal>(type: "numeric", nullable: false), // Amount = table.Column<decimal>(type: "numeric", nullable: false),
Discount = table.Column<decimal>(type: "numeric", nullable: false), // Discount = table.Column<decimal>(type: "numeric", nullable: false),
Enabled = table.Column<bool>(type: "boolean", nullable: false) // Enabled = table.Column<bool>(type: "boolean", nullable: false)
}, // },
constraints: table => // constraints: table =>
{ // {
table.PrimaryKey("PK_ProductPrice", x => x.Id); // table.PrimaryKey("PK_ProductPrice", x => x.Id);
table.ForeignKey( // table.ForeignKey(
name: "FK_ProductPrice_Products_ProductId", // name: "FK_ProductPrice_Products_ProductId",
column: x => x.ProductId, // column: x => x.ProductId,
principalTable: "Products", // principalTable: "Products",
principalColumn: "Id", // principalColumn: "Id",
onDelete: ReferentialAction.Cascade); // onDelete: ReferentialAction.Cascade);
}); // });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "Shippings", name: "Shippings",
@@ -334,9 +333,9 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
ContentType = table.Column<int>(type: "integer", nullable: false), ContentType = table.Column<int>(type: "integer", nullable: false),
Number = table.Column<int>(type: "integer", nullable: false, defaultValue: 0), Number = table.Column<int>(type: "integer", nullable: false, defaultValue: 0),
Content = table.Column<byte[]>(type: "bytea", nullable: false), Content = table.Column<byte[]>(type: "bytea", nullable: false),
Notes = table.Column<string>(type: "jsonb", nullable: true), Notes = table.Column<string[]>(type: "text[]", nullable: true),
References = table.Column<PageReference[]>(type: "jsonb", nullable: true), Enabled = table.Column<bool>(type: "boolean", nullable: false, defaultValue: true),
Enabled = table.Column<bool>(type: "boolean", nullable: false, defaultValue: true) References = table.Column<string>(type: "jsonb", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
@@ -1,7 +1,6 @@
// <auto-generated /> // <auto-generated />
using System; using System;
using LiteCharms.Features.MidrandBooks.Postgres; using LiteCharms.Features.MidrandBooks.Postgres;
using LiteCharms.Features.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
@@ -109,9 +108,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
b.Property<int>("PublisherType") b.Property<int>("PublisherType")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<SocialMedia[]>("SocialMedia")
.HasColumnType("jsonb");
b.Property<string>("ThumbnailImageUrl") b.Property<string>("ThumbnailImageUrl")
.HasMaxLength(2048) .HasMaxLength(2048)
.HasColumnType("character varying(2048)"); .HasColumnType("character varying(2048)");
@@ -288,9 +284,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
.IsRequired() .IsRequired()
.HasColumnType("text"); .HasColumnType("text");
b.Property<SocialMedia[]>("SocialMedia")
.HasColumnType("jsonb");
b.Property<DateTime?>("UpdatedAt") b.Property<DateTime?>("UpdatedAt")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
@@ -335,7 +328,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<decimal>("Total") b.Property<decimal>("Total")
.HasColumnType("decimal(18,2)"); .HasPrecision(18, 2)
.HasColumnType("numeric(18,2)");
b.Property<DateTime?>("UpdatedAt") b.Property<DateTime?>("UpdatedAt")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
@@ -491,17 +485,14 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
.HasColumnType("boolean") .HasColumnType("boolean")
.HasDefaultValue(true); .HasDefaultValue(true);
b.PrimitiveCollection<string>("Notes") b.PrimitiveCollection<string[]>("Notes")
.HasColumnType("jsonb"); .HasColumnType("text[]");
b.Property<int>("Number") b.Property<int>("Number")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("integer") .HasColumnType("integer")
.HasDefaultValue(0); .HasDefaultValue(0);
b.Property<PageReference[]>("References")
.HasColumnType("jsonb");
b.Property<int>("Type") b.Property<int>("Type")
.HasColumnType("integer"); .HasColumnType("integer");
@@ -567,8 +558,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id")); NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.PrimitiveCollection<string>("Categories") b.PrimitiveCollection<string[]>("Categories")
.HasColumnType("jsonb"); .HasColumnType("text[]");
b.Property<DateTime>("CreatedAt") b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
@@ -588,9 +579,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
.HasMaxLength(1024) .HasMaxLength(1024)
.HasColumnType("character varying(1024)"); .HasColumnType("character varying(1024)");
b.Property<ProductMetadata>("Metadata")
.HasColumnType("jsonb");
b.Property<string>("Name") b.Property<string>("Name")
.IsRequired() .IsRequired()
.HasMaxLength(255) .HasMaxLength(255)
@@ -601,8 +589,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
.HasMaxLength(512) .HasMaxLength(512)
.HasColumnType("character varying(512)"); .HasColumnType("character varying(512)");
b.PrimitiveCollection<string>("ThumbnailUrls") b.PrimitiveCollection<string[]>("ThumbnailUrls")
.HasColumnType("jsonb"); .HasColumnType("text[]");
b.Property<int>("Type") b.Property<int>("Type")
.HasColumnType("integer"); .HasColumnType("integer");
@@ -711,6 +699,38 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
b.Navigation("Product"); b.Navigation("Product");
}); });
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b =>
{
b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 =>
{
b1.Property<long>("AuthorId");
b1.Property<int>("__synthesizedOrdinal")
.ValueGeneratedOnAdd();
b1.Property<string>("ImageUrl");
b1.Property<string>("Name");
b1.Property<int>("Type");
b1.Property<string>("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 => modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b =>
{ {
b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer")
@@ -733,6 +753,38 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
b.Navigation("Customer"); b.Navigation("Customer");
}); });
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b =>
{
b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 =>
{
b1.Property<long>("CustomerId");
b1.Property<int>("__synthesizedOrdinal")
.ValueGeneratedOnAdd();
b1.Property<string>("ImageUrl");
b1.Property<string>("Name");
b1.Property<int>("Type");
b1.Property<string>("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 => modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b =>
{ {
b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "AuthorBook") b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "AuthorBook")
@@ -795,7 +847,34 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
.OnDelete(DeleteBehavior.NoAction) .OnDelete(DeleteBehavior.NoAction)
.IsRequired(); .IsRequired();
b.OwnsMany("LiteCharms.Features.Models.PageReference", "References", b1 =>
{
b1.Property<long>("BookPageId");
b1.Property<int>("__synthesizedOrdinal")
.ValueGeneratedOnAdd();
b1.Property<string>("Description");
b1.Property<string>("Tag");
b1.Property<string>("Url");
b1.HasKey("BookPageId", "__synthesizedOrdinal");
b1.ToTable("BookPages");
b1
.ToJson("References")
.HasColumnType("jsonb");
b1.WithOwner()
.HasForeignKey("BookPageId");
});
b.Navigation("Book"); b.Navigation("Book");
b.Navigation("References");
}); });
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b =>
@@ -809,6 +888,35 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
b.Navigation("Order"); b.Navigation("Order");
}); });
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b =>
{
b.OwnsOne("LiteCharms.Features.Models.ProductMetadata", "Metadata", b1 =>
{
b1.Property<long>("ProductId");
b1.Property<string>("CopyrightInfo");
b1.Property<string>("ManufactureDate");
b1.Property<string>("Manufacturer");
b1.Property<string>("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 => modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b =>
{ {
b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product")
@@ -14,9 +14,10 @@ public sealed class ProductConfiguration : IEntityTypeConfiguration<Product>
builder.Property(f => f.Summary).IsRequired().HasMaxLength(512); builder.Property(f => f.Summary).IsRequired().HasMaxLength(512);
builder.Property(f => f.Description).HasMaxLength(1024); builder.Property(f => f.Description).HasMaxLength(1024);
builder.Property(f => f.ImageUrl).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.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(); });
} }
} }
@@ -3,5 +3,5 @@
[EntityTypeConfiguration<ProductPriceConfiguration, ProductPrice>] [EntityTypeConfiguration<ProductPriceConfiguration, ProductPrice>]
public class ProductPrice : Models.ProductPrice public class ProductPrice : Models.ProductPrice
{ {
public virtual Product Product { get; set; } = new(); public virtual Product? Product { get; set; }
} }
@@ -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; }
}
@@ -20,3 +20,10 @@ public sealed record CreateProduct
public ProductMetadata? Metadata { get; set; } public ProductMetadata? Metadata { get; set; }
} }
public sealed class CreateProductPrice
{
public decimal Amount { get; set; }
public decimal Discount { get; set; }
}
@@ -228,8 +228,8 @@ public sealed class ProductService(IDbContextFactory<MidrandBooksDbContext> cont
{ {
try try
{ {
var fromDate = range.From.ToDateTime(TimeOnly.MinValue); var fromDate = range.From.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc);
var toDate = range.To.ToDateTime(TimeOnly.MaxValue); var toDate = range.To.ToDateTime(TimeOnly.MaxValue, DateTimeKind.Utc);
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -244,7 +244,7 @@ public sealed class ProductService(IDbContextFactory<MidrandBooksDbContext> cont
.AsSplitQuery() .AsSplitQuery()
.ToArrayAsync(cancellationToken); .ToArrayAsync(cancellationToken);
return await context.SaveChangesAsync(cancellationToken) > 0 return products?.Length > 0
? Result.Ok(products.Select(p => p.ToModel()).ToArray()) ? Result.Ok(products.Select(p => p.ToModel()).ToArray())
: Result.Fail<Product[]>(new Error("Failed to retrieve products.")); : Result.Fail<Product[]>(new Error("Failed to retrieve products."));
} }