Holy moly......

This commit is contained in:
lumijiez
2025-06-26 01:30:25 +03:00
parent 3cdfbf8954
commit a8ea4b41ee
34 changed files with 238 additions and 42 deletions

View File

@@ -27,7 +27,7 @@ public class CreateAddressCommand : IRequest<AddressDto>
public bool IsActive { get; set; } = true;
}
public class CreateAddressHandler(
public class CreateAddress(
IUnitOfWork uw,
IMapper mapper,
ICurrentUserService userService)

View File

@@ -12,7 +12,7 @@ public class GetAddressesByUserIdQuery : IRequest<IEnumerable<AddressDto>>
public string? AddressType { get; set; }
}
public class GetAddressesByUserIdHandler(
public class GetAddressesByUserId(
IUnitOfWork uw,
IMapper mapper)
: IRequestHandler<GetAddressesByUserIdQuery, IEnumerable<AddressDto>>

View File

@@ -8,7 +8,7 @@ namespace Imprink.Application.Commands.Addresses;
public class GetMyAddressesQuery : IRequest<IEnumerable<AddressDto?>>;
public class GetMyAddressesHandler(
public class GetMyAddresses(
IUnitOfWork uw,
IMapper mapper,
ICurrentUserService userService)

View File

@@ -14,7 +14,7 @@ public class CreateCategoryCommand : IRequest<CategoryDto>
public Guid? ParentCategoryId { get; set; }
}
public class CreateCategoryHandler(
public class CreateCategory(
IUnitOfWork unitOfWork)
: IRequestHandler<CreateCategoryCommand, CategoryDto>
{

View File

@@ -7,7 +7,7 @@ public class DeleteCategoryCommand : IRequest<bool>
public Guid Id { get; init; }
}
public class DeleteCategoryHandler(
public class DeleteCategory(
IUnitOfWork unitOfWork)
: IRequestHandler<DeleteCategoryCommand, bool>
{

View File

@@ -10,7 +10,7 @@ public class GetCategoriesQuery : IRequest<IEnumerable<CategoryDto>>
public bool RootCategoriesOnly { get; set; } = false;
}
public class GetCategoriesHandler(
public class GetCategories(
IUnitOfWork unitOfWork)
: IRequestHandler<GetCategoriesQuery, IEnumerable<CategoryDto>>
{

View File

@@ -15,7 +15,7 @@ public class UpdateCategoryCommand : IRequest<CategoryDto>
public Guid? ParentCategoryId { get; set; }
}
public class UpdateCategoryHandler(
public class UpdateCategory(
IUnitOfWork unitOfWork)
: IRequestHandler<UpdateCategoryCommand, CategoryDto>
{

View File

@@ -12,17 +12,14 @@ public class CreateOrderCommand : IRequest<OrderDto>
public int Quantity { get; set; }
public Guid ProductId { get; set; }
public Guid ProductVariantId { get; set; }
public string? Notes { get; set; }
public string? MerchantId { get; set; }
public string? ComposingImageUrl { get; set; }
public string[]? OriginalImageUrls { get; set; } = [];
public string? CustomizationImageUrl { get; set; } = null!;
public string? CustomizationDescription { get; set; } = null!;
public Guid AddressId { get; set; }
}
public class CreateOrderHandler(
public class CreateOrder(
IUnitOfWork uw,
IMapper mapper,
ICurrentUserService userService)

View File

@@ -11,7 +11,7 @@ public class GetOrderByIdQuery : IRequest<OrderDto?>
public bool IncludeDetails { get; set; }
}
public class GetOrderByIdHandler(
public class GetOrderById(
IUnitOfWork uw,
IMapper mapper)
: IRequestHandler<GetOrderByIdQuery, OrderDto?>

View File

@@ -11,7 +11,7 @@ public class GetOrdersByMerchantIdQuery : IRequest<IEnumerable<OrderDto>>
public bool IncludeDetails { get; set; }
}
public class GetOrdersByMerchantIdHandler(
public class GetOrdersByMerchantId(
IUnitOfWork uw,
IMapper mapper)
: IRequestHandler<GetOrdersByMerchantIdQuery, IEnumerable<OrderDto>>

View File

@@ -11,7 +11,7 @@ public class GetOrdersByUserIdQuery : IRequest<IEnumerable<OrderDto>>
public bool IncludeDetails { get; set; }
}
public class GetOrdersByUserIdHandler(
public class GetOrdersByUserId(
IUnitOfWork uw,
IMapper mapper)
: IRequestHandler<GetOrdersByUserIdQuery, IEnumerable<OrderDto>>

View File

@@ -17,7 +17,7 @@ public class CreateProductVariantCommand : IRequest<ProductVariantDto>
public bool IsActive { get; set; } = true;
}
public class CreateProductVariantHandler(
public class CreateProductVariant(
IUnitOfWork unitOfWork,
IMapper mapper)
: IRequestHandler<CreateProductVariantCommand, ProductVariantDto>

View File

@@ -7,7 +7,7 @@ public class DeleteProductVariantCommand : IRequest<bool>
public Guid Id { get; set; }
}
public class DeleteProductVariantHandler(
public class DeleteProductVariant(
IUnitOfWork unitOfWork)
: IRequestHandler<DeleteProductVariantCommand, bool>
{

View File

@@ -12,7 +12,7 @@ public class GetProductVariantsQuery : IRequest<IEnumerable<ProductVariantDto>>
public bool InStockOnly { get; set; } = false;
}
public class GetProductVariantsHandler(
public class GetProductVariants(
IUnitOfWork unitOfWork,
IMapper mapper)
: IRequestHandler<GetProductVariantsQuery, IEnumerable<ProductVariantDto>>

View File

@@ -18,7 +18,7 @@ public class UpdateProductVariantCommand : IRequest<ProductVariantDto>
public bool IsActive { get; set; }
}
public class UpdateProductVariantHandler(
public class UpdateProductVariant(
IUnitOfWork unitOfWork,
IMapper mapper)
: IRequestHandler<UpdateProductVariantCommand, ProductVariantDto>

View File

@@ -16,7 +16,7 @@ public class CreateProductCommand : IRequest<ProductDto>
public Guid? CategoryId { get; set; }
}
public class CreateProductHandler(
public class CreateProduct(
IUnitOfWork uw,
IMapper mapper)
: IRequestHandler<CreateProductCommand, ProductDto>

View File

@@ -8,7 +8,7 @@ public class DeleteProductCommand : IRequest
public Guid Id { get; set; }
}
public class DeleteProductHandler(
public class DeleteProduct(
IUnitOfWork uw)
: IRequestHandler<DeleteProductCommand>
{

View File

@@ -0,0 +1,6 @@
namespace Imprink.Application.Commands.Products;
public class GetProductById
{
}

View File

@@ -9,7 +9,7 @@ public class GetProductsQuery : IRequest<PagedResultDto<ProductDto>>
public ProductFilterParameters FilterParameters { get; set; } = new();
}
public class GetProductsHandler(
public class GetProducts(
IUnitOfWork unitOfWork)
: IRequestHandler<GetProductsQuery, PagedResultDto<ProductDto>>
{

View File

@@ -4,7 +4,7 @@ using MediatR;
namespace Imprink.Application.Commands.Products;
public class UpdateProductCommand : IRequest<ProductDto>
public class UpdateProduct : IRequest<ProductDto>
{
public Guid Id { get; set; }
public string Name { get; set; } = null!;
@@ -18,10 +18,10 @@ public class UpdateProductCommand : IRequest<ProductDto>
public class UpdateProductHandler(
IUnitOfWork unitOfWork)
: IRequestHandler<UpdateProductCommand, ProductDto>
: IRequestHandler<UpdateProduct, ProductDto>
{
public async Task<ProductDto> Handle(
UpdateProductCommand request,
UpdateProduct request,
CancellationToken cancellationToken)
{
await unitOfWork.BeginTransactionAsync(cancellationToken);

View File

@@ -7,7 +7,7 @@ namespace Imprink.Application.Commands.Users;
public record DeleteUserRoleCommand(string Sub, Guid RoleId) : IRequest<UserRoleDto?>;
public class DeleteUserRoleHandler(
public class DeleteUserRole(
IUnitOfWork uw,
IMapper mapper)
: IRequestHandler<DeleteUserRoleCommand, UserRoleDto?>

View File

@@ -6,7 +6,7 @@ namespace Imprink.Application.Commands.Users;
public record GetAllRolesCommand : IRequest<IEnumerable<RoleDto>>;
public class GetAllRolesHandler(
public class GetAllRoles(
IUnitOfWork uw,
IMapper mapper): IRequestHandler<GetAllRolesCommand, IEnumerable<RoleDto>>
{

View File

@@ -7,7 +7,7 @@ namespace Imprink.Application.Commands.Users;
public record GetUserRolesCommand(string Sub) : IRequest<IEnumerable<RoleDto>>;
public class GetUserRolesHandler(
public class GetUserRoles(
IUnitOfWork uw,
IMapper mapper)
: IRequestHandler<GetUserRolesCommand, IEnumerable<RoleDto>>

View File

@@ -8,7 +8,7 @@ namespace Imprink.Application.Commands.Users;
public record SetUserFullNameCommand(string FirstName, string LastName) : IRequest<UserDto?>;
public class SetUserFullNameHandler(
public class SetUserFullName(
IUnitOfWork uw,
IMapper mapper,
ICurrentUserService userService)

View File

@@ -8,7 +8,7 @@ namespace Imprink.Application.Commands.Users;
public record SetUserPhoneCommand(string PhoneNumber) : IRequest<UserDto?>;
public class SetUserPhoneHandler(
public class SetUserPhone(
IUnitOfWork uw,
IMapper mapper,
ICurrentUserService userService)

View File

@@ -8,7 +8,7 @@ namespace Imprink.Application.Commands.Users;
public record SetUserRoleCommand(string Sub, Guid RoleId) : IRequest<UserRoleDto?>;
public class SetUserRoleHandler(
public class SetUserRole(
IUnitOfWork uw,
IMapper mapper)
: IRequestHandler<SetUserRoleCommand, UserRoleDto?>

View File

@@ -7,7 +7,7 @@ namespace Imprink.Application.Commands.Users;
public record SyncUserCommand(Auth0User User) : IRequest<UserDto?>;
public class SyncUserHandler(
public class SyncUser(
IUnitOfWork uw,
IMapper mapper)
: IRequestHandler<SyncUserCommand, UserDto?>

View File

@@ -0,0 +1,117 @@
using FluentValidation;
using Imprink.Application.Commands.Addresses;
namespace Imprink.Application.Validation.Addresses;
public class CreateAddressCommandValidator : AbstractValidator<CreateAddressCommand>
{
public CreateAddressCommandValidator()
{
RuleFor(x => x.AddressType)
.NotEmpty()
.WithMessage("Address type is required.")
.MaximumLength(50)
.WithMessage("Address type must not exceed 50 characters.")
.Must(BeValidAddressType)
.WithMessage("Address type must be one of: Home, Work, Billing, Shipping, Other.");
RuleFor(x => x.FirstName)
.MaximumLength(100)
.WithMessage("First name must not exceed 100 characters.")
.Matches(@"^[a-zA-Z\s\-'\.]*$")
.When(x => !string.IsNullOrEmpty(x.FirstName))
.WithMessage("First name can only contain letters, spaces, hyphens, apostrophes, and periods.");
RuleFor(x => x.LastName)
.MaximumLength(100)
.WithMessage("Last name must not exceed 100 characters.")
.Matches(@"^[a-zA-Z\s\-'\.]*$")
.When(x => !string.IsNullOrEmpty(x.LastName))
.WithMessage("Last name can only contain letters, spaces, hyphens, apostrophes, and periods.");
RuleFor(x => x.Company)
.MaximumLength(200)
.WithMessage("Company name must not exceed 200 characters.");
RuleFor(x => x.AddressLine1)
.NotEmpty()
.WithMessage("Address line 1 is required.")
.MaximumLength(255)
.WithMessage("Address line 1 must not exceed 255 characters.");
RuleFor(x => x.AddressLine2)
.MaximumLength(255)
.WithMessage("Address line 2 must not exceed 255 characters.");
RuleFor(x => x.ApartmentNumber)
.MaximumLength(20)
.WithMessage("Apartment number must not exceed 20 characters.");
RuleFor(x => x.BuildingNumber)
.MaximumLength(20)
.WithMessage("Building number must not exceed 20 characters.");
RuleFor(x => x.Floor)
.MaximumLength(20)
.WithMessage("Floor must not exceed 20 characters.");
RuleFor(x => x.City)
.NotEmpty()
.WithMessage("City is required.")
.MaximumLength(100)
.WithMessage("City must not exceed 100 characters.")
.Matches(@"^[a-zA-Z\s\-'\.]*$")
.WithMessage("City can only contain letters, spaces, hyphens, apostrophes, and periods.");
RuleFor(x => x.State)
.NotEmpty()
.WithMessage("State is required.")
.MaximumLength(100)
.WithMessage("State must not exceed 100 characters.");
RuleFor(x => x.PostalCode)
.NotEmpty()
.WithMessage("Postal code is required.")
.MaximumLength(20)
.WithMessage("Postal code must not exceed 20 characters.")
.Matches(@"^[a-zA-Z0-9\s\-]*$")
.WithMessage("Postal code can only contain letters, numbers, spaces, and hyphens.");
RuleFor(x => x.Country)
.NotEmpty()
.WithMessage("Country is required.")
.MaximumLength(100)
.WithMessage("Country must not exceed 100 characters.")
.Matches(@"^[a-zA-Z\s\-'\.]*$")
.WithMessage("Country can only contain letters, spaces, hyphens, apostrophes, and periods.");
RuleFor(x => x.PhoneNumber)
.MaximumLength(20)
.WithMessage("Phone number must not exceed 20 characters.")
.Matches(@"^[\+]?[0-9\s\-\(\)\.]*$")
.When(x => !string.IsNullOrEmpty(x.PhoneNumber))
.WithMessage("Phone number format is invalid. Use numbers, spaces, hyphens, parentheses, periods, and optional + prefix.");
RuleFor(x => x.Instructions)
.MaximumLength(500)
.WithMessage("Instructions must not exceed 500 characters.");
RuleFor(x => x)
.Must(HaveNameOrCompany)
.WithMessage("Either first name and last name, or company name must be provided.")
.WithName("Address");
}
private static bool BeValidAddressType(string addressType)
{
var validTypes = new[] { "Home", "Work", "Billing", "Shipping", "Other" };
return validTypes.Contains(addressType, StringComparer.OrdinalIgnoreCase);
}
private static bool HaveNameOrCompany(CreateAddressCommand command)
{
var hasName = !string.IsNullOrWhiteSpace(command.FirstName) && !string.IsNullOrWhiteSpace(command.LastName);
var hasCompany = !string.IsNullOrWhiteSpace(command.Company);
return hasName || hasCompany;
}
}

View File

@@ -0,0 +1,76 @@
using FluentValidation;
using Imprink.Application.Commands.Orders;
namespace Imprink.Application.Validation.Orders;
public class CreateOrderCommandValidator : AbstractValidator<CreateOrderCommand>
{
public CreateOrderCommandValidator()
{
RuleFor(x => x.Quantity)
.GreaterThan(0)
.WithMessage("Quantity must be greater than 0.")
.LessThanOrEqualTo(1000)
.WithMessage("Quantity cannot exceed 1000 items per order.");
RuleFor(x => x.ProductId)
.NotEmpty()
.WithMessage("Product ID is required.");
RuleFor(x => x.ProductVariantId)
.NotEmpty()
.WithMessage("Product variant ID is required.");
RuleFor(x => x.AddressId)
.NotEmpty()
.WithMessage("Address ID is required.");
RuleFor(x => x.ComposingImageUrl)
.MaximumLength(2048)
.WithMessage("Composing image URL must not exceed 2048 characters.")
.Must(BeValidUrl)
.When(x => !string.IsNullOrEmpty(x.ComposingImageUrl))
.WithMessage("Composing image URL must be a valid URL.");
RuleFor(x => x.CustomizationImageUrl)
.MaximumLength(2048)
.WithMessage("Customization image URL must not exceed 2048 characters.")
.Must(BeValidUrl)
.When(x => !string.IsNullOrEmpty(x.CustomizationImageUrl))
.WithMessage("Customization image URL must be a valid URL.");
RuleFor(x => x.CustomizationDescription)
.MaximumLength(2000)
.WithMessage("Customization description must not exceed 2000 characters.");
RuleFor(x => x.OriginalImageUrls)
.Must(HaveValidImageUrls)
.When(x => x.OriginalImageUrls != null && x.OriginalImageUrls.Length > 0)
.WithMessage("All original image URLs must be valid URLs.")
.Must(x => x == null || x.Length <= 10)
.WithMessage("Cannot have more than 10 original images.");
RuleForEach(x => x.OriginalImageUrls)
.MaximumLength(2048)
.WithMessage("Each original image URL must not exceed 2048 characters.")
.Must(BeValidUrl)
.WithMessage("Each original image URL must be a valid URL.");
}
private static bool BeValidUrl(string url)
{
if (string.IsNullOrEmpty(url))
return true;
return Uri.TryCreate(url, UriKind.Absolute, out var result)
&& (result.Scheme == Uri.UriSchemeHttp || result.Scheme == Uri.UriSchemeHttps);
}
private static bool HaveValidImageUrls(string[]? urls)
{
if (urls == null || urls.Length == 0)
return true;
return urls.All(url => !string.IsNullOrEmpty(url) && BeValidUrl(url));
}
}

View File

@@ -3,7 +3,7 @@ using Imprink.Application.Commands.Products;
namespace Imprink.Application.Validation.Products;
public class UpdateProductCommandValidator : AbstractValidator<UpdateProductCommand>
public class UpdateProductCommandValidator : AbstractValidator<UpdateProduct>
{
public UpdateProductCommandValidator()
{

View File

@@ -34,7 +34,7 @@ public class ProductsController(IMediator mediator) : ControllerBase
[Authorize(Roles = "Admin")]
public async Task<ActionResult<ProductDto>> UpdateProduct(
Guid id,
[FromBody] UpdateProductCommand command)
[FromBody] UpdateProduct command)
{
if (id != command.Id) return BadRequest("ID mismatch");

View File

@@ -24,7 +24,7 @@ public static class Startup
services.AddDatabaseContexts(builder.Configuration);
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(CreateProductHandler).Assembly));
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(CreateProduct).Assembly));
services.AddValidatorsFromAssembly(typeof(Auth0UserValidator).Assembly);

View File

@@ -15,7 +15,7 @@ public class CreateCategoryHandlerIntegrationTest : IDisposable
{
private readonly ApplicationDbContext _context;
private readonly IServiceProvider _serviceProvider;
private readonly CreateCategoryHandler _handler;
private readonly CreateCategory _handler;
private readonly SqliteConnection _connection;
public CreateCategoryHandlerIntegrationTest()
@@ -38,11 +38,11 @@ public class CreateCategoryHandlerIntegrationTest : IDisposable
services.AddScoped<IAddressRepository, AddressRepository>();
services.AddScoped<IUnitOfWork, UnitOfWork>();
services.AddScoped<CreateCategoryHandler>();
services.AddScoped<CreateCategory>();
_serviceProvider = services.BuildServiceProvider();
_context = _serviceProvider.GetRequiredService<ApplicationDbContext>();
_handler = _serviceProvider.GetRequiredService<CreateCategoryHandler>();
_handler = _serviceProvider.GetRequiredService<CreateCategory>();
_context.Database.EnsureCreated();
}
@@ -216,7 +216,7 @@ public class CreateCategoryHandlerIntegrationTest : IDisposable
var initialCount = await _context.Categories.CountAsync();
var handler = _serviceProvider.GetRequiredService<CreateCategoryHandler>();
var handler = _serviceProvider.GetRequiredService<CreateCategory>();
var result = await handler.Handle(command, CancellationToken.None);

View File

@@ -6,13 +6,13 @@ using Moq;
namespace Imprink.Application.Tests;
public class UpdateCategoryHandlerTests
public class UpdateCategoryTests
{
private readonly Mock<IUnitOfWork> _unitOfWorkMock;
private readonly Mock<ICategoryRepository> _categoryRepositoryMock;
private readonly UpdateCategoryHandler _handler;
private readonly UpdateCategory _handler;
public UpdateCategoryHandlerTests()
public UpdateCategoryTests()
{
_unitOfWorkMock = new Mock<IUnitOfWork>();
_categoryRepositoryMock = new Mock<ICategoryRepository>();
@@ -20,7 +20,7 @@ public class UpdateCategoryHandlerTests
_unitOfWorkMock.Setup(x => x.CategoryRepository)
.Returns(_categoryRepositoryMock.Object);
_handler = new UpdateCategoryHandler(_unitOfWorkMock.Object);
_handler = new UpdateCategory(_unitOfWorkMock.Object);
}
[Fact]