From 02dd0ec74e380ca59dcd162357ea0b035e63849d Mon Sep 17 00:00:00 2001 From: Nouran Yasser Date: Sun, 29 Jun 2025 01:59:44 +0300 Subject: [PATCH 1/8] Q&A service, dtos and contoller --- .../AutoMapper/Answers/AnswerProfile.cs | 26 ++++ .../AutoMapper/Questions/QuestionProfile.cs | 31 +++++ Dentizone.Application/DI/Services.cs | 1 + .../DTOs/Q&A/AnswerDTO/AnswerViewDto.cs | 28 ++++ .../DTOs/Q&A/AnswerDTO/CreateAnswerDto.cs | 21 +++ .../DTOs/Q&A/AnswerDTO/UpdateAnswerDto.cs | 21 +++ .../DTOs/Q&A/QuestionDTO/CreateQuestionDto.cs | 24 ++++ .../DTOs/Q&A/QuestionDTO/QuestionViewDto.cs | 29 ++++ .../DTOs/Q&A/QuestionDTO/UpdateQuestionDto.cs | 21 +++ .../Interfaces/IQAService.cs | 21 +++ Dentizone.Application/Services/QAService.cs | 130 ++++++++++++++++++ .../Repositories/IQuestionRepository.cs | 4 +- .../DependencyInjection/AddRepositories.cs | 2 + .../Repositories/QuestionRepository.cs | 15 ++ .../Controllers/QAController.cs | 88 ++++++++++++ 15 files changed, 461 insertions(+), 1 deletion(-) create mode 100644 Dentizone.Application/AutoMapper/Answers/AnswerProfile.cs create mode 100644 Dentizone.Application/AutoMapper/Questions/QuestionProfile.cs create mode 100644 Dentizone.Application/DTOs/Q&A/AnswerDTO/AnswerViewDto.cs create mode 100644 Dentizone.Application/DTOs/Q&A/AnswerDTO/CreateAnswerDto.cs create mode 100644 Dentizone.Application/DTOs/Q&A/AnswerDTO/UpdateAnswerDto.cs create mode 100644 Dentizone.Application/DTOs/Q&A/QuestionDTO/CreateQuestionDto.cs create mode 100644 Dentizone.Application/DTOs/Q&A/QuestionDTO/QuestionViewDto.cs create mode 100644 Dentizone.Application/DTOs/Q&A/QuestionDTO/UpdateQuestionDto.cs create mode 100644 Dentizone.Application/Interfaces/IQAService.cs create mode 100644 Dentizone.Application/Services/QAService.cs create mode 100644 Dentizone.Presentaion/Controllers/QAController.cs diff --git a/Dentizone.Application/AutoMapper/Answers/AnswerProfile.cs b/Dentizone.Application/AutoMapper/Answers/AnswerProfile.cs new file mode 100644 index 0000000..bf7fa47 --- /dev/null +++ b/Dentizone.Application/AutoMapper/Answers/AnswerProfile.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using AutoMapper; + +namespace Dentizone.Application.AutoMapper.Answers +{ + public class AnswerProfile :Profile + { + public AnswerProfile() + { + CreateMap() + .ForMember(dest => dest.Id, opt => opt.MapFrom(src => Guid.NewGuid().ToString())) + .ForMember(dest => dest.CreatedAt, opt => opt.MapFrom(src => DateTime.UtcNow)) + .ReverseMap(); + CreateMap() + .ForMember(dest => dest.Text, opt => opt.MapFrom(src => src.Text)) + .ForMember(dest => dest.UpdatedAt, opt => opt.MapFrom(src => DateTime.UtcNow)) + .ReverseMap(); + CreateMap().ReverseMap(); + } + } + +} diff --git a/Dentizone.Application/AutoMapper/Questions/QuestionProfile.cs b/Dentizone.Application/AutoMapper/Questions/QuestionProfile.cs new file mode 100644 index 0000000..57fac4f --- /dev/null +++ b/Dentizone.Application/AutoMapper/Questions/QuestionProfile.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using AutoMapper; +using Dentizone.Application.DTOs.Q_A.QuestionDTO; + +namespace Dentizone.Application.AutoMapper.Questions +{ + public class QuestionProfile : Profile + { + public QuestionProfile() + { + CreateMap() + .ForMember(dest => dest.Id, opt => opt.MapFrom(src => Guid.NewGuid().ToString())) + .ForMember(dest => dest.CreatedAt, opt => opt.MapFrom(src => DateTime.UtcNow)) + .ReverseMap(); + CreateMap() + .ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id)) + .ForMember(dest => dest.User, opt => opt.MapFrom(src => src.AskerName)) + .ForMember(dest => dest.Text, opt => opt.MapFrom(src => src.Text)) + .ForMember(dest => dest.CreatedAt, opt => opt.MapFrom(src => src.CreatedAt)) + .ReverseMap(); + CreateMap() + .ForMember(dest => dest.Text, opt => opt.MapFrom(src => src.Text)) + .ForMember(dest => dest.UpdatedAt, opt => opt.MapFrom(src => DateTime.UtcNow)) + .ReverseMap(); + } + } +} diff --git a/Dentizone.Application/DI/Services.cs b/Dentizone.Application/DI/Services.cs index 61debf4..0d4fff0 100644 --- a/Dentizone.Application/DI/Services.cs +++ b/Dentizone.Application/DI/Services.cs @@ -45,6 +45,7 @@ public static IServiceCollection AddApplicationServices(this IServiceCollection services.AddScoped(); services.AddScoped(); + services.AddScoped(); return services; } } diff --git a/Dentizone.Application/DTOs/Q&A/AnswerDTO/AnswerViewDto.cs b/Dentizone.Application/DTOs/Q&A/AnswerDTO/AnswerViewDto.cs new file mode 100644 index 0000000..0ea23e4 --- /dev/null +++ b/Dentizone.Application/DTOs/Q&A/AnswerDTO/AnswerViewDto.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using FluentValidation; + +namespace Dentizone.Application.DTOs.Q_A.AnswerDTO +{ + public class AnswerViewDto + { + public string Id { get; set; } + public string ResponderName { get; set; } + public string Text { get; set; } + public DateTime CreatedAt { get; set; } + + } + public class AnswerViewDtoValidator : AbstractValidator + { + public AnswerViewDtoValidator() + { + RuleFor(x => x.Id).NotEmpty().WithMessage("Id is required."); + RuleFor(x => x.ResponderName).NotEmpty().WithMessage("ResponderName is required."); + RuleFor(x => x.Text).NotEmpty().WithMessage("Text is required."); + RuleFor(x => x.CreatedAt).NotEmpty().WithMessage("CreatedAt is required."); + } + } +} diff --git a/Dentizone.Application/DTOs/Q&A/AnswerDTO/CreateAnswerDto.cs b/Dentizone.Application/DTOs/Q&A/AnswerDTO/CreateAnswerDto.cs new file mode 100644 index 0000000..8c9aec3 --- /dev/null +++ b/Dentizone.Application/DTOs/Q&A/AnswerDTO/CreateAnswerDto.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using FluentValidation; + +namespace Dentizone.Application.DTOs.Q_A.AnswerDTO +{ + public class CreateAnswerDto + { + public string Text { get; set; } + } + public class CreateAnswerDtoValidator : AbstractValidator + { + public CreateAnswerDtoValidator() + { + RuleFor(x => x.Text).NotEmpty().WithMessage("Text is required."); + } + } +} diff --git a/Dentizone.Application/DTOs/Q&A/AnswerDTO/UpdateAnswerDto.cs b/Dentizone.Application/DTOs/Q&A/AnswerDTO/UpdateAnswerDto.cs new file mode 100644 index 0000000..c3a1f8a --- /dev/null +++ b/Dentizone.Application/DTOs/Q&A/AnswerDTO/UpdateAnswerDto.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using FluentValidation; + +namespace Dentizone.Application.DTOs.Q_A.AnswerDTO +{ + public class UpdateAnswerDto + { + public string Text { get; set; } + } + public class UpdateAnswerDtoValidator : AbstractValidator + { + public UpdateAnswerDtoValidator() + { + RuleFor(x => x.Text).NotEmpty().WithMessage("Text is required."); + } + } +} diff --git a/Dentizone.Application/DTOs/Q&A/QuestionDTO/CreateQuestionDto.cs b/Dentizone.Application/DTOs/Q&A/QuestionDTO/CreateQuestionDto.cs new file mode 100644 index 0000000..61af0ed --- /dev/null +++ b/Dentizone.Application/DTOs/Q&A/QuestionDTO/CreateQuestionDto.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using FluentValidation; + +namespace Dentizone.Application.DTOs.Q_A.QuestionDTO +{ + public class CreateQuestionDto + { + public string PostId { get; set; } + public string Text { get; set; } + } + public class CreateQuestionDtoValidator : AbstractValidator + { + public CreateQuestionDtoValidator() + { + RuleFor(x => x.PostId).NotEmpty().WithMessage("PostId is required."); + RuleFor(x => x.Text).NotEmpty().WithMessage("Text is required."); + } + + } +} diff --git a/Dentizone.Application/DTOs/Q&A/QuestionDTO/QuestionViewDto.cs b/Dentizone.Application/DTOs/Q&A/QuestionDTO/QuestionViewDto.cs new file mode 100644 index 0000000..f7d5519 --- /dev/null +++ b/Dentizone.Application/DTOs/Q&A/QuestionDTO/QuestionViewDto.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Dentizone.Application.DTOs.Q_A.AnswerDTO; +using FluentValidation; + +namespace Dentizone.Application.DTOs.Q_A.QuestionDTO +{ + public class QuestionViewDto + { + public string Id { get; set; } + public string AskerName { get; set; } + public string Text { get; set; } + public AnswerViewDto? Answer { get; set; } + public DateTime CreatedAt { get; set; } + } + public class QuestionViewDtoValidator : AbstractValidator + { + public QuestionViewDtoValidator() + { + RuleFor(x => x.Id).NotEmpty().WithMessage("Id is required."); + RuleFor(x => x.AskerName).NotEmpty().WithMessage("AskerName is required."); + RuleFor(x => x.Text).NotEmpty().WithMessage("Text is required."); + RuleFor(x => x.CreatedAt).NotEmpty().WithMessage("CreatedAt is required."); + } + } +} diff --git a/Dentizone.Application/DTOs/Q&A/QuestionDTO/UpdateQuestionDto.cs b/Dentizone.Application/DTOs/Q&A/QuestionDTO/UpdateQuestionDto.cs new file mode 100644 index 0000000..717909d --- /dev/null +++ b/Dentizone.Application/DTOs/Q&A/QuestionDTO/UpdateQuestionDto.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using FluentValidation; + +namespace Dentizone.Application.DTOs.Q_A.QuestionDTO +{ + public class UpdateQuestionDto + { + public string Text { get; set; } + } + public class UpdateQuestionDtoValidator : AbstractValidator + { + public UpdateQuestionDtoValidator() + { + RuleFor(x => x.Text).NotEmpty().WithMessage("Text is required."); + } + } +} diff --git a/Dentizone.Application/Interfaces/IQAService.cs b/Dentizone.Application/Interfaces/IQAService.cs new file mode 100644 index 0000000..bdffba8 --- /dev/null +++ b/Dentizone.Application/Interfaces/IQAService.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Dentizone.Application.DTOs.Q_A.AnswerDTO; +using Dentizone.Application.DTOs.Q_A.QuestionDTO; + +namespace Dentizone.Application.Interfaces +{ + public interface IQAService + { + Task AskQuestionAsync(CreateQuestionDto dto, string askerId); + Task> GetQuestionsForPostAsync(string postId); + Task AnswerQuestionAsync(string questionId, CreateAnswerDto dto, string responderId); + Task UpdateQuestionAsync(string questionId, UpdateQuestionDto dto); + Task DeleteQuestionAsync(string questionId); + Task UpdateAnswerAsync(string answerId, UpdateAnswerDto dto); + Task DeleteAnswerAsync(string answerId); + } +} diff --git a/Dentizone.Application/Services/QAService.cs b/Dentizone.Application/Services/QAService.cs new file mode 100644 index 0000000..6f4c496 --- /dev/null +++ b/Dentizone.Application/Services/QAService.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using System.Threading.Tasks; +using AutoMapper; +using Dentizone.Application.DTOs.Q_A.AnswerDTO; +using Dentizone.Application.DTOs.Q_A.QuestionDTO; +using Dentizone.Application.Interfaces; +using Dentizone.Domain.Entity; +using Dentizone.Domain.Exceptions; +using Dentizone.Domain.Interfaces.Repositories; + +namespace Dentizone.Application.Services +{ + public class QAService(IMapper mapper, IAnswerRepository answerRepository,IQuestionRepository questionRepository) : IQAService + { + public async Task AnswerQuestionAsync(string questionId, CreateAnswerDto dto, string responderId) + { + var question = await questionRepository.GetByIdAsync(questionId); + if (question == null) + { + throw new ArgumentException("Question not found", nameof(questionId)); + } + var post= question.Post; + if (post == null) + { + throw new ArgumentException("Post not found for the question", nameof(questionId)); + } + if(responderId != post.SellerId) + { + throw new UnauthorizedAccessException("You are not authorized to answer this question"); + } + var existingAnswer = await answerRepository.FindBy(a => a.QuestionId == questionId && !a.IsDeleted); + if (existingAnswer != null) + { + throw new BadActionException("This question has already been answered."); + } + var answer = mapper.Map(dto); + question.Status = Domain.Enums.QuestionStatus.Answered; + await answerRepository.CreateAsync(answer); + await questionRepository.UpdateAsync(question); + return mapper.Map(answer); + } + + public async Task AskQuestionAsync(CreateQuestionDto dto, string askerId) + { + var post = questionRepository.GetByIdAsync(dto.PostId); + if (post == null) + { + throw new ArgumentException("Post not found", nameof(dto.PostId)); + } + var question = mapper.Map(dto); + question.AskerId = askerId; + question.Status = Domain.Enums.QuestionStatus.Unanswered; + await questionRepository.CreateAsync(question); + return mapper.Map(question); + } + + public async Task DeleteAnswerAsync(string answerId) + { + var answer = await answerRepository.GetByIdAsync(answerId); + var question = await questionRepository.GetByIdAsync(answer.QuestionId); + var post = question.Post; + if (post == null) + { + throw new ArgumentException("Post not found for the question", nameof(answerId)); + } + await answerRepository.DeleteAsync(answerId); + question.Status = Domain.Enums.QuestionStatus.Unanswered; + await questionRepository.UpdateAsync(question); + } + + public async Task DeleteQuestionAsync(string questionId) + { + var question = await questionRepository.GetByIdAsync(questionId); + if (question == null) + { + throw new ArgumentException("Question not found", nameof(questionId)); + } + var post = question.Post; + if (post == null) + { + throw new ArgumentException("Post not found for the question", nameof(questionId)); + } + await questionRepository.DeleteAsync(questionId); + + } + + public async Task> GetQuestionsForPostAsync(string postId) + { + var includes = new Expression>[] + { + q => q.Answer + }; + + var questions = await questionRepository.FindAllBy( + q => q.PostId == postId && !q.IsDeleted, + includes ); + if (questions == null || !questions.Any()) + { + return Enumerable.Empty(); + } + return mapper.Map>(questions); + + } + + public async Task UpdateAnswerAsync(string answerId, UpdateAnswerDto dto) + { + var answer = await answerRepository.GetByIdAsync(answerId); + var question = await questionRepository.GetByIdAsync(answer.QuestionId); + var post = question.Post; + answer.Text = dto.Text; + await answerRepository.UpdateAsync(answer); + } + + public async Task UpdateQuestionAsync(string questionId, UpdateQuestionDto dto) + { + var question = await questionRepository.GetByIdAsync(questionId); + if (question == null) + { + throw new ArgumentException("Question not found", nameof(questionId)); + } + question.Text = dto.Text; + await questionRepository.UpdateAsync(question); + + } + } +} diff --git a/Dentizone.Domain/Interfaces/Repositories/IQuestionRepository.cs b/Dentizone.Domain/Interfaces/Repositories/IQuestionRepository.cs index 8dc25c3..fea350a 100644 --- a/Dentizone.Domain/Interfaces/Repositories/IQuestionRepository.cs +++ b/Dentizone.Domain/Interfaces/Repositories/IQuestionRepository.cs @@ -1,4 +1,5 @@ -using Dentizone.Domain.Entity; +using System.Linq.Expressions; +using Dentizone.Domain.Entity; namespace Dentizone.Domain.Interfaces.Repositories { @@ -6,5 +7,6 @@ public interface IQuestionRepository : IBaseRepo { Task UpdateAsync(Question entity); Task DeleteAsync(string id); + Task> FindAllBy(Expression> condition, Expression>[]? includes = null); } } \ No newline at end of file diff --git a/Dentizone.Infrastructure/DependencyInjection/AddRepositories.cs b/Dentizone.Infrastructure/DependencyInjection/AddRepositories.cs index 05c564a..1361146 100644 --- a/Dentizone.Infrastructure/DependencyInjection/AddRepositories.cs +++ b/Dentizone.Infrastructure/DependencyInjection/AddRepositories.cs @@ -25,6 +25,8 @@ public static IServiceCollection AddRepositories(this IServiceCollection service services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/Dentizone.Infrastructure/Repositories/QuestionRepository.cs b/Dentizone.Infrastructure/Repositories/QuestionRepository.cs index 9cbf509..f5431ef 100644 --- a/Dentizone.Infrastructure/Repositories/QuestionRepository.cs +++ b/Dentizone.Infrastructure/Repositories/QuestionRepository.cs @@ -55,5 +55,20 @@ public async Task UpdateAsync(Question entity) return entity; } + public async Task> FindAllBy(Expression> condition, Expression>[]? includes = null) + { + IQueryable query = dbContext.Questions; + + if (includes != null) + { + foreach (var include in includes) + { + query = query.Include(include); + } + } + + return await query.Where(condition).ToListAsync(); + } + } } \ No newline at end of file diff --git a/Dentizone.Presentaion/Controllers/QAController.cs b/Dentizone.Presentaion/Controllers/QAController.cs new file mode 100644 index 0000000..c6baedf --- /dev/null +++ b/Dentizone.Presentaion/Controllers/QAController.cs @@ -0,0 +1,88 @@ +using System.Runtime.CompilerServices; +using System.Security.Claims; +using Dentizone.Application.DTOs.Q_A.AnswerDTO; +using Dentizone.Application.DTOs.Q_A.QuestionDTO; +using Dentizone.Application.Interfaces; +using Dentizone.Application.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Dentizone.Presentaion.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class QAController(IQAService iQAService) : ControllerBase + { + [Authorize] + [HttpPost("/api/questions")] + public IActionResult AskQuestion([FromBody] CreateQuestionDto dto) + { + var userId = User.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value; + if (dto == null || string.IsNullOrEmpty(dto.PostId) || string.IsNullOrEmpty(dto.Text)) + { + return BadRequest("Invalid question data."); + } + var question = iQAService.AskQuestionAsync(dto, userId).Result; + return Ok(question); + } + [HttpGet("/api/posts/{postId}/questions")] + public async Task GetQuestionsForPost(string postId) + { + var questions = await iQAService.GetQuestionsForPostAsync(postId); + return Ok(questions); + } + + [Authorize] + [HttpPost("/api/questions/{questionId}/answers")] + public async Task AnswerQuestion(string questionId, [FromBody] CreateAnswerDto dto) + { + var responderId = User.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value; + if (responderId == null) + return Unauthorized(); + + var answer = await iQAService.AnswerQuestionAsync(questionId, dto, responderId); + return Ok(answer); + } + + [Authorize] + [HttpPut("/api/questions/{questionId}")] + public async Task UpdateQuestion(string questionId, [FromBody] UpdateQuestionDto dto) + { + if (dto == null || string.IsNullOrEmpty(dto.Text)) + { + return BadRequest("Invalid question data."); + } + await iQAService.UpdateQuestionAsync(questionId, dto); + return NoContent(); + } + + [Authorize] + [HttpDelete("/api/questions/{questionId}")] + public async Task DeleteQuestion(string questionId) + { + await iQAService.DeleteQuestionAsync(questionId); + return NoContent(); + } + + [Authorize] + [HttpPut("/api/answers/{answerId}")] + public async Task UpdateAnswer(string answerId, [FromBody] UpdateAnswerDto dto) + { + if (dto == null || string.IsNullOrEmpty(dto.Text)) + { + return BadRequest("Invalid answer data."); + } + await iQAService.UpdateAnswerAsync(answerId, dto); + return NoContent(); + } + + [Authorize] + [HttpDelete("/api/answers/{answerId}")] + public async Task DeleteAnswer(string answerId) + { + await iQAService.DeleteAnswerAsync(answerId); + return NoContent(); + } + } +} From 50df6d01f5def5685b3cd568b1d5c697203140eb Mon Sep 17 00:00:00 2001 From: Mahmoud Nasr <239.nasr@gmail.com> Date: Sun, 29 Jun 2025 13:51:14 +0300 Subject: [PATCH 2/8] Enhance DTO validation and refactor service logic Added validation classes for answer and question DTOs. Refactored mapping configurations in `AnswerProfile.cs` and `QuestionProfile.cs` for clarity. Updated `WithdrawalProfile.cs` with additional mappings. Improved exception handling and readability in `QAService.cs`. Enhanced structure in `ShippingService.cs` and optimized querying in `QuestionRepository.cs` with eager loading. --- .../AutoMapper/Answers/AnswerProfile.cs | 15 ++---- .../AutoMapper/Questions/QuestionProfile.cs | 9 +--- .../AutoMapper/WithdrawalProfile.cs | 12 ++--- .../DTOs/Q&A/AnswerDTO/AnswerViewDto.cs | 11 ++-- .../DTOs/Q&A/AnswerDTO/CreateAnswerDto.cs | 10 ++-- .../DTOs/Q&A/AnswerDTO/UpdateAnswerDto.cs | 10 ++-- .../DTOs/Q&A/QuestionDTO/CreateQuestionDto.cs | 11 ++-- .../DTOs/Q&A/QuestionDTO/QuestionViewDto.cs | 10 ++-- .../DTOs/Q&A/QuestionDTO/UpdateQuestionDto.cs | 10 ++-- .../DTOs/Withdrawal/ApproveWithdrawalDto.cs | 10 +--- .../DTOs/Withdrawal/WithdrawalRequestDto.cs | 9 +--- .../Interfaces/IQAService.cs | 9 +--- .../Interfaces/IWithdrawalService.cs | 16 ++---- .../Interfaces/Order/IShippingService.cs | 7 +-- Dentizone.Application/Services/QAService.cs | 54 ++++++++++--------- .../Services/ShippingService.cs | 10 +--- .../Repositories/QuestionRepository.cs | 11 ++-- 17 files changed, 76 insertions(+), 148 deletions(-) diff --git a/Dentizone.Application/AutoMapper/Answers/AnswerProfile.cs b/Dentizone.Application/AutoMapper/Answers/AnswerProfile.cs index bf7fa47..7ad1976 100644 --- a/Dentizone.Application/AutoMapper/Answers/AnswerProfile.cs +++ b/Dentizone.Application/AutoMapper/Answers/AnswerProfile.cs @@ -1,13 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using AutoMapper; +using AutoMapper; namespace Dentizone.Application.AutoMapper.Answers { - public class AnswerProfile :Profile + public class AnswerProfile : Profile { public AnswerProfile() { @@ -19,8 +14,8 @@ public AnswerProfile() .ForMember(dest => dest.Text, opt => opt.MapFrom(src => src.Text)) .ForMember(dest => dest.UpdatedAt, opt => opt.MapFrom(src => DateTime.UtcNow)) .ReverseMap(); - CreateMap().ReverseMap(); + CreateMap() + .ReverseMap(); } } - -} +} \ No newline at end of file diff --git a/Dentizone.Application/AutoMapper/Questions/QuestionProfile.cs b/Dentizone.Application/AutoMapper/Questions/QuestionProfile.cs index 57fac4f..a7bfdfc 100644 --- a/Dentizone.Application/AutoMapper/Questions/QuestionProfile.cs +++ b/Dentizone.Application/AutoMapper/Questions/QuestionProfile.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.DTOs.Q_A.QuestionDTO; namespace Dentizone.Application.AutoMapper.Questions @@ -28,4 +23,4 @@ public QuestionProfile() .ReverseMap(); } } -} +} \ No newline at end of file diff --git a/Dentizone.Application/AutoMapper/WithdrawalProfile.cs b/Dentizone.Application/AutoMapper/WithdrawalProfile.cs index 43e6ed0..cfaaafd 100644 --- a/Dentizone.Application/AutoMapper/WithdrawalProfile.cs +++ b/Dentizone.Application/AutoMapper/WithdrawalProfile.cs @@ -1,15 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using AutoMapper; +using AutoMapper; using Dentizone.Application.DTOs.Withdrawal; using Dentizone.Domain.Entity; namespace Dentizone.Application.AutoMapper { - public class WithdrawalProfile: Profile + public class WithdrawalProfile : Profile { public WithdrawalProfile() { @@ -18,6 +13,5 @@ public WithdrawalProfile() .ForMember(dest => dest.UserId, opt => opt.MapFrom(src => src.Wallet.UserId.ToString())) .ReverseMap(); } - } -} +} \ No newline at end of file diff --git a/Dentizone.Application/DTOs/Q&A/AnswerDTO/AnswerViewDto.cs b/Dentizone.Application/DTOs/Q&A/AnswerDTO/AnswerViewDto.cs index 0ea23e4..707f864 100644 --- a/Dentizone.Application/DTOs/Q&A/AnswerDTO/AnswerViewDto.cs +++ b/Dentizone.Application/DTOs/Q&A/AnswerDTO/AnswerViewDto.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using FluentValidation; +using FluentValidation; namespace Dentizone.Application.DTOs.Q_A.AnswerDTO { @@ -13,8 +8,8 @@ public class AnswerViewDto public string ResponderName { get; set; } public string Text { get; set; } public DateTime CreatedAt { get; set; } - } + public class AnswerViewDtoValidator : AbstractValidator { public AnswerViewDtoValidator() @@ -25,4 +20,4 @@ public AnswerViewDtoValidator() RuleFor(x => x.CreatedAt).NotEmpty().WithMessage("CreatedAt is required."); } } -} +} \ No newline at end of file diff --git a/Dentizone.Application/DTOs/Q&A/AnswerDTO/CreateAnswerDto.cs b/Dentizone.Application/DTOs/Q&A/AnswerDTO/CreateAnswerDto.cs index 8c9aec3..47a81be 100644 --- a/Dentizone.Application/DTOs/Q&A/AnswerDTO/CreateAnswerDto.cs +++ b/Dentizone.Application/DTOs/Q&A/AnswerDTO/CreateAnswerDto.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using FluentValidation; +using FluentValidation; namespace Dentizone.Application.DTOs.Q_A.AnswerDTO { @@ -11,6 +6,7 @@ public class CreateAnswerDto { public string Text { get; set; } } + public class CreateAnswerDtoValidator : AbstractValidator { public CreateAnswerDtoValidator() @@ -18,4 +14,4 @@ public CreateAnswerDtoValidator() RuleFor(x => x.Text).NotEmpty().WithMessage("Text is required."); } } -} +} \ No newline at end of file diff --git a/Dentizone.Application/DTOs/Q&A/AnswerDTO/UpdateAnswerDto.cs b/Dentizone.Application/DTOs/Q&A/AnswerDTO/UpdateAnswerDto.cs index c3a1f8a..e1fb393 100644 --- a/Dentizone.Application/DTOs/Q&A/AnswerDTO/UpdateAnswerDto.cs +++ b/Dentizone.Application/DTOs/Q&A/AnswerDTO/UpdateAnswerDto.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using FluentValidation; +using FluentValidation; namespace Dentizone.Application.DTOs.Q_A.AnswerDTO { @@ -11,6 +6,7 @@ public class UpdateAnswerDto { public string Text { get; set; } } + public class UpdateAnswerDtoValidator : AbstractValidator { public UpdateAnswerDtoValidator() @@ -18,4 +14,4 @@ public UpdateAnswerDtoValidator() RuleFor(x => x.Text).NotEmpty().WithMessage("Text is required."); } } -} +} \ No newline at end of file diff --git a/Dentizone.Application/DTOs/Q&A/QuestionDTO/CreateQuestionDto.cs b/Dentizone.Application/DTOs/Q&A/QuestionDTO/CreateQuestionDto.cs index 61af0ed..e2caca8 100644 --- a/Dentizone.Application/DTOs/Q&A/QuestionDTO/CreateQuestionDto.cs +++ b/Dentizone.Application/DTOs/Q&A/QuestionDTO/CreateQuestionDto.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using FluentValidation; +using FluentValidation; namespace Dentizone.Application.DTOs.Q_A.QuestionDTO { @@ -12,6 +7,7 @@ public class CreateQuestionDto public string PostId { get; set; } public string Text { get; set; } } + public class CreateQuestionDtoValidator : AbstractValidator { public CreateQuestionDtoValidator() @@ -19,6 +15,5 @@ public CreateQuestionDtoValidator() RuleFor(x => x.PostId).NotEmpty().WithMessage("PostId is required."); RuleFor(x => x.Text).NotEmpty().WithMessage("Text is required."); } - } -} +} \ No newline at end of file diff --git a/Dentizone.Application/DTOs/Q&A/QuestionDTO/QuestionViewDto.cs b/Dentizone.Application/DTOs/Q&A/QuestionDTO/QuestionViewDto.cs index f7d5519..b25559a 100644 --- a/Dentizone.Application/DTOs/Q&A/QuestionDTO/QuestionViewDto.cs +++ b/Dentizone.Application/DTOs/Q&A/QuestionDTO/QuestionViewDto.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Dentizone.Application.DTOs.Q_A.AnswerDTO; +using Dentizone.Application.DTOs.Q_A.AnswerDTO; using FluentValidation; namespace Dentizone.Application.DTOs.Q_A.QuestionDTO @@ -16,6 +11,7 @@ public class QuestionViewDto public AnswerViewDto? Answer { get; set; } public DateTime CreatedAt { get; set; } } + public class QuestionViewDtoValidator : AbstractValidator { public QuestionViewDtoValidator() @@ -26,4 +22,4 @@ public QuestionViewDtoValidator() RuleFor(x => x.CreatedAt).NotEmpty().WithMessage("CreatedAt is required."); } } -} +} \ No newline at end of file diff --git a/Dentizone.Application/DTOs/Q&A/QuestionDTO/UpdateQuestionDto.cs b/Dentizone.Application/DTOs/Q&A/QuestionDTO/UpdateQuestionDto.cs index 717909d..cf2c091 100644 --- a/Dentizone.Application/DTOs/Q&A/QuestionDTO/UpdateQuestionDto.cs +++ b/Dentizone.Application/DTOs/Q&A/QuestionDTO/UpdateQuestionDto.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using FluentValidation; +using FluentValidation; namespace Dentizone.Application.DTOs.Q_A.QuestionDTO { @@ -11,6 +6,7 @@ public class UpdateQuestionDto { public string Text { get; set; } } + public class UpdateQuestionDtoValidator : AbstractValidator { public UpdateQuestionDtoValidator() @@ -18,4 +14,4 @@ public UpdateQuestionDtoValidator() RuleFor(x => x.Text).NotEmpty().WithMessage("Text is required."); } } -} +} \ No newline at end of file diff --git a/Dentizone.Application/DTOs/Withdrawal/ApproveWithdrawalDto.cs b/Dentizone.Application/DTOs/Withdrawal/ApproveWithdrawalDto.cs index 9b150f1..8e91bda 100644 --- a/Dentizone.Application/DTOs/Withdrawal/ApproveWithdrawalDto.cs +++ b/Dentizone.Application/DTOs/Withdrawal/ApproveWithdrawalDto.cs @@ -1,13 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Dentizone.Application.DTOs.Withdrawal +namespace Dentizone.Application.DTOs.Withdrawal { public class ApproveWithdrawalDto { public string AdminNote { get; set; } } -} +} \ No newline at end of file diff --git a/Dentizone.Application/DTOs/Withdrawal/WithdrawalRequestDto.cs b/Dentizone.Application/DTOs/Withdrawal/WithdrawalRequestDto.cs index d80a122..b051717 100644 --- a/Dentizone.Application/DTOs/Withdrawal/WithdrawalRequestDto.cs +++ b/Dentizone.Application/DTOs/Withdrawal/WithdrawalRequestDto.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using FluentValidation; +using FluentValidation; namespace Dentizone.Application.DTOs.Withdrawal { @@ -24,4 +19,4 @@ public WithdrawalRequestDtoValidator() .NotEmpty().WithMessage("Wallet ID is required."); } } -} +} \ No newline at end of file diff --git a/Dentizone.Application/Interfaces/IQAService.cs b/Dentizone.Application/Interfaces/IQAService.cs index bdffba8..d1f743a 100644 --- a/Dentizone.Application/Interfaces/IQAService.cs +++ b/Dentizone.Application/Interfaces/IQAService.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Dentizone.Application.DTOs.Q_A.AnswerDTO; +using Dentizone.Application.DTOs.Q_A.AnswerDTO; using Dentizone.Application.DTOs.Q_A.QuestionDTO; namespace Dentizone.Application.Interfaces @@ -18,4 +13,4 @@ public interface IQAService Task UpdateAnswerAsync(string answerId, UpdateAnswerDto dto); Task DeleteAnswerAsync(string answerId); } -} +} \ No newline at end of file diff --git a/Dentizone.Application/Interfaces/IWithdrawalService.cs b/Dentizone.Application/Interfaces/IWithdrawalService.cs index d548ea0..9124433 100644 --- a/Dentizone.Application/Interfaces/IWithdrawalService.cs +++ b/Dentizone.Application/Interfaces/IWithdrawalService.cs @@ -1,20 +1,14 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Dentizone.Application.DTOs.Withdrawal; -using Dentizone.Domain.Entity; +using Dentizone.Application.DTOs.Withdrawal; namespace Dentizone.Application.Interfaces { public interface IWithdrawalService { - Task CreateWithdrawalRequestAsync(string userId, WithdrawalRequestDto withdrawalRequestDto); + Task CreateWithdrawalRequestAsync(string userId, + WithdrawalRequestDto withdrawalRequestDto); + Task> GetWithdrawalHistoryAsync(string userId, int page); Task ApproveWithdrawalAsync(string id, string adminNote); Task RejectWithdrawalAsync(string id, string adminNote); } -} - - +} \ No newline at end of file diff --git a/Dentizone.Application/Interfaces/Order/IShippingService.cs b/Dentizone.Application/Interfaces/Order/IShippingService.cs index 6b1884d..d92b031 100644 --- a/Dentizone.Application/Interfaces/Order/IShippingService.cs +++ b/Dentizone.Application/Interfaces/Order/IShippingService.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Dentizone.Domain.Enums; +using Dentizone.Domain.Enums; namespace Dentizone.Application.Interfaces.Order { diff --git a/Dentizone.Application/Services/QAService.cs b/Dentizone.Application/Services/QAService.cs index 6f4c496..da42dcf 100644 --- a/Dentizone.Application/Services/QAService.cs +++ b/Dentizone.Application/Services/QAService.cs @@ -1,44 +1,45 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Text; -using System.Threading.Tasks; +using System.Linq.Expressions; using AutoMapper; using Dentizone.Application.DTOs.Q_A.AnswerDTO; using Dentizone.Application.DTOs.Q_A.QuestionDTO; using Dentizone.Application.Interfaces; using Dentizone.Domain.Entity; +using Dentizone.Domain.Enums; using Dentizone.Domain.Exceptions; using Dentizone.Domain.Interfaces.Repositories; namespace Dentizone.Application.Services { - public class QAService(IMapper mapper, IAnswerRepository answerRepository,IQuestionRepository questionRepository) : IQAService + public class QAService(IMapper mapper, IAnswerRepository answerRepository, IQuestionRepository questionRepository) + : IQAService { public async Task AnswerQuestionAsync(string questionId, CreateAnswerDto dto, string responderId) { var question = await questionRepository.GetByIdAsync(questionId); if (question == null) { - throw new ArgumentException("Question not found", nameof(questionId)); + throw new NotFoundException("Question not found"); } - var post= question.Post; + + var post = question.Post; if (post == null) { - throw new ArgumentException("Post not found for the question", nameof(questionId)); + throw new NotFoundException("Post not found"); } - if(responderId != post.SellerId) + + if (responderId != post.SellerId) { throw new UnauthorizedAccessException("You are not authorized to answer this question"); } + var existingAnswer = await answerRepository.FindBy(a => a.QuestionId == questionId && !a.IsDeleted); if (existingAnswer != null) { throw new BadActionException("This question has already been answered."); } + var answer = mapper.Map(dto); - question.Status = Domain.Enums.QuestionStatus.Answered; + question.Status = QuestionStatus.Answered; await answerRepository.CreateAsync(answer); await questionRepository.UpdateAsync(question); return mapper.Map(answer); @@ -49,11 +50,12 @@ public async Task AskQuestionAsync(CreateQuestionDto dto, strin var post = questionRepository.GetByIdAsync(dto.PostId); if (post == null) { - throw new ArgumentException("Post not found", nameof(dto.PostId)); + throw new NotFoundException("Post not found"); } + var question = mapper.Map(dto); question.AskerId = askerId; - question.Status = Domain.Enums.QuestionStatus.Unanswered; + question.Status = QuestionStatus.Unanswered; await questionRepository.CreateAsync(question); return mapper.Map(question); } @@ -67,8 +69,9 @@ public async Task DeleteAnswerAsync(string answerId) { throw new ArgumentException("Post not found for the question", nameof(answerId)); } + await answerRepository.DeleteAsync(answerId); - question.Status = Domain.Enums.QuestionStatus.Unanswered; + question.Status = QuestionStatus.Unanswered; await questionRepository.UpdateAsync(question); } @@ -79,38 +82,37 @@ public async Task DeleteQuestionAsync(string questionId) { throw new ArgumentException("Question not found", nameof(questionId)); } + var post = question.Post; if (post == null) { throw new ArgumentException("Post not found for the question", nameof(questionId)); } - await questionRepository.DeleteAsync(questionId); + await questionRepository.DeleteAsync(questionId); } public async Task> GetQuestionsForPostAsync(string postId) { var includes = new Expression>[] - { - q => q.Answer - }; + { + q => q.Answer + }; var questions = await questionRepository.FindAllBy( q => q.PostId == postId && !q.IsDeleted, - includes ); + includes); if (questions == null || !questions.Any()) { - return Enumerable.Empty(); + return []; } - return mapper.Map>(questions); + return mapper.Map>(questions); } public async Task UpdateAnswerAsync(string answerId, UpdateAnswerDto dto) { var answer = await answerRepository.GetByIdAsync(answerId); - var question = await questionRepository.GetByIdAsync(answer.QuestionId); - var post = question.Post; answer.Text = dto.Text; await answerRepository.UpdateAsync(answer); } @@ -122,9 +124,9 @@ public async Task UpdateQuestionAsync(string questionId, UpdateQuestionDto dto) { throw new ArgumentException("Question not found", nameof(questionId)); } + question.Text = dto.Text; await questionRepository.UpdateAsync(question); - } } -} +} \ No newline at end of file diff --git a/Dentizone.Application/Services/ShippingService.cs b/Dentizone.Application/Services/ShippingService.cs index 22861cc..89b812f 100644 --- a/Dentizone.Application/Services/ShippingService.cs +++ b/Dentizone.Application/Services/ShippingService.cs @@ -1,18 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Text; -using System.Threading.Tasks; -using Dentizone.Application.Interfaces; -using Dentizone.Application.Interfaces.Order; +using Dentizone.Application.Interfaces.Order; using Dentizone.Application.Services.Authentication; using Dentizone.Domain.Entity; using Dentizone.Domain.Enums; using Dentizone.Domain.Exceptions; using Dentizone.Domain.Interfaces.Mail; using Dentizone.Domain.Interfaces.Repositories; -using Microsoft.Extensions.Hosting; namespace Dentizone.Application.Services { diff --git a/Dentizone.Infrastructure/Repositories/QuestionRepository.cs b/Dentizone.Infrastructure/Repositories/QuestionRepository.cs index f5431ef..6ea3580 100644 --- a/Dentizone.Infrastructure/Repositories/QuestionRepository.cs +++ b/Dentizone.Infrastructure/Repositories/QuestionRepository.cs @@ -15,7 +15,7 @@ public async Task CreateAsync(Question entity) } public async Task FindBy(Expression> condition, - Expression>[]? includes) + Expression>[]? includes) { IQueryable query = dbContext.Questions; if (includes != null) @@ -45,7 +45,9 @@ public async Task CreateAsync(Question entity) public async Task GetByIdAsync(string id) { - return await dbContext.Questions.FindAsync(id); + return await dbContext.Questions + .Include(p => p.Post) + .Where(q => q.Id == id).FirstOrDefaultAsync(); } public async Task UpdateAsync(Question entity) @@ -55,7 +57,9 @@ public async Task UpdateAsync(Question entity) return entity; } - public async Task> FindAllBy(Expression> condition, Expression>[]? includes = null) + + public async Task> FindAllBy(Expression> condition, + Expression>[]? includes = null) { IQueryable query = dbContext.Questions; @@ -69,6 +73,5 @@ public async Task> FindAllBy(Expression> con return await query.Where(condition).ToListAsync(); } - } } \ No newline at end of file From a48df654ddea06f7ace2e430c2325587cafb49a5 Mon Sep 17 00:00:00 2001 From: Mahmoud Nasr <239.nasr@gmail.com> Date: Sun, 29 Jun 2025 14:29:23 +0300 Subject: [PATCH 3/8] Enhance validation for Answer and Question DTOs Updated validation rules in `AnswerViewDto` and `CreateAnswerDto` to include maximum length checks and additional content validation. Modified `CreateQuestionDto` and `UpdateQuestionDto` to use nullable types for `Text` and added length validation. Improved overall data integrity and user feedback in the application. --- .../DTOs/Q&A/AnswerDTO/AnswerViewDto.cs | 12 +++++++++--- .../DTOs/Q&A/AnswerDTO/CreateAnswerDto.cs | 13 ++++++++++++- .../DTOs/Q&A/QuestionDTO/CreateQuestionDto.cs | 5 +++-- .../DTOs/Q&A/QuestionDTO/UpdateQuestionDto.cs | 6 ++++-- 4 files changed, 28 insertions(+), 8 deletions(-) diff --git a/Dentizone.Application/DTOs/Q&A/AnswerDTO/AnswerViewDto.cs b/Dentizone.Application/DTOs/Q&A/AnswerDTO/AnswerViewDto.cs index 707f864..4ca4fa4 100644 --- a/Dentizone.Application/DTOs/Q&A/AnswerDTO/AnswerViewDto.cs +++ b/Dentizone.Application/DTOs/Q&A/AnswerDTO/AnswerViewDto.cs @@ -15,9 +15,15 @@ public class AnswerViewDtoValidator : AbstractValidator public AnswerViewDtoValidator() { RuleFor(x => x.Id).NotEmpty().WithMessage("Id is required."); - RuleFor(x => x.ResponderName).NotEmpty().WithMessage("ResponderName is required."); - RuleFor(x => x.Text).NotEmpty().WithMessage("Text is required."); - RuleFor(x => x.CreatedAt).NotEmpty().WithMessage("CreatedAt is required."); + RuleFor(x => x.ResponderName) + .NotEmpty().WithMessage("ResponderName is required.") + .MaximumLength(100).WithMessage("ResponderName cannot exceed 100 characters."); + RuleFor(x => x.Text) + .NotEmpty().WithMessage("Text is required.") + .MaximumLength(500).WithMessage("Text cannot exceed 500 characters."); + RuleFor(x => x.CreatedAt) + .Must(date => date != default).WithMessage("CreatedAt must be a valid date.") + .LessThanOrEqualTo(DateTime.UtcNow).WithMessage("CreatedAt cannot be in the future."); } } } \ No newline at end of file diff --git a/Dentizone.Application/DTOs/Q&A/AnswerDTO/CreateAnswerDto.cs b/Dentizone.Application/DTOs/Q&A/AnswerDTO/CreateAnswerDto.cs index 47a81be..d088e6e 100644 --- a/Dentizone.Application/DTOs/Q&A/AnswerDTO/CreateAnswerDto.cs +++ b/Dentizone.Application/DTOs/Q&A/AnswerDTO/CreateAnswerDto.cs @@ -11,7 +11,18 @@ public class CreateAnswerDtoValidator : AbstractValidator { public CreateAnswerDtoValidator() { - RuleFor(x => x.Text).NotEmpty().WithMessage("Text is required."); + RuleFor(x => x.Text) + .NotEmpty().WithMessage("Text is required.") + .MaximumLength(500).WithMessage("Text cannot exceed 500 characters.") + .Must(text => !string.IsNullOrWhiteSpace(text)).WithMessage("Text cannot be whitespace only.") + .Must(text => !ContainsProhibitedContent(text)).WithMessage("Text contains prohibited content."); + } + + private bool ContainsProhibitedContent(string text) + { + // Placeholder for prohibited content check. Add logic as needed. + // Example: return text.Contains("badword"); + return false; } } } \ No newline at end of file diff --git a/Dentizone.Application/DTOs/Q&A/QuestionDTO/CreateQuestionDto.cs b/Dentizone.Application/DTOs/Q&A/QuestionDTO/CreateQuestionDto.cs index e2caca8..fd56f9c 100644 --- a/Dentizone.Application/DTOs/Q&A/QuestionDTO/CreateQuestionDto.cs +++ b/Dentizone.Application/DTOs/Q&A/QuestionDTO/CreateQuestionDto.cs @@ -2,10 +2,11 @@ namespace Dentizone.Application.DTOs.Q_A.QuestionDTO { + #nullable enable public class CreateQuestionDto { - public string PostId { get; set; } - public string Text { get; set; } + public Guid PostId { get; set; } + public string? Text { get; set; } } public class CreateQuestionDtoValidator : AbstractValidator diff --git a/Dentizone.Application/DTOs/Q&A/QuestionDTO/UpdateQuestionDto.cs b/Dentizone.Application/DTOs/Q&A/QuestionDTO/UpdateQuestionDto.cs index cf2c091..a9f6bec 100644 --- a/Dentizone.Application/DTOs/Q&A/QuestionDTO/UpdateQuestionDto.cs +++ b/Dentizone.Application/DTOs/Q&A/QuestionDTO/UpdateQuestionDto.cs @@ -4,14 +4,16 @@ namespace Dentizone.Application.DTOs.Q_A.QuestionDTO { public class UpdateQuestionDto { - public string Text { get; set; } + public string? Text { get; set; } } public class UpdateQuestionDtoValidator : AbstractValidator { public UpdateQuestionDtoValidator() { - RuleFor(x => x.Text).NotEmpty().WithMessage("Text is required."); + RuleFor(x => x.Text) + .NotEmpty().WithMessage("Text is required.") + .MaximumLength(500).WithMessage("Text cannot exceed 500 characters."); } } } \ No newline at end of file From 4313c3dca380867b843f28d3aefa154e059de493 Mon Sep 17 00:00:00 2001 From: Mahmoud Nasr <239.nasr@gmail.com> Date: Sun, 29 Jun 2025 19:11:19 +0300 Subject: [PATCH 4/8] Refactor AutoMapper configurations in QuestionProfile Updated mapping configurations in `QuestionProfile.cs` to simplify DTO references and improve clarity. Adjusted member mappings for `CreateQuestionDto`, `QuestionViewDto`, and `UpdateQuestionDto`, including changes to how user information is handled and ensuring proper mapping for new properties. --- .../AutoMapper/Questions/QuestionProfile.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Dentizone.Application/AutoMapper/Questions/QuestionProfile.cs b/Dentizone.Application/AutoMapper/Questions/QuestionProfile.cs index a7bfdfc..6eed682 100644 --- a/Dentizone.Application/AutoMapper/Questions/QuestionProfile.cs +++ b/Dentizone.Application/AutoMapper/Questions/QuestionProfile.cs @@ -7,17 +7,18 @@ public class QuestionProfile : Profile { public QuestionProfile() { - CreateMap() + CreateMap() .ForMember(dest => dest.Id, opt => opt.MapFrom(src => Guid.NewGuid().ToString())) .ForMember(dest => dest.CreatedAt, opt => opt.MapFrom(src => DateTime.UtcNow)) .ReverseMap(); - CreateMap() + CreateMap() .ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id)) - .ForMember(dest => dest.User, opt => opt.MapFrom(src => src.AskerName)) + .ForMember(dest => dest.User, opt => opt.Ignore()) .ForMember(dest => dest.Text, opt => opt.MapFrom(src => src.Text)) .ForMember(dest => dest.CreatedAt, opt => opt.MapFrom(src => src.CreatedAt)) - .ReverseMap(); - CreateMap() + .ReverseMap() + .ForMember(dest => dest.AskerName, opt => opt.MapFrom(src => src.User != null ? src.User.FullName : string.Empty)); + CreateMap() .ForMember(dest => dest.Text, opt => opt.MapFrom(src => src.Text)) .ForMember(dest => dest.UpdatedAt, opt => opt.MapFrom(src => DateTime.UtcNow)) .ReverseMap(); From 862fb32c38ef26541852a00a6f9ebe6a6452a17e Mon Sep 17 00:00:00 2001 From: Mahmoud Nasr <239.nasr@gmail.com> Date: Sun, 29 Jun 2025 19:28:23 +0300 Subject: [PATCH 5/8] Refactor QAController for improved dependency injection - Updated `QAController` to use `QAService` via dependency injection. - Moved `[Authorize]` attribute to the class level for consistency. - Simplified HTTP route attributes to use relative paths. - Enhanced asynchronous handling by using `await` in service calls. - Removed error handling for null or invalid data in certain methods. - Fixed minor typo in the route definition for `DeleteQuestion`. --- .../Controllers/QAController.cs | 56 +++++++------------ 1 file changed, 19 insertions(+), 37 deletions(-) diff --git a/Dentizone.Presentaion/Controllers/QAController.cs b/Dentizone.Presentaion/Controllers/QAController.cs index c6baedf..ca131fc 100644 --- a/Dentizone.Presentaion/Controllers/QAController.cs +++ b/Dentizone.Presentaion/Controllers/QAController.cs @@ -12,77 +12,59 @@ namespace Dentizone.Presentaion.Controllers { [Route("api/[controller]")] [ApiController] - public class QAController(IQAService iQAService) : ControllerBase + [Authorize] + public class QAController(IQAService QAService) : ControllerBase { - [Authorize] - [HttpPost("/api/questions")] - public IActionResult AskQuestion([FromBody] CreateQuestionDto dto) + [HttpPost()] + public async Task AskQuestion([FromBody] CreateQuestionDto dto) { var userId = User.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value; - if (dto == null || string.IsNullOrEmpty(dto.PostId) || string.IsNullOrEmpty(dto.Text)) - { - return BadRequest("Invalid question data."); - } - var question = iQAService.AskQuestionAsync(dto, userId).Result; + var question = await QAService.AskQuestionAsync(dto, userId); return Ok(question); } - [HttpGet("/api/posts/{postId}/questions")] + + [HttpGet("questions/{postId}")] public async Task GetQuestionsForPost(string postId) { - var questions = await iQAService.GetQuestionsForPostAsync(postId); + var questions = await QAService.GetQuestionsForPostAsync(postId); return Ok(questions); } - [Authorize] - [HttpPost("/api/questions/{questionId}/answers")] + [HttpPost("answer/{questionId}")] public async Task AnswerQuestion(string questionId, [FromBody] CreateAnswerDto dto) { var responderId = User.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value; - if (responderId == null) - return Unauthorized(); - var answer = await iQAService.AnswerQuestionAsync(questionId, dto, responderId); + var answer = await QAService.AnswerQuestionAsync(questionId, dto, responderId); return Ok(answer); } - [Authorize] - [HttpPut("/api/questions/{questionId}")] + [HttpPut("{questionId}")] public async Task UpdateQuestion(string questionId, [FromBody] UpdateQuestionDto dto) { - if (dto == null || string.IsNullOrEmpty(dto.Text)) - { - return BadRequest("Invalid question data."); - } - await iQAService.UpdateQuestionAsync(questionId, dto); + await QAService.UpdateQuestionAsync(questionId, dto); return NoContent(); } - [Authorize] - [HttpDelete("/api/questions/{questionId}")] + [HttpDelete("questionId}")] public async Task DeleteQuestion(string questionId) { - await iQAService.DeleteQuestionAsync(questionId); + await QAService.DeleteQuestionAsync(questionId); return NoContent(); } - [Authorize] - [HttpPut("/api/answers/{answerId}")] + [HttpPut("answers/{answerId}")] public async Task UpdateAnswer(string answerId, [FromBody] UpdateAnswerDto dto) { - if (dto == null || string.IsNullOrEmpty(dto.Text)) - { - return BadRequest("Invalid answer data."); - } - await iQAService.UpdateAnswerAsync(answerId, dto); + await QAService.UpdateAnswerAsync(answerId, dto); return NoContent(); } - [Authorize] - [HttpDelete("/api/answers/{answerId}")] + [HttpDelete("answers/{answerId}")] public async Task DeleteAnswer(string answerId) { - await iQAService.DeleteAnswerAsync(answerId); + await QAService.DeleteAnswerAsync(answerId); return NoContent(); } } -} +} \ No newline at end of file From b8a3566f3bf359bbaf8c075a7e2b1e1c9e4281d7 Mon Sep 17 00:00:00 2001 From: Mahmoud Nasr <239.nasr@gmail.com> Date: Sun, 29 Jun 2025 19:39:59 +0300 Subject: [PATCH 6/8] Fix route syntax for DeleteQuestion method Updated the route attribute in `QAController.cs` for the `DeleteQuestion` method to correct a syntax error. The route now properly maps to the HTTP DELETE request with the `questionId` parameter, ensuring correct functionality. The method implementation remains unchanged. --- Dentizone.Presentaion/Controllers/QAController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dentizone.Presentaion/Controllers/QAController.cs b/Dentizone.Presentaion/Controllers/QAController.cs index ca131fc..bdf6dd2 100644 --- a/Dentizone.Presentaion/Controllers/QAController.cs +++ b/Dentizone.Presentaion/Controllers/QAController.cs @@ -46,7 +46,7 @@ public async Task UpdateQuestion(string questionId, [FromBody] Up return NoContent(); } - [HttpDelete("questionId}")] + [HttpDelete("{questionId}")] public async Task DeleteQuestion(string questionId) { await QAService.DeleteQuestionAsync(questionId); From d7a434ee4bf791623fc2eff0b333ecfa64d8d713 Mon Sep 17 00:00:00 2001 From: Mahmoud Nasr <239.nasr@gmail.com> Date: Sun, 29 Jun 2025 19:41:03 +0300 Subject: [PATCH 7/8] Remove prohibited content check from CreateAnswerDtoValidator The validation rule for checking prohibited content in the `Text` property has been removed from the `CreateAnswerDtoValidator` constructor. Additionally, the `ContainsProhibitedContent` method has been deleted. The remaining validation ensures that the `Text` is not empty and does not consist solely of whitespace. --- .../DTOs/Q&A/AnswerDTO/CreateAnswerDto.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/Dentizone.Application/DTOs/Q&A/AnswerDTO/CreateAnswerDto.cs b/Dentizone.Application/DTOs/Q&A/AnswerDTO/CreateAnswerDto.cs index d088e6e..c07f497 100644 --- a/Dentizone.Application/DTOs/Q&A/AnswerDTO/CreateAnswerDto.cs +++ b/Dentizone.Application/DTOs/Q&A/AnswerDTO/CreateAnswerDto.cs @@ -14,15 +14,7 @@ public CreateAnswerDtoValidator() RuleFor(x => x.Text) .NotEmpty().WithMessage("Text is required.") .MaximumLength(500).WithMessage("Text cannot exceed 500 characters.") - .Must(text => !string.IsNullOrWhiteSpace(text)).WithMessage("Text cannot be whitespace only.") - .Must(text => !ContainsProhibitedContent(text)).WithMessage("Text contains prohibited content."); - } - - private bool ContainsProhibitedContent(string text) - { - // Placeholder for prohibited content check. Add logic as needed. - // Example: return text.Contains("badword"); - return false; + .Must(text => !string.IsNullOrWhiteSpace(text)).WithMessage("Text cannot be whitespace only."); } } } \ No newline at end of file From 27ec423a4a8450fc96b4c5dde4c4cb06e19edabe Mon Sep 17 00:00:00 2001 From: Mahmoud Nasr <239.nasr@gmail.com> Date: Sun, 29 Jun 2025 19:52:38 +0300 Subject: [PATCH 8/8] Refactor DTOs and repositories for improved validation - Changed `PostId` type in `CreateQuestionDto` from `Guid` to `string` and added a validation rule for GUID parsing. - Updated method signatures in multiple repository classes to remove unnecessary default values for the `includes` parameter. - Refactored LINQ queries in several repository methods for better readability. - Introduced a new `GUIDValidator` class for reusable GUID validation logic. - Overall improvements enhance code quality and maintainability across the application. --- .../DTOs/Q&A/QuestionDTO/CreateQuestionDto.cs | 9 +-- .../Validators/GUIDValidator.cs | 19 ++++++ .../Interfaces/Repositories/IBaseRepo.cs | 2 +- .../Repositories/AnswerRepository.cs | 4 +- .../Repositories/AssetRepository.cs | 4 +- .../Repositories/CartRepository.cs | 16 ++--- .../Repositories/CategoryRepository.cs | 8 +-- .../Repositories/FavouriteRepository.cs | 12 ++-- .../Repositories/OrderItemRepository.cs | 4 +- .../Repositories/OrderPickupRepository.cs | 4 +- .../Repositories/OrderStatusRepository.cs | 8 +-- .../Repositories/PaymentRepository.cs | 2 +- .../Repositories/PostAssetRepository.cs | 2 +- .../Repositories/PostRepsitory.cs | 64 +++++++++---------- .../Repositories/SaleTransactionRepository.cs | 4 +- .../Repositories/ShipInfoRepository.cs | 4 +- .../ShipmentActivityRepository.cs | 4 +- .../Repositories/SubCategoryRepository.cs | 8 +-- .../Repositories/UniversityRepository.cs | 18 +++--- .../Repositories/UserActivityRepository.cs | 12 ++-- .../Repositories/UserAssetRepository.cs | 12 ++-- .../Repositories/UserRepository.cs | 20 +++--- .../WithdrawalRequestRepository.cs | 16 ++--- 23 files changed, 138 insertions(+), 118 deletions(-) create mode 100644 Dentizone.Application/Validators/GUIDValidator.cs diff --git a/Dentizone.Application/DTOs/Q&A/QuestionDTO/CreateQuestionDto.cs b/Dentizone.Application/DTOs/Q&A/QuestionDTO/CreateQuestionDto.cs index fd56f9c..4685c38 100644 --- a/Dentizone.Application/DTOs/Q&A/QuestionDTO/CreateQuestionDto.cs +++ b/Dentizone.Application/DTOs/Q&A/QuestionDTO/CreateQuestionDto.cs @@ -1,11 +1,12 @@ -using FluentValidation; +using Dentizone.Application.Validators; +using FluentValidation; namespace Dentizone.Application.DTOs.Q_A.QuestionDTO { - #nullable enable +#nullable enable public class CreateQuestionDto { - public Guid PostId { get; set; } + public string PostId { get; set; } public string? Text { get; set; } } @@ -13,7 +14,7 @@ public class CreateQuestionDtoValidator : AbstractValidator { public CreateQuestionDtoValidator() { - RuleFor(x => x.PostId).NotEmpty().WithMessage("PostId is required."); + RuleFor(x => x.PostId).NotEmpty().WithMessage("PostId is required.").MustBeParsableGuid(); RuleFor(x => x.Text).NotEmpty().WithMessage("Text is required."); } } diff --git a/Dentizone.Application/Validators/GUIDValidator.cs b/Dentizone.Application/Validators/GUIDValidator.cs new file mode 100644 index 0000000..9b4fdbd --- /dev/null +++ b/Dentizone.Application/Validators/GUIDValidator.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using FluentValidation; + +namespace Dentizone.Application.Validators +{ + public static class GUIDValidator + { + public static IRuleBuilderOptions MustBeParsableGuid(this IRuleBuilder ruleBuilder) + { + return ruleBuilder + .Must(value => Guid.TryParse(value, out var g) && g != Guid.Empty) + .WithMessage("Invalid or empty GUID."); + } + } +} \ No newline at end of file diff --git a/Dentizone.Domain/Interfaces/Repositories/IBaseRepo.cs b/Dentizone.Domain/Interfaces/Repositories/IBaseRepo.cs index e2637d4..49ac43d 100644 --- a/Dentizone.Domain/Interfaces/Repositories/IBaseRepo.cs +++ b/Dentizone.Domain/Interfaces/Repositories/IBaseRepo.cs @@ -8,6 +8,6 @@ public interface IBaseRepo where TEntity : class Task CreateAsync(TEntity entity); Task FindBy(Expression> condition, - Expression>[]? includes = null); + Expression>[]? includes = null); } } \ No newline at end of file diff --git a/Dentizone.Infrastructure/Repositories/AnswerRepository.cs b/Dentizone.Infrastructure/Repositories/AnswerRepository.cs index 5b9920f..a06ba9b 100644 --- a/Dentizone.Infrastructure/Repositories/AnswerRepository.cs +++ b/Dentizone.Infrastructure/Repositories/AnswerRepository.cs @@ -16,13 +16,13 @@ public async Task CreateAsync(Answer entity) public async Task FindBy(Expression> condition, - Expression>[]? includes) + Expression>[]? includes) { var query = dbContext.Answers.AsQueryable(); if (includes == null) return await dbContext.Answers - .FirstOrDefaultAsync(condition); + .FirstOrDefaultAsync(condition); foreach (var include in includes) diff --git a/Dentizone.Infrastructure/Repositories/AssetRepository.cs b/Dentizone.Infrastructure/Repositories/AssetRepository.cs index 7b75000..eeaece8 100644 --- a/Dentizone.Infrastructure/Repositories/AssetRepository.cs +++ b/Dentizone.Infrastructure/Repositories/AssetRepository.cs @@ -15,7 +15,7 @@ public async Task CreateAsync(Asset entity) } public async Task FindBy(Expression> condition, - Expression>[]? includes = null) + Expression>[]? includes = null) { var query = dbContext.Assets.AsQueryable(); if (includes == null) @@ -49,7 +49,7 @@ public async Task DeleteByIdAsync(string assetId) public async Task GetByIdAsync(string id) { return await dbContext.Assets - .FirstOrDefaultAsync(a => a.Id == id && !a.IsDeleted); + .FirstOrDefaultAsync(a => a.Id == id && !a.IsDeleted); } public async Task UpdateAsync(Asset entity) diff --git a/Dentizone.Infrastructure/Repositories/CartRepository.cs b/Dentizone.Infrastructure/Repositories/CartRepository.cs index ba9a44d..3239d9e 100644 --- a/Dentizone.Infrastructure/Repositories/CartRepository.cs +++ b/Dentizone.Infrastructure/Repositories/CartRepository.cs @@ -10,7 +10,7 @@ internal class CartRepository(AppDbContext dbContext) : AbstractRepository(dbCon public async Task GetByIdAsync(string id) { return await dbContext.Carts - .FirstOrDefaultAsync(c => c.Id == id && !c.IsDeleted); + .FirstOrDefaultAsync(c => c.Id == id && !c.IsDeleted); } @@ -22,7 +22,7 @@ public async Task CreateAsync(Cart entity) } public async Task FindBy(Expression> condition, - Expression>[]? includes) + Expression>[]? includes) { IQueryable query = dbContext.Carts; if (includes == null) return await query.FirstOrDefaultAsync(condition); @@ -48,7 +48,7 @@ public async Task CreateAsync(Cart entity) } public async Task> FindAllBy(Expression> condition, - Expression>[]? includes = null) + Expression>[]? includes = null) { IQueryable query = dbContext.Carts; @@ -66,11 +66,11 @@ public async Task> FindAllBy(Expression> cond public async Task> GetCartItemsByUserId(string userId) { var baseQuery = dbContext.Carts - .Where(c => c.UserId == userId && !c.IsDeleted) - .Include(c => c.User) - .Include(c => c.Post) - .ThenInclude(p => p.PostAssets) - .ThenInclude(pa => pa.Asset); + .Where(c => c.UserId == userId && !c.IsDeleted) + .Include(c => c.User) + .Include(c => c.Post) + .ThenInclude(p => p.PostAssets) + .ThenInclude(pa => pa.Asset); return await baseQuery.ToListAsync(); } diff --git a/Dentizone.Infrastructure/Repositories/CategoryRepository.cs b/Dentizone.Infrastructure/Repositories/CategoryRepository.cs index bd66255..c663e91 100644 --- a/Dentizone.Infrastructure/Repositories/CategoryRepository.cs +++ b/Dentizone.Infrastructure/Repositories/CategoryRepository.cs @@ -17,7 +17,7 @@ public async Task CreateAsync(Category entity) } public async Task FindBy(Expression> condition - , Expression>[]? includes) + , Expression>[]? includes) { IQueryable query = DbContext.Categories; if (includes == null) return await query.FirstOrDefaultAsync(condition); @@ -33,7 +33,7 @@ public async Task CreateAsync(Category entity) public async Task GetByIdAsync(string id) { var category = await DbContext.Categories.Where(c => c.Id == id && !c.IsDeleted) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(); return category; } @@ -61,8 +61,8 @@ public async Task CreateAsync(Category entity) public async Task> GetAll() { return await DbContext.Categories - .Where(c => !c.IsDeleted) - .ToListAsync(); + .Where(c => !c.IsDeleted) + .ToListAsync(); } } } \ No newline at end of file diff --git a/Dentizone.Infrastructure/Repositories/FavouriteRepository.cs b/Dentizone.Infrastructure/Repositories/FavouriteRepository.cs index f1339e7..69dcc7f 100644 --- a/Dentizone.Infrastructure/Repositories/FavouriteRepository.cs +++ b/Dentizone.Infrastructure/Repositories/FavouriteRepository.cs @@ -10,7 +10,7 @@ public class FavouriteRepository(AppDbContext dbContext) : AbstractRepository(db public async Task GetByIdAsync(string id) { return await dbContext.Favourites - .FirstOrDefaultAsync(f => f.Id == id && !f.IsDeleted); + .FirstOrDefaultAsync(f => f.Id == id && !f.IsDeleted); } @@ -22,7 +22,7 @@ public async Task CreateAsync(Favourite entity) } public async Task FindBy(Expression> condition, - Expression>[]? incldues) + Expression>[]? incldues) { IQueryable query = dbContext.Favourites; if (incldues != null) @@ -54,10 +54,10 @@ public async Task> FindAllByAsync(Expression query = dbContext.Favourites; query = query.Include(f => f.Post) - .ThenInclude(pa => pa.PostAssets) - .ThenInclude(pa => pa.Asset) - .AsNoTracking() - .AsSplitQuery(); + .ThenInclude(pa => pa.PostAssets) + .ThenInclude(pa => pa.Asset) + .AsNoTracking() + .AsSplitQuery(); query = query.Include(f => f.Post.Seller); return await query.Where(condition).ToListAsync(); diff --git a/Dentizone.Infrastructure/Repositories/OrderItemRepository.cs b/Dentizone.Infrastructure/Repositories/OrderItemRepository.cs index dceb5d9..58de54b 100644 --- a/Dentizone.Infrastructure/Repositories/OrderItemRepository.cs +++ b/Dentizone.Infrastructure/Repositories/OrderItemRepository.cs @@ -11,7 +11,7 @@ public class OrderItemRepository(AppDbContext dbContext) : AbstractRepository(db { return await dbContext.OrderItems - .FirstOrDefaultAsync(o => o.Id == id); + .FirstOrDefaultAsync(o => o.Id == id); } @@ -23,7 +23,7 @@ public async Task CreateAsync(OrderItem entity) } public async Task FindBy(Expression> condition, - Expression>[]? includes) + Expression>[]? includes) { IQueryable query = dbContext.OrderItems; if (includes != null) diff --git a/Dentizone.Infrastructure/Repositories/OrderPickupRepository.cs b/Dentizone.Infrastructure/Repositories/OrderPickupRepository.cs index f876c65..3e92919 100644 --- a/Dentizone.Infrastructure/Repositories/OrderPickupRepository.cs +++ b/Dentizone.Infrastructure/Repositories/OrderPickupRepository.cs @@ -15,7 +15,7 @@ public async Task CreateAsync(OrderPickup entity) } public async Task FindBy(Expression> condition, - Expression>[]? includes) + Expression>[]? includes) { IQueryable query = dbContext.OrderPickups; if (includes != null) @@ -55,6 +55,6 @@ public async Task UpdateAsync(OrderPickup entity) public async Task GetByIdAsync(string id) { return await dbContext.OrderPickups - .FirstOrDefaultAsync(o => o.Id == id && !o.IsDeleted); + .FirstOrDefaultAsync(o => o.Id == id && !o.IsDeleted); } } \ No newline at end of file diff --git a/Dentizone.Infrastructure/Repositories/OrderStatusRepository.cs b/Dentizone.Infrastructure/Repositories/OrderStatusRepository.cs index b042d2c..7540ae4 100644 --- a/Dentizone.Infrastructure/Repositories/OrderStatusRepository.cs +++ b/Dentizone.Infrastructure/Repositories/OrderStatusRepository.cs @@ -10,7 +10,7 @@ public class OrderStatusRepository(AppDbContext dbContext) : AbstractRepository( public async Task GetByIdAsync(string id) { return await dbContext.OrderStatuses - .FirstOrDefaultAsync(o => o.Id == id); + .FirstOrDefaultAsync(o => o.Id == id); } @@ -22,7 +22,7 @@ public async Task CreateAsync(OrderStatus entity) } public async Task FindBy(Expression> condition, - Expression>[]? includes) + Expression>[]? includes) { IQueryable query = dbContext.OrderStatuses; if (includes != null) @@ -40,7 +40,7 @@ public async Task CreateAsync(OrderStatus entity) public async Task> Find(Expression> filter) { return await dbContext.OrderStatuses - .Where(filter) - .ToListAsync(); + .Where(filter) + .ToListAsync(); } } \ No newline at end of file diff --git a/Dentizone.Infrastructure/Repositories/PaymentRepository.cs b/Dentizone.Infrastructure/Repositories/PaymentRepository.cs index 6ff4f64..37dd762 100644 --- a/Dentizone.Infrastructure/Repositories/PaymentRepository.cs +++ b/Dentizone.Infrastructure/Repositories/PaymentRepository.cs @@ -15,7 +15,7 @@ public async Task CreateAsync(Payment entity) } public async Task FindBy(Expression> condition, - Expression>[]? includes) + Expression>[]? includes) { IQueryable query = dbContext.Payments; if (includes != null) diff --git a/Dentizone.Infrastructure/Repositories/PostAssetRepository.cs b/Dentizone.Infrastructure/Repositories/PostAssetRepository.cs index 965cac5..9c0a37d 100644 --- a/Dentizone.Infrastructure/Repositories/PostAssetRepository.cs +++ b/Dentizone.Infrastructure/Repositories/PostAssetRepository.cs @@ -15,7 +15,7 @@ public async Task CreateAsync(PostAsset entity) } public async Task FindBy(Expression> condition, - Expression>[]? includes) + Expression>[]? includes) { IQueryable query = dbContext.PostAssets; if (includes == null) return await query.FirstOrDefaultAsync(condition); diff --git a/Dentizone.Infrastructure/Repositories/PostRepsitory.cs b/Dentizone.Infrastructure/Repositories/PostRepsitory.cs index 05498f8..84ca4bb 100644 --- a/Dentizone.Infrastructure/Repositories/PostRepsitory.cs +++ b/Dentizone.Infrastructure/Repositories/PostRepsitory.cs @@ -16,7 +16,7 @@ public async Task CreateAsync(Post entity) } public async Task FindBy(Expression> condition, - Expression>[]? includes) + Expression>[]? includes) { IQueryable query = dbContext.Posts; if (includes != null) @@ -49,14 +49,14 @@ public async Task> GetAllAsync(int page) { int skippedPages = CalculatePagination(page); return await dbContext.Posts - .Skip(skippedPages) - .Take(DefaultPageSize) - .ToListAsync(); + .Skip(skippedPages) + .Take(DefaultPageSize) + .ToListAsync(); } public async Task> GetAllAsync(int page, Expression>? filter, - Expression>? orderBy, - Expression>[]? includes = null) + Expression>? orderBy, + Expression>[]? includes = null) { IQueryable query = dbContext.Posts; if (filter != null) @@ -82,8 +82,8 @@ public async Task> GetAllAsync(int page, Expression GetAllAsync(Expression>? filter, - Expression>? orderBy = null, - Expression>[]? includes = null) + Expression>? orderBy = null, + Expression>[]? includes = null) { IQueryable query = dbContext.Posts; if (filter != null) @@ -110,13 +110,13 @@ public IQueryable GetAllAsync(Expression>? filter, public async Task GetByIdAsync(string id) { return await dbContext.Posts - .Include(p => p.Seller) - .Include(p => p.Category) - .Include(p => p.SubCategory) - .Include(p => p.PostAssets) - .ThenInclude(p => p.Asset) - .ThenInclude(p => p.User.University) - .FirstOrDefaultAsync(p => p.Id == id); + .Include(p => p.Seller) + .Include(p => p.Category) + .Include(p => p.SubCategory) + .Include(p => p.PostAssets) + .ThenInclude(p => p.Asset) + .ThenInclude(p => p.User.University) + .FirstOrDefaultAsync(p => p.Id == id); } public async Task UpdateAsync(Post entity) @@ -140,20 +140,20 @@ public async Task UpdatePostStatus(string postId, PostStatus status) } public async Task> SearchAsync(string? keyword, string? city, string? category, - string? subcategory, PostItemCondition? condition, - decimal? minPrice, decimal? maxPrice, string? sortBy, - bool sortDirection, int page) + string? subcategory, PostItemCondition? condition, + decimal? minPrice, decimal? maxPrice, string? sortBy, + bool sortDirection, int page) { var posts = dbContext.Posts - .Where(p => !p.IsDeleted && p.Status == PostStatus.Active) - .AsQueryable(); + .Where(p => !p.IsDeleted && p.Status == PostStatus.Active) + .AsQueryable(); if (!string.IsNullOrWhiteSpace(keyword)) { var kw = keyword.Trim().ToLower(); posts = posts.Where(p => - p.Title.ToLower().Contains(kw) || - p.Description.ToLower().Contains(kw)); + p.Title.ToLower().Contains(kw) || + p.Description.ToLower().Contains(kw)); } if (!string.IsNullOrWhiteSpace(city)) @@ -209,12 +209,12 @@ public async Task AveragePostsPriceAsync() public async Task> GetPostCountPerCategoryAsync() { var result = await dbContext.Posts - .AsNoTracking() - .Include(p => p.Category) - .Where(p => !p.IsDeleted && !p.Category.IsDeleted) - .GroupBy(p => p.Category.Name) - .AsSplitQuery() - .ToDictionaryAsync(g => g.Key, g => g.Count()); + .AsNoTracking() + .Include(p => p.Category) + .Where(p => !p.IsDeleted && !p.Category.IsDeleted) + .GroupBy(p => p.Category.Name) + .AsSplitQuery() + .ToDictionaryAsync(g => g.Key, g => g.Count()); return result; } @@ -222,10 +222,10 @@ public async Task> GetPostCountPerCategoryAsync() public async Task> ValidatePostsByState(List postIds, PostStatus state) { var posts = await dbContext.Posts - .Where(p => postIds.Contains(p.Id) && p.Status == state) - .Include(p => p.Seller) - .ThenInclude(s => s.Wallet) - .ToListAsync(); + .Where(p => postIds.Contains(p.Id) && p.Status == state) + .Include(p => p.Seller) + .ThenInclude(s => s.Wallet) + .ToListAsync(); return posts; } diff --git a/Dentizone.Infrastructure/Repositories/SaleTransactionRepository.cs b/Dentizone.Infrastructure/Repositories/SaleTransactionRepository.cs index 1d3620b..e1b10a4 100644 --- a/Dentizone.Infrastructure/Repositories/SaleTransactionRepository.cs +++ b/Dentizone.Infrastructure/Repositories/SaleTransactionRepository.cs @@ -11,7 +11,7 @@ public class SaleTransactionRepository(AppDbContext dbContext) public async Task GetByIdAsync(string id) { return await dbContext.SalesTransactions - .FirstOrDefaultAsync(st => st.Id == id); + .FirstOrDefaultAsync(st => st.Id == id); } @@ -23,7 +23,7 @@ public async Task CreateAsync(SalesTransaction entity) } public async Task FindBy(Expression> condition, - Expression>[]? includes) + Expression>[]? includes) { IQueryable query = dbContext.SalesTransactions; if (includes != null) diff --git a/Dentizone.Infrastructure/Repositories/ShipInfoRepository.cs b/Dentizone.Infrastructure/Repositories/ShipInfoRepository.cs index df607a3..dd5abae 100644 --- a/Dentizone.Infrastructure/Repositories/ShipInfoRepository.cs +++ b/Dentizone.Infrastructure/Repositories/ShipInfoRepository.cs @@ -10,7 +10,7 @@ internal class ShipInfoRepository(AppDbContext dbContext) : AbstractRepository(d public async Task GetByIdAsync(string id) { return await dbContext.ShipInfos - .FirstOrDefaultAsync(s => s.Id == id); + .FirstOrDefaultAsync(s => s.Id == id); } @@ -22,7 +22,7 @@ public async Task CreateAsync(ShipInfo entity) } public async Task FindBy(Expression> condition, - Expression>[]? includes) + Expression>[]? includes) { IQueryable query = dbContext.ShipInfos; if (includes != null) diff --git a/Dentizone.Infrastructure/Repositories/ShipmentActivityRepository.cs b/Dentizone.Infrastructure/Repositories/ShipmentActivityRepository.cs index 26d80ea..5221095 100644 --- a/Dentizone.Infrastructure/Repositories/ShipmentActivityRepository.cs +++ b/Dentizone.Infrastructure/Repositories/ShipmentActivityRepository.cs @@ -12,7 +12,7 @@ internal class ShipmentActivityRepository(AppDbContext dbContext) { return await dbContext.ShipmentActivities - .FirstOrDefaultAsync(s => s.Id == id); + .FirstOrDefaultAsync(s => s.Id == id); } @@ -24,7 +24,7 @@ public async Task CreateAsync(ShipmentActivity entity) } public async Task FindBy(Expression> condition, - Expression>[]? includes) + Expression>[]? includes) { IQueryable query = dbContext.ShipmentActivities; if (includes != null) diff --git a/Dentizone.Infrastructure/Repositories/SubCategoryRepository.cs b/Dentizone.Infrastructure/Repositories/SubCategoryRepository.cs index 82a19b6..d7d6669 100644 --- a/Dentizone.Infrastructure/Repositories/SubCategoryRepository.cs +++ b/Dentizone.Infrastructure/Repositories/SubCategoryRepository.cs @@ -11,7 +11,7 @@ internal class SubCategoryRepository(AppDbContext dbContext) : AbstractRepositor public async Task GetByIdAsync(string id) { return await dbContext.SubCategories - .FirstOrDefaultAsync(sc => sc.Id == id); + .FirstOrDefaultAsync(sc => sc.Id == id); } @@ -23,7 +23,7 @@ public async Task CreateAsync(SubCategory entity) } public async Task FindBy(Expression> condition, - Expression>[]? includes) + Expression>[]? includes) { IQueryable query = dbContext.SubCategories; if (includes != null) @@ -55,8 +55,8 @@ public async Task CreateAsync(SubCategory entity) public async Task> GetAll() { return await dbContext.SubCategories - .Include(sc => sc.Category) - .ToListAsync(); + .Include(sc => sc.Category) + .ToListAsync(); } public async Task Update(SubCategory entity) diff --git a/Dentizone.Infrastructure/Repositories/UniversityRepository.cs b/Dentizone.Infrastructure/Repositories/UniversityRepository.cs index 5a41811..91f275e 100644 --- a/Dentizone.Infrastructure/Repositories/UniversityRepository.cs +++ b/Dentizone.Infrastructure/Repositories/UniversityRepository.cs @@ -22,7 +22,7 @@ public async Task CreateAsync(University entity) } public async Task FindBy(Expression> condition, - Expression>[]? includes) + Expression>[]? includes) { IQueryable query = dbContext.Universities.Where(u => !u.IsDeleted); if (includes != null) @@ -62,17 +62,17 @@ public async Task Update(University entity) public async Task> GetAll() { return await dbContext.Universities - .Where(u => !u.IsDeleted) - .OrderByDescending(u => u.CreatedAt) - .AsNoTracking() - .ToListAsync(); + .Where(u => !u.IsDeleted) + .OrderByDescending(u => u.CreatedAt) + .AsNoTracking() + .ToListAsync(); } public async Task> GetAll(int page, Expression>? filter) { var query = dbContext.Universities.AsQueryable(); query = query.Where(u => !u.IsDeleted) - .OrderByDescending(u => u.CreatedAt); + .OrderByDescending(u => u.CreatedAt); var totalCount = await query.CountAsync(); @@ -83,9 +83,9 @@ public async Task> GetAll(int page, Expression { Items = items, diff --git a/Dentizone.Infrastructure/Repositories/UserActivityRepository.cs b/Dentizone.Infrastructure/Repositories/UserActivityRepository.cs index 2a02009..4dd2206 100644 --- a/Dentizone.Infrastructure/Repositories/UserActivityRepository.cs +++ b/Dentizone.Infrastructure/Repositories/UserActivityRepository.cs @@ -10,7 +10,7 @@ public class UserActivityRepository(AppDbContext dbContext) : AbstractRepository public async Task GetByIdAsync(string id) { return await dbContext.UserActivities - .FirstOrDefaultAsync(u => u.Id == id); + .FirstOrDefaultAsync(u => u.Id == id); } @@ -22,7 +22,7 @@ public async Task CreateAsync(UserActivity entity) } public async Task FindBy(Expression> condition, - Expression>[]? includes) + Expression>[]? includes) { IQueryable query = dbContext.UserActivities; if (includes != null) @@ -45,10 +45,10 @@ public async Task> GetAllBy(int page, Expression u.CreatedAt) - .Skip(CalculatePagination(page)) - .Take(DefaultPageSize) - .ToListAsync(); + .OrderByDescending(u => u.CreatedAt) + .Skip(CalculatePagination(page)) + .Take(DefaultPageSize) + .ToListAsync(); } } } \ No newline at end of file diff --git a/Dentizone.Infrastructure/Repositories/UserAssetRepository.cs b/Dentizone.Infrastructure/Repositories/UserAssetRepository.cs index 2ab88b8..4f60058 100644 --- a/Dentizone.Infrastructure/Repositories/UserAssetRepository.cs +++ b/Dentizone.Infrastructure/Repositories/UserAssetRepository.cs @@ -10,7 +10,7 @@ internal class UserAssetRepository(AppDbContext dbContext) : AbstractRepository( public async Task GetByIdAsync(string id) { return await dbContext.UserAssets - .FirstOrDefaultAsync(u => u.Id == id && !u.IsDeleted); + .FirstOrDefaultAsync(u => u.Id == id && !u.IsDeleted); } @@ -22,7 +22,7 @@ public async Task CreateAsync(UserAsset entity) } public async Task FindBy(Expression> condition, - Expression>[]? includes) + Expression>[]? includes) { IQueryable query = dbContext.UserAssets.Where(u => !u.IsDeleted); if (includes != null) @@ -58,10 +58,10 @@ public async Task> GetAllByAsync(int page, Expression u.CreatedAt) - .Skip(CalculatePagination(page)) - .Take(DefaultPageSize) - .ToListAsync(); + .OrderByDescending(u => u.CreatedAt) + .Skip(CalculatePagination(page)) + .Take(DefaultPageSize) + .ToListAsync(); } } } \ No newline at end of file diff --git a/Dentizone.Infrastructure/Repositories/UserRepository.cs b/Dentizone.Infrastructure/Repositories/UserRepository.cs index 7623d2c..90cf087 100644 --- a/Dentizone.Infrastructure/Repositories/UserRepository.cs +++ b/Dentizone.Infrastructure/Repositories/UserRepository.cs @@ -11,15 +11,15 @@ internal class UserRepository(AppDbContext dbContext) : AbstractRepository(dbCon { return await dbContext.AppUsers - .FirstOrDefaultAsync(u => u.Id == id && !u.IsDeleted); + .FirstOrDefaultAsync(u => u.Id == id && !u.IsDeleted); } public async Task> GetAllAsync(int page = 1, - Expression>? filter = null) + Expression>? filter = null) { var query = dbContext.AppUsers - .Skip(CalculatePagination(page)) - .Take(DefaultPageSize); + .Skip(CalculatePagination(page)) + .Take(DefaultPageSize); if (filter != null) { @@ -37,7 +37,7 @@ public async Task CreateAsync(AppUser entity) } public async Task FindBy(Expression> condition, - Expression>[]? includes) + Expression>[]? includes) { IQueryable query = dbContext.AppUsers.Where(u => !u.IsDeleted); if (includes != null) @@ -80,23 +80,23 @@ public async Task GetCountOfUsersAsync() public async Task GetCount7DaysAsync() { var count = await dbContext.AppUsers.Where(u => !u.IsDeleted && u.CreatedAt >= DateTime.UtcNow.AddDays(-7)) - .CountAsync(); + .CountAsync(); return count; } public async Task GetCount30DaysAsync() { var count = await dbContext.AppUsers.Where(u => !u.IsDeleted && u.CreatedAt >= DateTime.UtcNow.AddDays(-30)) - .CountAsync(); + .CountAsync(); return count; } public async Task> GetStudentCountPerUniversityAsync() { var result = await dbContext.AppUsers - .Where(a => !a.IsDeleted && a.University != null) - .GroupBy(a => a.University.Name) - .ToDictionaryAsync(g => g.Key, g => g.Count()); + .Where(a => !a.IsDeleted && a.University != null) + .GroupBy(a => a.University.Name) + .ToDictionaryAsync(g => g.Key, g => g.Count()); return result; } diff --git a/Dentizone.Infrastructure/Repositories/WithdrawalRequestRepository.cs b/Dentizone.Infrastructure/Repositories/WithdrawalRequestRepository.cs index c508942..d030424 100644 --- a/Dentizone.Infrastructure/Repositories/WithdrawalRequestRepository.cs +++ b/Dentizone.Infrastructure/Repositories/WithdrawalRequestRepository.cs @@ -18,7 +18,7 @@ public async Task CreateAsync(WithdrawalRequest entity) } public async Task FindBy(Expression> condition, - Expression>[]? includes) + Expression>[]? includes) { IQueryable query = DbContext.WithdrawalRequests; if (includes != null) @@ -42,10 +42,10 @@ public async Task> GetAllAsync( } return await query - .OrderByDescending(u => u.CreatedAt) - .Skip(CalculatePagination(page)) - .Take(DefaultPageSize) - .ToListAsync(); + .OrderByDescending(u => u.CreatedAt) + .Skip(CalculatePagination(page)) + .Take(DefaultPageSize) + .ToListAsync(); } public async Task DeleteAsync(string id) @@ -62,9 +62,9 @@ public async Task> GetAllAsync( public async Task GetByIdAsync(string id) { var request = await DbContext.WithdrawalRequests.Where(w => w.Id == id) - .Include(w => w.Wallet) - .Include(w => w.Wallet.User) - .FirstOrDefaultAsync(); + .Include(w => w.Wallet) + .Include(w => w.Wallet.User) + .FirstOrDefaultAsync(); return request; }