littleshop/LittleShop/Services/ReviewService.cs
SysAdmin e1b377a042 Initial commit of LittleShop project (excluding large archives)
- BTCPay Server integration
- TeleBot Telegram bot
- Review system
- Admin area
- Docker deployment configuration

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-17 15:07:38 +01:00

300 lines
11 KiB
C#

using Microsoft.EntityFrameworkCore;
using LittleShop.Data;
using LittleShop.Models;
using LittleShop.DTOs;
using LittleShop.Enums;
namespace LittleShop.Services;
public interface IReviewService
{
Task<ReviewDto?> GetReviewByIdAsync(Guid id);
Task<IEnumerable<ReviewDto>> GetReviewsByProductAsync(Guid productId, bool approvedOnly = true);
Task<IEnumerable<ReviewDto>> GetReviewsByCustomerAsync(Guid customerId);
Task<IEnumerable<ReviewDto>> GetPendingReviewsAsync();
Task<ReviewSummaryDto?> GetProductReviewSummaryAsync(Guid productId);
Task<CustomerReviewEligibilityDto> CheckReviewEligibilityAsync(Guid customerId, Guid productId);
Task<ReviewDto> CreateReviewAsync(CreateReviewDto createReviewDto);
Task<bool> UpdateReviewAsync(Guid id, UpdateReviewDto updateReviewDto);
Task<bool> ApproveReviewAsync(Guid id, Guid approvedByUserId);
Task<bool> DeleteReviewAsync(Guid id);
Task<bool> CanCustomerReviewProductAsync(Guid customerId, Guid productId);
}
public class ReviewService : IReviewService
{
private readonly LittleShopContext _context;
private readonly ILogger<ReviewService> _logger;
public ReviewService(LittleShopContext context, ILogger<ReviewService> logger)
{
_context = context;
_logger = logger;
}
public async Task<ReviewDto?> 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<IEnumerable<ReviewDto>> 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<IEnumerable<ReviewDto>> 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<IEnumerable<ReviewDto>> 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<ReviewSummaryDto?> 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<CustomerReviewEligibilityDto> 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<ReviewDto> 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<bool> 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<bool> 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<bool> 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<bool> 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
};
}
}