diff --git a/src/Imprink.Application/Products/Create/CreateCategoryHandler.cs b/src/Imprink.Application/Domains/Categories/CreateCategoryHandler.cs similarity index 97% rename from src/Imprink.Application/Products/Create/CreateCategoryHandler.cs rename to src/Imprink.Application/Domains/Categories/CreateCategoryHandler.cs index 6b3cda4..2b67a75 100644 --- a/src/Imprink.Application/Products/Create/CreateCategoryHandler.cs +++ b/src/Imprink.Application/Domains/Categories/CreateCategoryHandler.cs @@ -2,7 +2,7 @@ using Imprink.Application.Products.Dtos; using Imprink.Domain.Entities.Product; using MediatR; -namespace Imprink.Application.Products.Create; +namespace Imprink.Application.Domains.Categories; public class CreateCategoryCommand : IRequest { diff --git a/src/Imprink.Application/Products/Delete/DeleteCategoryHandler.cs b/src/Imprink.Application/Domains/Categories/DeleteCategoryHandler.cs similarity index 95% rename from src/Imprink.Application/Products/Delete/DeleteCategoryHandler.cs rename to src/Imprink.Application/Domains/Categories/DeleteCategoryHandler.cs index fe6dfc2..b3d5850 100644 --- a/src/Imprink.Application/Products/Delete/DeleteCategoryHandler.cs +++ b/src/Imprink.Application/Domains/Categories/DeleteCategoryHandler.cs @@ -1,6 +1,6 @@ using MediatR; -namespace Imprink.Application.Products.Delete; +namespace Imprink.Application.Domains.Categories; public class DeleteCategoryCommand : IRequest { diff --git a/src/Imprink.Application/Products/Dtos/CategoryDto.cs b/src/Imprink.Application/Domains/Categories/Dtos/CategoryDto.cs similarity index 100% rename from src/Imprink.Application/Products/Dtos/CategoryDto.cs rename to src/Imprink.Application/Domains/Categories/Dtos/CategoryDto.cs diff --git a/src/Imprink.Application/Products/Query/GetCategoriesHandler.cs b/src/Imprink.Application/Domains/Categories/GetCategoriesHandler.cs similarity index 96% rename from src/Imprink.Application/Products/Query/GetCategoriesHandler.cs rename to src/Imprink.Application/Domains/Categories/GetCategoriesHandler.cs index 0baf581..a0b13f1 100644 --- a/src/Imprink.Application/Products/Query/GetCategoriesHandler.cs +++ b/src/Imprink.Application/Domains/Categories/GetCategoriesHandler.cs @@ -2,7 +2,7 @@ using Imprink.Application.Products.Dtos; using Imprink.Domain.Entities.Product; using MediatR; -namespace Imprink.Application.Products.Query; +namespace Imprink.Application.Domains.Categories; public class GetCategoriesQuery : IRequest> { diff --git a/src/Imprink.Application/Domains/Categories/UpdateCategoryHandler.cs b/src/Imprink.Application/Domains/Categories/UpdateCategoryHandler.cs new file mode 100644 index 0000000..a10eb1f --- /dev/null +++ b/src/Imprink.Application/Domains/Categories/UpdateCategoryHandler.cs @@ -0,0 +1,62 @@ +using Imprink.Application.Exceptions; +using Imprink.Application.Products.Dtos; +using MediatR; + +namespace Imprink.Application.Domains.Categories; + +public class UpdateCategoryCommand : IRequest +{ + public Guid Id { get; set; } + public string Name { get; set; } = null!; + public string Description { get; set; } = null!; + public string? ImageUrl { get; set; } + public int SortOrder { get; set; } + public bool IsActive { get; set; } + public Guid? ParentCategoryId { get; set; } +} + +public class UpdateCategoryHandler(IUnitOfWork unitOfWork) : IRequestHandler +{ + public async Task Handle(UpdateCategoryCommand request, CancellationToken cancellationToken) + { + await unitOfWork.BeginTransactionAsync(cancellationToken); + + try + { + var existingCategory = await unitOfWork.CategoryRepository.GetByIdAsync(request.Id, cancellationToken); + + if (existingCategory == null) + { + throw new NotFoundException($"Category with ID {request.Id} not found."); + } + + existingCategory.Name = request.Name; + existingCategory.Description = request.Description; + existingCategory.ImageUrl = request.ImageUrl; + existingCategory.SortOrder = request.SortOrder; + existingCategory.IsActive = request.IsActive; + existingCategory.ParentCategoryId = request.ParentCategoryId; + + var updatedCategory = await unitOfWork.CategoryRepository.UpdateAsync(existingCategory, cancellationToken); + await unitOfWork.CommitTransactionAsync(cancellationToken); + + return new CategoryDto + { + Id = updatedCategory.Id, + Name = updatedCategory.Name, + Description = updatedCategory.Description, + ImageUrl = updatedCategory.ImageUrl, + SortOrder = updatedCategory.SortOrder, + IsActive = updatedCategory.IsActive, + ParentCategoryId = updatedCategory.ParentCategoryId, + CreatedAt = updatedCategory.CreatedAt, + ModifiedAt = updatedCategory.ModifiedAt + }; + } + catch + { + await unitOfWork.RollbackTransactionAsync(cancellationToken); + throw; + } + } +} \ No newline at end of file diff --git a/src/Imprink.Application/Products/Create/CreateProductVariantHandler.cs b/src/Imprink.Application/Domains/ProductVariants/CreateProductVariantHandler.cs similarity index 97% rename from src/Imprink.Application/Products/Create/CreateProductVariantHandler.cs rename to src/Imprink.Application/Domains/ProductVariants/CreateProductVariantHandler.cs index c01eacd..9a6aef9 100644 --- a/src/Imprink.Application/Products/Create/CreateProductVariantHandler.cs +++ b/src/Imprink.Application/Domains/ProductVariants/CreateProductVariantHandler.cs @@ -2,7 +2,7 @@ using Imprink.Application.Products.Dtos; using Imprink.Domain.Entities.Product; using MediatR; -namespace Imprink.Application.Products.Create; +namespace Imprink.Application.Domains.ProductVariants; public class CreateProductVariantCommand : IRequest { diff --git a/src/Imprink.Application/Products/Delete/DeleteProductVariantHandler.cs b/src/Imprink.Application/Domains/ProductVariants/DeleteProductVariantHandler.cs similarity index 95% rename from src/Imprink.Application/Products/Delete/DeleteProductVariantHandler.cs rename to src/Imprink.Application/Domains/ProductVariants/DeleteProductVariantHandler.cs index 410fb3a..10e257e 100644 --- a/src/Imprink.Application/Products/Delete/DeleteProductVariantHandler.cs +++ b/src/Imprink.Application/Domains/ProductVariants/DeleteProductVariantHandler.cs @@ -1,6 +1,6 @@ using MediatR; -namespace Imprink.Application.Products.Delete; +namespace Imprink.Application.Domains.ProductVariants; public class DeleteProductVariantCommand : IRequest { diff --git a/src/Imprink.Application/Products/Dtos/ProductVariantDto.cs b/src/Imprink.Application/Domains/ProductVariants/Dtos/ProductVariantDto.cs similarity index 100% rename from src/Imprink.Application/Products/Dtos/ProductVariantDto.cs rename to src/Imprink.Application/Domains/ProductVariants/Dtos/ProductVariantDto.cs diff --git a/src/Imprink.Application/Products/Query/GetProductVariantsHandler.cs b/src/Imprink.Application/Domains/ProductVariants/GetProductVariantsHandler.cs similarity index 97% rename from src/Imprink.Application/Products/Query/GetProductVariantsHandler.cs rename to src/Imprink.Application/Domains/ProductVariants/GetProductVariantsHandler.cs index adfa831..609036b 100644 --- a/src/Imprink.Application/Products/Query/GetProductVariantsHandler.cs +++ b/src/Imprink.Application/Domains/ProductVariants/GetProductVariantsHandler.cs @@ -2,7 +2,7 @@ using Imprink.Application.Products.Dtos; using Imprink.Domain.Entities.Product; using MediatR; -namespace Imprink.Application.Products.Query; +namespace Imprink.Application.Domains.ProductVariants; public class GetProductVariantsQuery : IRequest> { diff --git a/src/Imprink.Application/Domains/ProductVariants/UpdateProductVariantHandler.cs b/src/Imprink.Application/Domains/ProductVariants/UpdateProductVariantHandler.cs new file mode 100644 index 0000000..b7574b1 --- /dev/null +++ b/src/Imprink.Application/Domains/ProductVariants/UpdateProductVariantHandler.cs @@ -0,0 +1,69 @@ +using Imprink.Application.Exceptions; +using Imprink.Application.Products.Dtos; +using MediatR; + +namespace Imprink.Application.Domains.ProductVariants; + +public class UpdateProductVariantCommand : IRequest +{ + public Guid Id { get; set; } + public Guid ProductId { get; set; } + public string Size { get; set; } = null!; + public string? Color { get; set; } + public decimal Price { get; set; } + public string? ImageUrl { get; set; } + public string Sku { get; set; } = null!; + public int StockQuantity { get; set; } + public bool IsActive { get; set; } +} + +public class UpdateProductVariantHandler(IUnitOfWork unitOfWork) + : IRequestHandler +{ + public async Task Handle(UpdateProductVariantCommand request, CancellationToken cancellationToken) + { + await unitOfWork.BeginTransactionAsync(cancellationToken); + + try + { + var existingVariant = await unitOfWork.ProductVariantRepository.GetByIdAsync(request.Id, cancellationToken); + + if (existingVariant == null) + { + throw new NotFoundException($"Product variant with ID {request.Id} not found."); + } + + existingVariant.ProductId = request.ProductId; + existingVariant.Size = request.Size; + existingVariant.Color = request.Color; + existingVariant.Price = request.Price; + existingVariant.ImageUrl = request.ImageUrl; + existingVariant.Sku = request.Sku; + existingVariant.StockQuantity = request.StockQuantity; + existingVariant.IsActive = request.IsActive; + + var updatedVariant = await unitOfWork.ProductVariantRepository.UpdateAsync(existingVariant, cancellationToken); + await unitOfWork.CommitTransactionAsync(cancellationToken); + + return new ProductVariantDto + { + Id = updatedVariant.Id, + ProductId = updatedVariant.ProductId, + Size = updatedVariant.Size, + Color = updatedVariant.Color, + Price = updatedVariant.Price, + ImageUrl = updatedVariant.ImageUrl, + Sku = updatedVariant.Sku, + StockQuantity = updatedVariant.StockQuantity, + IsActive = updatedVariant.IsActive, + CreatedAt = updatedVariant.CreatedAt, + ModifiedAt = updatedVariant.ModifiedAt + }; + } + catch + { + await unitOfWork.RollbackTransactionAsync(cancellationToken); + throw; + } + } +} \ No newline at end of file diff --git a/src/Imprink.Application/Products/Create/CreateProductHandler.cs b/src/Imprink.Application/Domains/Products/CreateProductHandler.cs similarity index 98% rename from src/Imprink.Application/Products/Create/CreateProductHandler.cs rename to src/Imprink.Application/Domains/Products/CreateProductHandler.cs index 8cdce4c..a9a0df7 100644 --- a/src/Imprink.Application/Products/Create/CreateProductHandler.cs +++ b/src/Imprink.Application/Domains/Products/CreateProductHandler.cs @@ -2,7 +2,7 @@ using Imprink.Application.Products.Dtos; using Imprink.Domain.Entities.Product; using MediatR; -namespace Imprink.Application.Products.Create; +namespace Imprink.Application.Domains.Products; public class CreateProductCommand : IRequest { diff --git a/src/Imprink.Application/Products/Delete/DeleteProductHandler.cs b/src/Imprink.Application/Domains/Products/DeleteProductHandler.cs similarity index 95% rename from src/Imprink.Application/Products/Delete/DeleteProductHandler.cs rename to src/Imprink.Application/Domains/Products/DeleteProductHandler.cs index 70edc5a..c81d3d8 100644 --- a/src/Imprink.Application/Products/Delete/DeleteProductHandler.cs +++ b/src/Imprink.Application/Domains/Products/DeleteProductHandler.cs @@ -1,6 +1,6 @@ using MediatR; -namespace Imprink.Application.Products.Delete; +namespace Imprink.Application.Domains.Products; public class DeleteProductCommand : IRequest { diff --git a/src/Imprink.Application/Products/Dtos/PagedResultDto.cs b/src/Imprink.Application/Domains/Products/Dtos/PagedResultDto.cs similarity index 100% rename from src/Imprink.Application/Products/Dtos/PagedResultDto.cs rename to src/Imprink.Application/Domains/Products/Dtos/PagedResultDto.cs diff --git a/src/Imprink.Application/Products/Dtos/ProductDto.cs b/src/Imprink.Application/Domains/Products/Dtos/ProductDto.cs similarity index 100% rename from src/Imprink.Application/Products/Dtos/ProductDto.cs rename to src/Imprink.Application/Domains/Products/Dtos/ProductDto.cs diff --git a/src/Imprink.Application/Products/GetProductsHandler.cs b/src/Imprink.Application/Domains/Products/GetProductsHandler.cs similarity index 97% rename from src/Imprink.Application/Products/GetProductsHandler.cs rename to src/Imprink.Application/Domains/Products/GetProductsHandler.cs index afd64cc..615553d 100644 --- a/src/Imprink.Application/Products/GetProductsHandler.cs +++ b/src/Imprink.Application/Domains/Products/GetProductsHandler.cs @@ -2,7 +2,7 @@ using Imprink.Application.Products.Dtos; using Imprink.Domain.Models; using MediatR; -namespace Imprink.Application.Products; +namespace Imprink.Application.Domains.Products; public class GetProductsQuery : IRequest> { diff --git a/src/Imprink.Application/Domains/Products/UpdateProductCommand.cs b/src/Imprink.Application/Domains/Products/UpdateProductCommand.cs new file mode 100644 index 0000000..3aadffa --- /dev/null +++ b/src/Imprink.Application/Domains/Products/UpdateProductCommand.cs @@ -0,0 +1,78 @@ +using Imprink.Application.Exceptions; +using Imprink.Application.Products.Dtos; +using MediatR; + +namespace Imprink.Application.Domains.Products; + +public class UpdateProductCommand : IRequest +{ + public Guid Id { get; set; } + public string Name { get; set; } = null!; + public string? Description { get; set; } + public decimal BasePrice { get; set; } + public bool IsCustomizable { get; set; } + public bool IsActive { get; set; } + public string? ImageUrl { get; set; } + public Guid? CategoryId { get; set; } +} + +public class UpdateProductHandler(IUnitOfWork unitOfWork) : IRequestHandler +{ + public async Task Handle(UpdateProductCommand request, CancellationToken cancellationToken) + { + await unitOfWork.BeginTransactionAsync(cancellationToken); + + try + { + var existingProduct = await unitOfWork.ProductRepository.GetByIdAsync(request.Id, cancellationToken); + + if (existingProduct == null) + throw new NotFoundException($"Product with ID {request.Id} not found."); + + existingProduct.Name = request.Name; + existingProduct.Description = request.Description; + existingProduct.BasePrice = request.BasePrice; + existingProduct.IsCustomizable = request.IsCustomizable; + existingProduct.IsActive = request.IsActive; + existingProduct.ImageUrl = request.ImageUrl; + existingProduct.CategoryId = request.CategoryId; + + var updatedProduct = await unitOfWork.ProductRepository.UpdateAsync(existingProduct, cancellationToken); + + var categoryDto = new CategoryDto + { + Id = updatedProduct.Category.Id, + Name = updatedProduct.Category.Name, + Description = updatedProduct.Category.Description, + ImageUrl = updatedProduct.Category.ImageUrl, + SortOrder = updatedProduct.Category.SortOrder, + IsActive = updatedProduct.Category.IsActive, + ParentCategoryId = updatedProduct.Category.ParentCategoryId, + CreatedAt = updatedProduct.Category.CreatedAt, + ModifiedAt = updatedProduct.Category.ModifiedAt + }; + + await unitOfWork.CommitTransactionAsync(cancellationToken); + + return new ProductDto + { + Id = updatedProduct.Id, + Name = updatedProduct.Name, + Description = updatedProduct.Description, + BasePrice = updatedProduct.BasePrice, + IsCustomizable = updatedProduct.IsCustomizable, + IsActive = updatedProduct.IsActive, + ImageUrl = updatedProduct.ImageUrl, + CategoryId = updatedProduct.CategoryId, + Category = categoryDto, + CreatedAt = updatedProduct.CreatedAt, + ModifiedAt = updatedProduct.ModifiedAt + }; + } + catch + { + await unitOfWork.RollbackTransactionAsync(cancellationToken); + throw; + } + } +} \ No newline at end of file diff --git a/src/Imprink.Application/Users/DeleteUserRoleHandler.cs b/src/Imprink.Application/Domains/Users/DeleteUserRoleHandler.cs similarity index 95% rename from src/Imprink.Application/Users/DeleteUserRoleHandler.cs rename to src/Imprink.Application/Domains/Users/DeleteUserRoleHandler.cs index a5424ee..28bed6d 100644 --- a/src/Imprink.Application/Users/DeleteUserRoleHandler.cs +++ b/src/Imprink.Application/Domains/Users/DeleteUserRoleHandler.cs @@ -1,9 +1,8 @@ using Imprink.Application.Exceptions; using Imprink.Application.Users.Dtos; -using Imprink.Domain.Entities.Users; using MediatR; -namespace Imprink.Application.Users; +namespace Imprink.Application.Domains.Users; public record DeleteUserRoleCommand(string Sub, Guid RoleId) : IRequest; diff --git a/src/Imprink.Application/Users/Dtos/RoleDto.cs b/src/Imprink.Application/Domains/Users/Dtos/RoleDto.cs similarity index 100% rename from src/Imprink.Application/Users/Dtos/RoleDto.cs rename to src/Imprink.Application/Domains/Users/Dtos/RoleDto.cs diff --git a/src/Imprink.Application/Users/Dtos/UserDto.cs b/src/Imprink.Application/Domains/Users/Dtos/UserDto.cs similarity index 100% rename from src/Imprink.Application/Users/Dtos/UserDto.cs rename to src/Imprink.Application/Domains/Users/Dtos/UserDto.cs diff --git a/src/Imprink.Application/Users/Dtos/UserRoleDto.cs b/src/Imprink.Application/Domains/Users/Dtos/UserRoleDto.cs similarity index 100% rename from src/Imprink.Application/Users/Dtos/UserRoleDto.cs rename to src/Imprink.Application/Domains/Users/Dtos/UserRoleDto.cs diff --git a/src/Imprink.Application/Users/GetUserRolesHandler.cs b/src/Imprink.Application/Domains/Users/GetUserRolesHandler.cs similarity index 94% rename from src/Imprink.Application/Users/GetUserRolesHandler.cs rename to src/Imprink.Application/Domains/Users/GetUserRolesHandler.cs index dca1ed0..acd95a2 100644 --- a/src/Imprink.Application/Users/GetUserRolesHandler.cs +++ b/src/Imprink.Application/Domains/Users/GetUserRolesHandler.cs @@ -2,7 +2,7 @@ using Imprink.Application.Exceptions; using Imprink.Application.Users.Dtos; using MediatR; -namespace Imprink.Application.Users; +namespace Imprink.Application.Domains.Users; public record GetUserRolesCommand(string Sub) : IRequest>; diff --git a/src/Imprink.Application/Users/SetUserFullNameHandler.cs b/src/Imprink.Application/Domains/Users/SetUserFullNameHandler.cs similarity index 95% rename from src/Imprink.Application/Users/SetUserFullNameHandler.cs rename to src/Imprink.Application/Domains/Users/SetUserFullNameHandler.cs index 68efb8a..b8b4ce1 100644 --- a/src/Imprink.Application/Users/SetUserFullNameHandler.cs +++ b/src/Imprink.Application/Domains/Users/SetUserFullNameHandler.cs @@ -1,9 +1,9 @@ using Imprink.Application.Exceptions; -using Imprink.Application.Service; +using Imprink.Application.Services; using Imprink.Application.Users.Dtos; using MediatR; -namespace Imprink.Application.Users; +namespace Imprink.Application.Domains.Users; public record SetUserFullNameCommand(string FirstName, string LastName) : IRequest; diff --git a/src/Imprink.Application/Users/SetUserPhoneHandler.cs b/src/Imprink.Application/Domains/Users/SetUserPhoneHandler.cs similarity index 95% rename from src/Imprink.Application/Users/SetUserPhoneHandler.cs rename to src/Imprink.Application/Domains/Users/SetUserPhoneHandler.cs index 47c064b..b7b5289 100644 --- a/src/Imprink.Application/Users/SetUserPhoneHandler.cs +++ b/src/Imprink.Application/Domains/Users/SetUserPhoneHandler.cs @@ -1,9 +1,9 @@ using Imprink.Application.Exceptions; -using Imprink.Application.Service; +using Imprink.Application.Services; using Imprink.Application.Users.Dtos; using MediatR; -namespace Imprink.Application.Users; +namespace Imprink.Application.Domains.Users; public record SetUserPhoneCommand(string PhoneNumber) : IRequest; diff --git a/src/Imprink.Application/Users/SetUserRoleHandler.cs b/src/Imprink.Application/Domains/Users/SetUserRoleHandler.cs similarity index 96% rename from src/Imprink.Application/Users/SetUserRoleHandler.cs rename to src/Imprink.Application/Domains/Users/SetUserRoleHandler.cs index f077e99..426eb8b 100644 --- a/src/Imprink.Application/Users/SetUserRoleHandler.cs +++ b/src/Imprink.Application/Domains/Users/SetUserRoleHandler.cs @@ -3,7 +3,7 @@ using Imprink.Application.Users.Dtos; using Imprink.Domain.Entities.Users; using MediatR; -namespace Imprink.Application.Users; +namespace Imprink.Application.Domains.Users; public record SetUserRoleCommand(string Sub, Guid RoleId) : IRequest; diff --git a/src/Imprink.Application/Users/SyncUserHandler.cs b/src/Imprink.Application/Domains/Users/SyncUserHandler.cs similarity index 96% rename from src/Imprink.Application/Users/SyncUserHandler.cs rename to src/Imprink.Application/Domains/Users/SyncUserHandler.cs index edcf817..503521a 100644 --- a/src/Imprink.Application/Users/SyncUserHandler.cs +++ b/src/Imprink.Application/Domains/Users/SyncUserHandler.cs @@ -2,7 +2,7 @@ using Imprink.Application.Users.Dtos; using Imprink.Domain.Models; using MediatR; -namespace Imprink.Application.Users; +namespace Imprink.Application.Domains.Users; public record SyncUserCommand(Auth0User User) : IRequest; diff --git a/src/Imprink.Application/Imprink.Application.csproj b/src/Imprink.Application/Imprink.Application.csproj index b274257..5a13bfc 100644 --- a/src/Imprink.Application/Imprink.Application.csproj +++ b/src/Imprink.Application/Imprink.Application.csproj @@ -20,7 +20,7 @@ - + diff --git a/src/Imprink.Application/Service/ICurrentUserService.cs b/src/Imprink.Application/Services/ICurrentUserService.cs similarity index 64% rename from src/Imprink.Application/Service/ICurrentUserService.cs rename to src/Imprink.Application/Services/ICurrentUserService.cs index bd6ad1e..83bbe41 100644 --- a/src/Imprink.Application/Service/ICurrentUserService.cs +++ b/src/Imprink.Application/Services/ICurrentUserService.cs @@ -1,4 +1,4 @@ -namespace Imprink.Application.Service; +namespace Imprink.Application.Services; public interface ICurrentUserService { diff --git a/src/Imprink.Application/Validation/Categories/CreateCategoryCommandValidator.cs b/src/Imprink.Application/Validation/Categories/CreateCategoryCommandValidator.cs new file mode 100644 index 0000000..6f20535 --- /dev/null +++ b/src/Imprink.Application/Validation/Categories/CreateCategoryCommandValidator.cs @@ -0,0 +1,34 @@ +using FluentValidation; +using Imprink.Application.Domains.Categories; + +namespace Imprink.Application.Validation.Categories; + +public class CreateCategoryCommandValidator : AbstractValidator +{ + public CreateCategoryCommandValidator() + { + RuleFor(x => x.Name) + .NotEmpty().WithMessage("Name is required.") + .Length(1, 100).WithMessage("Name must be between 1 and 100 characters."); + + RuleFor(x => x.Description) + .NotEmpty().WithMessage("Description is required.") + .Length(1, 500).WithMessage("Description must be between 1 and 500 characters."); + + RuleFor(x => x.ImageUrl) + .Must(BeValidUrl).When(x => !string.IsNullOrWhiteSpace(x.ImageUrl)) + .WithMessage("ImageUrl must be a valid URL."); + + RuleFor(x => x.SortOrder) + .GreaterThanOrEqualTo(0).WithMessage("SortOrder cannot be negative."); + + RuleFor(x => x.ParentCategoryId) + .NotEqual(Guid.Empty).When(x => x.ParentCategoryId.HasValue) + .WithMessage("ParentCategoryId must be a valid GUID."); + } + + private static bool BeValidUrl(string? url) + { + return Uri.TryCreate(url, UriKind.Absolute, out _); + } +} \ No newline at end of file diff --git a/src/Imprink.Application/Validation/Categories/DeleteCategoryCommandValidator.cs b/src/Imprink.Application/Validation/Categories/DeleteCategoryCommandValidator.cs new file mode 100644 index 0000000..d1f9cb3 --- /dev/null +++ b/src/Imprink.Application/Validation/Categories/DeleteCategoryCommandValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Imprink.Application.Domains.Categories; + +namespace Imprink.Application.Validation.Categories; + +public class DeleteCategoryCommandValidator : AbstractValidator +{ + public DeleteCategoryCommandValidator() + { + RuleFor(x => x.Id) + .NotEmpty().WithMessage("Id is required.") + .NotEqual(Guid.Empty).WithMessage("Id must be a valid GUID."); + } +} \ No newline at end of file diff --git a/src/Imprink.Application/Validation/Categories/UpdateCategoryCommandValidator.cs b/src/Imprink.Application/Validation/Categories/UpdateCategoryCommandValidator.cs new file mode 100644 index 0000000..7e493bf --- /dev/null +++ b/src/Imprink.Application/Validation/Categories/UpdateCategoryCommandValidator.cs @@ -0,0 +1,40 @@ +using FluentValidation; +using Imprink.Application.Domains.Categories; + +namespace Imprink.Application.Validation.Categories; + +public class UpdateCategoryCommandValidator : AbstractValidator +{ + public UpdateCategoryCommandValidator() + { + RuleFor(x => x.Id) + .NotEmpty().WithMessage("Id is required.") + .NotEqual(Guid.Empty).WithMessage("Id must be a valid GUID."); + + RuleFor(x => x.Name) + .NotEmpty().WithMessage("Name is required.") + .Length(1, 100).WithMessage("Name must be between 1 and 100 characters."); + + RuleFor(x => x.Description) + .NotEmpty().WithMessage("Description is required.") + .Length(1, 500).WithMessage("Description must be between 1 and 500 characters."); + + RuleFor(x => x.ImageUrl) + .Must(BeValidUrl).When(x => !string.IsNullOrWhiteSpace(x.ImageUrl)) + .WithMessage("ImageUrl must be a valid URL."); + + RuleFor(x => x.SortOrder) + .GreaterThanOrEqualTo(0).WithMessage("SortOrder cannot be negative."); + + RuleFor(x => x.ParentCategoryId) + .NotEqual(Guid.Empty).When(x => x.ParentCategoryId.HasValue) + .WithMessage("ParentCategoryId must be a valid GUID.") + .Must((command, parentId) => parentId != command.Id).When(x => x.ParentCategoryId.HasValue) + .WithMessage("Category cannot be its own parent."); + } + + private static bool BeValidUrl(string? url) + { + return Uri.TryCreate(url, UriKind.Absolute, out _); + } +} \ No newline at end of file diff --git a/src/Imprink.Application/Validation/Models/OrderFilterParametersValidator.cs b/src/Imprink.Application/Validation/Models/OrderFilterParametersValidator.cs new file mode 100644 index 0000000..0efcdad --- /dev/null +++ b/src/Imprink.Application/Validation/Models/OrderFilterParametersValidator.cs @@ -0,0 +1,69 @@ +using FluentValidation; +using Imprink.Domain.Models; + +namespace Imprink.Application.Validation.Models; + +public class OrderFilterParametersValidator : AbstractValidator +{ + public OrderFilterParametersValidator() + { + RuleFor(x => x.PageNumber) + .GreaterThan(0).WithMessage("PageNumber must be greater than 0."); + + RuleFor(x => x.PageSize) + .InclusiveBetween(1, 100).WithMessage("PageSize must be between 1 and 100."); + + RuleFor(x => x.UserId) + .Length(1, 450).WithMessage("UserId length must be between 1 and 450 characters.") + .When(x => !string.IsNullOrWhiteSpace(x.UserId)); + + RuleFor(x => x.OrderNumber) + .Length(1, 50).WithMessage("OrderNumber length must be between 1 and 50 characters.") + .When(x => !string.IsNullOrWhiteSpace(x.OrderNumber)); + + RuleFor(x => x.OrderStatusId) + .GreaterThan(0).When(x => x.OrderStatusId.HasValue) + .WithMessage("OrderStatusId must be greater than 0."); + + RuleFor(x => x.ShippingStatusId) + .GreaterThan(0).When(x => x.ShippingStatusId.HasValue) + .WithMessage("ShippingStatusId must be greater than 0."); + + RuleFor(x => x.StartDate) + .LessThanOrEqualTo(DateTime.UtcNow.AddDays(1)).When(x => x.StartDate.HasValue) + .WithMessage("StartDate cannot be in the future."); + + RuleFor(x => x.EndDate) + .LessThanOrEqualTo(DateTime.UtcNow.AddDays(1)).When(x => x.EndDate.HasValue) + .WithMessage("EndDate cannot be in the future."); + + RuleFor(x => x) + .Must(x => !x.StartDate.HasValue || !x.EndDate.HasValue || x.StartDate <= x.EndDate) + .WithMessage("StartDate cannot be greater than EndDate."); + + RuleFor(x => x.MinTotalPrice) + .GreaterThanOrEqualTo(0).When(x => x.MinTotalPrice.HasValue) + .WithMessage("MinTotalPrice cannot be negative."); + + RuleFor(x => x.MaxTotalPrice) + .GreaterThanOrEqualTo(0).When(x => x.MaxTotalPrice.HasValue) + .WithMessage("MaxTotalPrice cannot be negative."); + + RuleFor(x => x) + .Must(x => !x.MinTotalPrice.HasValue || !x.MaxTotalPrice.HasValue || x.MinTotalPrice <= x.MaxTotalPrice) + .WithMessage("MinTotalPrice cannot be greater than MaxTotalPrice."); + + RuleFor(x => x.SortBy) + .NotEmpty().WithMessage("SortBy is required.") + .Must(value => AllowedSortColumns.Contains(value, StringComparer.OrdinalIgnoreCase)) + .WithMessage("SortBy must be one of: OrderDate, TotalPrice, OrderNumber."); + + RuleFor(x => x.SortDirection) + .NotEmpty().WithMessage("SortDirection is required.") + .Must(value => value.Equals("ASC", StringComparison.OrdinalIgnoreCase) + || value.Equals("DESC", StringComparison.OrdinalIgnoreCase)) + .WithMessage("SortDirection must be 'ASC' or 'DESC'."); + } + + private static readonly string[] AllowedSortColumns = ["OrderDate", "TotalPrice", "OrderNumber"]; +} \ No newline at end of file diff --git a/src/Imprink.Application/Validation/ProductVariants/CreateProducVariantCommandValidator.cs b/src/Imprink.Application/Validation/ProductVariants/CreateProducVariantCommandValidator.cs new file mode 100644 index 0000000..c3d3957 --- /dev/null +++ b/src/Imprink.Application/Validation/ProductVariants/CreateProducVariantCommandValidator.cs @@ -0,0 +1,41 @@ +using FluentValidation; +using Imprink.Application.Domains.ProductVariants; + +namespace Imprink.Application.Validation.ProductVariants; + +public class CreateProductVariantCommandValidator : AbstractValidator +{ + public CreateProductVariantCommandValidator() + { + RuleFor(x => x.ProductId) + .NotEmpty().WithMessage("ProductId is required.") + .NotEqual(Guid.Empty).WithMessage("ProductId must be a valid GUID."); + + RuleFor(x => x.Size) + .NotEmpty().WithMessage("Size is required.") + .Length(1, 50).WithMessage("Size must be between 1 and 50 characters."); + + RuleFor(x => x.Color) + .Length(1, 50).WithMessage("Color must be between 1 and 50 characters.") + .When(x => !string.IsNullOrWhiteSpace(x.Color)); + + RuleFor(x => x.Price) + .GreaterThan(0).WithMessage("Price must be greater than 0."); + + RuleFor(x => x.ImageUrl) + .Must(BeValidUrl).When(x => !string.IsNullOrWhiteSpace(x.ImageUrl)) + .WithMessage("ImageUrl must be a valid URL."); + + RuleFor(x => x.Sku) + .NotEmpty().WithMessage("SKU is required.") + .Length(1, 50).WithMessage("SKU must be between 1 and 50 characters."); + + RuleFor(x => x.StockQuantity) + .GreaterThanOrEqualTo(0).WithMessage("StockQuantity cannot be negative."); + } + + private static bool BeValidUrl(string? url) + { + return Uri.TryCreate(url, UriKind.Absolute, out _); + } +} \ No newline at end of file diff --git a/src/Imprink.Application/Validation/ProductVariants/DeleteProductVariantCommandValidator.cs b/src/Imprink.Application/Validation/ProductVariants/DeleteProductVariantCommandValidator.cs new file mode 100644 index 0000000..acda88f --- /dev/null +++ b/src/Imprink.Application/Validation/ProductVariants/DeleteProductVariantCommandValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Imprink.Application.Domains.ProductVariants; + +namespace Imprink.Application.Validation.ProductVariants; + +public class DeleteProductVariantCommandValidator : AbstractValidator +{ + public DeleteProductVariantCommandValidator() + { + RuleFor(x => x.Id) + .NotEmpty().WithMessage("Id is required.") + .NotEqual(Guid.Empty).WithMessage("Id must be a valid GUID."); + } +} \ No newline at end of file diff --git a/src/Imprink.Application/Validation/ProductVariants/GetProductVariantsQueryValidator.cs b/src/Imprink.Application/Validation/ProductVariants/GetProductVariantsQueryValidator.cs new file mode 100644 index 0000000..2ad4d09 --- /dev/null +++ b/src/Imprink.Application/Validation/ProductVariants/GetProductVariantsQueryValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Imprink.Application.Domains.ProductVariants; + +namespace Imprink.Application.Validation.ProductVariants; + +public class GetProductVariantsQueryValidator : AbstractValidator +{ + public GetProductVariantsQueryValidator() + { + RuleFor(x => x.ProductId) + .NotEqual(Guid.Empty).When(x => x.ProductId.HasValue) + .WithMessage("ProductId must be a valid GUID when provided."); + } +} \ No newline at end of file diff --git a/src/Imprink.Application/Validation/ProductVariants/UpdateProductVariantCommandValidator.cs b/src/Imprink.Application/Validation/ProductVariants/UpdateProductVariantCommandValidator.cs new file mode 100644 index 0000000..dd9fe23 --- /dev/null +++ b/src/Imprink.Application/Validation/ProductVariants/UpdateProductVariantCommandValidator.cs @@ -0,0 +1,45 @@ +using FluentValidation; +using Imprink.Application.Domains.ProductVariants; + +namespace Imprink.Application.Validation.ProductVariants; + +public class UpdateProductVariantCommandValidator : AbstractValidator +{ + public UpdateProductVariantCommandValidator() + { + RuleFor(x => x.Id) + .NotEmpty().WithMessage("Id is required.") + .NotEqual(Guid.Empty).WithMessage("Id must be a valid GUID."); + + RuleFor(x => x.ProductId) + .NotEmpty().WithMessage("ProductId is required.") + .NotEqual(Guid.Empty).WithMessage("ProductId must be a valid GUID."); + + RuleFor(x => x.Size) + .NotEmpty().WithMessage("Size is required.") + .Length(1, 50).WithMessage("Size must be between 1 and 50 characters."); + + RuleFor(x => x.Color) + .Length(1, 50).WithMessage("Color must be between 1 and 50 characters.") + .When(x => !string.IsNullOrWhiteSpace(x.Color)); + + RuleFor(x => x.Price) + .GreaterThan(0).WithMessage("Price must be greater than 0."); + + RuleFor(x => x.ImageUrl) + .Must(BeValidUrl).When(x => !string.IsNullOrWhiteSpace(x.ImageUrl)) + .WithMessage("ImageUrl must be a valid URL."); + + RuleFor(x => x.Sku) + .NotEmpty().WithMessage("SKU is required.") + .Length(1, 50).WithMessage("SKU must be between 1 and 50 characters."); + + RuleFor(x => x.StockQuantity) + .GreaterThanOrEqualTo(0).WithMessage("StockQuantity cannot be negative."); + } + + private static bool BeValidUrl(string? url) + { + return Uri.TryCreate(url, UriKind.Absolute, out _); + } +} \ No newline at end of file diff --git a/src/Imprink.Application/Validation/Products/CreateProductCommandValidator.cs b/src/Imprink.Application/Validation/Products/CreateProductCommandValidator.cs new file mode 100644 index 0000000..60da2bd --- /dev/null +++ b/src/Imprink.Application/Validation/Products/CreateProductCommandValidator.cs @@ -0,0 +1,34 @@ +using FluentValidation; +using Imprink.Application.Domains.Products; + +namespace Imprink.Application.Validation.Products; + +public class CreateProductCommandValidator : AbstractValidator +{ + public CreateProductCommandValidator() + { + RuleFor(x => x.Name) + .NotEmpty().WithMessage("Name is required.") + .Length(1, 100).WithMessage("Name must be between 1 and 100 characters."); + + RuleFor(x => x.Description) + .Length(1, 1000).WithMessage("Description must be between 1 and 1000 characters.") + .When(x => !string.IsNullOrWhiteSpace(x.Description)); + + RuleFor(x => x.BasePrice) + .GreaterThan(0).WithMessage("BasePrice must be greater than 0."); + + RuleFor(x => x.ImageUrl) + .Must(BeValidUrl).When(x => !string.IsNullOrWhiteSpace(x.ImageUrl)) + .WithMessage("ImageUrl must be a valid URL."); + + RuleFor(x => x.CategoryId) + .NotEqual(Guid.Empty).When(x => x.CategoryId.HasValue) + .WithMessage("CategoryId must be a valid GUID."); + } + + private static bool BeValidUrl(string? url) + { + return Uri.TryCreate(url, UriKind.Absolute, out _); + } +} \ No newline at end of file diff --git a/src/Imprink.Application/Validation/Products/DeleteProductCommandValidator.cs b/src/Imprink.Application/Validation/Products/DeleteProductCommandValidator.cs new file mode 100644 index 0000000..5cb3cea --- /dev/null +++ b/src/Imprink.Application/Validation/Products/DeleteProductCommandValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Imprink.Application.Domains.Products; + +namespace Imprink.Application.Validation.Products; + +public class DeleteProductCommandValidator : AbstractValidator +{ + public DeleteProductCommandValidator() + { + RuleFor(x => x.Id) + .NotEmpty().WithMessage("Id is required.") + .NotEqual(Guid.Empty).WithMessage("Id must be a valid GUID."); + } +} \ No newline at end of file diff --git a/src/Imprink.Application/Validation/Products/GetProductsQueryValidator.cs b/src/Imprink.Application/Validation/Products/GetProductsQueryValidator.cs index b888657..9026474 100644 --- a/src/Imprink.Application/Validation/Products/GetProductsQueryValidator.cs +++ b/src/Imprink.Application/Validation/Products/GetProductsQueryValidator.cs @@ -1,5 +1,5 @@ using FluentValidation; -using Imprink.Application.Products; +using Imprink.Application.Domains.Products; using Imprink.Application.Validation.Models; namespace Imprink.Application.Validation.Products; diff --git a/src/Imprink.Application/Validation/Products/UpdateProductCommandValidator.cs b/src/Imprink.Application/Validation/Products/UpdateProductCommandValidator.cs new file mode 100644 index 0000000..afc7cbf --- /dev/null +++ b/src/Imprink.Application/Validation/Products/UpdateProductCommandValidator.cs @@ -0,0 +1,38 @@ +using FluentValidation; +using Imprink.Application.Domains.Products; + +namespace Imprink.Application.Validation.Products; + +public class UpdateProductCommandValidator : AbstractValidator +{ + public UpdateProductCommandValidator() + { + RuleFor(x => x.Id) + .NotEmpty().WithMessage("Id is required.") + .NotEqual(Guid.Empty).WithMessage("Id must be a valid GUID."); + + RuleFor(x => x.Name) + .NotEmpty().WithMessage("Name is required.") + .Length(1, 100).WithMessage("Name must be between 1 and 100 characters."); + + RuleFor(x => x.Description) + .Length(1, 1000).WithMessage("Description must be between 1 and 1000 characters.") + .When(x => !string.IsNullOrWhiteSpace(x.Description)); + + RuleFor(x => x.BasePrice) + .GreaterThan(0).WithMessage("BasePrice must be greater than 0."); + + RuleFor(x => x.ImageUrl) + .Must(BeValidUrl).When(x => !string.IsNullOrWhiteSpace(x.ImageUrl)) + .WithMessage("ImageUrl must be a valid URL."); + + RuleFor(x => x.CategoryId) + .NotEqual(Guid.Empty).When(x => x.CategoryId.HasValue) + .WithMessage("CategoryId must be a valid GUID."); + } + + private static bool BeValidUrl(string? url) + { + return Uri.TryCreate(url, UriKind.Absolute, out _); + } +} \ No newline at end of file diff --git a/src/Imprink.Application/Validation/Users/SetUserFullNameCommandValidator.cs b/src/Imprink.Application/Validation/Users/SetUserFullNameCommandValidator.cs new file mode 100644 index 0000000..b97eed6 --- /dev/null +++ b/src/Imprink.Application/Validation/Users/SetUserFullNameCommandValidator.cs @@ -0,0 +1,18 @@ +using FluentValidation; +using Imprink.Application.Domains.Users; + +namespace Imprink.Application.Validation.Users; + +public class SetUserFullNameCommandValidator : AbstractValidator +{ + public SetUserFullNameCommandValidator() + { + RuleFor(x => x.FirstName) + .NotEmpty().WithMessage("FirstName is required.") + .Length(1, 50).WithMessage("FirstName must be between 1 and 50 characters."); + + RuleFor(x => x.LastName) + .NotEmpty().WithMessage("LastName is required.") + .Length(1, 50).WithMessage("LastName must be between 1 and 50 characters."); + } +} \ No newline at end of file diff --git a/src/Imprink.Application/Validation/Users/SetUserPhoneCommandValidator.cs b/src/Imprink.Application/Validation/Users/SetUserPhoneCommandValidator.cs new file mode 100644 index 0000000..54a73a8 --- /dev/null +++ b/src/Imprink.Application/Validation/Users/SetUserPhoneCommandValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Imprink.Application.Domains.Users; + +namespace Imprink.Application.Validation.Users; + +public class SetUserPhoneCommandValidator : AbstractValidator +{ + public SetUserPhoneCommandValidator() + { + RuleFor(x => x.PhoneNumber) + .NotEmpty().WithMessage("PhoneNumber is required.") + .Matches(@"^\+?[1-9]\d{1,14}$").WithMessage("PhoneNumber must be a valid phone number format."); + } +} \ No newline at end of file diff --git a/src/Imprink.Domain/Models/OrderFilterParameters.cs b/src/Imprink.Domain/Models/OrderFilterParameters.cs new file mode 100644 index 0000000..f917a8e --- /dev/null +++ b/src/Imprink.Domain/Models/OrderFilterParameters.cs @@ -0,0 +1,46 @@ +namespace Imprink.Domain.Models; + +public class OrderFilterParameters +{ + public int PageNumber { get; set; } = 1; + + public int PageSize { get; set; } = 10; + + public string? UserId { get; set; } + + public string? OrderNumber { get; set; } + + public int? OrderStatusId { get; set; } + + public int? ShippingStatusId { get; set; } + + public DateTime? StartDate { get; set; } + + public DateTime? EndDate { get; set; } + + public decimal? MinTotalPrice { get; set; } + + public decimal? MaxTotalPrice { get; set; } + + public string SortBy { get; set; } = "OrderDate"; + + public string SortDirection { get; set; } = "DESC"; + + public bool IsValidDateRange() + { + if (StartDate.HasValue && EndDate.HasValue) + { + return StartDate.Value <= EndDate.Value; + } + return true; + } + + public bool IsValidPriceRange() + { + if (MinTotalPrice.HasValue && MaxTotalPrice.HasValue) + { + return MinTotalPrice.Value <= MaxTotalPrice.Value; + } + return true; + } +} \ No newline at end of file diff --git a/src/Imprink.Domain/Repositories/Orders/IOrderItemRepository.cs b/src/Imprink.Domain/Repositories/Orders/IOrderItemRepository.cs new file mode 100644 index 0000000..d3699d9 --- /dev/null +++ b/src/Imprink.Domain/Repositories/Orders/IOrderItemRepository.cs @@ -0,0 +1,27 @@ +using Imprink.Domain.Entities.Orders; + +namespace Imprink.Domain.Repositories.Orders; + +public interface IOrderItemRepository +{ + Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default); + Task GetByIdWithProductAsync(Guid id, CancellationToken cancellationToken = default); + Task GetByIdWithVariantAsync(Guid id, CancellationToken cancellationToken = default); + Task GetByIdFullAsync(Guid id, CancellationToken cancellationToken = default); + Task> GetByOrderIdAsync(Guid orderId, CancellationToken cancellationToken = default); + Task> GetByOrderIdWithProductsAsync(Guid orderId, CancellationToken cancellationToken = default); + Task> GetByProductIdAsync(Guid productId, CancellationToken cancellationToken = default); + Task> GetByProductVariantIdAsync(Guid productVariantId, CancellationToken cancellationToken = default); + Task> GetCustomizedItemsAsync(CancellationToken cancellationToken = default); + Task> GetByDateRangeAsync(DateTime startDate, DateTime endDate, CancellationToken cancellationToken = default); + Task AddAsync(OrderItem orderItem, CancellationToken cancellationToken = default); + Task> AddRangeAsync(IEnumerable orderItems, CancellationToken cancellationToken = default); + Task UpdateAsync(OrderItem orderItem, CancellationToken cancellationToken = default); + Task DeleteAsync(Guid id, CancellationToken cancellationToken = default); + Task DeleteByOrderIdAsync(Guid orderId, CancellationToken cancellationToken = default); + Task ExistsAsync(Guid id, CancellationToken cancellationToken = default); + Task GetTotalValueByOrderIdAsync(Guid orderId, CancellationToken cancellationToken = default); + Task GetQuantityByProductIdAsync(Guid productId, CancellationToken cancellationToken = default); + Task GetQuantityByVariantIdAsync(Guid productVariantId, CancellationToken cancellationToken = default); + Task> GetProductSalesCountAsync(DateTime? startDate = null, DateTime? endDate = null, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/Imprink.Domain/Repositories/Orders/IOrderRepository.cs b/src/Imprink.Domain/Repositories/Orders/IOrderRepository.cs new file mode 100644 index 0000000..c45ae48 --- /dev/null +++ b/src/Imprink.Domain/Repositories/Orders/IOrderRepository.cs @@ -0,0 +1,27 @@ +using Imprink.Domain.Entities.Orders; +using Imprink.Domain.Models; + +namespace Imprink.Domain.Repositories.Orders; + +public interface IOrderRepository +{ + Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default); + Task GetByIdWithItemsAsync(Guid id, CancellationToken cancellationToken = default); + Task GetByIdWithAddressAsync(Guid id, CancellationToken cancellationToken = default); + Task GetByIdFullAsync(Guid id, CancellationToken cancellationToken = default); + Task GetByOrderNumberAsync(string orderNumber, CancellationToken cancellationToken = default); + Task> GetPagedAsync(OrderFilterParameters filterParameters, CancellationToken cancellationToken = default); + Task> GetByUserIdAsync(string userId, CancellationToken cancellationToken = default); + Task> GetByUserIdPagedAsync(string userId, int pageNumber, int pageSize, CancellationToken cancellationToken = default); + Task> GetByOrderStatusAsync(int orderStatusId, CancellationToken cancellationToken = default); + Task> GetByShippingStatusAsync(int shippingStatusId, CancellationToken cancellationToken = default); + Task> GetByDateRangeAsync(DateTime startDate, DateTime endDate, CancellationToken cancellationToken = default); + Task AddAsync(Order order, CancellationToken cancellationToken = default); + Task UpdateAsync(Order order, CancellationToken cancellationToken = default); + Task DeleteAsync(Guid id, CancellationToken cancellationToken = default); + Task ExistsAsync(Guid id, CancellationToken cancellationToken = default); + Task OrderNumberExistsAsync(string orderNumber, Guid? excludeId = null, CancellationToken cancellationToken = default); + Task GetTotalRevenueAsync(CancellationToken cancellationToken = default); + Task GetTotalRevenueByDateRangeAsync(DateTime startDate, DateTime endDate, CancellationToken cancellationToken = default); + Task GetOrderCountByStatusAsync(int orderStatusId, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/Imprink.Infrastructure/Repositories/Orders/OrderItemRepository.cs b/src/Imprink.Infrastructure/Repositories/Orders/OrderItemRepository.cs new file mode 100644 index 0000000..ed9e91f --- /dev/null +++ b/src/Imprink.Infrastructure/Repositories/Orders/OrderItemRepository.cs @@ -0,0 +1,204 @@ +using Imprink.Domain.Entities.Orders; +using Imprink.Domain.Repositories.Orders; +using Imprink.Infrastructure.Database; +using Microsoft.EntityFrameworkCore; + +namespace Imprink.Infrastructure.Repositories.Orders; + +public class OrderItemRepository(ApplicationDbContext context) : IOrderItemRepository +{ + public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) + { + return await context.OrderItems + .FirstOrDefaultAsync(oi => oi.Id == id, cancellationToken); + } + + public async Task GetByIdWithProductAsync(Guid id, CancellationToken cancellationToken = default) + { + return await context.OrderItems + .Include(oi => oi.Product) + .ThenInclude(p => p.Category) + .FirstOrDefaultAsync(oi => oi.Id == id, cancellationToken); + } + + public async Task GetByIdWithVariantAsync(Guid id, CancellationToken cancellationToken = default) + { + return await context.OrderItems + .Include(oi => oi.ProductVariant) + .FirstOrDefaultAsync(oi => oi.Id == id, cancellationToken); + } + + public async Task GetByIdFullAsync(Guid id, CancellationToken cancellationToken = default) + { + return await context.OrderItems + .Include(oi => oi.Order) + .ThenInclude(o => o.User) + .Include(oi => oi.Product) + .ThenInclude(p => p.Category) + .Include(oi => oi.ProductVariant) + .FirstOrDefaultAsync(oi => oi.Id == id, cancellationToken); + } + + public async Task> GetByOrderIdAsync(Guid orderId, CancellationToken cancellationToken = default) + { + return await context.OrderItems + .Where(oi => oi.OrderId == orderId) + .OrderBy(oi => oi.CreatedAt) + .ToListAsync(cancellationToken); + } + + public async Task> GetByOrderIdWithProductsAsync(Guid orderId, CancellationToken cancellationToken = default) + { + return await context.OrderItems + .Include(oi => oi.Product) + .ThenInclude(p => p.Category) + .Include(oi => oi.ProductVariant) + .Where(oi => oi.OrderId == orderId) + .OrderBy(oi => oi.CreatedAt) + .ToListAsync(cancellationToken); + } + + public async Task> GetByProductIdAsync(Guid productId, CancellationToken cancellationToken = default) + { + return await context.OrderItems + .Include(oi => oi.Order) + .Where(oi => oi.ProductId == productId) + .OrderByDescending(oi => oi.CreatedAt) + .ToListAsync(cancellationToken); + } + + public async Task> GetByProductVariantIdAsync(Guid productVariantId, CancellationToken cancellationToken = default) + { + return await context.OrderItems + .Include(oi => oi.Order) + .Where(oi => oi.ProductVariantId == productVariantId) + .OrderByDescending(oi => oi.CreatedAt) + .ToListAsync(cancellationToken); + } + + public async Task> GetCustomizedItemsAsync(CancellationToken cancellationToken = default) + { + return await context.OrderItems + .Include(oi => oi.Product) + .Include(oi => oi.Order) + .Where(oi => !string.IsNullOrEmpty(oi.CustomizationImageUrl) || !string.IsNullOrEmpty(oi.CustomizationDescription)) + .OrderByDescending(oi => oi.CreatedAt) + .ToListAsync(cancellationToken); + } + + public async Task> GetByDateRangeAsync(DateTime startDate, DateTime endDate, CancellationToken cancellationToken = default) + { + return await context.OrderItems + .Include(oi => oi.Order) + .Include(oi => oi.Product) + .Where(oi => oi.Order.OrderDate >= startDate && oi.Order.OrderDate <= endDate) + .OrderByDescending(oi => oi.Order.OrderDate) + .ToListAsync(cancellationToken); + } + + public Task AddAsync(OrderItem orderItem, CancellationToken cancellationToken = default) + { + orderItem.Id = Guid.NewGuid(); + orderItem.CreatedAt = DateTime.UtcNow; + orderItem.ModifiedAt = DateTime.UtcNow; + + context.OrderItems.Add(orderItem); + return Task.FromResult(orderItem); + } + + public Task> AddRangeAsync(IEnumerable orderItems, CancellationToken cancellationToken = default) + { + var items = orderItems.ToList(); + var utcNow = DateTime.UtcNow; + + foreach (var item in items) + { + item.Id = Guid.NewGuid(); + item.CreatedAt = utcNow; + item.ModifiedAt = utcNow; + } + + context.OrderItems.AddRange(items); + return Task.FromResult>(items); + } + + public Task UpdateAsync(OrderItem orderItem, CancellationToken cancellationToken = default) + { + orderItem.ModifiedAt = DateTime.UtcNow; + context.OrderItems.Update(orderItem); + return Task.FromResult(orderItem); + } + + public async Task DeleteAsync(Guid id, CancellationToken cancellationToken = default) + { + var orderItem = await GetByIdAsync(id, cancellationToken); + if (orderItem != null) + { + context.OrderItems.Remove(orderItem); + } + } + + public async Task DeleteByOrderIdAsync(Guid orderId, CancellationToken cancellationToken = default) + { + var orderItems = await context.OrderItems + .Where(oi => oi.OrderId == orderId) + .ToListAsync(cancellationToken); + + if (orderItems.Any()) + { + context.OrderItems.RemoveRange(orderItems); + } + } + + public async Task ExistsAsync(Guid id, CancellationToken cancellationToken = default) + { + return await context.OrderItems + .AnyAsync(oi => oi.Id == id, cancellationToken); + } + + public async Task GetTotalValueByOrderIdAsync(Guid orderId, CancellationToken cancellationToken = default) + { + return await context.OrderItems + .Where(oi => oi.OrderId == orderId) + .SumAsync(oi => oi.TotalPrice, cancellationToken); + } + + public async Task GetQuantityByProductIdAsync(Guid productId, CancellationToken cancellationToken = default) + { + return await context.OrderItems + .Where(oi => oi.ProductId == productId) + .SumAsync(oi => oi.Quantity, cancellationToken); + } + + public async Task GetQuantityByVariantIdAsync(Guid productVariantId, CancellationToken cancellationToken = default) + { + return await context.OrderItems + .Where(oi => oi.ProductVariantId == productVariantId) + .SumAsync(oi => oi.Quantity, cancellationToken); + } + + public async Task> GetProductSalesCountAsync( + DateTime? startDate = null, + DateTime? endDate = null, + CancellationToken cancellationToken = default) + { + var query = context.OrderItems + .Include(oi => oi.Order) + .AsQueryable(); + + if (startDate.HasValue) + { + query = query.Where(oi => oi.Order.OrderDate >= startDate.Value); + } + + if (endDate.HasValue) + { + query = query.Where(oi => oi.Order.OrderDate <= endDate.Value); + } + + return await query + .GroupBy(oi => oi.ProductId) + .Select(g => new { ProductId = g.Key, TotalQuantity = g.Sum(oi => oi.Quantity) }) + .ToDictionaryAsync(x => x.ProductId, x => x.TotalQuantity, cancellationToken); + } +} \ No newline at end of file diff --git a/src/Imprink.Infrastructure/Repositories/Orders/OrderRepository.cs b/src/Imprink.Infrastructure/Repositories/Orders/OrderRepository.cs new file mode 100644 index 0000000..b285a94 --- /dev/null +++ b/src/Imprink.Infrastructure/Repositories/Orders/OrderRepository.cs @@ -0,0 +1,265 @@ +using Imprink.Domain.Entities.Orders; +using Imprink.Domain.Models; +using Imprink.Domain.Repositories.Orders; +using Imprink.Infrastructure.Database; +using Microsoft.EntityFrameworkCore; + +namespace Imprink.Infrastructure.Repositories.Orders; + +public class OrderRepository(ApplicationDbContext context) : IOrderRepository +{ + public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) + { + return await context.Orders + .FirstOrDefaultAsync(o => o.Id == id, cancellationToken); + } + + public async Task GetByIdWithItemsAsync(Guid id, CancellationToken cancellationToken = default) + { + return await context.Orders + .Include(o => o.OrderItems) + .ThenInclude(oi => oi.Product) + .Include(o => o.OrderItems) + .ThenInclude(oi => oi.ProductVariant) + .FirstOrDefaultAsync(o => o.Id == id, cancellationToken); + } + + public async Task GetByIdWithAddressAsync(Guid id, CancellationToken cancellationToken = default) + { + return await context.Orders + .Include(o => o.OrderAddress) + .FirstOrDefaultAsync(o => o.Id == id, cancellationToken); + } + + public async Task GetByIdFullAsync(Guid id, CancellationToken cancellationToken = default) + { + return await context.Orders + .Include(o => o.User) + .Include(o => o.OrderStatus) + .Include(o => o.ShippingStatus) + .Include(o => o.OrderAddress) + .Include(o => o.OrderItems) + .ThenInclude(oi => oi.Product) + .ThenInclude(p => p.Category) + .Include(o => o.OrderItems) + .ThenInclude(oi => oi.ProductVariant) + .FirstOrDefaultAsync(o => o.Id == id, cancellationToken); + } + + public async Task GetByOrderNumberAsync(string orderNumber, CancellationToken cancellationToken = default) + { + return await context.Orders + .Include(o => o.OrderStatus) + .Include(o => o.ShippingStatus) + .FirstOrDefaultAsync(o => o.OrderNumber == orderNumber, cancellationToken); + } + + public async Task> GetPagedAsync( + OrderFilterParameters filterParameters, + CancellationToken cancellationToken = default) + { + var query = context.Orders + .Include(o => o.User) + .Include(o => o.OrderStatus) + .Include(o => o.ShippingStatus) + .AsQueryable(); + + if (!string.IsNullOrEmpty(filterParameters.UserId)) + { + query = query.Where(o => o.UserId == filterParameters.UserId); + } + + if (!string.IsNullOrEmpty(filterParameters.OrderNumber)) + { + query = query.Where(o => o.OrderNumber.Contains(filterParameters.OrderNumber)); + } + + if (filterParameters.OrderStatusId.HasValue) + { + query = query.Where(o => o.OrderStatusId == filterParameters.OrderStatusId.Value); + } + + if (filterParameters.ShippingStatusId.HasValue) + { + query = query.Where(o => o.ShippingStatusId == filterParameters.ShippingStatusId.Value); + } + + if (filterParameters.StartDate.HasValue) + { + query = query.Where(o => o.OrderDate >= filterParameters.StartDate.Value); + } + + if (filterParameters.EndDate.HasValue) + { + query = query.Where(o => o.OrderDate <= filterParameters.EndDate.Value); + } + + if (filterParameters.MinTotalPrice.HasValue) + { + query = query.Where(o => o.TotalPrice >= filterParameters.MinTotalPrice.Value); + } + + if (filterParameters.MaxTotalPrice.HasValue) + { + query = query.Where(o => o.TotalPrice <= filterParameters.MaxTotalPrice.Value); + } + + query = filterParameters.SortBy.ToLower() switch + { + "orderdate" => filterParameters.SortDirection.Equals("DESC", StringComparison.CurrentCultureIgnoreCase) + ? query.OrderByDescending(o => o.OrderDate) + : query.OrderBy(o => o.OrderDate), + "totalprice" => filterParameters.SortDirection.Equals("DESC", StringComparison.CurrentCultureIgnoreCase) + ? query.OrderByDescending(o => o.TotalPrice) + : query.OrderBy(o => o.TotalPrice), + "ordernumber" => filterParameters.SortDirection.Equals("DESC", StringComparison.CurrentCultureIgnoreCase) + ? query.OrderByDescending(o => o.OrderNumber) + : query.OrderBy(o => o.OrderNumber), + _ => query.OrderByDescending(o => o.OrderDate) + }; + + var totalCount = await query.CountAsync(cancellationToken); + + var items = await query + .Skip((filterParameters.PageNumber - 1) * filterParameters.PageSize) + .Take(filterParameters.PageSize) + .ToListAsync(cancellationToken); + + return new PagedResult + { + Items = items, + TotalCount = totalCount, + PageNumber = filterParameters.PageNumber, + PageSize = filterParameters.PageSize + }; + } + + public async Task> GetByUserIdAsync(string userId, CancellationToken cancellationToken = default) + { + return await context.Orders + .Include(o => o.OrderStatus) + .Include(o => o.ShippingStatus) + .Where(o => o.UserId == userId) + .OrderByDescending(o => o.OrderDate) + .ToListAsync(cancellationToken); + } + + public async Task> GetByUserIdPagedAsync( + string userId, + int pageNumber, + int pageSize, + CancellationToken cancellationToken = default) + { + return await context.Orders + .Include(o => o.OrderStatus) + .Include(o => o.ShippingStatus) + .Where(o => o.UserId == userId) + .OrderByDescending(o => o.OrderDate) + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize) + .ToListAsync(cancellationToken); + } + + public async Task> GetByOrderStatusAsync(int orderStatusId, CancellationToken cancellationToken = default) + { + return await context.Orders + .Include(o => o.User) + .Include(o => o.OrderStatus) + .Include(o => o.ShippingStatus) + .Where(o => o.OrderStatusId == orderStatusId) + .OrderByDescending(o => o.OrderDate) + .ToListAsync(cancellationToken); + } + + public async Task> GetByShippingStatusAsync(int shippingStatusId, CancellationToken cancellationToken = default) + { + return await context.Orders + .Include(o => o.User) + .Include(o => o.OrderStatus) + .Include(o => o.ShippingStatus) + .Where(o => o.ShippingStatusId == shippingStatusId) + .OrderByDescending(o => o.OrderDate) + .ToListAsync(cancellationToken); + } + + public async Task> GetByDateRangeAsync( + DateTime startDate, + DateTime endDate, + CancellationToken cancellationToken = default) + { + return await context.Orders + .Include(o => o.User) + .Include(o => o.OrderStatus) + .Include(o => o.ShippingStatus) + .Where(o => o.OrderDate >= startDate && o.OrderDate <= endDate) + .OrderByDescending(o => o.OrderDate) + .ToListAsync(cancellationToken); + } + + public Task AddAsync(Order order, CancellationToken cancellationToken = default) + { + order.Id = Guid.NewGuid(); + order.CreatedAt = DateTime.UtcNow; + order.ModifiedAt = DateTime.UtcNow; + + context.Orders.Add(order); + return Task.FromResult(order); + } + + public Task UpdateAsync(Order order, CancellationToken cancellationToken = default) + { + order.ModifiedAt = DateTime.UtcNow; + context.Orders.Update(order); + return Task.FromResult(order); + } + + public async Task DeleteAsync(Guid id, CancellationToken cancellationToken = default) + { + var order = await GetByIdAsync(id, cancellationToken); + if (order != null) + { + context.Orders.Remove(order); + } + } + + public async Task ExistsAsync(Guid id, CancellationToken cancellationToken = default) + { + return await context.Orders + .AnyAsync(o => o.Id == id, cancellationToken); + } + + public async Task OrderNumberExistsAsync(string orderNumber, Guid? excludeId = null, CancellationToken cancellationToken = default) + { + var query = context.Orders.Where(o => o.OrderNumber == orderNumber); + + if (excludeId.HasValue) + { + query = query.Where(o => o.Id != excludeId.Value); + } + + return await query.AnyAsync(cancellationToken); + } + + public async Task GetTotalRevenueAsync(CancellationToken cancellationToken = default) + { + return await context.Orders + .Where(o => o.OrderStatusId != 5) // Assuming 5 is cancelled status + .SumAsync(o => o.TotalPrice, cancellationToken); + } + + public async Task GetTotalRevenueByDateRangeAsync( + DateTime startDate, + DateTime endDate, + CancellationToken cancellationToken = default) + { + return await context.Orders + .Where(o => o.OrderDate >= startDate && o.OrderDate <= endDate && o.OrderStatusId != 5) + .SumAsync(o => o.TotalPrice, cancellationToken); + } + + public async Task GetOrderCountByStatusAsync(int orderStatusId, CancellationToken cancellationToken = default) + { + return await context.Orders + .CountAsync(o => o.OrderStatusId == orderStatusId, cancellationToken); + } +} \ No newline at end of file diff --git a/src/Imprink.Infrastructure/Services/CurrentUserService.cs b/src/Imprink.Infrastructure/Services/CurrentUserService.cs index 6579e54..89ffbe9 100644 --- a/src/Imprink.Infrastructure/Services/CurrentUserService.cs +++ b/src/Imprink.Infrastructure/Services/CurrentUserService.cs @@ -1,5 +1,5 @@ using System.Security.Claims; -using Imprink.Application.Service; +using Imprink.Application.Services; using Microsoft.AspNetCore.Http; namespace Imprink.Infrastructure.Services; diff --git a/src/Imprink.WebApi/Controllers/Products/CategoriesController.cs b/src/Imprink.WebApi/Controllers/Products/CategoriesController.cs index 33a28de..ba087ae 100644 --- a/src/Imprink.WebApi/Controllers/Products/CategoriesController.cs +++ b/src/Imprink.WebApi/Controllers/Products/CategoriesController.cs @@ -1,6 +1,7 @@ +using Imprink.Application.Domains.Categories; using Imprink.Application.Products.Dtos; -using Imprink.Application.Products.Query; using MediatR; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace Imprink.WebApi.Controllers; @@ -10,8 +11,38 @@ namespace Imprink.WebApi.Controllers; public class CategoriesController(IMediator mediator) : ControllerBase { [HttpGet] - public async Task>> GetCategories([FromQuery] GetCategoriesQuery query) + [AllowAnonymous] + public async Task>> GetCategories( + [FromQuery] GetCategoriesQuery query) { return Ok(await mediator.Send(query)); } + + [HttpPost] + [Authorize(Roles = "Admin")] + public async Task> CreateCategory( + [FromBody] CreateCategoryCommand command) + { + var result = await mediator.Send(command); + return CreatedAtAction(nameof(GetCategories), new { id = result.Id }, result); + } + + [HttpPut("{id:guid}")] + [Authorize(Roles = "Admin")] + public async Task> UpdateCategory( + Guid id, + [FromBody] UpdateCategoryCommand command) + { + if (id != command.Id) return BadRequest("ID mismatch"); + + return Ok(await mediator.Send(command)); + } + + [HttpDelete("{id:guid}")] + [Authorize(Roles = "Admin")] + public async Task DeleteCategory(Guid id) + { + await mediator.Send(new DeleteCategoryCommand { Id = id }); + return NoContent(); + } } \ No newline at end of file diff --git a/src/Imprink.WebApi/Controllers/Products/ProductVariantsController.cs b/src/Imprink.WebApi/Controllers/Products/ProductVariantsController.cs index 95950ba..745de3e 100644 --- a/src/Imprink.WebApi/Controllers/Products/ProductVariantsController.cs +++ b/src/Imprink.WebApi/Controllers/Products/ProductVariantsController.cs @@ -1,6 +1,7 @@ +using Imprink.Application.Domains.ProductVariants; using Imprink.Application.Products.Dtos; -using Imprink.Application.Products.Query; using MediatR; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace Imprink.WebApi.Controllers; @@ -10,9 +11,38 @@ namespace Imprink.WebApi.Controllers; public class ProductVariantsController(IMediator mediator) : ControllerBase { [HttpGet] + [AllowAnonymous] public async Task>> GetProductVariants( [FromQuery] GetProductVariantsQuery query) { return Ok(await mediator.Send(query)); } + + [HttpPost] + [Authorize(Roles = "Admin")] + public async Task> CreateProductVariant( + [FromBody] CreateProductVariantCommand command) + { + var result = await mediator.Send(command); + return CreatedAtAction(nameof(GetProductVariants), new { id = result.Id }, result); + } + + [HttpPut("{id:guid}")] + [Authorize(Roles = "Admin")] + public async Task> UpdateProductVariant( + Guid id, + [FromBody] UpdateProductVariantCommand command) + { + if (id != command.Id) return BadRequest("ID mismatch"); + + return Ok(await mediator.Send(command)); + } + + [HttpDelete("{id:guid}")] + [Authorize(Roles = "Admin")] + public async Task DeleteProductVariant(Guid id) + { + await mediator.Send(new DeleteProductVariantCommand { Id = id }); + return NoContent(); + } } \ No newline at end of file diff --git a/src/Imprink.WebApi/Controllers/Products/ProductsController.cs b/src/Imprink.WebApi/Controllers/Products/ProductsController.cs index 6567082..e1f7fc5 100644 --- a/src/Imprink.WebApi/Controllers/Products/ProductsController.cs +++ b/src/Imprink.WebApi/Controllers/Products/ProductsController.cs @@ -1,3 +1,4 @@ +using Imprink.Application.Domains.Products; using Imprink.Application.Products; using Imprink.Application.Products.Dtos; using Imprink.Domain.Models; @@ -20,4 +21,33 @@ public class ProductsController(IMediator mediator) : ControllerBase var result = await mediator.Send(new GetProductsQuery { FilterParameters = filterParameters}); return Ok(result); } + + [HttpPost] + [Authorize(Roles = "Admin")] + public async Task>> CreateProduct( + [FromBody] CreateProductCommand command) + { + var result = await mediator.Send(command); + return Ok(result); + } + + [HttpPut("{id:guid}")] + [Authorize(Roles = "Admin")] + public async Task> UpdateProduct( + Guid id, + [FromBody] UpdateProductCommand command) + { + if (id != command.Id) return BadRequest("ID mismatch"); + + var result = await mediator.Send(command); + return Ok(result); + } + + [HttpDelete("{id:guid}")] + [Authorize(Roles = "Admin")] + public async Task DeleteProduct(Guid id) + { + await mediator.Send(new DeleteProductCommand { Id = id }); + return NoContent(); + } } \ No newline at end of file diff --git a/src/Imprink.WebApi/Controllers/Users/UsersController.cs b/src/Imprink.WebApi/Controllers/Users/UsersController.cs index 606f313..53b6830 100644 --- a/src/Imprink.WebApi/Controllers/Users/UsersController.cs +++ b/src/Imprink.WebApi/Controllers/Users/UsersController.cs @@ -1,5 +1,5 @@ using System.Security.Claims; -using Imprink.Application.Users; +using Imprink.Application.Domains.Users; using Imprink.Domain.Models; using MediatR; using Microsoft.AspNetCore.Authorization; diff --git a/src/Imprink.WebApi/Startup.cs b/src/Imprink.WebApi/Startup.cs index 3ea0697..b7af072 100644 --- a/src/Imprink.WebApi/Startup.cs +++ b/src/Imprink.WebApi/Startup.cs @@ -1,8 +1,9 @@ using System.Security.Claims; using FluentValidation; using Imprink.Application; -using Imprink.Application.Products.Create; -using Imprink.Application.Service; +using Imprink.Application.Domains.Products; +using Imprink.Application.Products; +using Imprink.Application.Services; using Imprink.Application.Validation.Models; using Imprink.Domain.Repositories; using Imprink.Domain.Repositories.Products;