From aa1ee9526380c7eae85a6d927f73cf9e78ecf7f8 Mon Sep 17 00:00:00 2001 From: Nouran Date: Thu, 3 Jul 2025 19:05:38 +0300 Subject: [PATCH 1/3] Add post approval and rejection endpoints with email notification Introduced ApprovePost and RejectPost endpoints in PostsController for admins to update post status. PostService now sends an email notification to the seller when a post is approved. --- Dentizone.Application/Services/PostService.cs | 12 +++++++++++- .../Controllers/PostsController.cs | 17 +++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/Dentizone.Application/Services/PostService.cs b/Dentizone.Application/Services/PostService.cs index b3c29dd..7834658 100644 --- a/Dentizone.Application/Services/PostService.cs +++ b/Dentizone.Application/Services/PostService.cs @@ -1,4 +1,5 @@ using AutoMapper; +using Azure.Core; using Dentizone.Application.DTOs.Post; using Dentizone.Application.DTOs.Post.PostFilterDto; using Dentizone.Application.Interfaces; @@ -6,6 +7,7 @@ using Dentizone.Domain.Enums; using Dentizone.Domain.Exceptions; using Dentizone.Domain.Interfaces; +using Dentizone.Domain.Interfaces.Mail; using Dentizone.Domain.Interfaces.Repositories; using Dentizone.Infrastructure; using Dentizone.Infrastructure.Cache; @@ -24,7 +26,8 @@ public class PostService( ISubCategoryRepository subCategoryRepository, IAssetService assetService, AppDbContext dbContext, - IRedisService redisService) + IRedisService redisService, + IMailService mailService) : BaseService(accessor), IPostService { public async Task> ValidatePosts(List postIds) @@ -212,6 +215,13 @@ public async Task UpdatePostStatus(string postId, PostStatus status post.Status = status; var updatedPost = await repo.UpdateAsync(post); + + var email = post.Seller.Email; + var subject = "Post Approved"; + var postTitle = post.Title; + var body = $"Your Post '{postTitle}' has been approved."; + await mailService.Send(email, subject, body); + return mapper.Map(updatedPost); } diff --git a/Dentizone.Presentaion/Controllers/PostsController.cs b/Dentizone.Presentaion/Controllers/PostsController.cs index 0d7edaf..96ba544 100644 --- a/Dentizone.Presentaion/Controllers/PostsController.cs +++ b/Dentizone.Presentaion/Controllers/PostsController.cs @@ -4,6 +4,7 @@ using Dentizone.Application.DTOs.Post; using Dentizone.Application.DTOs.Post.PostFilterDto; using Dentizone.Application.Interfaces; +using Dentizone.Domain.Enums; namespace Dentizone.Presentaion.Controllers { @@ -76,5 +77,21 @@ public async Task Search([FromQuery] UserPreferenceDto userPrefer var searchResult = await postService.Search(userPreferenceDto); return Ok(searchResult); } + + [Authorize("IsAdmin")] + [HttpPatch("posts/{postId}/approve")] + public async Task ApprovePost(string postId) + { + var updatedPost = await postService.UpdatePostStatus(postId, PostStatus.Active); + return Ok(updatedPost); + } + + [Authorize("IsAdmin")] + [HttpPatch("posts/{postId}/reject")] + public async Task RejectPost(string postId) + { + var updatedPost = await postService.UpdatePostStatus(postId, PostStatus.Rejected); + return Ok(updatedPost); + } } } \ No newline at end of file From 5d30c1d0712e1dbcb98e8bba68b016b72cd823f0 Mon Sep 17 00:00:00 2001 From: Mahmoud Nasr Date: Thu, 3 Jul 2025 20:16:00 +0300 Subject: [PATCH 2/3] Refactor post status update functionality - Updated `IPostService` to include a nullable `reason` parameter in `UpdatePostStatus`. - Modified `PostService` to implement the new parameter and added `NotifySellerAsync` for email notifications. - Replaced `ApprovePost` and `RejectPost` methods in `PostsController` with a unified `AdjustStatus` method using `UpdatePostStateDto`. - Introduced `UpdatePostStateDto` to encapsulate post status update parameters. - Cleaned up unused `using` directives in `PostService` and `PostsController`. --- .../DTOs/Post/UpdatePostStateDto.cs | 10 +++++ .../Interfaces/IPostService.cs | 2 +- Dentizone.Application/Services/PostService.cs | 41 +++++++++++++++---- .../Controllers/PostsController.cs | 23 ++++------- 4 files changed, 51 insertions(+), 25 deletions(-) create mode 100644 Dentizone.Application/DTOs/Post/UpdatePostStateDto.cs diff --git a/Dentizone.Application/DTOs/Post/UpdatePostStateDto.cs b/Dentizone.Application/DTOs/Post/UpdatePostStateDto.cs new file mode 100644 index 0000000..720da17 --- /dev/null +++ b/Dentizone.Application/DTOs/Post/UpdatePostStateDto.cs @@ -0,0 +1,10 @@ +using Dentizone.Domain.Enums; + +namespace Dentizone.Application.DTOs.Post; + +public class UpdatePostStateDto +{ + public required string PostId { get; set; } + public required PostStatus Status { get; set; } + public string? Reason { get; set; } +} \ No newline at end of file diff --git a/Dentizone.Application/Interfaces/IPostService.cs b/Dentizone.Application/Interfaces/IPostService.cs index ac93442..b433e3b 100644 --- a/Dentizone.Application/Interfaces/IPostService.cs +++ b/Dentizone.Application/Interfaces/IPostService.cs @@ -16,6 +16,6 @@ public interface IPostService Task> Search(UserPreferenceDto userPreferenceDto); Task> ValidatePosts(List postIds); - Task UpdatePostStatus(string postId, PostStatus status); + Task UpdatePostStatus(string postId, PostStatus status, string? reason); } } \ No newline at end of file diff --git a/Dentizone.Application/Services/PostService.cs b/Dentizone.Application/Services/PostService.cs index 7834658..0a88ea3 100644 --- a/Dentizone.Application/Services/PostService.cs +++ b/Dentizone.Application/Services/PostService.cs @@ -1,5 +1,4 @@ using AutoMapper; -using Azure.Core; using Dentizone.Application.DTOs.Post; using Dentizone.Application.DTOs.Post.PostFilterDto; using Dentizone.Application.Interfaces; @@ -205,22 +204,27 @@ public async Task UpdatePost(string postId, UpdatePostDto updatePos return mapper.Map(updatedPost); } - public async Task UpdatePostStatus(string postId, PostStatus status) + public async Task UpdatePostStatus(string postId, PostStatus status, string? reason) { - var post = await repo.GetByIdAsync(postId); + var post = await repo.GetAllAsync(p => p.Id == postId && !p.IsDeleted, includes: [p => p.Seller]) + .FirstOrDefaultAsync(); if (post == null) { throw new NotFoundException("Post not found"); } + post.Status = status; var updatedPost = await repo.UpdateAsync(post); - var email = post.Seller.Email; - var subject = "Post Approved"; - var postTitle = post.Title; - var body = $"Your Post '{postTitle}' has been approved."; - await mailService.Send(email, subject, body); + if (updatedPost == null) + { + throw new NotFoundException("Post not found"); + } + + // Notify the seller about the status change + await NotifySellerAsync(post, status, reason); + return mapper.Map(updatedPost); } @@ -347,5 +351,26 @@ protected override async Task GetOwnerIdAsync(string resourceId) return post.SellerId; } + + private async Task NotifySellerAsync(Post post, PostStatus status, string? reason) + { + var email = post.Seller.Email; + var postTitle = post.Title; + + switch (status) + { + case PostStatus.Active: + await mailService.Send(email, "Post Approved", $"Your post '{postTitle}' has been approved"); + break; + + case PostStatus.Rejected when !string.IsNullOrEmpty(reason): + await mailService.Send(email, "Post Rejected", + $"Your post '{postTitle}' has been rejected. Reason: {reason}"); + break; + + default: + break; + } + } } } \ No newline at end of file diff --git a/Dentizone.Presentaion/Controllers/PostsController.cs b/Dentizone.Presentaion/Controllers/PostsController.cs index 96ba544..97ccf65 100644 --- a/Dentizone.Presentaion/Controllers/PostsController.cs +++ b/Dentizone.Presentaion/Controllers/PostsController.cs @@ -1,10 +1,9 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using System.Security.Claims; -using Dentizone.Application.DTOs.Post; +using Dentizone.Application.DTOs.Post; using Dentizone.Application.DTOs.Post.PostFilterDto; using Dentizone.Application.Interfaces; -using Dentizone.Domain.Enums; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.Security.Claims; namespace Dentizone.Presentaion.Controllers { @@ -79,18 +78,10 @@ public async Task Search([FromQuery] UserPreferenceDto userPrefer } [Authorize("IsAdmin")] - [HttpPatch("posts/{postId}/approve")] - public async Task ApprovePost(string postId) - { - var updatedPost = await postService.UpdatePostStatus(postId, PostStatus.Active); - return Ok(updatedPost); - } - - [Authorize("IsAdmin")] - [HttpPatch("posts/{postId}/reject")] - public async Task RejectPost(string postId) + [HttpPatch("{postId}/status")] + public async Task AdjustStatus([FromBody] UpdatePostStateDto state) { - var updatedPost = await postService.UpdatePostStatus(postId, PostStatus.Rejected); + var updatedPost = await postService.UpdatePostStatus(state.PostId, state.Status, state.Reason); return Ok(updatedPost); } } From e394581f4b0f09a50d083f97f577dbe471df323e Mon Sep 17 00:00:00 2001 From: Mahmoud Nasr Date: Thu, 3 Jul 2025 20:20:28 +0300 Subject: [PATCH 3/3] Enhance OrderService with payment handling and readability - Added using directive for Dentizone.Application.DTOs.Payment. - Updated CancelOrderAsync to include reason for post status change. - Modified email notifications for order cancellation and new orders for consistency. - Improved code readability by removing unnecessary line breaks and reformatting method calls. --- .../Services/OrderService.cs | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/Dentizone.Application/Services/OrderService.cs b/Dentizone.Application/Services/OrderService.cs index 236be24..ba1e9a3 100644 --- a/Dentizone.Application/Services/OrderService.cs +++ b/Dentizone.Application/Services/OrderService.cs @@ -1,6 +1,7 @@ using AutoMapper; using Dentizone.Application.DTOs; using Dentizone.Application.DTOs.Order; +using Dentizone.Application.DTOs.Payment; using Dentizone.Application.Interfaces; using Dentizone.Domain.Entity; using Dentizone.Domain.Enums; @@ -8,9 +9,8 @@ using Dentizone.Domain.Interfaces.Mail; using Dentizone.Domain.Interfaces.Repositories; using Dentizone.Infrastructure; -using System.Linq.Expressions; -using Dentizone.Application.DTOs.Payment; using Microsoft.AspNetCore.Http; +using System.Linq.Expressions; namespace Dentizone.Application.Services { @@ -32,7 +32,7 @@ internal class OrderService( public async Task CancelOrderAsync(string orderId) { var order = await orderRepository.FindBy(o => o.Id == orderId, - [o => o.OrderStatuses, o => o.OrderItems]); + [o => o.OrderStatuses, o => o.OrderItems]); if (order == null) { @@ -72,10 +72,10 @@ internal class OrderService( var post = await postService.GetPostById(orderItem.PostId); if (post is not null) { - await postService.UpdatePostStatus(post.Id, PostStatus.Active); + await postService.UpdatePostStatus(post.Id, PostStatus.Active, "Order Cancelled"); 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@!"); } } @@ -95,7 +95,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}"); } } @@ -150,7 +150,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 @@ -167,7 +167,7 @@ await paymentService.CreateSaleTransaction( // Mark the post as Sold foreach (var post in posts) { - await postService.UpdatePostStatus(post.Id, PostStatus.Sold); + await postService.UpdatePostStatus(post.Id, PostStatus.Sold, "Order Sold"); } // Get Buyer and Seller Emails @@ -273,9 +273,9 @@ public async Task> GetOrders(int page, FilterOrderD var order = await orderRepository.GetAllAsync( - page, - filterExpression - ); + page, + filterExpression + ); return mapper.Map>(order); } @@ -283,9 +283,10 @@ public async Task> GetOrders(int page, FilterOrderD public async Task> GetReviewedOrdersByUserId(string userId) { var orders = await orderRepository.GetAllAsync( - null, - o => o.IsReviewed && o.OrderItems.Any(oi => oi.Post.SellerId == userId) - ); + null, + o => o.IsReviewed && + o.OrderItems.Any(oi => oi.Post.SellerId == userId) + ); return orders.Items; }