feat ( ReviewEntitiy )

- Add review entity and CQRS
subProduct
Amir Hossein Khademi 2024-09-01 12:28:42 +03:30
parent 2e8501190e
commit fda7088abb
23 changed files with 91 additions and 45 deletions

View File

@ -1 +1 @@
1.2.11.13 1.3.12.16

View File

@ -38,12 +38,19 @@ public class ProductController : ICarterModule
.RequireAuthorization(builder => builder.AddAuthenticationSchemes("Bearer").RequireAuthenticatedUser().RequireClaim(CustomClaimType.Permission, ApplicationPermission.ManageProducts)) .RequireAuthorization(builder => builder.AddAuthenticationSchemes("Bearer").RequireAuthenticatedUser().RequireClaim(CustomClaimType.Permission, ApplicationPermission.ManageProducts))
.HasApiVersion(1.0); .HasApiVersion(1.0);
group.MapGet("{productId}/review",GetProductReviewsAsync)
.WithDisplayName("Get Product Reviews")
.HasApiVersion(1.0);
group.MapDelete("{id}", Delete) group.MapDelete("{id}", Delete)
.WithDisplayName("Delete Product") .WithDisplayName("Delete Product")
.RequireAuthorization(builder => builder.AddAuthenticationSchemes("Bearer").RequireAuthenticatedUser().RequireClaim(CustomClaimType.Permission, ApplicationPermission.ManageProducts)) .RequireAuthorization(builder => builder.AddAuthenticationSchemes("Bearer").RequireAuthenticatedUser().RequireClaim(CustomClaimType.Permission, ApplicationPermission.ManageProducts))
.HasApiVersion(1.0); .HasApiVersion(1.0);
} }
private async Task<IResult> GetProductReviewsAsync([FromQuery] int page, [FromRoute] Guid productId, IMediator mediator, CancellationToken cancellationToken)
=> TypedResults.Ok(await mediator.Send(new GetReviewsQuery(page, productId), cancellationToken));
// GET:Get All Entity // GET:Get All Entity
public async Task<IResult> GetAllAsync([FromQuery] int page, public async Task<IResult> GetAllAsync([FromQuery] int page,
[FromQuery] string? productName, [FromQuery] string? productName,

View File

@ -8,27 +8,27 @@ public class ProductReviewController : ICarterModule
.MapGroup("product/review"); .MapGroup("product/review");
group.MapGet("{id}", GetAsync) group.MapGet("{id}", GetAsync)
.WithDisplayName("Get ProductReview") .WithDisplayName("Get Product Review")
.RequireAuthorization(builder => builder.AddAuthenticationSchemes("Bearer").RequireAuthenticatedUser().RequireClaim(CustomClaimType.Permission, ApplicationPermission.ViewAllReviews,ApplicationPermission.ManageReview)) .RequireAuthorization(builder => builder.AddAuthenticationSchemes("Bearer").RequireAuthenticatedUser().RequireClaim(CustomClaimType.Permission, ApplicationPermission.ViewAllReviews,ApplicationPermission.ManageReview))
.HasApiVersion(1.0); .HasApiVersion(1.0);
group.MapGet("", GetAllAsync) group.MapGet("", GetAllAsync)
.WithDisplayName("Get ProductReview") .WithDisplayName("Get Product Reviews")
.RequireAuthorization(builder => builder.AddAuthenticationSchemes("Bearer").RequireAuthenticatedUser().RequireClaim(CustomClaimType.Permission, ApplicationPermission.ViewAllReviews, ApplicationPermission.ManageReview)) .RequireAuthorization(builder => builder.AddAuthenticationSchemes("Bearer").RequireAuthenticatedUser().RequireClaim(CustomClaimType.Permission, ApplicationPermission.ViewAllReviews, ApplicationPermission.ManageReview))
.HasApiVersion(1.0); .HasApiVersion(1.0);
group.MapPost("", PostAsync) group.MapPost("", PostAsync)
.WithDisplayName("Create ProductReview") .WithDisplayName("Create Product Review")
.RequireAuthorization(builder => builder.AddAuthenticationSchemes("Bearer").RequireAuthenticatedUser().RequireClaim(CustomClaimType.Permission, ApplicationPermission.ManageReview, ApplicationPermission.AddReview)) .RequireAuthorization(builder => builder.AddAuthenticationSchemes("Bearer").RequireAuthenticatedUser())
.HasApiVersion(1.0); .HasApiVersion(1.0);
group.MapPut("confirm/{id}", ConfirmAsync) group.MapPut("confirm/{id}", ConfirmAsync)
.WithDisplayName("Confirm ProductReview") .WithDisplayName("Confirm Product Review")
.RequireAuthorization(builder => builder.AddAuthenticationSchemes("Bearer").RequireAuthenticatedUser().RequireClaim(CustomClaimType.Permission, ApplicationPermission.ConfirmReview, ApplicationPermission.ManageReview)) .RequireAuthorization(builder => builder.AddAuthenticationSchemes("Bearer").RequireAuthenticatedUser().RequireClaim(CustomClaimType.Permission, ApplicationPermission.ConfirmReview, ApplicationPermission.ManageReview))
.HasApiVersion(1.0); .HasApiVersion(1.0);
group.MapDelete("{id}", DeleteAsync) group.MapDelete("{id}", DeleteAsync)
.WithDisplayName("Delete ProductReview") .WithDisplayName("Delete Product Review")
.RequireAuthorization(builder => builder.AddAuthenticationSchemes("Bearer").RequireAuthenticatedUser().RequireClaim(CustomClaimType.Permission, ApplicationPermission.ManageReview)) .RequireAuthorization(builder => builder.AddAuthenticationSchemes("Bearer").RequireAuthenticatedUser().RequireClaim(CustomClaimType.Permission, ApplicationPermission.ManageReview))
.HasApiVersion(1.0); .HasApiVersion(1.0);
} }

