From 5eaa2f4df99be55f3c6d45c54dc322976c720828 Mon Sep 17 00:00:00 2001 From: NourhaneAmir <92952125+NourhaneAmir@users.noreply.github.com> Date: Fri, 27 Jun 2025 00:35:41 +0300 Subject: [PATCH 01/12] Add Review Service and Controller --- .../AutoMapper/Review/ReviewProfile.cs | 22 +++++++ Dentizone.Application/DI/Services.cs | 2 + .../DTOs/Review/CreateReviewDTO.cs | 38 ++++++++++++ .../DTOs/Review/ReviewDTO.cs | 29 ++++++++++ .../DTOs/Review/UpdateReviewDTO.cs | 23 ++++++++ .../Interfaces/Review/IReviewService.cs | 23 ++++++++ .../Services/ReviewService.cs | 58 +++++++++++++++++++ .../DependencyInjection/AddRepositories.cs | 2 +- .../Controllers/ReviewController.cs | 48 +++++++++++++++ 9 files changed, 244 insertions(+), 1 deletion(-) create mode 100644 Dentizone.Application/AutoMapper/Review/ReviewProfile.cs create mode 100644 Dentizone.Application/DTOs/Review/CreateReviewDTO.cs create mode 100644 Dentizone.Application/DTOs/Review/ReviewDTO.cs create mode 100644 Dentizone.Application/DTOs/Review/UpdateReviewDTO.cs create mode 100644 Dentizone.Application/Interfaces/Review/IReviewService.cs create mode 100644 Dentizone.Application/Services/ReviewService.cs create mode 100644 Dentizone.Presentaion/Controllers/ReviewController.cs diff --git a/Dentizone.Application/AutoMapper/Review/ReviewProfile.cs b/Dentizone.Application/AutoMapper/Review/ReviewProfile.cs new file mode 100644 index 0000000..11a26b4 --- /dev/null +++ b/Dentizone.Application/AutoMapper/Review/ReviewProfile.cs @@ -0,0 +1,22 @@ +using AutoMapper; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Dentizone.Application.DTOs.Review; +using Dentizone.Domain.Entity; + +namespace Dentizone.Application.AutoMapper.Review +{ + internal class ReviewProfile:Profile + { + public ReviewProfile() + { + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + } + } + +} diff --git a/Dentizone.Application/DI/Services.cs b/Dentizone.Application/DI/Services.cs index 19db800..31690e6 100644 --- a/Dentizone.Application/DI/Services.cs +++ b/Dentizone.Application/DI/Services.cs @@ -7,6 +7,7 @@ using Dentizone.Application.Interfaces.Favorites; using Dentizone.Application.Interfaces.Order; using Dentizone.Application.Interfaces.Post; +using Dentizone.Application.Interfaces.Review; using Dentizone.Application.Interfaces.User; using Dentizone.Application.Services; using Dentizone.Application.Services.Authentication; @@ -41,6 +42,7 @@ public static IServiceCollection AddApplicationServices(this IServiceCollection services.AddScoped(); services.AddScoped(); + services.AddScoped(); return services; } diff --git a/Dentizone.Application/DTOs/Review/CreateReviewDTO.cs b/Dentizone.Application/DTOs/Review/CreateReviewDTO.cs new file mode 100644 index 0000000..9fe7924 --- /dev/null +++ b/Dentizone.Application/DTOs/Review/CreateReviewDTO.cs @@ -0,0 +1,38 @@ +using FluentValidation; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Dentizone.Application.DTOs.Review +{ + public class CreateReviewDTO + { + public string ID { get; set; } + public string UserId { get; set; } + public string OrderItemId { get; set; } + public decimal Stars { get; set; } + public string Comment { get; set; } + + } + public class CreateReviewDtoValidation : AbstractValidator + { + public CreateReviewDtoValidation() + { + RuleFor(x => x.UserId) + .NotEmpty().WithMessage("User ID is required.") + .NotNull().WithMessage("User ID cannot be null."); + + RuleFor(x => x.OrderItemId) + .NotEmpty().WithMessage("Order Item ID is required.") + .NotNull().WithMessage("Order Item ID cannot be null."); + + RuleFor(x => x.Stars) + .InclusiveBetween(1, 5).WithMessage("Stars must be between 1 and 5."); + + RuleFor(x => x.Comment) + .MaximumLength(500).WithMessage("Comment must not exceed 500 characters."); + } + } +} diff --git a/Dentizone.Application/DTOs/Review/ReviewDTO.cs b/Dentizone.Application/DTOs/Review/ReviewDTO.cs new file mode 100644 index 0000000..50dc16c --- /dev/null +++ b/Dentizone.Application/DTOs/Review/ReviewDTO.cs @@ -0,0 +1,29 @@ +using FluentValidation; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Dentizone.Application.DTOs.Review +{ + public class ReviewDTO + { + public string Comment { get; set; } + public decimal Stars { get; set; } + } + + public class ReviewDtoValidation : AbstractValidator + { + public ReviewDtoValidation() + { + RuleFor(x => x.Comment) + .NotNull().WithMessage("Comment cannot be null.") + .MaximumLength(500).WithMessage("Comment must not exceed 500 characters."); + + RuleFor(x => x.Stars) + .InclusiveBetween(1, 5).WithMessage("Stars must be between 1 and 5."); + } + } + +} diff --git a/Dentizone.Application/DTOs/Review/UpdateReviewDTO.cs b/Dentizone.Application/DTOs/Review/UpdateReviewDTO.cs new file mode 100644 index 0000000..0872f8a --- /dev/null +++ b/Dentizone.Application/DTOs/Review/UpdateReviewDTO.cs @@ -0,0 +1,23 @@ +using FluentValidation; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Dentizone.Application.DTOs.Review +{ + public class UpdateReviewDTO + { + public string Comment { get; set; } + } + public class UpdateReviewDtoValidation : AbstractValidator + { + public UpdateReviewDtoValidation() + { + RuleFor(x => x.Comment) + .NotNull().WithMessage("Comment cannot be null.") + .MaximumLength(500).WithMessage("Comment must not exceed 500 characters."); + } + } +} diff --git a/Dentizone.Application/Interfaces/Review/IReviewService.cs b/Dentizone.Application/Interfaces/Review/IReviewService.cs new file mode 100644 index 0000000..9addf1f --- /dev/null +++ b/Dentizone.Application/Interfaces/Review/IReviewService.cs @@ -0,0 +1,23 @@ +using Dentizone.Application.DTOs.Review; +using Dentizone.Domain.Entity; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.Extensions.Hosting; +using Pipelines.Sockets.Unofficial; +using StackExchange.Redis; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using static Microsoft.EntityFrameworkCore.DbLoggerCategory; + +namespace Dentizone.Application.Interfaces.Review +{ + public interface IReviewService + { + Task CreateOrderReviewAsync(string userId,CreateReviewDTO createReviewDto); + Task UpdateReviewAsync(string reviewId, UpdateReviewDTO updateReviewDto); + Task DeleteReviewAsync(string reviewId); + Task > GetUserReviewsTaken(string userId); + } +} diff --git a/Dentizone.Application/Services/ReviewService.cs b/Dentizone.Application/Services/ReviewService.cs new file mode 100644 index 0000000..dc4de09 --- /dev/null +++ b/Dentizone.Application/Services/ReviewService.cs @@ -0,0 +1,58 @@ +using AutoMapper; +using Dentizone.Application.DTOs.Review; +using Dentizone.Application.Interfaces.Review; +using Dentizone.Domain.Entity; +using Dentizone.Domain.Interfaces.Repositories; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Dentizone.Application.Services +{ + public class ReviewService(IMapper mapper,IReviewRepository repo) : IReviewService + { + public async Task CreateOrderReviewAsync(string userId, CreateReviewDTO createReviewDto) + { + createReviewDto.UserId = userId; + + var review = mapper.Map(createReviewDto); + + review.CreatedAt = DateTime.UtcNow; + review.UpdatedAt = DateTime.UtcNow; + review.IsDeleted = false; + + await repo.CreateAsync(review); + + } + + public async Task DeleteReviewAsync(string reviewId) + { + await repo.DeleteAsync(reviewId); + } + + public async Task> GetUserReviewsTaken(string userId) + { + var review = await repo.FindBy(r => r.UserId == userId && !r.IsDeleted); + + if (review == null) + return Enumerable.Empty(); + + return new List { mapper.Map(review) }; + } + + public async Task UpdateReviewAsync(string reviewId, UpdateReviewDTO updateReviewDto) + { + var review = await repo.GetByIdAsync(reviewId); + if (review == null || review.IsDeleted) + throw new KeyNotFoundException("Review not found."); + + // Update fields + review.Text = updateReviewDto.Comment; + review.UpdatedAt = DateTime.UtcNow; + + await repo.Update(review); + } + } +} diff --git a/Dentizone.Infrastructure/DependencyInjection/AddRepositories.cs b/Dentizone.Infrastructure/DependencyInjection/AddRepositories.cs index 5c5feff..fa1d761 100644 --- a/Dentizone.Infrastructure/DependencyInjection/AddRepositories.cs +++ b/Dentizone.Infrastructure/DependencyInjection/AddRepositories.cs @@ -25,7 +25,7 @@ public static IServiceCollection AddRepositories(this IServiceCollection service services.AddScoped(); services.AddScoped(); services.AddScoped(); - + services.AddScoped(); return services; } } diff --git a/Dentizone.Presentaion/Controllers/ReviewController.cs b/Dentizone.Presentaion/Controllers/ReviewController.cs new file mode 100644 index 0000000..06f6cdf --- /dev/null +++ b/Dentizone.Presentaion/Controllers/ReviewController.cs @@ -0,0 +1,48 @@ +using Dentizone.Application.DTOs.Review; +using Dentizone.Application.Interfaces.Review; +using Microsoft.AspNetCore.Mvc; + +namespace Dentizone.Presentaion.Controllers +{ + [ApiController] + [Route("api/[controller]")] + + public class ReviewController:ControllerBase + { + private readonly IReviewService _reviewService; + public ReviewController(IReviewService reviewService) + { + _reviewService = reviewService; + } + [HttpPost] + public async Task CreateOrderReview([FromBody] CreateReviewDTO createReviewDto) + { + var userId = createReviewDto.UserId; + if (string.IsNullOrEmpty(userId)) + return BadRequest("UserId is required."); + + await _reviewService.CreateOrderReviewAsync(userId, createReviewDto); + return Ok(); + } + [HttpPut("{reviewId}")] + public async Task UpdateReview(string reviewId, [FromBody] UpdateReviewDTO updateReviewDto) + { + await _reviewService.UpdateReviewAsync(reviewId, updateReviewDto); + return Ok(); + } + + [HttpDelete("{reviewId}")] + public async Task DeleteReview(string reviewId) + { + await _reviewService.DeleteReviewAsync(reviewId); + return Ok(); + } + + [HttpGet("user/{userId}")] + public async Task GetUserReviews(string userId) + { + var reviews = await _reviewService.GetUserReviewsTaken(userId); + return Ok(reviews); + } + } +} From 42e8f673d3752fe9ccb885efda92e69c79c4d05f Mon Sep 17 00:00:00 2001 From: Mahmoud Nasr <239.nasr@gmail.com> Date: Sat, 28 Jun 2025 11:15:14 +0300 Subject: [PATCH 02/12] Refactor DTOs and update related services and profiles - Updated `using` directives in `WalletProfile.cs` and `ReviewProfile.cs` to include necessary namespaces. - Renamed `ReviewDTO`, `CreateReviewDTO`, and `UpdateReviewDTO` to `ReviewDto`, `CreateReviewDto`, and `UpdateReviewDto` across multiple files. - Adjusted mapping configurations in `ReviewProfile.cs` to reflect new naming conventions. - Modified `IReviewService` and `ReviewService.cs` to utilize the updated DTO names. - Reformatted constructors in `WalletService.cs` and `UserService.cs` for improved readability. - Added `FindAllBy` method to `IReviewRepository` and implemented it in `ReviewRepository.cs`. - Updated `ReviewController.cs` to align with new DTO method signatures. - Removed unused `using` directives in `AuthService.cs` and `Program.cs` to streamline code. --- .../AutoMapper/Payments/WalletProfile.cs | 7 +--- .../AutoMapper/Review/ReviewProfile.cs | 18 +++------- .../DTOs/Review/CreateReviewDTO.cs | 19 +++------- .../DTOs/Review/ReviewDTO.cs | 12 ++----- .../DTOs/Review/UpdateReviewDTO.cs | 12 +++---- .../Interfaces/Review/IReviewService.cs | 21 +++-------- .../Services/Authentication/AuthService.cs | 1 - .../Services/Payment/WalletService.cs | 7 ++-- .../Services/ReviewService.cs | 35 ++++++------------- Dentizone.Application/Services/UserService.cs | 19 +++++----- .../Repositories/IReviewRepository.cs | 4 ++- .../Repositories/ReviewRepository.cs | 10 ++++-- .../Controllers/ReviewController.cs | 12 ++++--- Dentizone.Presentaion/Program.cs | 1 - 14 files changed, 66 insertions(+), 112 deletions(-) diff --git a/Dentizone.Application/AutoMapper/Payments/WalletProfile.cs b/Dentizone.Application/AutoMapper/Payments/WalletProfile.cs index 2f99d9a..2972cf4 100644 --- a/Dentizone.Application/AutoMapper/Payments/WalletProfile.cs +++ b/Dentizone.Application/AutoMapper/Payments/WalletProfile.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using AutoMapper; +using AutoMapper; using Dentizone.Application.Services.Payment; using Dentizone.Domain.Entity; diff --git a/Dentizone.Application/AutoMapper/Review/ReviewProfile.cs b/Dentizone.Application/AutoMapper/Review/ReviewProfile.cs index 11a26b4..5c087a5 100644 --- a/Dentizone.Application/AutoMapper/Review/ReviewProfile.cs +++ b/Dentizone.Application/AutoMapper/Review/ReviewProfile.cs @@ -1,22 +1,14 @@ using AutoMapper; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Dentizone.Application.DTOs.Review; -using Dentizone.Domain.Entity; namespace Dentizone.Application.AutoMapper.Review { - internal class ReviewProfile:Profile + internal class ReviewProfile : Profile { public ReviewProfile() { - CreateMap().ReverseMap(); - CreateMap().ReverseMap(); - CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); } } - -} +} \ No newline at end of file diff --git a/Dentizone.Application/DTOs/Review/CreateReviewDTO.cs b/Dentizone.Application/DTOs/Review/CreateReviewDTO.cs index 9fe7924..3946a52 100644 --- a/Dentizone.Application/DTOs/Review/CreateReviewDTO.cs +++ b/Dentizone.Application/DTOs/Review/CreateReviewDTO.cs @@ -1,29 +1,18 @@ using FluentValidation; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Dentizone.Application.DTOs.Review { - public class CreateReviewDTO + public class CreateReviewDto { - public string ID { get; set; } - public string UserId { get; set; } public string OrderItemId { get; set; } public decimal Stars { get; set; } public string Comment { get; set; } - } - public class CreateReviewDtoValidation : AbstractValidator + + public class CreateReviewDtoValidation : AbstractValidator { public CreateReviewDtoValidation() { - RuleFor(x => x.UserId) - .NotEmpty().WithMessage("User ID is required.") - .NotNull().WithMessage("User ID cannot be null."); - RuleFor(x => x.OrderItemId) .NotEmpty().WithMessage("Order Item ID is required.") .NotNull().WithMessage("Order Item ID cannot be null."); @@ -35,4 +24,4 @@ public CreateReviewDtoValidation() .MaximumLength(500).WithMessage("Comment must not exceed 500 characters."); } } -} +} \ No newline at end of file diff --git a/Dentizone.Application/DTOs/Review/ReviewDTO.cs b/Dentizone.Application/DTOs/Review/ReviewDTO.cs index 50dc16c..29ea88b 100644 --- a/Dentizone.Application/DTOs/Review/ReviewDTO.cs +++ b/Dentizone.Application/DTOs/Review/ReviewDTO.cs @@ -1,19 +1,14 @@ using FluentValidation; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Dentizone.Application.DTOs.Review { - public class ReviewDTO + public class ReviewDto { public string Comment { get; set; } public decimal Stars { get; set; } } - public class ReviewDtoValidation : AbstractValidator + public class ReviewDtoValidation : AbstractValidator { public ReviewDtoValidation() { @@ -25,5 +20,4 @@ public ReviewDtoValidation() .InclusiveBetween(1, 5).WithMessage("Stars must be between 1 and 5."); } } - -} +} \ No newline at end of file diff --git a/Dentizone.Application/DTOs/Review/UpdateReviewDTO.cs b/Dentizone.Application/DTOs/Review/UpdateReviewDTO.cs index 0872f8a..f99f458 100644 --- a/Dentizone.Application/DTOs/Review/UpdateReviewDTO.cs +++ b/Dentizone.Application/DTOs/Review/UpdateReviewDTO.cs @@ -1,17 +1,13 @@ using FluentValidation; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Dentizone.Application.DTOs.Review { - public class UpdateReviewDTO + public class UpdateReviewDto { public string Comment { get; set; } } - public class UpdateReviewDtoValidation : AbstractValidator + + public class UpdateReviewDtoValidation : AbstractValidator { public UpdateReviewDtoValidation() { @@ -20,4 +16,4 @@ public UpdateReviewDtoValidation() .MaximumLength(500).WithMessage("Comment must not exceed 500 characters."); } } -} +} \ No newline at end of file diff --git a/Dentizone.Application/Interfaces/Review/IReviewService.cs b/Dentizone.Application/Interfaces/Review/IReviewService.cs index 9addf1f..4a77bf5 100644 --- a/Dentizone.Application/Interfaces/Review/IReviewService.cs +++ b/Dentizone.Application/Interfaces/Review/IReviewService.cs @@ -1,23 +1,12 @@ using Dentizone.Application.DTOs.Review; -using Dentizone.Domain.Entity; -using Microsoft.AspNetCore.Http.HttpResults; -using Microsoft.Extensions.Hosting; -using Pipelines.Sockets.Unofficial; -using StackExchange.Redis; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using static Microsoft.EntityFrameworkCore.DbLoggerCategory; namespace Dentizone.Application.Interfaces.Review { public interface IReviewService { - Task CreateOrderReviewAsync(string userId,CreateReviewDTO createReviewDto); - Task UpdateReviewAsync(string reviewId, UpdateReviewDTO updateReviewDto); - Task DeleteReviewAsync(string reviewId); - Task > GetUserReviewsTaken(string userId); + Task CreateOrderReviewAsync(string userId, CreateReviewDto createReviewDto); + Task UpdateReviewAsync(string reviewId, UpdateReviewDto updateReviewDto); + Task DeleteReviewAsync(string reviewId); + Task> GetUserReviewsTaken(string userId); } -} +} \ No newline at end of file diff --git a/Dentizone.Application/Services/Authentication/AuthService.cs b/Dentizone.Application/Services/Authentication/AuthService.cs index 84050cb..cae13a5 100644 --- a/Dentizone.Application/Services/Authentication/AuthService.cs +++ b/Dentizone.Application/Services/Authentication/AuthService.cs @@ -1,7 +1,6 @@ using Dentizone.Application.DTOs.Auth; using Dentizone.Application.DTOs.User; using Dentizone.Application.Interfaces; -using Dentizone.Application.Services.Payment; using Dentizone.Domain.Enums; using Dentizone.Domain.Exceptions; using Dentizone.Domain.Interfaces; diff --git a/Dentizone.Application/Services/Payment/WalletService.cs b/Dentizone.Application/Services/Payment/WalletService.cs index b6364e0..d74c1a0 100644 --- a/Dentizone.Application/Services/Payment/WalletService.cs +++ b/Dentizone.Application/Services/Payment/WalletService.cs @@ -3,7 +3,6 @@ using Dentizone.Domain.Enums; using Dentizone.Domain.Exceptions; using Dentizone.Domain.Interfaces.Repositories; -using Microsoft.EntityFrameworkCore; namespace Dentizone.Application.Services.Payment { @@ -27,7 +26,10 @@ public interface IWalletService Task GetWalletBalanceAsync(string userId); } - public class WalletService(IWalletRepository walletRepository, IMapper mapper, Infrastructure.AppDbContext dbContext) : IWalletService + public class WalletService( + IWalletRepository walletRepository, + IMapper mapper, + Infrastructure.AppDbContext dbContext) : IWalletService { public async Task CreateWallet(string userId) { @@ -58,6 +60,7 @@ public async Task CreateWallet(string userId) { throw new ArgumentException("userId cannot be null or empty.", nameof(userId)); } + return await walletRepository.FindBy(w => w.UserId == userId); } diff --git a/Dentizone.Application/Services/ReviewService.cs b/Dentizone.Application/Services/ReviewService.cs index dc4de09..9365ab9 100644 --- a/Dentizone.Application/Services/ReviewService.cs +++ b/Dentizone.Application/Services/ReviewService.cs @@ -3,28 +3,17 @@ using Dentizone.Application.Interfaces.Review; using Dentizone.Domain.Entity; using Dentizone.Domain.Interfaces.Repositories; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using Dentizone.Domain.Exceptions; +using Microsoft.EntityFrameworkCore; namespace Dentizone.Application.Services { - public class ReviewService(IMapper mapper,IReviewRepository repo) : IReviewService + public class ReviewService(IMapper mapper, IReviewRepository repo) : IReviewService { - public async Task CreateOrderReviewAsync(string userId, CreateReviewDTO createReviewDto) + public async Task CreateOrderReviewAsync(string userId, CreateReviewDto createReviewDto) { - createReviewDto.UserId = userId; - var review = mapper.Map(createReviewDto); - - review.CreatedAt = DateTime.UtcNow; - review.UpdatedAt = DateTime.UtcNow; - review.IsDeleted = false; - await repo.CreateAsync(review); - } public async Task DeleteReviewAsync(string reviewId) @@ -32,27 +21,23 @@ public async Task DeleteReviewAsync(string reviewId) await repo.DeleteAsync(reviewId); } - public async Task> GetUserReviewsTaken(string userId) + public async Task> GetUserReviewsTaken(string userId) { - var review = await repo.FindBy(r => r.UserId == userId && !r.IsDeleted); + var review = repo.FindAllBy(r => r.UserId == userId && !r.IsDeleted); - if (review == null) - return Enumerable.Empty(); - return new List { mapper.Map(review) }; + return await review.Select(r => mapper.Map(r)).ToListAsync(); } - public async Task UpdateReviewAsync(string reviewId, UpdateReviewDTO updateReviewDto) + public async Task UpdateReviewAsync(string reviewId, UpdateReviewDto updateReviewDto) { var review = await repo.GetByIdAsync(reviewId); if (review == null || review.IsDeleted) - throw new KeyNotFoundException("Review not found."); + throw new NotFoundException("Review not found."); - // Update fields review.Text = updateReviewDto.Comment; - review.UpdatedAt = DateTime.UtcNow; await repo.Update(review); } } -} +} \ No newline at end of file diff --git a/Dentizone.Application/Services/UserService.cs b/Dentizone.Application/Services/UserService.cs index e02dbb0..75f31cc 100644 --- a/Dentizone.Application/Services/UserService.cs +++ b/Dentizone.Application/Services/UserService.cs @@ -7,11 +7,14 @@ using Dentizone.Domain.Exceptions; using Dentizone.Domain.Interfaces.Repositories; using System.Linq.Expressions; -using Microsoft.EntityFrameworkCore; namespace Dentizone.Application.Services { - public class UserService(IUserRepository userRepository, IMapper mapper, IWalletService walletService, Infrastructure.AppDbContext dbContext) + public class UserService( + IUserRepository userRepository, + IMapper mapper, + IWalletService walletService, + Infrastructure.AppDbContext dbContext) : IUserService { public async Task CreateAsync(CreateAppUser userDto) @@ -46,7 +49,7 @@ public async Task DeleteAsync(string id) } public async Task> GetAllAsync(int page, string? searchByName = null, - Expression>? filterExpression = null) + Expression>? filterExpression = null) { var users = await userRepository.GetAllAsync(page, filterExpression); if (users == null) @@ -57,7 +60,7 @@ public async Task> GetAllAsync(int page, string? searchByN if (!string.IsNullOrEmpty(searchByName)) { users = users.Where(u => u.FullName.Contains(searchByName, StringComparison.OrdinalIgnoreCase)) - .ToList(); + .ToList(); } return mapper.Map>(users); @@ -66,10 +69,10 @@ public async Task> GetAllAsync(int page, string? searchByN public async Task GetByIdAsync(string id) { var user = await userRepository.FindBy(u => u.Id == id, - [ - u => u.University - ] - ); + [ + u => u.University + ] + ); if (user == null) { throw new NotFoundException($"User with id {id} not found."); diff --git a/Dentizone.Domain/Interfaces/Repositories/IReviewRepository.cs b/Dentizone.Domain/Interfaces/Repositories/IReviewRepository.cs index 281346a..74eed21 100644 --- a/Dentizone.Domain/Interfaces/Repositories/IReviewRepository.cs +++ b/Dentizone.Domain/Interfaces/Repositories/IReviewRepository.cs @@ -1,4 +1,5 @@ -using Dentizone.Domain.Entity; +using System.Linq.Expressions; +using Dentizone.Domain.Entity; namespace Dentizone.Domain.Interfaces.Repositories; @@ -6,4 +7,5 @@ public interface IReviewRepository : IBaseRepo { Task Update(Review entity); Task DeleteAsync(string id); + IQueryable FindAllBy(Expression> condition); } \ No newline at end of file diff --git a/Dentizone.Infrastructure/Repositories/ReviewRepository.cs b/Dentizone.Infrastructure/Repositories/ReviewRepository.cs index aa8cf76..831558e 100644 --- a/Dentizone.Infrastructure/Repositories/ReviewRepository.cs +++ b/Dentizone.Infrastructure/Repositories/ReviewRepository.cs @@ -10,7 +10,7 @@ internal class ReviewRepository(AppDbContext dbContext) : AbstractRepository(dbC public async Task GetByIdAsync(string id) { return await dbContext.Reviews - .FirstOrDefaultAsync(r => r.Id == id && !r.IsDeleted); + .FirstOrDefaultAsync(r => r.Id == id && !r.IsDeleted); } @@ -22,7 +22,7 @@ public async Task CreateAsync(Review entity) } public async Task FindBy(Expression> condition, - Expression>[]? includes) + Expression>[]? includes) { IQueryable query = dbContext.Reviews; if (includes != null) @@ -54,5 +54,11 @@ public async Task Update(Review entity) await dbContext.SaveChangesAsync(); return entity; } + + public IQueryable FindAllBy(Expression> condition) + { + return dbContext.Reviews + .Where(condition).AsQueryable(); + } } } \ No newline at end of file diff --git a/Dentizone.Presentaion/Controllers/ReviewController.cs b/Dentizone.Presentaion/Controllers/ReviewController.cs index 06f6cdf..5870c75 100644 --- a/Dentizone.Presentaion/Controllers/ReviewController.cs +++ b/Dentizone.Presentaion/Controllers/ReviewController.cs @@ -6,16 +6,17 @@ namespace Dentizone.Presentaion.Controllers { [ApiController] [Route("api/[controller]")] - - public class ReviewController:ControllerBase + public class ReviewController : ControllerBase { private readonly IReviewService _reviewService; + public ReviewController(IReviewService reviewService) { _reviewService = reviewService; } + [HttpPost] - public async Task CreateOrderReview([FromBody] CreateReviewDTO createReviewDto) + public async Task CreateOrderReview([FromBody] CreateReviewDto createReviewDto) { var userId = createReviewDto.UserId; if (string.IsNullOrEmpty(userId)) @@ -24,8 +25,9 @@ public async Task CreateOrderReview([FromBody] CreateReviewDTO cr await _reviewService.CreateOrderReviewAsync(userId, createReviewDto); return Ok(); } + [HttpPut("{reviewId}")] - public async Task UpdateReview(string reviewId, [FromBody] UpdateReviewDTO updateReviewDto) + public async Task UpdateReview(string reviewId, [FromBody] UpdateReviewDto updateReviewDto) { await _reviewService.UpdateReviewAsync(reviewId, updateReviewDto); return Ok(); @@ -45,4 +47,4 @@ public async Task GetUserReviews(string userId) return Ok(reviews); } } -} +} \ No newline at end of file diff --git a/Dentizone.Presentaion/Program.cs b/Dentizone.Presentaion/Program.cs index d4fedf4..9ce67ab 100644 --- a/Dentizone.Presentaion/Program.cs +++ b/Dentizone.Presentaion/Program.cs @@ -4,7 +4,6 @@ using Dentizone.Infrastructure.DependencyInjection; using Dentizone.Infrastructure.Filters; using Dentizone.Infrastructure.Identity; -using Dentizone.Infrastructure.Persistence.Seeder; using Dentizone.Presentaion.Context; using Dentizone.Presentaion.Extensions; using Dentizone.Presentaion.Middlewares; From 01a49ad4f7a9af833fa7a0b0d842ed35c5fc5df5 Mon Sep 17 00:00:00 2001 From: Mahmoud Nasr <239.nasr@gmail.com> Date: Sat, 28 Jun 2025 11:35:50 +0300 Subject: [PATCH 03/12] Enhance ReviewDtoValidation for Comment property Updated the `ReviewDtoValidation` class to add new validation rules for the `Comment` property. The rules ensure that the comment is not empty, cannot be null (with a specific error message), and does not exceed 500 characters. These improvements strengthen the validation logic for the `ReviewDto` class. --- Dentizone.Application/DTOs/Review/ReviewDTO.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Dentizone.Application/DTOs/Review/ReviewDTO.cs b/Dentizone.Application/DTOs/Review/ReviewDTO.cs index 29ea88b..58b8567 100644 --- a/Dentizone.Application/DTOs/Review/ReviewDTO.cs +++ b/Dentizone.Application/DTOs/Review/ReviewDTO.cs @@ -13,6 +13,7 @@ public class ReviewDtoValidation : AbstractValidator public ReviewDtoValidation() { RuleFor(x => x.Comment) + .NotEmpty() .NotNull().WithMessage("Comment cannot be null.") .MaximumLength(500).WithMessage("Comment must not exceed 500 characters."); From c261a64c21dbee168537d8aad9891c671267ca03 Mon Sep 17 00:00:00 2001 From: Mahmoud Nasr <239.nasr@gmail.com> Date: Sat, 28 Jun 2025 11:39:22 +0300 Subject: [PATCH 04/12] Add authorization and improve user ID handling in reviews Updated `ReviewController.cs` to include the `Authorize` attribute, ensuring only authenticated users can access endpoints. Modified `CreateOrderReview` to retrieve the user ID from claims instead of the request body, enhancing security. Updated `GetUserReviews` to eliminate the `userId` parameter, simplifying the API. Cleaned up unused `using` directives for better code organization. --- .../Controllers/ReviewController.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/Dentizone.Presentaion/Controllers/ReviewController.cs b/Dentizone.Presentaion/Controllers/ReviewController.cs index 5870c75..0c94c04 100644 --- a/Dentizone.Presentaion/Controllers/ReviewController.cs +++ b/Dentizone.Presentaion/Controllers/ReviewController.cs @@ -1,11 +1,14 @@ -using Dentizone.Application.DTOs.Review; +using System.Security.Claims; +using Dentizone.Application.DTOs.Review; using Dentizone.Application.Interfaces.Review; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace Dentizone.Presentaion.Controllers { [ApiController] [Route("api/[controller]")] + [Authorize] public class ReviewController : ControllerBase { private readonly IReviewService _reviewService; @@ -18,9 +21,7 @@ public ReviewController(IReviewService reviewService) [HttpPost] public async Task CreateOrderReview([FromBody] CreateReviewDto createReviewDto) { - var userId = createReviewDto.UserId; - if (string.IsNullOrEmpty(userId)) - return BadRequest("UserId is required."); + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); await _reviewService.CreateOrderReviewAsync(userId, createReviewDto); return Ok(); @@ -40,9 +41,11 @@ public async Task DeleteReview(string reviewId) return Ok(); } - [HttpGet("user/{userId}")] - public async Task GetUserReviews(string userId) + [HttpGet] + public async Task GetUserReviews() { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + var reviews = await _reviewService.GetUserReviewsTaken(userId); return Ok(reviews); } From 83f6039b3ce90a662fee95ae09c3e1e1db93484c Mon Sep 17 00:00:00 2001 From: Mahmoud Nasr <239.nasr@gmail.com> Date: Sat, 28 Jun 2025 12:03:33 +0300 Subject: [PATCH 05/12] Refactor review-related classes and methods - Updated `CreateReviewDto` to change `OrderItemId` to `OrderId` and adjusted validation rules accordingly. - Refactored `AddToBalance` method in `WalletService.cs` for improved clarity and error handling, removing unnecessary transaction management. - Modified `ReviewController` to use constructor injection directly, simplifying the code and enhancing readability. --- .../DTOs/Review/CreateReviewDTO.cs | 8 ++-- .../Services/Payment/WalletService.cs | 39 ++++++------------- .../Controllers/ReviewController.cs | 17 +++----- 3 files changed, 21 insertions(+), 43 deletions(-) diff --git a/Dentizone.Application/DTOs/Review/CreateReviewDTO.cs b/Dentizone.Application/DTOs/Review/CreateReviewDTO.cs index 3946a52..b927061 100644 --- a/Dentizone.Application/DTOs/Review/CreateReviewDTO.cs +++ b/Dentizone.Application/DTOs/Review/CreateReviewDTO.cs @@ -4,7 +4,7 @@ namespace Dentizone.Application.DTOs.Review { public class CreateReviewDto { - public string OrderItemId { get; set; } + public string OrderId { get; set; } public decimal Stars { get; set; } public string Comment { get; set; } } @@ -13,9 +13,9 @@ public class CreateReviewDtoValidation : AbstractValidator { public CreateReviewDtoValidation() { - RuleFor(x => x.OrderItemId) - .NotEmpty().WithMessage("Order Item ID is required.") - .NotNull().WithMessage("Order Item ID cannot be null."); + RuleFor(x => x.OrderId) + .NotEmpty().WithMessage("Order ID is required.") + .NotNull().WithMessage("Order ID cannot be null."); RuleFor(x => x.Stars) .InclusiveBetween(1, 5).WithMessage("Stars must be between 1 and 5."); diff --git a/Dentizone.Application/Services/Payment/WalletService.cs b/Dentizone.Application/Services/Payment/WalletService.cs index d74c1a0..376deab 100644 --- a/Dentizone.Application/Services/Payment/WalletService.cs +++ b/Dentizone.Application/Services/Payment/WalletService.cs @@ -67,40 +67,25 @@ public async Task CreateWallet(string userId) public async Task AddToBalance(decimal amount, string walletId) { - if (amount == 0) + var wallet = await walletRepository.FindBy(w => w.Id == walletId && w.Status == UserWallet.ACTIVE); + + if (wallet is null) { - throw new ArgumentException("Amount must not be zero.", nameof(amount)); + throw new BadActionException("No Wallet for this Wallet Id"); } - await using var transaction = await dbContext.Database.BeginTransactionAsync(); - try + var newBalance = wallet.Balance + amount; + if (newBalance < 0) { - var wallet = await walletRepository.FindBy(w => w.Id == walletId && w.Status == UserWallet.ACTIVE); - - if (wallet is null) - { - throw new BadActionException("No Wallet for this Wallet Id"); - } - - var newBalance = wallet.Balance + amount; - if (newBalance < 0) - { - throw new BadActionException("Insufficient wallet balance."); - } + throw new BadActionException("Insufficient wallet balance."); + } - wallet.Balance = newBalance; - await walletRepository.UpdateAsync(wallet); + wallet.Balance = newBalance; + await walletRepository.UpdateAsync(wallet); - await transaction.CommitAsync(); - var view = mapper.Map(wallet); - return view; - } - catch - { - await transaction.RollbackAsync(); - throw; - } + var view = mapper.Map(wallet); + return view; } public async Task GetWalletBalanceAsync(string userId) diff --git a/Dentizone.Presentaion/Controllers/ReviewController.cs b/Dentizone.Presentaion/Controllers/ReviewController.cs index 0c94c04..8def2eb 100644 --- a/Dentizone.Presentaion/Controllers/ReviewController.cs +++ b/Dentizone.Presentaion/Controllers/ReviewController.cs @@ -9,35 +9,28 @@ namespace Dentizone.Presentaion.Controllers [ApiController] [Route("api/[controller]")] [Authorize] - public class ReviewController : ControllerBase + public class ReviewController(IReviewService reviewService) : ControllerBase { - private readonly IReviewService _reviewService; - - public ReviewController(IReviewService reviewService) - { - _reviewService = reviewService; - } - [HttpPost] public async Task CreateOrderReview([FromBody] CreateReviewDto createReviewDto) { var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); - await _reviewService.CreateOrderReviewAsync(userId, createReviewDto); + await reviewService.CreateOrderReviewAsync(userId, createReviewDto); return Ok(); } [HttpPut("{reviewId}")] public async Task UpdateReview(string reviewId, [FromBody] UpdateReviewDto updateReviewDto) { - await _reviewService.UpdateReviewAsync(reviewId, updateReviewDto); + await reviewService.UpdateReviewAsync(reviewId, updateReviewDto); return Ok(); } [HttpDelete("{reviewId}")] public async Task DeleteReview(string reviewId) { - await _reviewService.DeleteReviewAsync(reviewId); + await reviewService.DeleteReviewAsync(reviewId); return Ok(); } @@ -46,7 +39,7 @@ public async Task GetUserReviews() { var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); - var reviews = await _reviewService.GetUserReviewsTaken(userId); + var reviews = await reviewService.GetUserReviewsTaken(userId); return Ok(reviews); } } From 8555bb5ac274b0dd88cb1b9ef45a224e881329c2 Mon Sep 17 00:00:00 2001 From: Mahmoud Nasr <239.nasr@gmail.com> Date: Sat, 28 Jun 2025 12:09:50 +0300 Subject: [PATCH 06/12] Refactor Review creation in CreateOrderReviewAsync Updated the CreateOrderReviewAsync method in the ReviewService class to instantiate the Review object directly, setting its properties from createReviewDto and userId. This change enhances clarity and control over the Review object creation process. --- Dentizone.Application/Services/ReviewService.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Dentizone.Application/Services/ReviewService.cs b/Dentizone.Application/Services/ReviewService.cs index 9365ab9..478ab1b 100644 --- a/Dentizone.Application/Services/ReviewService.cs +++ b/Dentizone.Application/Services/ReviewService.cs @@ -12,7 +12,13 @@ public class ReviewService(IMapper mapper, IReviewRepository repo) : IReviewServ { public async Task CreateOrderReviewAsync(string userId, CreateReviewDto createReviewDto) { - var review = mapper.Map(createReviewDto); + var review = new Review() + { + OrderId = createReviewDto.OrderId, + Text = createReviewDto.Comment, + UserId = userId + }; + await repo.CreateAsync(review); } From c37dce793fcc8cbc49612c8d9bc6617c7bc8c4bc Mon Sep 17 00:00:00 2001 From: Mahmoud Nasr <239.nasr@gmail.com> Date: Sat, 28 Jun 2025 12:12:23 +0300 Subject: [PATCH 07/12] Rename review methods for clarity and access control Updated `IReviewService` and `ReviewService` to replace `GetUserReviewsTaken` with `GetSubmittedReviews` for consistency in terminology. Added "ADMIN ONLY" comments to HTTP PUT and DELETE methods in `ReviewController`, and renamed `GetUserReviews` to `GetSubmittedReviews` to align with service changes. --- Dentizone.Application/Interfaces/Review/IReviewService.cs | 2 +- Dentizone.Application/Services/ReviewService.cs | 2 +- Dentizone.Presentaion/Controllers/ReviewController.cs | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Dentizone.Application/Interfaces/Review/IReviewService.cs b/Dentizone.Application/Interfaces/Review/IReviewService.cs index 4a77bf5..93f5a23 100644 --- a/Dentizone.Application/Interfaces/Review/IReviewService.cs +++ b/Dentizone.Application/Interfaces/Review/IReviewService.cs @@ -7,6 +7,6 @@ public interface IReviewService Task CreateOrderReviewAsync(string userId, CreateReviewDto createReviewDto); Task UpdateReviewAsync(string reviewId, UpdateReviewDto updateReviewDto); Task DeleteReviewAsync(string reviewId); - Task> GetUserReviewsTaken(string userId); + Task> GetSubmittedReviews(string userId); } } \ No newline at end of file diff --git a/Dentizone.Application/Services/ReviewService.cs b/Dentizone.Application/Services/ReviewService.cs index 478ab1b..aece662 100644 --- a/Dentizone.Application/Services/ReviewService.cs +++ b/Dentizone.Application/Services/ReviewService.cs @@ -27,7 +27,7 @@ public async Task DeleteReviewAsync(string reviewId) await repo.DeleteAsync(reviewId); } - public async Task> GetUserReviewsTaken(string userId) + public async Task> GetSubmittedReviews(string userId) { var review = repo.FindAllBy(r => r.UserId == userId && !r.IsDeleted); diff --git a/Dentizone.Presentaion/Controllers/ReviewController.cs b/Dentizone.Presentaion/Controllers/ReviewController.cs index 8def2eb..59b89f6 100644 --- a/Dentizone.Presentaion/Controllers/ReviewController.cs +++ b/Dentizone.Presentaion/Controllers/ReviewController.cs @@ -20,14 +20,14 @@ public async Task CreateOrderReview([FromBody] CreateReviewDto cr return Ok(); } - [HttpPut("{reviewId}")] + [HttpPut("{reviewId}")] // ADMIN ONLY public async Task UpdateReview(string reviewId, [FromBody] UpdateReviewDto updateReviewDto) { await reviewService.UpdateReviewAsync(reviewId, updateReviewDto); return Ok(); } - [HttpDelete("{reviewId}")] + [HttpDelete("{reviewId}")] // ADMIN ONLY public async Task DeleteReview(string reviewId) { await reviewService.DeleteReviewAsync(reviewId); @@ -35,11 +35,11 @@ public async Task DeleteReview(string reviewId) } [HttpGet] - public async Task GetUserReviews() + public async Task GetSubmittedReviews() { var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); - var reviews = await reviewService.GetUserReviewsTaken(userId); + var reviews = await reviewService.GetSubmittedReviews(userId); return Ok(reviews); } } From 615d1d3aaf8607b1b95e309d92105a03865deb0f Mon Sep 17 00:00:00 2001 From: Mahmoud Nasr <239.nasr@gmail.com> Date: Sat, 28 Jun 2025 12:51:58 +0300 Subject: [PATCH 08/12] Enhance order and review services functionality - Added `GetReviewedOrdersByUserId` method in `IOrderService` to retrieve reviewed orders by user. - Introduced `GetReceivedReviews` method in `IReviewService` for fetching received reviews. - Implemented `GetReviewedOrdersByUserId` in `OrderService`. - Corrected syntax in `CancelOrderAsync` method and improved email notifications. - Modified `GetAllAsync` in `IOrderRepository` to accept nullable `page` parameter. - Removed `ReviewUx` property from `Order` entity. - Updated `ReviewService` to include `IOrderService` and implement `GetReceivedReviews`. - Improved pagination and filtering in `OrderRepository`. - Added endpoint in `ReviewController` for retrieving received reviews. --- .../Interfaces/Order/IOrderService.cs | 2 + .../Interfaces/Review/IReviewService.cs | 1 + .../Services/OrderService.cs | 23 ++++-- .../Services/ReviewService.cs | 18 ++++- Dentizone.Domain/Entity/Order.cs | 1 - .../Repositories/IOrderRepository.cs | 2 +- .../Configurations/ReviewUxConfiguration.cs | 6 -- .../Repositories/OrderRepository.cs | 75 +++++++++++-------- .../Controllers/ReviewController.cs | 7 ++ 9 files changed, 87 insertions(+), 48 deletions(-) diff --git a/Dentizone.Application/Interfaces/Order/IOrderService.cs b/Dentizone.Application/Interfaces/Order/IOrderService.cs index 586d541..76efa66 100644 --- a/Dentizone.Application/Interfaces/Order/IOrderService.cs +++ b/Dentizone.Application/Interfaces/Order/IOrderService.cs @@ -11,5 +11,7 @@ public interface IOrderService Task CancelOrderAsync(string orderId, string userId); Task CompleteOrder(string orderId); Task> GetOrders(int page, FilterOrderDto filters); + + Task> GetReviewedOrdersByUserId(string userId); } } \ No newline at end of file diff --git a/Dentizone.Application/Interfaces/Review/IReviewService.cs b/Dentizone.Application/Interfaces/Review/IReviewService.cs index 93f5a23..1124eea 100644 --- a/Dentizone.Application/Interfaces/Review/IReviewService.cs +++ b/Dentizone.Application/Interfaces/Review/IReviewService.cs @@ -8,5 +8,6 @@ public interface IReviewService Task UpdateReviewAsync(string reviewId, UpdateReviewDto updateReviewDto); Task DeleteReviewAsync(string reviewId); Task> GetSubmittedReviews(string userId); + Task> GetReceivedReviews(string userId); } } \ No newline at end of file diff --git a/Dentizone.Application/Services/OrderService.cs b/Dentizone.Application/Services/OrderService.cs index 7de66c4..94f266f 100644 --- a/Dentizone.Application/Services/OrderService.cs +++ b/Dentizone.Application/Services/OrderService.cs @@ -33,7 +33,7 @@ internal class OrderService( public async Task CancelOrderAsync(string orderId, string userId) { var order = await orderRepository.FindBy(o => o.Id == orderId, - [o => o.OrderStatuses, o => o.OrderItems]); + [o => o.OrderStatuses, o => o.OrderItems]); if (order == null) { @@ -79,7 +79,7 @@ internal class OrderService( await postService.UpdatePostStatus(post.Id, PostStatus.Active); var seller = await authService.GetById(post.Seller.Id); await mailService.Send(seller.Email, "Order Cancelled", - $"Your post {post.Title} has been cancelled by the buyer. we relisted it now for sale again@!"); + $"Your post {post.Title} has been cancelled by the buyer. we relisted it now for sale again@!"); } } @@ -99,7 +99,7 @@ private async Task SendConfirmationEmail(List sellerEmails, string buyer foreach (var email in sellerEmails) { await mailService.Send(email, "New Order Placed", - $"Your post has been sold. Wait for pickup. Order ID: {orderId}"); + $"Your post has been sold. Wait for pickup. Order ID: {orderId}"); } } @@ -154,7 +154,7 @@ public async Task CreateOrderAsync(CreateOrderDto createOrderDto, string await orderItemRepository.CreateAsync(orderItem); // Create a Sale Transaction for each order item await paymentService.CreateSaleTransaction( - payment.Id, post.Seller.Wallet.Id, post.Price); + payment.Id, post.Seller.Wallet.Id, post.Price); } // Create Ship Info @@ -262,11 +262,20 @@ public async Task> GetOrders(int page, FilterOrderD var order = await orderRepository.GetAllAsync( - page, - filterExpression - ); + page, + filterExpression + ); return mapper.Map>(order); } + + public async Task> GetReviewedOrdersByUserId(string userId) + { + var orders = await orderRepository.GetAllAsync( + null, + o => o.IsReviewed && o.OrderItems.Any(oi => oi.Post.SellerId == userId) + ); + return orders.Items; + } } } \ No newline at end of file diff --git a/Dentizone.Application/Services/ReviewService.cs b/Dentizone.Application/Services/ReviewService.cs index aece662..f230458 100644 --- a/Dentizone.Application/Services/ReviewService.cs +++ b/Dentizone.Application/Services/ReviewService.cs @@ -1,5 +1,6 @@ using AutoMapper; using Dentizone.Application.DTOs.Review; +using Dentizone.Application.Interfaces.Order; using Dentizone.Application.Interfaces.Review; using Dentizone.Domain.Entity; using Dentizone.Domain.Interfaces.Repositories; @@ -8,7 +9,7 @@ namespace Dentizone.Application.Services { - public class ReviewService(IMapper mapper, IReviewRepository repo) : IReviewService + public class ReviewService(IMapper mapper, IReviewRepository repo, IOrderService orderService) : IReviewService { public async Task CreateOrderReviewAsync(string userId, CreateReviewDto createReviewDto) { @@ -45,5 +46,20 @@ public async Task UpdateReviewAsync(string reviewId, UpdateReviewDto updateRevie await repo.Update(review); } + + public async Task> GetReceivedReviews(string userId) + { + var reviews = await orderService.GetReviewedOrdersByUserId(userId); + + + // Get the reviews for the orders + var reviewDtos = reviews.Select(r => new ReviewDto + { + Comment = r.Review.Text ?? "No Comment", + Stars = r.Review.Stars, + }); + + return reviewDtos.ToList(); + } } } \ No newline at end of file diff --git a/Dentizone.Domain/Entity/Order.cs b/Dentizone.Domain/Entity/Order.cs index 7806f46..3769bf4 100644 --- a/Dentizone.Domain/Entity/Order.cs +++ b/Dentizone.Domain/Entity/Order.cs @@ -21,6 +21,5 @@ public class Order : IBaseEntity, IDeletable, IUpdatable public DateTime UpdatedAt { get; set; } public bool IsDeleted { get; set; } public virtual Review Review { get; set; } - public virtual ReviewUx ReviewUx { get; set; } } } \ No newline at end of file diff --git a/Dentizone.Domain/Interfaces/Repositories/IOrderRepository.cs b/Dentizone.Domain/Interfaces/Repositories/IOrderRepository.cs index c32cd4a..293ed6d 100644 --- a/Dentizone.Domain/Interfaces/Repositories/IOrderRepository.cs +++ b/Dentizone.Domain/Interfaces/Repositories/IOrderRepository.cs @@ -8,7 +8,7 @@ public interface IOrderRepository : IBaseRepo Task UpdateAsync(Order entity); Task GetOrderDetails(string orderId, string buyerId); Task> GetOrdersWithDetails(string buyerId); - Task> GetAllAsync(int page, Expression> filter); + Task> GetAllAsync(int? page, Expression> filter); Task CountTotalOrders(); Task AverageValueOfOrders(); diff --git a/Dentizone.Infrastructure/Persistence/Configurations/ReviewUxConfiguration.cs b/Dentizone.Infrastructure/Persistence/Configurations/ReviewUxConfiguration.cs index 1a0b36a..9adf67f 100644 --- a/Dentizone.Infrastructure/Persistence/Configurations/ReviewUxConfiguration.cs +++ b/Dentizone.Infrastructure/Persistence/Configurations/ReviewUxConfiguration.cs @@ -46,12 +46,6 @@ public void Configure(EntityTypeBuilder builder) .WithMany(u => u.UXReviews) .HasForeignKey(r => r.UserId) .OnDelete(DeleteBehavior.NoAction); - - - builder.HasOne(r => r.Order) - .WithOne(r => r.ReviewUx) - .HasForeignKey(r => r.OrderId) - .OnDelete(DeleteBehavior.NoAction); } } } \ No newline at end of file diff --git a/Dentizone.Infrastructure/Repositories/OrderRepository.cs b/Dentizone.Infrastructure/Repositories/OrderRepository.cs index 7939e22..d14ce31 100644 --- a/Dentizone.Infrastructure/Repositories/OrderRepository.cs +++ b/Dentizone.Infrastructure/Repositories/OrderRepository.cs @@ -11,7 +11,7 @@ public class OrderRepository(AppDbContext dbContext) : AbstractRepository(dbCont public async Task GetByIdAsync(string id) { return await dbContext.Orders - .FirstOrDefaultAsync(o => o.Id == id); + .FirstOrDefaultAsync(o => o.Id == id); } public async Task UpdateAsync(Order entity) @@ -29,7 +29,7 @@ public async Task CreateAsync(Order entity) } public async Task FindBy(Expression> condition, - Expression>[]? includes = null) + Expression>[]? includes = null) { IQueryable query = dbContext.Orders; if (includes != null) @@ -43,30 +43,41 @@ public async Task CreateAsync(Order entity) return await query.FirstOrDefaultAsync(condition); } - private IQueryable BuildPagedQuery(int page, Expression>? filter) + private IQueryable BuildPagedQuery(int page, Expression>? filter, + IQueryable query) { - IQueryable query = dbContext.Orders; + if (page < 1) + { + page = 1; + } + if (filter != null) { query = query.OrderByDescending(o => o.CreatedAt) - .Skip(CalculatePagination(page)) - .Take(DefaultPageSize); + .Skip(CalculatePagination(page)) + .Take(DefaultPageSize); } return query; } - public async Task> GetAllAsync(int page, Expression> filter) + public async Task> GetAllAsync(int? page, Expression> filter) { - var query = BuildPagedQuery(page, filter); + var query = dbContext.Orders.AsQueryable(); + if (page is not null) + { + query = BuildPagedQuery(page.Value, filter, query); + } + var totalCount = await query.CountAsync(); query = query.Include(o => o.Buyer) - .Include(o => o.OrderItems) - .ThenInclude(p => p.Post) - .Include(o => o.ShipInfo) - .Include(o => o.OrderStatuses); + .Include(o => o.OrderItems) + .ThenInclude(p => p.Post) + .Include(o => o.ShipInfo) + .Include(o => o.OrderStatuses) + .Include(o => o.Review); query = query.Where(filter); @@ -74,7 +85,7 @@ public async Task> GetAllAsync(int page, Expression { Items = await query.AsNoTracking().ToListAsync(), - Page = page, + Page = page ?? 1, PageSize = DefaultPageSize, TotalCount = totalCount }; @@ -84,28 +95,28 @@ public async Task> GetAllAsync(int page, Expression GetOrderDetails(string orderId, string buyerId) { var query = dbContext.Orders - .AsNoTracking() - .Where(o => o.Id == orderId && o.BuyerId == buyerId) - .Include(o => o.Buyer) - .Include(o => o.OrderItems) - .ThenInclude(p => p.Post) - .Include(o => o.ShipInfo) - .Include(o => o.OrderStatuses); + .AsNoTracking() + .Where(o => o.Id == orderId && o.BuyerId == buyerId) + .Include(o => o.Buyer) + .Include(o => o.OrderItems) + .ThenInclude(p => p.Post) + .Include(o => o.ShipInfo) + .Include(o => o.OrderStatuses); return await query.FirstOrDefaultAsync(); } public async Task> GetOrdersWithDetails(string buyerId) { return await dbContext.Orders - .AsNoTracking() - .Where(o => o.BuyerId == buyerId) - .Include(o => o.Buyer) - .Include(o => o.OrderItems) - .ThenInclude(p => p.Post) - .Include(o => o.ShipInfo) - .Include(o => o.OrderStatuses) - .OrderByDescending(o => o.CreatedAt) - .ToListAsync(); + .AsNoTracking() + .Where(o => o.BuyerId == buyerId) + .Include(o => o.Buyer) + .Include(o => o.OrderItems) + .ThenInclude(p => p.Post) + .Include(o => o.ShipInfo) + .Include(o => o.OrderStatuses) + .OrderByDescending(o => o.CreatedAt) + .ToListAsync(); } public async Task CountTotalOrders() @@ -117,9 +128,9 @@ public async Task CountTotalOrders() public async Task AverageValueOfOrders() { var average = await dbContext.Orders - .AsNoTracking() - .Where(o => !o.IsDeleted) - .AverageAsync(o => (decimal?)o.TotalAmount); + .AsNoTracking() + .Where(o => !o.IsDeleted) + .AverageAsync(o => (decimal?)o.TotalAmount); return average ?? 0m; } } diff --git a/Dentizone.Presentaion/Controllers/ReviewController.cs b/Dentizone.Presentaion/Controllers/ReviewController.cs index 59b89f6..6ab1495 100644 --- a/Dentizone.Presentaion/Controllers/ReviewController.cs +++ b/Dentizone.Presentaion/Controllers/ReviewController.cs @@ -42,5 +42,12 @@ public async Task GetSubmittedReviews() var reviews = await reviewService.GetSubmittedReviews(userId); return Ok(reviews); } + + [HttpGet("received-review")] + public async Task GetReceivedReviews() + { + var reviews = await reviewService.GetReceivedReviews(User.FindFirstValue(ClaimTypes.NameIdentifier)); + return Ok(reviews); + } } } \ No newline at end of file From b2f9eaa216035d4ec419a8d60a4aab5d02679091 Mon Sep 17 00:00:00 2001 From: Mahmoud Nasr <239.nasr@gmail.com> Date: Sat, 28 Jun 2025 12:59:00 +0300 Subject: [PATCH 09/12] Remove ReviewUxConfiguration class and its dependencies The `ReviewUxConfiguration` class has been completely removed, including all associated using directives and configuration methods for the `ReviewUx` entity. This change eliminates the setup for entity properties such as keys, required fields, default values, and relationships with other entities. --- .../Configurations/ReviewUxConfiguration.cs | 51 ------------------- 1 file changed, 51 deletions(-) delete mode 100644 Dentizone.Infrastructure/Persistence/Configurations/ReviewUxConfiguration.cs diff --git a/Dentizone.Infrastructure/Persistence/Configurations/ReviewUxConfiguration.cs b/Dentizone.Infrastructure/Persistence/Configurations/ReviewUxConfiguration.cs deleted file mode 100644 index 9adf67f..0000000 --- a/Dentizone.Infrastructure/Persistence/Configurations/ReviewUxConfiguration.cs +++ /dev/null @@ -1,51 +0,0 @@ -using Dentizone.Domain.Entity; -using Dentizone.Domain.Enums; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace Dentizone.Infrastructure.Persistence.Configurations -{ - internal class ReviewUxConfiguration : IEntityTypeConfiguration - { - public void Configure(EntityTypeBuilder builder) - { - builder.HasKey(r => r.Id); - - builder.Property(r => r.Id) - .IsRequired(); - builder.Property(r => r.UserId) - .IsRequired(); - builder.Property(r => r.IsDeleted) - .IsRequired() - .HasDefaultValue(false); - - - builder.Property(r => r.Text) - .HasMaxLength(100); - - builder.Property(r => r.Stars) - .HasDefaultValue(5); - - - builder.Property(r => r.OrderId) - .IsRequired(); - - builder.Property(r => r.Status) - .HasDefaultValue(ReviewStatus.PENDING) - .HasConversion(); - - - builder.Property(r => r.CreatedAt) - .HasDefaultValueSql(SqlCommon.Date) - .ValueGeneratedOnAdd(); - - builder.Property(r => r.UpdatedAt) - .IsRequired(); - - builder.HasOne(r => r.User) - .WithMany(u => u.UXReviews) - .HasForeignKey(r => r.UserId) - .OnDelete(DeleteBehavior.NoAction); - } - } -} \ No newline at end of file From dc74659712cece91742fdc13f0fa0a8a3a8edaa1 Mon Sep 17 00:00:00 2001 From: Mahmoud Nasr <239.nasr@gmail.com> Date: Sat, 28 Jun 2025 13:04:48 +0300 Subject: [PATCH 10/12] Remove ReviewUx class and related using directives The `ReviewUx` class has been completely removed from the `ReviewUX.cs` file, along with its properties and the associated using directives for `Dentizone.Domain.Enums` and `Dentizone.Domain.Interfaces`. This change eliminates the class definition and its functionality from the codebase. --- .../DTOs/Review/CreateReviewDTO.cs | 2 +- .../DTOs/Review/ReviewDTO.cs | 2 +- .../DTOs/Review/UpdateReviewDTO.cs | 3 +++ Dentizone.Domain/Entity/ReviewUX.cs | 25 ------------------- 4 files changed, 5 insertions(+), 27 deletions(-) delete mode 100644 Dentizone.Domain/Entity/ReviewUX.cs diff --git a/Dentizone.Application/DTOs/Review/CreateReviewDTO.cs b/Dentizone.Application/DTOs/Review/CreateReviewDTO.cs index b927061..93e3234 100644 --- a/Dentizone.Application/DTOs/Review/CreateReviewDTO.cs +++ b/Dentizone.Application/DTOs/Review/CreateReviewDTO.cs @@ -5,7 +5,7 @@ namespace Dentizone.Application.DTOs.Review public class CreateReviewDto { public string OrderId { get; set; } - public decimal Stars { get; set; } + public int Stars { get; set; } public string Comment { get; set; } } diff --git a/Dentizone.Application/DTOs/Review/ReviewDTO.cs b/Dentizone.Application/DTOs/Review/ReviewDTO.cs index 58b8567..f8030ff 100644 --- a/Dentizone.Application/DTOs/Review/ReviewDTO.cs +++ b/Dentizone.Application/DTOs/Review/ReviewDTO.cs @@ -5,7 +5,7 @@ namespace Dentizone.Application.DTOs.Review public class ReviewDto { public string Comment { get; set; } - public decimal Stars { get; set; } + public int Stars { get; set; } } public class ReviewDtoValidation : AbstractValidator diff --git a/Dentizone.Application/DTOs/Review/UpdateReviewDTO.cs b/Dentizone.Application/DTOs/Review/UpdateReviewDTO.cs index f99f458..9affd4b 100644 --- a/Dentizone.Application/DTOs/Review/UpdateReviewDTO.cs +++ b/Dentizone.Application/DTOs/Review/UpdateReviewDTO.cs @@ -5,6 +5,7 @@ namespace Dentizone.Application.DTOs.Review public class UpdateReviewDto { public string Comment { get; set; } + public int Stars { get; set; } } public class UpdateReviewDtoValidation : AbstractValidator @@ -14,6 +15,8 @@ public UpdateReviewDtoValidation() RuleFor(x => x.Comment) .NotNull().WithMessage("Comment cannot be null.") .MaximumLength(500).WithMessage("Comment must not exceed 500 characters."); + RuleFor(x => x.Stars) + .InclusiveBetween(1, 5).WithMessage("Stars must be between 1 and 5."); } } } \ No newline at end of file diff --git a/Dentizone.Domain/Entity/ReviewUX.cs b/Dentizone.Domain/Entity/ReviewUX.cs deleted file mode 100644 index 19625e0..0000000 --- a/Dentizone.Domain/Entity/ReviewUX.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Dentizone.Domain.Enums; -using Dentizone.Domain.Interfaces; - -namespace Dentizone.Domain.Entity -{ - public class ReviewUx : IBaseEntity , IDeletable, IUpdatable - { - public string Id { get; set; } = Guid.NewGuid().ToString(); - public DateTime CreatedAt { get; set; } - public DateTime UpdatedAt { get; set; } - public bool IsDeleted { get; set; } - - - public string UserId { get; set; } - public virtual AppUser User { get; set; } - - public virtual Order Order { get; set; } - public string OrderId { get; set; } - - public string? Text { get; set; } - public int Stars { get; set; } - - public ReviewStatus Status { get; set; } - } -} \ No newline at end of file From 5c754b02a8a84576ff77019c89882c6d54d52527 Mon Sep 17 00:00:00 2001 From: Mahmoud Nasr <239.nasr@gmail.com> Date: Sat, 28 Jun 2025 13:06:48 +0300 Subject: [PATCH 11/12] Refactor review service methods and query logic Updated `UpdateReviewAsync` and `DeleteReviewAsync` methods in `IReviewService.cs` to return `Task` for success indication. Modified query logic in `OrderRepository.cs` to ensure proper ordering and pagination, including a new filter condition for improved result handling. --- .../Interfaces/Review/IReviewService.cs | 4 ++-- .../Repositories/OrderRepository.cs | 11 ++++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/Dentizone.Application/Interfaces/Review/IReviewService.cs b/Dentizone.Application/Interfaces/Review/IReviewService.cs index 1124eea..429bf3c 100644 --- a/Dentizone.Application/Interfaces/Review/IReviewService.cs +++ b/Dentizone.Application/Interfaces/Review/IReviewService.cs @@ -5,8 +5,8 @@ namespace Dentizone.Application.Interfaces.Review public interface IReviewService { Task CreateOrderReviewAsync(string userId, CreateReviewDto createReviewDto); - Task UpdateReviewAsync(string reviewId, UpdateReviewDto updateReviewDto); - Task DeleteReviewAsync(string reviewId); + Task UpdateReviewAsync(string reviewId, UpdateReviewDto updateReviewDto); + Task DeleteReviewAsync(string reviewId); Task> GetSubmittedReviews(string userId); Task> GetReceivedReviews(string userId); } diff --git a/Dentizone.Infrastructure/Repositories/OrderRepository.cs b/Dentizone.Infrastructure/Repositories/OrderRepository.cs index d14ce31..b7cdfea 100644 --- a/Dentizone.Infrastructure/Repositories/OrderRepository.cs +++ b/Dentizone.Infrastructure/Repositories/OrderRepository.cs @@ -51,13 +51,18 @@ private IQueryable BuildPagedQuery(int page, Expression page = 1; } + // Always order before pagination + query = query.OrderByDescending(o => o.CreatedAt); + + // Apply filter if present if (filter != null) { - query = query.OrderByDescending(o => o.CreatedAt) - .Skip(CalculatePagination(page)) - .Take(DefaultPageSize); + query = query.Where(filter); } + // Always apply pagination + query = query.Skip(CalculatePagination(page)).Take(DefaultPageSize); + return query; } From 84c8f8641de94a7d66ca27965bf5a9603fe36c53 Mon Sep 17 00:00:00 2001 From: Mahmoud Nasr <239.nasr@gmail.com> Date: Sat, 28 Jun 2025 13:15:24 +0300 Subject: [PATCH 12/12] Make Comment property nullable in UpdateReviewDto Updated the `Comment` property to be nullable, allowing it to hold null values. Adjusted validation logic to enforce maximum length only when a comment is provided, with a revised error message for clarity. --- Dentizone.Application/DTOs/Review/UpdateReviewDTO.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Dentizone.Application/DTOs/Review/UpdateReviewDTO.cs b/Dentizone.Application/DTOs/Review/UpdateReviewDTO.cs index 9affd4b..190b613 100644 --- a/Dentizone.Application/DTOs/Review/UpdateReviewDTO.cs +++ b/Dentizone.Application/DTOs/Review/UpdateReviewDTO.cs @@ -4,7 +4,7 @@ namespace Dentizone.Application.DTOs.Review { public class UpdateReviewDto { - public string Comment { get; set; } + public string? Comment { get; set; } public int Stars { get; set; } } @@ -13,8 +13,9 @@ public class UpdateReviewDtoValidation : AbstractValidator public UpdateReviewDtoValidation() { RuleFor(x => x.Comment) - .NotNull().WithMessage("Comment cannot be null.") - .MaximumLength(500).WithMessage("Comment must not exceed 500 characters."); + .MaximumLength(500) + .When(x => x.Comment != null) + .WithMessage("Comment must not exceed 500 characters when provided."); RuleFor(x => x.Stars) .InclusiveBetween(1, 5).WithMessage("Stars must be between 1 and 5."); }