From cd4d23dbf921cbbc3413c8a2ef4eee34ba77de31 Mon Sep 17 00:00:00 2001 From: Mahmoud Nasr <239.nasr@gmail.com> Date: Tue, 8 Jul 2025 19:46:43 +0300 Subject: [PATCH 1/2] Refactor services and enhance user activity logging - Removed `GetSubmittedReviews` from `IReviewService` and `UpdateUserVerificationState` from `IVerificationService`. - Updated role parsing in `AuthService` to ensure correct user role retrieval. - Corrected spelling of "Lockdout" to "Lockedout" in user activity logging. - Added user activity logging for registration, email verification, password reset, order actions, and QA interactions. - Refactored `VerificationService` for improved constructor handling and removed unnecessary methods. - Updated `UserActivityService` to set a default fingerprint token. - Enhanced `UserActivities` enum with new activity types and corrected spelling errors. --- .../Interfaces/IReviewService.cs | 1 - .../Interfaces/IVerificationService.cs | 2 - .../Services/Authentication/AuthService.cs | 17 +++--- .../Authentication/VerificationService.cs | 54 ++++++------------- .../Services/OrderService.cs | 4 ++ Dentizone.Application/Services/QAService.cs | 20 ++++--- .../Services/ReviewService.cs | 9 ++-- .../Services/UserActivityService.cs | 2 +- .../Services/WithdrawalService.cs | 4 ++ Dentizone.Domain/Enums/UserActivity.cs | 13 +++-- 10 files changed, 60 insertions(+), 66 deletions(-) diff --git a/Dentizone.Application/Interfaces/IReviewService.cs b/Dentizone.Application/Interfaces/IReviewService.cs index de0cb8b..77b60c0 100644 --- a/Dentizone.Application/Interfaces/IReviewService.cs +++ b/Dentizone.Application/Interfaces/IReviewService.cs @@ -8,7 +8,6 @@ public interface IReviewService Task CreateOrderReviewAsync(string userId, CreateReviewDto createReviewDto); Task UpdateReviewAsync(string reviewId, UpdateReviewDto updateReviewDto); Task DeleteReviewAsync(string reviewId); - Task> GetSubmittedReviews(string userId); Task> GetReceivedReviews(string userId); Task> GetAllReviewsAsync(int page); } diff --git a/Dentizone.Application/Interfaces/IVerificationService.cs b/Dentizone.Application/Interfaces/IVerificationService.cs index 5ab0154..08317ec 100644 --- a/Dentizone.Application/Interfaces/IVerificationService.cs +++ b/Dentizone.Application/Interfaces/IVerificationService.cs @@ -3,11 +3,9 @@ namespace Dentizone.Application.Interfaces; - public interface IVerificationService { Task GetVerificationStatusAsync(string sessionId); Task StartSessionAsync(string userId); Task UpdateUserNationalId(string userId, string nationalId); - Task UpdateUserVerificationState(string userId, string status); } \ No newline at end of file diff --git a/Dentizone.Application/Services/Authentication/AuthService.cs b/Dentizone.Application/Services/Authentication/AuthService.cs index 82f4c03..d2659bc 100644 --- a/Dentizone.Application/Services/Authentication/AuthService.cs +++ b/Dentizone.Application/Services/Authentication/AuthService.cs @@ -36,7 +36,7 @@ public async Task GetUserRoleAsync(string userId) throw new NotFoundException("User does not have any roles assigned"); } - return Enum.Parse(currentRoles.First() ?? UserRoles.Ghost.ToString()); + return Enum.Parse(currentRoles.First()); } public async Task AlternateUserRoleAsync(UserRoles newRole, string userId) @@ -71,10 +71,10 @@ public async Task LoginWithEmailAndPassword(string email, string p if (isLockedOut) { - await userActivityService.CreateAsync(UserActivities.Lockdout, DateTime.Now, user.Id); + await userActivityService.CreateAsync(UserActivities.Lockedout, DateTime.Now, user.Id); throw new UserLockedOutException( - "User is locked out due to too many failed login attempts. Please try again later."); + "User is locked out due to too many failed login attempts. Please try again later."); } @@ -111,7 +111,7 @@ public async Task LoginWithEmailAndPassword(string email, string p return new LoggedInUser() { User = user, - Role = Enum.Parse(roles.LastOrDefault()) + Role = Enum.Parse(roles.Last()) }; } @@ -145,6 +145,7 @@ public async Task RegisterWithEmailAndPassword(RegisterRequestDto // 4. Send Verification Email await SendVerificationEmail(user.Email); + await userActivityService.CreateAsync(UserActivities.Registered, DateTime.Now, user.Id); return new LoggedInUser() { User = user, @@ -201,7 +202,8 @@ public async Task SendVerificationEmail(string email) var verificationLink = $"https://dentizone.vercel.app/auth/mail-verify?userId={user.Id}&token={token}"; // 3. Send Verification Email await mailService.Send(email, "Dentizone: Verify your email", - $"Please click the following link to verify your email: Verify Email"); + $"Please click the following link to verify your email: Verify Email"); + await userActivityService.CreateAsync(UserActivities.EmailVerificationSent, DateTime.Now, user.Id); } public async Task SendForgetPasswordEmail(string email) @@ -218,7 +220,8 @@ public async Task SendForgetPasswordEmail(string email) var resetLink = $"https://dentizone.vercel.app/auth/forgot-password?email={user.Email}&token={token}"; // 3. Send Reset Password Email await mailService.Send(email, "Dentizone: Reset your password", - $"Please click the following link to reset your password: Reset Password"); + $"Please click the following link to reset your password: Reset Password"); + await userActivityService.CreateAsync(UserActivities.PasswordResetRequested, DateTime.Now, user.Id); } public async Task GetById(string userId) @@ -250,7 +253,7 @@ public async Task ResetPassword(string email, string token, string newPa throw new NotFoundException("User does not have any roles assigned"); } - await userActivityService.CreateAsync(UserActivities.PasswordReset); + await userActivityService.CreateAsync(UserActivities.PasswordReset, DateTime.Now, user.Id); // 3. Generate token return GenerateToken(user.Id, user.Email, roles.FirstOrDefault()); } diff --git a/Dentizone.Application/Services/Authentication/VerificationService.cs b/Dentizone.Application/Services/Authentication/VerificationService.cs index 889187b..3b2a96b 100644 --- a/Dentizone.Application/Services/Authentication/VerificationService.cs +++ b/Dentizone.Application/Services/Authentication/VerificationService.cs @@ -16,36 +16,25 @@ public class Metadata } - public class VerificationService(IDiditApi diditApi, ISecretService secretService, IAuthService authService, - IUserService userService, IMailService mailService) : IVerificationService + public class VerificationService( + IDiditApi diditApi, + ISecretService secretService, + IAuthService authService, + IUserService userService, + IUserActivityService userActivityService, + IMailService mailService) : IVerificationService { - private readonly IDiditApi _diditApi = diditApi; - private readonly ISecretService _secretService = secretService; - private readonly IAuthService _authService = authService; - private readonly IUserService _userService = userService; - - - - private static Dictionary MapVerificationStatusToEnum() - { - return new Dictionary - { - { "approved", KycStatus.Approved }, - { "declined", KycStatus.Rejected }, - { "pending", KycStatus.Pending } - }; - } - public async Task StartSessionAsync(string userId) { - var user = await _authService.GetById(userId); - var domainUser = await _userService.GetByIdAsync(userId); + var user = await authService.GetById(userId); + var domainUser = await userService.GetByIdAsync(userId); if (domainUser.KycStatus.Equals(KycStatus.NotSubmitted.ToString())) { throw new BadActionException("KYC is not submitted yet"); } + if (domainUser.KycStatus.Equals(KycStatus.Approved.ToString())) { throw new BadActionException("Your account is Already Active"); @@ -54,7 +43,7 @@ public async Task StartSessionAsync(string userId) var request = new CreateSessionRequest { - WorkflowId = _secretService.GetSecret("DiditWorkflowId"), + WorkflowId = secretService.GetSecret("DiditWorkflowId"), VendorData = userId, Callback = "https://dentizone.vercel.app/auth/kyc/status", Metadata = JsonConvert.SerializeObject(new Metadata() @@ -72,38 +61,27 @@ public async Task StartSessionAsync(string userId) } }; - var session = await _diditApi.CreateSessionAsync(request, _secretService.GetSecret("DiditApi")); - await _userService.SetKycStatusAsync(userId, KycStatus.NotSubmitted); + var session = await diditApi.CreateSessionAsync(request, secretService.GetSecret("DiditApi")); + await userService.SetKycStatusAsync(userId, KycStatus.NotSubmitted); await mailService.Send(user.Email!, "Dentizone: Verification Started", "Thank you for starting the email verification process." + " You can use this url to verify your identity" + $" Verify Now" ); + return session; } public async Task GetVerificationStatusAsync(string sessionId) { - return await _diditApi.GetSessionDecisionAsync(sessionId, _secretService.GetSecret("DiditApi")); + return await diditApi.GetSessionDecisionAsync(sessionId, secretService.GetSecret("DiditApi")); } - public async Task UpdateUserVerificationState(string userId, string status) - { - if (!MapVerificationStatusToEnum().TryGetValue(status.ToLower(), out var kycStatus)) - { - throw new ArgumentException($"Invalid verification status: {status}"); - } - - var output = await _userService.SetKycStatusAsync(userId, kycStatus); - - return output; - } - public async Task UpdateUserNationalId(string userId, string nationalId) { - var output = await _userService.SetNationalId(userId, nationalId); + var output = await userService.SetNationalId(userId, nationalId); return output; } } diff --git a/Dentizone.Application/Services/OrderService.cs b/Dentizone.Application/Services/OrderService.cs index e650ef7..6e4cfed 100644 --- a/Dentizone.Application/Services/OrderService.cs +++ b/Dentizone.Application/Services/OrderService.cs @@ -26,6 +26,7 @@ internal class OrderService( IPaymentService paymentService, ICartService cartService, IHttpContextAccessor accessor, + IUserActivityService userActivityService, AppDbContext dbContext) : BaseService(accessor), IOrderService { @@ -84,6 +85,7 @@ await mailService.Send(seller.Email, "Order Cancelled", var dto = mapper.Map(order); + await userActivityService.CreateAsync(UserActivities.OrderCancelled, new DateTime(), order.BuyerId); return dto; } @@ -188,6 +190,8 @@ await paymentService.CreateSaleTransaction( await cartService.ClearCartAsync(buyerId); await transaction.CommitAsync(); + await userActivityService.CreateAsync(UserActivities.OrderPlaced, new DateTime(), buyerId); + return result.Id; } catch (Exception) diff --git a/Dentizone.Application/Services/QAService.cs b/Dentizone.Application/Services/QAService.cs index 59f02ec..455b5fc 100644 --- a/Dentizone.Application/Services/QAService.cs +++ b/Dentizone.Application/Services/QAService.cs @@ -16,7 +16,8 @@ public class QaService( IMapper mapper, IAnswerRepository answerRepository, IQuestionRepository questionRepository, - IBackgroundJobService _backgroundJob) + IBackgroundJobService _backgroundJob, + IUserActivityService userActivity) : IQaService { public async Task AnswerQuestionAsync(string questionId, CreateAnswerDto dto, string responderId) @@ -50,7 +51,8 @@ public async Task AnswerQuestionAsync(string questionId, CreateAn await questionRepository.UpdateAsync(question); _backgroundJob.Enqueue(job => - job.ReviewAnswerAsync(answer.Id, answer.Text)); + job.ReviewAnswerAsync(answer.Id, answer.Text)); + await userActivity.CreateAsync(UserActivities.AnsweredQuestion, DateTime.UtcNow, responderId); return mapper.Map(answer); } @@ -62,7 +64,9 @@ public async Task AskQuestionAsync(CreateQuestionDto dto, strin await questionRepository.CreateAsync(question); _backgroundJob.Enqueue(job => - job.ReviewQuestionAsync(question.Id, question.Text)); + job.ReviewQuestionAsync(question.Id, question.Text)); + + await userActivity.CreateAsync(UserActivities.AskedQuestion, DateTime.UtcNow, askerId); return mapper.Map(question); } @@ -107,13 +111,13 @@ public async Task DeleteQuestionAsync(string 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); + q => q.PostId == postId && !q.IsDeleted, + includes); return mapper.Map>(questions); diff --git a/Dentizone.Application/Services/ReviewService.cs b/Dentizone.Application/Services/ReviewService.cs index 573ec80..af76c5e 100644 --- a/Dentizone.Application/Services/ReviewService.cs +++ b/Dentizone.Application/Services/ReviewService.cs @@ -3,6 +3,7 @@ using Dentizone.Application.DTOs.Review; using Dentizone.Application.Interfaces; using Dentizone.Domain.Entity; +using Dentizone.Domain.Enums; using Dentizone.Domain.Exceptions; using Dentizone.Domain.Interfaces; using Dentizone.Domain.Interfaces.Repositories; @@ -15,7 +16,8 @@ public class ReviewService( IMapper mapper, IReviewRepository repo, IOrderService orderService, - IBackgroundJobService backgroundJob) : BaseService(accessor), IReviewService + IBackgroundJobService backgroundJob, + IUserActivityService userActivityService) : BaseService(accessor), IReviewService { public async Task CreateOrderReviewAsync(string userId, CreateReviewDto createReviewDto) { @@ -36,6 +38,7 @@ public async Task CreateOrderReviewAsync(string userId, CreateReviewDto createRe await repo.CreateAsync(review); await orderService.MarkOrderAsReviewed(order.Id); backgroundJob.Enqueue(job => job.ReviewReviewAsync(review.Id, review.Text)); + await userActivityService.CreateAsync(UserActivities.ReviewedOrder, DateTime.UtcNow, userId); } public async Task DeleteReviewAsync(string reviewId) @@ -44,10 +47,6 @@ public async Task DeleteReviewAsync(string reviewId) await repo.DeleteAsync(reviewId); } - public async Task> GetSubmittedReviews(string userId) - { - return []; - } public async Task UpdateReviewAsync(string reviewId, UpdateReviewDto updateReviewDto) { diff --git a/Dentizone.Application/Services/UserActivityService.cs b/Dentizone.Application/Services/UserActivityService.cs index 7916f3f..50f25c8 100644 --- a/Dentizone.Application/Services/UserActivityService.cs +++ b/Dentizone.Application/Services/UserActivityService.cs @@ -20,7 +20,7 @@ public async Task CreateAsync(UserActivities activity, { var userActivity = new UserActivity { - FingerprintToken = requestContextService.GetFingerprint(), + FingerprintToken = "NON", IpAddress = requestContextService.GetIpAddress(), UserAgent = requestContextService.GetUserAgent(), Device = requestContextService.GetDeviceType(), diff --git a/Dentizone.Application/Services/WithdrawalService.cs b/Dentizone.Application/Services/WithdrawalService.cs index 3343c0b..0895dfe 100644 --- a/Dentizone.Application/Services/WithdrawalService.cs +++ b/Dentizone.Application/Services/WithdrawalService.cs @@ -17,6 +17,7 @@ public class WithdrawalService( IWalletService walletService, IWithdrawalRequestRepository withdrawalRepo, IMailService mailService, + IUserActivityService userActivityService, IMapper mapper) : IWithdrawalService { public async Task CreateWithdrawalRequestAsync( @@ -46,6 +47,9 @@ public async Task CreateWithdrawalRequestAsync( var withdrawalView = mapper.Map(createdRequest); + await userActivityService.CreateAsync( + UserActivities.WithdrawalRequestCreated, DateTime.UtcNow, userId); + return withdrawalView; } diff --git a/Dentizone.Domain/Enums/UserActivity.cs b/Dentizone.Domain/Enums/UserActivity.cs index 3181038..cdb3c5f 100644 --- a/Dentizone.Domain/Enums/UserActivity.cs +++ b/Dentizone.Domain/Enums/UserActivity.cs @@ -8,10 +8,15 @@ public enum UserActivities PasswordReset, OrderPlaced, OrderCancelled, - OrderCompleted, - ProfileUpdated, AccountVerified, - Lockdout, - EmailConfirmed + Lockedout, + EmailConfirmed, + AnsweredQuestion, + AskedQuestion, + ReviewedOrder, + WithdrawalRequestCreated, + Registered, + EmailVerificationSent, + PasswordResetRequested } } \ No newline at end of file From 6be348ff912424d91c90d4381e47abb3372d0ecb Mon Sep 17 00:00:00 2001 From: Mahmoud Nasr <239.nasr@gmail.com> Date: Tue, 8 Jul 2025 22:54:22 +0300 Subject: [PATCH 2/2] Update order cancellation timestamp and clean up reviews Modified OrderService to use DateTime.UtcNow for user activity timestamp. Removed GetSubmittedReviews and GetReceivedReviews methods from ReviewController, retaining only GetAllReviews for admin access. --- Dentizone.Application/Services/OrderService.cs | 2 +- .../Controllers/ReviewController.cs | 15 --------------- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/Dentizone.Application/Services/OrderService.cs b/Dentizone.Application/Services/OrderService.cs index 6e4cfed..ef7b1c7 100644 --- a/Dentizone.Application/Services/OrderService.cs +++ b/Dentizone.Application/Services/OrderService.cs @@ -85,7 +85,7 @@ await mailService.Send(seller.Email, "Order Cancelled", var dto = mapper.Map(order); - await userActivityService.CreateAsync(UserActivities.OrderCancelled, new DateTime(), order.BuyerId); + await userActivityService.CreateAsync(UserActivities.OrderCancelled, DateTime.UtcNow, order.BuyerId); return dto; } diff --git a/Dentizone.Presentaion/Controllers/ReviewController.cs b/Dentizone.Presentaion/Controllers/ReviewController.cs index eae4493..7abd245 100644 --- a/Dentizone.Presentaion/Controllers/ReviewController.cs +++ b/Dentizone.Presentaion/Controllers/ReviewController.cs @@ -36,21 +36,6 @@ public async Task DeleteReview(string reviewId) return Ok(); } - [HttpGet] - public async Task GetSubmittedReviews() - { - var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); - - var reviews = await reviewService.GetSubmittedReviews(userId); - return Ok(reviews); - } - - [HttpGet("received-review")] - public async Task GetReceivedReviews() - { - var reviews = await reviewService.GetReceivedReviews(User.FindFirstValue(ClaimTypes.NameIdentifier)); - return Ok(reviews); - } [HttpGet("all")] [Authorize("IsAdmin")]