Retructured solution

This commit is contained in:
Khwezi Mngoma
2026-05-13 20:06:24 +02:00
parent 26075cd9a7
commit a42c51d7b2
231 changed files with 1618 additions and 1408 deletions
@@ -0,0 +1,7 @@
namespace LiteCharms.Features.Shop.Products.Entities;
[EntityTypeConfiguration<ProductConfiguration, Product>]
public class Product : Models.Product
{
public virtual ICollection<ProductPrice>? ProductPrices { get; set; }
}
@@ -0,0 +1,17 @@
namespace LiteCharms.Features.Shop.Products.Entities;
public class ProductConfiguration : IEntityTypeConfiguration<Product>
{
public void Configure(EntityTypeBuilder<Product> builder)
{
builder.ToTable(nameof(Product));
builder.HasKey(f => f.Id);
builder.Property(f => f.Name).IsRequired();
builder.Property(f => f.Summary).IsRequired().HasMaxLength(512);
builder.Property(f => f.Description).IsRequired().HasMaxLength(2048);
builder.Property(f => f.ImageUrl).IsRequired(false).HasMaxLength(2048);
builder.Property(f => f.Thumbnails).HasColumnType("jsonb").IsRequired(false);
builder.Property(f => f.Active).HasDefaultValue(true);
}
}
@@ -0,0 +1,7 @@
namespace LiteCharms.Features.Shop.Products.Entities;
[EntityTypeConfiguration<ProductPriceConfiguration, ProductPrice>]
public class ProductPrice : Models.ProductPrice
{
public virtual Product? Product { get; set; }
}
@@ -0,0 +1,23 @@
namespace LiteCharms.Features.Shop.Products.Entities;
public class ProductPriceConfiguration : IEntityTypeConfiguration<ProductPrice>
{
public void Configure(EntityTypeBuilder<ProductPrice> builder)
{
builder.ToTable(nameof(ProductPrice));
builder.HasKey(f => f.Id);
builder.Property(f => f.CreatedAt).ValueGeneratedOnAdd().HasDefaultValueSql("now()");
builder.Property(f => f.UpdatedAt).IsRequired(false);
builder.Property(f => f.ProductId).IsRequired();
builder.Property(f => f.Price).IsRequired().HasPrecision(18, 2);
builder.Property(f => f.Discount).HasPrecision(18, 2);
builder.Property(f => f.Active);
builder.HasOne(f => f.Product)
.WithMany(f => f.ProductPrices)
.HasForeignKey(pp => pp.ProductId)
.IsRequired()
.OnDelete(DeleteBehavior.Restrict);
}
}
@@ -0,0 +1,18 @@
namespace LiteCharms.Features.Shop.Products.Models;
public class Product
{
public Guid Id { get; set; }
public string? Name { get; set; }
public string? Summary { get; set; }
public string? Description { get; set; }
public string? ImageUrl { get; set; }
public string[]? Thumbnails { get; set; }
public bool Active { get; set; }
}
@@ -0,0 +1,18 @@
namespace LiteCharms.Features.Shop.Products.Models;
public class ProductPrice
{
public Guid Id { get; set; }
public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset? UpdatedAt { get; set; }
public Guid ProductId { get; set; }
public decimal Price { get; set; }
public decimal Discount { get; set; }
public bool Active { get; set; }
}
@@ -0,0 +1,18 @@
using LiteCharms.Features.Shop.Products.Models;
namespace LiteCharms.Features.Products.Queries;
public class GetProductPriceQuery : IRequest<Result<ProductPrice>>
{
public Guid ProductId { get; set; }
private GetProductPriceQuery(Guid productId) => ProductId = productId;
public static GetProductPriceQuery Create(Guid productId)
{
if (productId == Guid.Empty)
throw new ArgumentException("ProductId is required.", nameof(productId));
return new(productId);
}
}
@@ -0,0 +1,18 @@
using LiteCharms.Features.Shop.Products.Models;
namespace LiteCharms.Features.Products.Queries;
public class GetProductPricesQuery : IRequest<Result<ProductPrice[]>>
{
public int MaxRecords { get; set; }
private GetProductPricesQuery(int maxRecords = 1000) => MaxRecords = maxRecords;
public static GetProductPricesQuery Create(int maxRecords = 1000)
{
if (maxRecords <= 0)
throw new ArgumentOutOfRangeException(nameof(maxRecords), "MaxRecords must be greater than zero.");
return new(maxRecords);
}
}
@@ -0,0 +1,18 @@
using LiteCharms.Features.Shop.Products.Models;
namespace LiteCharms.Features.Products.Queries;
public class GetProductQuery : IRequest<Result<Product>>
{
public Guid ProductId { get; set; }
private GetProductQuery(Guid productId) => ProductId = productId;
public static GetProductQuery Create(Guid productId)
{
if(productId == Guid.Empty)
throw new ArgumentException("Product ID is required.", nameof(productId));
return new(productId);
}
}
@@ -0,0 +1,18 @@
using LiteCharms.Features.Shop.Products.Models;
namespace LiteCharms.Features.Products.Queries;
public class GetProductsQuery : IRequest<Result<Product[]>>
{
public int MaxRecords { get; set; }
private GetProductsQuery(int maxRecords = 1000) => MaxRecords = maxRecords;
public static GetProductsQuery Create(int maxRecords = 1000)
{
if (maxRecords <= 0)
throw new ArgumentException("MaxRecords must be a positive integer.");
return new(maxRecords);
}
}
@@ -0,0 +1,32 @@
using LiteCharms.Extensions;
using LiteCharms.Features.Shop.Postgres;
using LiteCharms.Features.Shop.Products.Models;
namespace LiteCharms.Features.Products.Queries.Handlers;
public class GetProductPriceQueryHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<GetProductPriceQuery, Result<ProductPrice>>
{
public async ValueTask<Result<ProductPrice>> Handle(GetProductPriceQuery request, CancellationToken cancellationToken)
{
try
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if(!await context.Products.AnyAsync(p => p.Id == request.ProductId, cancellationToken))
return Result.Fail<ProductPrice>(new Error($"Product {request.ProductId} not found."));
var productPrice = await context.ProductPrices.AsNoTracking()
.Where(pp => pp.ProductId == request.ProductId && pp.Active)
.OrderByDescending(pp => pp.CreatedAt)
.FirstOrDefaultAsync(cancellationToken);
return productPrice is not null
? Result.Ok(productPrice.ToModel())
: Result.Fail<ProductPrice>(new Error($"Product price {request.ProductId} not found."));
}
catch (Exception ex)
{
return Result.Fail<ProductPrice>(new Error(ex.Message).CausedBy(ex));
}
}
}
@@ -0,0 +1,27 @@
using LiteCharms.Extensions;
using LiteCharms.Features.Shop.Postgres;
using LiteCharms.Features.Shop.Products.Models;
namespace LiteCharms.Features.Products.Queries.Handlers;
public class GetProductPricesQueryHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<GetProductPricesQuery, Result<ProductPrice[]>>
{
public async ValueTask<Result<ProductPrice[]>> Handle(GetProductPricesQuery request, CancellationToken cancellationToken)
{
try
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var products = await context.ProductPrices.AsNoTracking()
.OrderByDescending(o => o.Id)
.Take(request.MaxRecords)
.ToArrayAsync(cancellationToken);
return Result.Ok(products.Select(p => p.ToModel()).ToArray());
}
catch (Exception ex)
{
return Result.Fail<ProductPrice[]>(new Error(ex.Message).CausedBy(ex));
}
}
}
@@ -0,0 +1,26 @@
using LiteCharms.Extensions;
using LiteCharms.Features.Shop.Postgres;
using LiteCharms.Features.Shop.Products.Models;
namespace LiteCharms.Features.Products.Queries.Handlers;
public class GetProductQueryHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<GetProductQuery, Result<Product>>
{
public async ValueTask<Result<Product>> Handle(GetProductQuery request, CancellationToken cancellationToken)
{
try
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var product = await context.Products.FirstOrDefaultAsync(p => p.Id == request.ProductId, cancellationToken);
return product is not null
? Result.Ok(product.ToModel())
: Result.Fail<Product>(new Error($"Product with ID {request.ProductId} not found."));
}
catch (Exception ex)
{
return Result.Fail<Product>(new Error(ex.Message).CausedBy(ex));
}
}
}
@@ -0,0 +1,27 @@
using LiteCharms.Extensions;
using LiteCharms.Features.Shop.Postgres;
using LiteCharms.Features.Shop.Products.Models;
namespace LiteCharms.Features.Products.Queries.Handlers;
public class GetProductsQueryHandler(IDbContextFactory<ShopDbContext> contextFactory) : IRequestHandler<GetProductsQuery, Result<Product[]>>
{
public async ValueTask<Result<Product[]>> Handle(GetProductsQuery request, CancellationToken cancellationToken)
{
try
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var products = await context.Products.AsNoTracking()
.OrderByDescending(o => o.Id)
.Take(request.MaxRecords)
.ToArrayAsync(cancellationToken);
return Result.Ok(products.Select(p => p.ToModel()).ToArray());
}
catch (Exception ex)
{
return Result.Fail<Product[]>(new Error(ex.Message).CausedBy(ex));
}
}
}