Dev #2
@@ -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<CategoryDto>
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using MediatR;
|
||||
|
||||
namespace Imprink.Application.Products.Delete;
|
||||
namespace Imprink.Application.Domains.Categories;
|
||||
|
||||
public class DeleteCategoryCommand : IRequest<bool>
|
||||
{
|
||||
@@ -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<IEnumerable<CategoryDto>>
|
||||
{
|
||||
@@ -0,0 +1,62 @@
|
||||
using Imprink.Application.Exceptions;
|
||||
using Imprink.Application.Products.Dtos;
|
||||
using MediatR;
|
||||
|
||||
namespace Imprink.Application.Domains.Categories;
|
||||
|
||||
public class UpdateCategoryCommand : IRequest<CategoryDto>
|
||||
{
|
||||
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<UpdateCategoryCommand, CategoryDto>
|
||||
{
|
||||
public async Task<CategoryDto> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ProductVariantDto>
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using MediatR;
|
||||
|
||||
namespace Imprink.Application.Products.Delete;
|
||||
namespace Imprink.Application.Domains.ProductVariants;
|
||||
|
||||
public class DeleteProductVariantCommand : IRequest<bool>
|
||||
{
|
||||
@@ -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<IEnumerable<ProductVariantDto>>
|
||||
{
|
||||
@@ -0,0 +1,69 @@
|
||||
using Imprink.Application.Exceptions;
|
||||
using Imprink.Application.Products.Dtos;
|
||||
using MediatR;
|
||||
|
||||
namespace Imprink.Application.Domains.ProductVariants;
|
||||
|
||||
public class UpdateProductVariantCommand : IRequest<ProductVariantDto>
|
||||
{
|
||||
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<UpdateProductVariantCommand, ProductVariantDto>
|
||||
{
|
||||
public async Task<ProductVariantDto> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ProductDto>
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using MediatR;
|
||||
|
||||
namespace Imprink.Application.Products.Delete;
|
||||
namespace Imprink.Application.Domains.Products;
|
||||
|
||||
public class DeleteProductCommand : IRequest<bool>
|
||||
{
|
||||
@@ -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<PagedResultDto<ProductDto>>
|
||||
{
|
||||
@@ -0,0 +1,78 @@
|
||||
using Imprink.Application.Exceptions;
|
||||
using Imprink.Application.Products.Dtos;
|
||||
using MediatR;
|
||||
|
||||
namespace Imprink.Application.Domains.Products;
|
||||
|
||||
public class UpdateProductCommand : IRequest<ProductDto>
|
||||
{
|
||||
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<UpdateProductCommand, ProductDto>
|
||||
{
|
||||
public async Task<ProductDto> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<UserRoleDto?>;
|
||||
|
||||
@@ -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<IEnumerable<RoleDto>>;
|
||||
|
||||
@@ -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<UserDto?>;
|
||||
|
||||
@@ -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<UserDto?>;
|
||||
|
||||
@@ -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<UserRoleDto?>;
|
||||
|
||||
@@ -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<UserDto?>;
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Validation\Users\" />
|
||||
<Folder Include="Domains\Orders\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Imprink.Application.Service;
|
||||
namespace Imprink.Application.Services;
|
||||
|
||||
public interface ICurrentUserService
|
||||
{
|
||||
@@ -0,0 +1,34 @@
|
||||
using FluentValidation;
|
||||
using Imprink.Application.Domains.Categories;
|
||||
|
||||
namespace Imprink.Application.Validation.Categories;
|
||||
|
||||
public class CreateCategoryCommandValidator : AbstractValidator<CreateCategoryCommand>
|
||||
{
|
||||
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 _);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using FluentValidation;
|
||||
using Imprink.Application.Domains.Categories;
|
||||
|
||||
namespace Imprink.Application.Validation.Categories;
|
||||
|
||||
public class DeleteCategoryCommandValidator : AbstractValidator<DeleteCategoryCommand>
|
||||
{
|
||||
public DeleteCategoryCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.Id)
|
||||
.NotEmpty().WithMessage("Id is required.")
|
||||
.NotEqual(Guid.Empty).WithMessage("Id must be a valid GUID.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using FluentValidation;
|
||||
using Imprink.Application.Domains.Categories;
|
||||
|
||||
namespace Imprink.Application.Validation.Categories;
|
||||
|
||||
public class UpdateCategoryCommandValidator : AbstractValidator<UpdateCategoryCommand>
|
||||
{
|
||||
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 _);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using FluentValidation;
|
||||
using Imprink.Domain.Models;
|
||||
|
||||
namespace Imprink.Application.Validation.Models;
|
||||
|
||||
public class OrderFilterParametersValidator : AbstractValidator<OrderFilterParameters>
|
||||
{
|
||||
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"];
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using FluentValidation;
|
||||
using Imprink.Application.Domains.ProductVariants;
|
||||
|
||||
namespace Imprink.Application.Validation.ProductVariants;
|
||||
|
||||
public class CreateProductVariantCommandValidator : AbstractValidator<CreateProductVariantCommand>
|
||||
{
|
||||
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 _);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using FluentValidation;
|
||||
using Imprink.Application.Domains.ProductVariants;
|
||||
|
||||
namespace Imprink.Application.Validation.ProductVariants;
|
||||
|
||||
public class DeleteProductVariantCommandValidator : AbstractValidator<DeleteProductVariantCommand>
|
||||
{
|
||||
public DeleteProductVariantCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.Id)
|
||||
.NotEmpty().WithMessage("Id is required.")
|
||||
.NotEqual(Guid.Empty).WithMessage("Id must be a valid GUID.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using FluentValidation;
|
||||
using Imprink.Application.Domains.ProductVariants;
|
||||
|
||||
namespace Imprink.Application.Validation.ProductVariants;
|
||||
|
||||
public class GetProductVariantsQueryValidator : AbstractValidator<GetProductVariantsQuery>
|
||||
{
|
||||
public GetProductVariantsQueryValidator()
|
||||
{
|
||||
RuleFor(x => x.ProductId)
|
||||
.NotEqual(Guid.Empty).When(x => x.ProductId.HasValue)
|
||||
.WithMessage("ProductId must be a valid GUID when provided.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using FluentValidation;
|
||||
using Imprink.Application.Domains.ProductVariants;
|
||||
|
||||
namespace Imprink.Application.Validation.ProductVariants;
|
||||
|
||||
public class UpdateProductVariantCommandValidator : AbstractValidator<UpdateProductVariantCommand>
|
||||
{
|
||||
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 _);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using FluentValidation;
|
||||
using Imprink.Application.Domains.Products;
|
||||
|
||||
namespace Imprink.Application.Validation.Products;
|
||||
|
||||
public class CreateProductCommandValidator : AbstractValidator<CreateProductCommand>
|
||||
{
|
||||
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 _);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using FluentValidation;
|
||||
using Imprink.Application.Domains.Products;
|
||||
|
||||
namespace Imprink.Application.Validation.Products;
|
||||
|
||||
public class DeleteProductCommandValidator : AbstractValidator<DeleteProductCommand>
|
||||
{
|
||||
public DeleteProductCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.Id)
|
||||
.NotEmpty().WithMessage("Id is required.")
|
||||
.NotEqual(Guid.Empty).WithMessage("Id must be a valid GUID.");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
using FluentValidation;
|
||||
using Imprink.Application.Domains.Products;
|
||||
|
||||
namespace Imprink.Application.Validation.Products;
|
||||
|
||||
public class UpdateProductCommandValidator : AbstractValidator<UpdateProductCommand>
|
||||
{
|
||||
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 _);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using FluentValidation;
|
||||
using Imprink.Application.Domains.Users;
|
||||
|
||||
namespace Imprink.Application.Validation.Users;
|
||||
|
||||
public class SetUserFullNameCommandValidator : AbstractValidator<SetUserFullNameCommand>
|
||||
{
|
||||
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.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using FluentValidation;
|
||||
using Imprink.Application.Domains.Users;
|
||||
|
||||
namespace Imprink.Application.Validation.Users;
|
||||
|
||||
public class SetUserPhoneCommandValidator : AbstractValidator<SetUserPhoneCommand>
|
||||
{
|
||||
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.");
|
||||
}
|
||||
}
|
||||
46
src/Imprink.Domain/Models/OrderFilterParameters.cs
Normal file
46
src/Imprink.Domain/Models/OrderFilterParameters.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using Imprink.Domain.Entities.Orders;
|
||||
|
||||
namespace Imprink.Domain.Repositories.Orders;
|
||||
|
||||
public interface IOrderItemRepository
|
||||
{
|
||||
Task<OrderItem?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
Task<OrderItem?> GetByIdWithProductAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
Task<OrderItem?> GetByIdWithVariantAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
Task<OrderItem?> GetByIdFullAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
Task<IEnumerable<OrderItem>> GetByOrderIdAsync(Guid orderId, CancellationToken cancellationToken = default);
|
||||
Task<IEnumerable<OrderItem>> GetByOrderIdWithProductsAsync(Guid orderId, CancellationToken cancellationToken = default);
|
||||
Task<IEnumerable<OrderItem>> GetByProductIdAsync(Guid productId, CancellationToken cancellationToken = default);
|
||||
Task<IEnumerable<OrderItem>> GetByProductVariantIdAsync(Guid productVariantId, CancellationToken cancellationToken = default);
|
||||
Task<IEnumerable<OrderItem>> GetCustomizedItemsAsync(CancellationToken cancellationToken = default);
|
||||
Task<IEnumerable<OrderItem>> GetByDateRangeAsync(DateTime startDate, DateTime endDate, CancellationToken cancellationToken = default);
|
||||
Task<OrderItem> AddAsync(OrderItem orderItem, CancellationToken cancellationToken = default);
|
||||
Task<IEnumerable<OrderItem>> AddRangeAsync(IEnumerable<OrderItem> orderItems, CancellationToken cancellationToken = default);
|
||||
Task<OrderItem> UpdateAsync(OrderItem orderItem, CancellationToken cancellationToken = default);
|
||||
Task DeleteAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
Task DeleteByOrderIdAsync(Guid orderId, CancellationToken cancellationToken = default);
|
||||
Task<bool> ExistsAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
Task<decimal> GetTotalValueByOrderIdAsync(Guid orderId, CancellationToken cancellationToken = default);
|
||||
Task<int> GetQuantityByProductIdAsync(Guid productId, CancellationToken cancellationToken = default);
|
||||
Task<int> GetQuantityByVariantIdAsync(Guid productVariantId, CancellationToken cancellationToken = default);
|
||||
Task<Dictionary<Guid, int>> GetProductSalesCountAsync(DateTime? startDate = null, DateTime? endDate = null, CancellationToken cancellationToken = default);
|
||||
}
|
||||
27
src/Imprink.Domain/Repositories/Orders/IOrderRepository.cs
Normal file
27
src/Imprink.Domain/Repositories/Orders/IOrderRepository.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using Imprink.Domain.Entities.Orders;
|
||||
using Imprink.Domain.Models;
|
||||
|
||||
namespace Imprink.Domain.Repositories.Orders;
|
||||
|
||||
public interface IOrderRepository
|
||||
{
|
||||
Task<Order?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
Task<Order?> GetByIdWithItemsAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
Task<Order?> GetByIdWithAddressAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
Task<Order?> GetByIdFullAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
Task<Order?> GetByOrderNumberAsync(string orderNumber, CancellationToken cancellationToken = default);
|
||||
Task<PagedResult<Order>> GetPagedAsync(OrderFilterParameters filterParameters, CancellationToken cancellationToken = default);
|
||||
Task<IEnumerable<Order>> GetByUserIdAsync(string userId, CancellationToken cancellationToken = default);
|
||||
Task<IEnumerable<Order>> GetByUserIdPagedAsync(string userId, int pageNumber, int pageSize, CancellationToken cancellationToken = default);
|
||||
Task<IEnumerable<Order>> GetByOrderStatusAsync(int orderStatusId, CancellationToken cancellationToken = default);
|
||||
Task<IEnumerable<Order>> GetByShippingStatusAsync(int shippingStatusId, CancellationToken cancellationToken = default);
|
||||
Task<IEnumerable<Order>> GetByDateRangeAsync(DateTime startDate, DateTime endDate, CancellationToken cancellationToken = default);
|
||||
Task<Order> AddAsync(Order order, CancellationToken cancellationToken = default);
|
||||
Task<Order> UpdateAsync(Order order, CancellationToken cancellationToken = default);
|
||||
Task DeleteAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
Task<bool> ExistsAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
Task<bool> OrderNumberExistsAsync(string orderNumber, Guid? excludeId = null, CancellationToken cancellationToken = default);
|
||||
Task<decimal> GetTotalRevenueAsync(CancellationToken cancellationToken = default);
|
||||
Task<decimal> GetTotalRevenueByDateRangeAsync(DateTime startDate, DateTime endDate, CancellationToken cancellationToken = default);
|
||||
Task<int> GetOrderCountByStatusAsync(int orderStatusId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -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<OrderItem?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await context.OrderItems
|
||||
.FirstOrDefaultAsync(oi => oi.Id == id, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<OrderItem?> 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<OrderItem?> GetByIdWithVariantAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await context.OrderItems
|
||||
.Include(oi => oi.ProductVariant)
|
||||
.FirstOrDefaultAsync(oi => oi.Id == id, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<OrderItem?> 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<IEnumerable<OrderItem>> GetByOrderIdAsync(Guid orderId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await context.OrderItems
|
||||
.Where(oi => oi.OrderId == orderId)
|
||||
.OrderBy(oi => oi.CreatedAt)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<OrderItem>> 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<IEnumerable<OrderItem>> 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<IEnumerable<OrderItem>> 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<IEnumerable<OrderItem>> 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<IEnumerable<OrderItem>> 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<OrderItem> 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<IEnumerable<OrderItem>> AddRangeAsync(IEnumerable<OrderItem> 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<IEnumerable<OrderItem>>(items);
|
||||
}
|
||||
|
||||
public Task<OrderItem> 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<bool> ExistsAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await context.OrderItems
|
||||
.AnyAsync(oi => oi.Id == id, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<decimal> GetTotalValueByOrderIdAsync(Guid orderId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await context.OrderItems
|
||||
.Where(oi => oi.OrderId == orderId)
|
||||
.SumAsync(oi => oi.TotalPrice, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<int> GetQuantityByProductIdAsync(Guid productId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await context.OrderItems
|
||||
.Where(oi => oi.ProductId == productId)
|
||||
.SumAsync(oi => oi.Quantity, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<int> GetQuantityByVariantIdAsync(Guid productVariantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await context.OrderItems
|
||||
.Where(oi => oi.ProductVariantId == productVariantId)
|
||||
.SumAsync(oi => oi.Quantity, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<Dictionary<Guid, int>> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<Order?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await context.Orders
|
||||
.FirstOrDefaultAsync(o => o.Id == id, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<Order?> 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<Order?> GetByIdWithAddressAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await context.Orders
|
||||
.Include(o => o.OrderAddress)
|
||||
.FirstOrDefaultAsync(o => o.Id == id, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<Order?> 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<Order?> 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<PagedResult<Order>> 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<Order>
|
||||
{
|
||||
Items = items,
|
||||
TotalCount = totalCount,
|
||||
PageNumber = filterParameters.PageNumber,
|
||||
PageSize = filterParameters.PageSize
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Order>> 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<IEnumerable<Order>> 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<IEnumerable<Order>> 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<IEnumerable<Order>> 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<IEnumerable<Order>> 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<Order> 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<Order> 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<bool> ExistsAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await context.Orders
|
||||
.AnyAsync(o => o.Id == id, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<bool> 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<decimal> 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<decimal> 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<int> GetOrderCountByStatusAsync(int orderStatusId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await context.Orders
|
||||
.CountAsync(o => o.OrderStatusId == orderStatusId, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
using System.Security.Claims;
|
||||
using Imprink.Application.Service;
|
||||
using Imprink.Application.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Imprink.Infrastructure.Services;
|
||||
|
||||
@@ -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<ActionResult<IEnumerable<CategoryDto>>> GetCategories([FromQuery] GetCategoriesQuery query)
|
||||
[AllowAnonymous]
|
||||
public async Task<ActionResult<IEnumerable<CategoryDto>>> GetCategories(
|
||||
[FromQuery] GetCategoriesQuery query)
|
||||
{
|
||||
return Ok(await mediator.Send(query));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize(Roles = "Admin")]
|
||||
public async Task<ActionResult<CategoryDto>> 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<ActionResult<CategoryDto>> 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<ActionResult> DeleteCategory(Guid id)
|
||||
{
|
||||
await mediator.Send(new DeleteCategoryCommand { Id = id });
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
@@ -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<ActionResult<IEnumerable<ProductVariantDto>>> GetProductVariants(
|
||||
[FromQuery] GetProductVariantsQuery query)
|
||||
{
|
||||
return Ok(await mediator.Send(query));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize(Roles = "Admin")]
|
||||
public async Task<ActionResult<ProductVariantDto>> 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<ActionResult<ProductVariantDto>> 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<ActionResult> DeleteProductVariant(Guid id)
|
||||
{
|
||||
await mediator.Send(new DeleteProductVariantCommand { Id = id });
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
@@ -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<ActionResult<PagedResultDto<ProductDto>>> CreateProduct(
|
||||
[FromBody] CreateProductCommand command)
|
||||
{
|
||||
var result = await mediator.Send(command);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
[Authorize(Roles = "Admin")]
|
||||
public async Task<ActionResult<ProductDto>> 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<ActionResult> DeleteProduct(Guid id)
|
||||
{
|
||||
await mediator.Send(new DeleteProductCommand { Id = id });
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user