View File

@ -6,8 +6,8 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<InvariantGlobalization>true</InvariantGlobalization> <InvariantGlobalization>true</InvariantGlobalization>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS> <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<AssemblyVersion>1.2.11.13</AssemblyVersion> <AssemblyVersion>1.3.12.16</AssemblyVersion>
<FileVersion>1.2.11.13</FileVersion> <FileVersion>1.3.12.16</FileVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@ -7,6 +7,7 @@ builder.Host.UseSerilog();
LoggerConfig.ConfigureSerilog(); LoggerConfig.ConfigureSerilog();
string env = builder.Environment.IsDevelopment() == true ? "Development" : "Production"; string env = builder.Environment.IsDevelopment() == true ? "Development" : "Production";
builder.Host.UseContentRoot(Directory.GetCurrentDirectory()); builder.Host.UseContentRoot(Directory.GetCurrentDirectory());
if (builder.Environment.IsDevelopment()) if (builder.Environment.IsDevelopment())
{ {
string projectName = "Vesmeh"; string projectName = "Vesmeh";

View File

@ -104,7 +104,10 @@ public class PageService(
{ {
var newLink = pageSetting.RedirectItems.FirstOrDefault(f => f.OldUrl.ToLower().Trim() == oldEncode.ToLower().Trim()); var newLink = pageSetting.RedirectItems.FirstOrDefault(f => f.OldUrl.ToLower().Trim() == oldEncode.ToLower().Trim());
if (newLink != null) if (newLink != null)
return newLink.NewUrl; {
var response = newLink.NewUrl[0] == '/' ? newLink.NewUrl : $"/{newLink.NewUrl}";
return response;
}
else else
throw new BaseApiException(ApiResultStatusCode.NotFound, "Url not found"); throw new BaseApiException(ApiResultStatusCode.NotFound, "Url not found");
} }

View File

@ -1,9 +1,11 @@
namespace Netina.Core.EntityServices.ReviewHandlers; using Review = Netina.Domain.Entities.Reviews.Review;
namespace Netina.Core.EntityServices.ReviewHandlers;
public class ConfirmReviewCommandHandler(IRepositoryWrapper repositoryWrapper) public class ConfirmReviewCommandHandler(IRepositoryWrapper repositoryWrapper)
: IRequestHandler<ConfirmReviewCommand, bool> : IRequestHandler<ConfirmReviewCommand, Guid>
{ {
public async Task<bool> Handle(ConfirmReviewCommand request, CancellationToken cancellationToken) public async Task<Guid> Handle(ConfirmReviewCommand request, CancellationToken cancellationToken)
{ {
var review = await repositoryWrapper.SetRepository<Review>().TableNoTracking var review = await repositoryWrapper.SetRepository<Review>().TableNoTracking
.FirstOrDefaultAsync(r => r.Id == request.Id, cancellationToken); .FirstOrDefaultAsync(r => r.Id == request.Id, cancellationToken);
@ -13,6 +15,6 @@ public class ConfirmReviewCommandHandler(IRepositoryWrapper repositoryWrapper)
repositoryWrapper.SetRepository<Review>().Update(review); repositoryWrapper.SetRepository<Review>().Update(review);
await repositoryWrapper.SaveChangesAsync(cancellationToken); await repositoryWrapper.SaveChangesAsync(cancellationToken);
return true; return review.Id;
} }
} }

View File

@ -1,6 +1,6 @@
namespace Netina.Domain.CommandQueries.Commands; namespace Netina.Domain.CommandQueries.Commands;
public sealed record CreateReviewCommand(string Title, string Comment, float Rate, bool IsBuyer, Guid ProductId, Guid UserId) : IRequest<ReviewSDto>; public sealed record CreateReviewCommand(string Title, string Comment, float Rate, bool IsBuyer, Guid ProductId, Guid UserId) : IRequest<Guid>;
public sealed record UpdateReviewCommand(Guid Id,string Title, string Comment, float Rate, bool IsBuyer, Guid ProductId, Guid UserId): IRequest<bool>; public sealed record UpdateReviewCommand(Guid Id,string Title, string Comment, float Rate, bool IsBuyer, Guid ProductId, Guid UserId): IRequest<Guid>;
public sealed record ConfirmReviewCommand(Guid Id) : IRequest<bool>; public sealed record ConfirmReviewCommand(Guid Id) : IRequest<Guid>;
public sealed record DeleteReviewCommand(Guid Id) : IRequest<bool>; public sealed record DeleteReviewCommand(Guid Id) : IRequest<Guid>;

View File

@ -1,4 +1,4 @@
namespace Netina.Domain.CommandQueries.Queries; namespace Netina.Domain.CommandQueries.Queries;
public record GetReviewsQuery(int Page = 0) : IRequest<List<ReviewSDto>>; public record GetReviewsQuery(int Page = 0,Guid? ProductId = null) : IRequest<List<ReviewSDto>>;
public record GetReviewQuery(Guid Id) : IRequest<ReviewLDto>; public record GetReviewQuery(Guid Id) : IRequest<ReviewLDto>;

View File

@ -1,4 +1,6 @@
namespace Netina.Domain.Dtos.LargDtos; using Review = Netina.Domain.Entities.Reviews.Review;
namespace Netina.Domain.Dtos.LargDtos;
public class ReviewLDto : BaseDto<ReviewLDto,Review> public class ReviewLDto : BaseDto<ReviewLDto,Review>
{ {

View File

@ -1,4 +1,6 @@
namespace Netina.Domain.Dtos.SmallDtos; using Review = Netina.Domain.Entities.Reviews.Review;
namespace Netina.Domain.Dtos.SmallDtos;
public class ReviewSDto : BaseDto<ReviewSDto, Review> public class ReviewSDto : BaseDto<ReviewSDto, Review>
{ {

View File

@ -69,16 +69,7 @@ public partial class ProductStorageFile
} }
} }
public partial class Review
{
public static Review Create(string title, string comment, float rate, bool isBuyer, Guid productId, Guid userId)
{
return new Review(title, comment, rate, isBuyer, productId, userId);
}
public void ConfirmReview()
=> IsConfirmed = true;
}
public partial class Specification public partial class Specification
{ {

View File

@ -76,7 +76,7 @@ public partial class Product : ApiEntity
public ProductCategory? Category { get; internal set; } public ProductCategory? Category { get; internal set; }
public List<Specification> Specifications { get; internal set; } = new(); public List<Specification> Specifications { get; internal set; } = new();
public List<Review> Reviews { get; internal set; } = new(); public List<Reviews.Review> Reviews { get; internal set; } = new();
public List<ProductStorageFile> Files { get; internal set; } = new(); public List<ProductStorageFile> Files { get; internal set; } = new();
public List<OrderProduct> OrderProducts { get; internal set; } = new(); public List<OrderProduct> OrderProducts { get; internal set; } = new();

View File

@ -0,0 +1,12 @@
namespace Netina.Domain.Entities.Reviews;
public partial class Review
{
public static Reviews.Review Create(string title, string comment, float rate, bool isBuyer, Guid productId, Guid userId)
{
return new Reviews.Review(title, comment, rate, isBuyer, productId, userId);
}
public void ConfirmReview()
=> IsConfirmed = true;
}

View File

@ -1,4 +1,4 @@
namespace Netina.Domain.Entities.Products; namespace Netina.Domain.Entities.Reviews;
[AdaptTwoWays("[name]LDto", IgnoreAttributes = new[] { typeof(AdaptIgnoreAttribute) }, MapType = MapType.Map | MapType.MapToTarget | MapType.Projection)] [AdaptTwoWays("[name]LDto", IgnoreAttributes = new[] { typeof(AdaptIgnoreAttribute) }, MapType = MapType.Map | MapType.MapToTarget | MapType.Projection)]
[AdaptTwoWays("[name]SDto", IgnoreAttributes = new[] { typeof(AdaptIgnoreAttribute) }, MapType = MapType.Map | MapType.MapToTarget)] [AdaptTwoWays("[name]SDto", IgnoreAttributes = new[] { typeof(AdaptIgnoreAttribute) }, MapType = MapType.Map | MapType.MapToTarget)]
[AdaptTo("[name]SDto", IgnoreAttributes = new[] { typeof(AdaptIgnoreAttribute) }, MapType = MapType.Projection)] [AdaptTo("[name]SDto", IgnoreAttributes = new[] { typeof(AdaptIgnoreAttribute) }, MapType = MapType.Projection)]

View File

@ -9,6 +9,7 @@ using Netina.Domain.Dtos.SmallDtos;
using Netina.Domain.Entities.Brands; using Netina.Domain.Entities.Brands;
using Netina.Domain.Entities.ProductCategories; using Netina.Domain.Entities.ProductCategories;
using Netina.Domain.Entities.Products; using Netina.Domain.Entities.Products;
using Review = Netina.Domain.Entities.Reviews.Review;
namespace Netina.Domain.Mappers namespace Netina.Domain.Mappers
{ {

View File

@ -3,6 +3,7 @@ using System.Linq.Expressions;
using Netina.Domain.Dtos.LargDtos; using Netina.Domain.Dtos.LargDtos;
using Netina.Domain.Dtos.SmallDtos; using Netina.Domain.Dtos.SmallDtos;
using Netina.Domain.Entities.Products; using Netina.Domain.Entities.Products;
using Review = Netina.Domain.Entities.Reviews.Review;
namespace Netina.Domain.Mappers namespace Netina.Domain.Mappers
{ {

View File

@ -60,7 +60,7 @@
<ItemGroup> <ItemGroup>
<Using Include="Mapster" /> <Using Include="Mapster" />
<Using Include="MediatR" /> <Using Include="MediatR" />
<Using Include="Microsoft.EntityFrameworkCore"/> <Using Include="Microsoft.EntityFrameworkCore" />
<Using Include="Microsoft.AspNetCore.Identity" /> <Using Include="Microsoft.AspNetCore.Identity" />
<Using Include="Microsoft.Extensions.Logging" /> <Using Include="Microsoft.Extensions.Logging" />
<Using Include="Netina.Common.Extensions" /> <Using Include="Netina.Common.Extensions" />

View File

@ -2,13 +2,19 @@
namespace Netina.Repository.Handlers.Reviews; namespace Netina.Repository.Handlers.Reviews;
public class CreateReviewCommandHandler(IRepositoryWrapper repositoryWrapper) public class CreateReviewCommandHandler(IRepositoryWrapper repositoryWrapper,ICurrentUserService currentUserService)
: IRequestHandler<CreateReviewCommand, ReviewSDto> : IRequestHandler<CreateReviewCommand, Guid>
{ {
public async Task<ReviewSDto> Handle(CreateReviewCommand request, CancellationToken cancellationToken) public async Task<Guid> Handle(CreateReviewCommand request, CancellationToken cancellationToken)
{ {
Guid userId = request.UserId;
if (userId == default)
{
if (!Guid.TryParse(currentUserService.UserId, out userId))
throw new AppException("User id is wrong", ApiResultStatusCode.BadRequest);
}
var review = Review.Create(request.Title, request.Comment, request.Rate, request.IsBuyer, request.ProductId, var review = Review.Create(request.Title, request.Comment, request.Rate, request.IsBuyer, request.ProductId,
request.UserId); userId);
var product = await repositoryWrapper.SetRepository<Product>() var product = await repositoryWrapper.SetRepository<Product>()
.TableNoTracking .TableNoTracking
.FirstOrDefaultAsync(p => p.Id == request.ProductId, cancellationToken); .FirstOrDefaultAsync(p => p.Id == request.ProductId, cancellationToken);
@ -23,6 +29,6 @@ public class CreateReviewCommandHandler(IRepositoryWrapper repositoryWrapper)
repositoryWrapper.SetRepository<Review>().Add(review); repositoryWrapper.SetRepository<Review>().Add(review);
await repositoryWrapper.SaveChangesAsync(cancellationToken); await repositoryWrapper.SaveChangesAsync(cancellationToken);
return review.AdaptToSDto(); return review.Id;
} }
} }

View File

@ -1,17 +1,26 @@
namespace Netina.Repository.Handlers.Reviews; namespace Netina.Repository.Handlers.Reviews;
public class DeleteReviewCommandHandler(IRepositoryWrapper repositoryWrapper) public class DeleteReviewCommandHandler(IRepositoryWrapper repositoryWrapper)
: IRequestHandler<DeleteReviewCommand, bool> : IRequestHandler<DeleteReviewCommand, Guid>
{ {
public async Task<bool> Handle(DeleteReviewCommand request, CancellationToken cancellationToken) public async Task<Guid> Handle(DeleteReviewCommand request, CancellationToken cancellationToken)
{ {
var review = await repositoryWrapper.SetRepository<Review>().TableNoTracking var review = await repositoryWrapper.SetRepository<Review>().TableNoTracking
.FirstOrDefaultAsync(r => r.Id == request.Id, cancellationToken); .FirstOrDefaultAsync(r => r.Id == request.Id, cancellationToken);
if (review == null) if (review == null)
throw new AppException("Review not found", ApiResultStatusCode.NotFound); throw new AppException("Review not found", ApiResultStatusCode.NotFound);
var product = await repositoryWrapper.SetRepository<Product>()
.TableNoTracking
.FirstOrDefaultAsync(p => p.Id == review.ProductId, cancellationToken);
if (product != null)
{
product.RemoveRate(review.Rate);
repositoryWrapper.SetRepository<Product>().Update(product);
await repositoryWrapper.SaveChangesAsync(cancellationToken);
}
repositoryWrapper.SetRepository<Review>().Delete(review); repositoryWrapper.SetRepository<Review>().Delete(review);
await repositoryWrapper.SaveChangesAsync(cancellationToken); await repositoryWrapper.SaveChangesAsync(cancellationToken);
return true; return review.Id;
} }
} }

View File

@ -1,4 +1,6 @@
namespace Netina.Repository.Handlers.Reviews; using Review = Netina.Domain.Entities.Reviews.Review;
namespace Netina.Repository.Handlers.Reviews;
public class GetReviewQueryHandler(IRepositoryWrapper repositoryWrapper) : IRequestHandler<GetReviewQuery, ReviewLDto> public class GetReviewQueryHandler(IRepositoryWrapper repositoryWrapper) : IRequestHandler<GetReviewQuery, ReviewLDto>
{ {

View File

@ -1,12 +1,18 @@
namespace Netina.Repository.Handlers.Reviews; using Review = Netina.Domain.Entities.Reviews.Review;
namespace Netina.Repository.Handlers.Reviews;
public class GetReviewsQueryHandler(IRepositoryWrapper repositoryWrapper) public class GetReviewsQueryHandler(IRepositoryWrapper repositoryWrapper)
: IRequestHandler<GetReviewsQuery, List<ReviewSDto>> : IRequestHandler<GetReviewsQuery, List<ReviewSDto>>
{ {
public async Task<List<ReviewSDto>> Handle(GetReviewsQuery request, CancellationToken cancellationToken) public async Task<List<ReviewSDto>> Handle(GetReviewsQuery request, CancellationToken cancellationToken)
{ {
return await repositoryWrapper.SetRepository<Review>() var query = repositoryWrapper.SetRepository<Review>()
.TableNoTracking .TableNoTracking;
if (request.ProductId != null)
query = query.Where(q => q.ProductId == request.ProductId);
return await query
.OrderByDescending(r => r.CreatedAt) .OrderByDescending(r => r.CreatedAt)
.Skip(request.Page * 15) .Skip(request.Page * 15)
.Take(15) .Take(15)

View File

@ -62,6 +62,7 @@
<Using Include="Netina.Domain.Entities.Discounts" /> <Using Include="Netina.Domain.Entities.Discounts" />
<Using Include="Netina.Domain.Entities.ProductCategories" /> <Using Include="Netina.Domain.Entities.ProductCategories" />
<Using Include="Netina.Domain.Entities.Products" /> <Using Include="Netina.Domain.Entities.Products" />
<Using Include="Netina.Domain.Entities.Reviews" />
<Using Include="Netina.Domain.Entities.Users" /> <Using Include="Netina.Domain.Entities.Users" />
<Using Include="Netina.Domain.Enums" /> <Using Include="Netina.Domain.Enums" />
<Using Include="Netina.Domain.Extensions" /> <Using Include="Netina.Domain.Extensions" />