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/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; } diff --git a/Dentizone.Application/Services/PostService.cs b/Dentizone.Application/Services/PostService.cs index b3c29dd..0a88ea3 100644 --- a/Dentizone.Application/Services/PostService.cs +++ b/Dentizone.Application/Services/PostService.cs @@ -6,6 +6,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 +25,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) @@ -202,16 +204,28 @@ 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); + + 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); } @@ -337,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 0d7edaf..97ccf65 100644 --- a/Dentizone.Presentaion/Controllers/PostsController.cs +++ b/Dentizone.Presentaion/Controllers/PostsController.cs @@ -1,9 +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 Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.Security.Claims; namespace Dentizone.Presentaion.Controllers { @@ -76,5 +76,13 @@ public async Task Search([FromQuery] UserPreferenceDto userPrefer var searchResult = await postService.Search(userPreferenceDto); return Ok(searchResult); } + + [Authorize("IsAdmin")] + [HttpPatch("{postId}/status")] + public async Task AdjustStatus([FromBody] UpdatePostStateDto state) + { + var updatedPost = await postService.UpdatePostStatus(state.PostId, state.Status, state.Reason); + return Ok(updatedPost); + } } } \ No newline at end of file