using Microsoft.EntityFrameworkCore; using LittleShop.Data; using LittleShop.Models; using LittleShop.DTOs; using LittleShop.Enums; namespace LittleShop.Services; public interface IReviewService { Task GetReviewByIdAsync(Guid id); Task> GetReviewsByProductAsync(Guid productId, bool approvedOnly = true); Task> GetReviewsByCustomerAsync(Guid customerId); Task> GetPendingReviewsAsync(); Task GetProductReviewSummaryAsync(Guid productId); Task CheckReviewEligibilityAsync(Guid customerId, Guid productId); Task CreateReviewAsync(CreateReviewDto createReviewDto); Task UpdateReviewAsync(Guid id, UpdateReviewDto updateReviewDto); Task ApproveReviewAsync(Guid id, Guid approvedByUserId); Task DeleteReviewAsync(Guid id); Task CanCustomerReviewProductAsync(Guid customerId, Guid productId); } public class ReviewService : IReviewService { private readonly LittleShopContext _context; private readonly ILogger _logger; public ReviewService(LittleShopContext context, ILogger logger) { _context = context; _logger = logger; } public async Task GetReviewByIdAsync(Guid id) { var review = await _context.Reviews .Include(r => r.Product) .Include(r => r.Customer) .Include(r => r.ApprovedByUser) .FirstOrDefaultAsync(r => r.Id == id); return review == null ? null : MapToDto(review); } public async Task> GetReviewsByProductAsync(Guid productId, bool approvedOnly = true) { var query = _context.Reviews .Include(r => r.Customer) .Include(r => r.ApprovedByUser) .Where(r => r.ProductId == productId && r.IsActive); if (approvedOnly) { query = query.Where(r => r.IsApproved); } var reviews = await query .OrderByDescending(r => r.CreatedAt) .ToListAsync(); return reviews.Select(MapToDto); } public async Task> GetReviewsByCustomerAsync(Guid customerId) { var reviews = await _context.Reviews .Include(r => r.Product) .Include(r => r.ApprovedByUser) .Where(r => r.CustomerId == customerId && r.IsActive) .OrderByDescending(r => r.CreatedAt) .ToListAsync(); return reviews.Select(MapToDto); } public async Task> GetPendingReviewsAsync() { var reviews = await _context.Reviews .Include(r => r.Product) .Include(r => r.Customer) .Where(r => !r.IsApproved && r.IsActive) .OrderBy(r => r.CreatedAt) .ToListAsync(); return reviews.Select(MapToDto); } public async Task GetProductReviewSummaryAsync(Guid productId) { var product = await _context.Products .Include(p => p.Reviews.Where(r => r.IsApproved && r.IsActive)) .FirstOrDefaultAsync(p => p.Id == productId); if (product == null) return null; var approvedReviews = product.Reviews.Where(r => r.IsApproved && r.IsActive).ToList(); if (!approvedReviews.Any()) { return new ReviewSummaryDto { ProductId = productId, ProductName = product.Name, TotalReviews = 0, ApprovedReviews = 0, AverageRating = 0 }; } return new ReviewSummaryDto { ProductId = productId, ProductName = product.Name, TotalReviews = approvedReviews.Count, ApprovedReviews = approvedReviews.Count, AverageRating = Math.Round(approvedReviews.Average(r => r.Rating), 1), FiveStars = approvedReviews.Count(r => r.Rating == 5), FourStars = approvedReviews.Count(r => r.Rating == 4), ThreeStars = approvedReviews.Count(r => r.Rating == 3), TwoStars = approvedReviews.Count(r => r.Rating == 2), OneStar = approvedReviews.Count(r => r.Rating == 1), LatestReviewDate = approvedReviews.Max(r => r.CreatedAt) }; } public async Task CheckReviewEligibilityAsync(Guid customerId, Guid productId) { // Check if customer has already reviewed this product var existingReview = await _context.Reviews .FirstOrDefaultAsync(r => r.CustomerId == customerId && r.ProductId == productId && r.IsActive); // Get shipped orders containing this product for this customer var eligibleOrders = await _context.Orders .Include(o => o.Items) .Where(o => o.CustomerId == customerId && o.Status == OrderStatus.Shipped && o.Items.Any(oi => oi.ProductId == productId)) .Select(o => o.Id) .ToListAsync(); var canReview = eligibleOrders.Any() && existingReview == null; var reason = !eligibleOrders.Any() ? "You must have a shipped order containing this product to leave a review" : existingReview != null ? "You have already reviewed this product" : null; return new CustomerReviewEligibilityDto { CustomerId = customerId, ProductId = productId, CanReview = canReview, Reason = reason, EligibleOrderIds = eligibleOrders, HasExistingReview = existingReview != null, ExistingReviewId = existingReview?.Id }; } public async Task CreateReviewAsync(CreateReviewDto createReviewDto) { // Verify customer can review this product var eligibility = await CheckReviewEligibilityAsync(createReviewDto.CustomerId, createReviewDto.ProductId); if (!eligibility.CanReview) { throw new InvalidOperationException(eligibility.Reason ?? "Cannot create review"); } // Verify the order exists and contains the product var order = await _context.Orders .Include(o => o.Items) .FirstOrDefaultAsync(o => o.Id == createReviewDto.OrderId && o.CustomerId == createReviewDto.CustomerId && o.Status == OrderStatus.Shipped && o.Items.Any(oi => oi.ProductId == createReviewDto.ProductId)); if (order == null) { throw new InvalidOperationException("Invalid order or product not found in shipped order"); } var review = new Review { Id = Guid.NewGuid(), ProductId = createReviewDto.ProductId, CustomerId = createReviewDto.CustomerId, OrderId = createReviewDto.OrderId, Rating = createReviewDto.Rating, Title = createReviewDto.Title, Comment = createReviewDto.Comment, IsVerifiedPurchase = true, IsApproved = false, // Reviews require admin approval IsActive = true, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }; _context.Reviews.Add(review); await _context.SaveChangesAsync(); _logger.LogInformation("Review created: {ReviewId} for product {ProductId} by customer {CustomerId}", review.Id, createReviewDto.ProductId, createReviewDto.CustomerId); // Load navigation properties for return DTO review = await _context.Reviews .Include(r => r.Product) .Include(r => r.Customer) .FirstAsync(r => r.Id == review.Id); return MapToDto(review); } public async Task UpdateReviewAsync(Guid id, UpdateReviewDto updateReviewDto) { var review = await _context.Reviews.FindAsync(id); if (review == null) return false; if (updateReviewDto.Rating.HasValue) review.Rating = updateReviewDto.Rating.Value; if (updateReviewDto.Title != null) review.Title = updateReviewDto.Title; if (updateReviewDto.Comment != null) review.Comment = updateReviewDto.Comment; if (updateReviewDto.IsApproved.HasValue) review.IsApproved = updateReviewDto.IsApproved.Value; if (updateReviewDto.IsActive.HasValue) review.IsActive = updateReviewDto.IsActive.Value; review.UpdatedAt = DateTime.UtcNow; await _context.SaveChangesAsync(); _logger.LogInformation("Review updated: {ReviewId}", id); return true; } public async Task ApproveReviewAsync(Guid id, Guid approvedByUserId) { var review = await _context.Reviews.FindAsync(id); if (review == null) return false; review.IsApproved = true; review.ApprovedAt = DateTime.UtcNow; review.ApprovedByUserId = approvedByUserId; review.UpdatedAt = DateTime.UtcNow; await _context.SaveChangesAsync(); _logger.LogInformation("Review approved: {ReviewId} by user {UserId}", id, approvedByUserId); return true; } public async Task DeleteReviewAsync(Guid id) { var review = await _context.Reviews.FindAsync(id); if (review == null) return false; review.IsActive = false; review.UpdatedAt = DateTime.UtcNow; await _context.SaveChangesAsync(); _logger.LogInformation("Review soft deleted: {ReviewId}", id); return true; } public async Task CanCustomerReviewProductAsync(Guid customerId, Guid productId) { var eligibility = await CheckReviewEligibilityAsync(customerId, productId); return eligibility.CanReview; } private static ReviewDto MapToDto(Review review) { return new ReviewDto { Id = review.Id, ProductId = review.ProductId, ProductName = review.Product?.Name ?? "", CustomerId = review.CustomerId, CustomerDisplayName = review.Customer?.TelegramDisplayName ?? "Anonymous", OrderId = review.OrderId, Rating = review.Rating, Title = review.Title, Comment = review.Comment, IsVerifiedPurchase = review.IsVerifiedPurchase, IsApproved = review.IsApproved, IsActive = review.IsActive, CreatedAt = review.CreatedAt, UpdatedAt = review.UpdatedAt, ApprovedAt = review.ApprovedAt, ApprovedByUsername = review.ApprovedByUser?.Username }; } }