Completed refactor

This commit is contained in:
Khwezi Mngoma
2026-05-14 01:33:21 +02:00
parent 42001998d6
commit 134d8429c0
129 changed files with 1870 additions and 3165 deletions
@@ -0,0 +1,14 @@
namespace LiteCharms.Features.Shop.Products.Models;
public record CreateProduct
{
public required string Name { get; set; }
public required string Summary { get; set; }
public required string Description { get; set; }
public required string ImageUrl { get; set; }
public string[]? Thumbnails { get; set; }
}
@@ -0,0 +1,229 @@
using LiteCharms.Features.Extensions;
using LiteCharms.Features.Shop.Postgres;
using LiteCharms.Features.Shop.Products.Models;
using static LiteCharms.Features.Extensions.Timezones;
namespace LiteCharms.Features.Shop.Products;
public class ProductService(IDbContextFactory<ShopDbContext> contextFactory)
{
public async ValueTask<Result> ChangeProductPriceStatusAsync(Guid productPriceId, bool active, CancellationToken cancellationToken = default)
{
try
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var price = await context.ProductPrices.FirstOrDefaultAsync(p => p.Id == productPriceId, cancellationToken);
if (price is null)
return Result.Fail($"Could not find product price with ID {productPriceId}");
price.Active = active;
price.UpdatedAt = SouthAfricanTimeZone.UtcNow();
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok()
: Result.Fail($"Failed to change product price by ID {productPriceId}");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> ChangeProductStatusAsync(Guid productId, bool active, CancellationToken cancellationToken = default)
{
try
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var product = await context.Products.FirstOrDefaultAsync(p => p.Id == productId, cancellationToken);
if (product is null)
return Result.Fail($"Could not find product with ID {productId}");
product.Active = active;
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok()
: Result.Fail($"Failed to change product status by ID {productId}");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Guid>> CreateProductAsync(CreateProduct request, CancellationToken cancellationToken = default)
{
try
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (!await context.Products.AnyAsync(p => p.Name == request.Name, cancellationToken))
return Result.Fail<Guid>($"A product by the same name '{request.Name}' already exists");
var newProduct = context.Products.Add(new Entities.Product
{
Name = request.Name,
Summary = request.Summary,
Description = request.Description,
ImageUrl = request.ImageUrl,
Thumbnails = request.Thumbnails
});
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok(newProduct.Entity.Id)
: Result.Fail($"Failed to create new product '{request.Name}'");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Guid>> CreateProductPriceAsync(Guid productId, decimal price, decimal discount = 0, CancellationToken cancellationToken = default)
{
try
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var newProductPrice = context.ProductPrices.Add(new Entities.ProductPrice
{
Price = price,
Discount = discount,
ProductId = productId
});
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok(newProductPrice.Entity.Id)
: Result.Fail($"Failed to create new product price for product id {productId}");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Product>> GetProductAsync(Guid productId, CancellationToken cancellationToken = default)
{
try
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var product = await context.Products.FirstOrDefaultAsync(p => p.Id == productId, cancellationToken);
return product is not null
? Result.Ok(product.ToModel())
: Result.Fail<Product>(new Error($"Product with ID {productId} not found."));
}
catch (Exception ex)
{
return Result.Fail<Product>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<ProductPrice>> GetProductPriceAsync(Guid productPriceId, CancellationToken cancellationToken = default)
{
try
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (!await context.Products.AnyAsync(p => p.Id == productPriceId, cancellationToken))
return Result.Fail<ProductPrice>(new Error($"Product {productPriceId} not found."));
var productPrice = await context.ProductPrices.AsNoTracking()
.OrderByDescending(pp => pp.CreatedAt)
.FirstOrDefaultAsync(pp => pp.Id == productPriceId, cancellationToken);
return productPrice is not null
? Result.Ok(productPrice.ToModel())
: Result.Fail<ProductPrice>(new Error($"Product price {productPriceId} not found."));
}
catch (Exception ex)
{
return Result.Fail<ProductPrice>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<ProductPrice[]>> GetProductPricesAsync(int maxRecords = 1000, CancellationToken cancellationToken = default)
{
try
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var products = await context.ProductPrices.AsNoTracking()
.OrderByDescending(o => o.Id)
.Take(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));
}
}
public async ValueTask<Result<Product[]>> GetProductsAsync(int maxRecords = 1000, CancellationToken cancellationToken = default)
{
try
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var products = await context.Products.AsNoTracking()
.OrderByDescending(o => o.Id)
.Take(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));
}
}
public async ValueTask<Result<Guid>> ReplaceProductPriceAsync(Guid productPriceId, decimal price, decimal discount = 0, CancellationToken cancellationToken = default)
{
try
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var existingPrice = await context.ProductPrices.FirstOrDefaultAsync(p => p.Id == productPriceId, cancellationToken);
if (existingPrice is null)
return Result.Fail($"Could not find product price with ID {productPriceId}");
existingPrice.Active = false;
existingPrice.UpdatedAt = SouthAfricanTimeZone.UtcNow();
if (!(await context.SaveChangesAsync(cancellationToken) > 0))
return Result.Fail<Guid>($"Failed to deactivate existing price of ID {productPriceId}, try again later");
var result = await CreateProductPriceAsync(existingPrice.ProductId, price, discount, cancellationToken);
if(result.IsFailed)
{
var deactivatedPrice = await context.ProductPrices.FirstOrDefaultAsync(p => p.Id == productPriceId, cancellationToken);
existingPrice.Active = true;
existingPrice.UpdatedAt = SouthAfricanTimeZone.UtcNow();
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Fail<Guid>("Reverted to old price, creation of new price failed")
: Result.Fail<Guid>($"Failed to reactivate price of ID {productPriceId} after new price creation failed");
}
return Result.Ok(result.Value);
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
}
@@ -1,18 +0,0 @@
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);
}
}
@@ -1,18 +0,0 @@
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);
}
}
@@ -1,18 +0,0 @@
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);
}
}
@@ -1,18 +0,0 @@
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);
}
}
@@ -1,32 +0,0 @@
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));
}
}
}
@@ -1,27 +0,0 @@
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));
}
}
}
@@ -1,26 +0,0 @@
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));
}
}
}
@@ -1,27 +0,0 @@
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));
}
}
}