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>
This commit is contained in:
300
LittleShop/Services/ReviewService.cs
Normal file
300
LittleShop/Services/ReviewService.cs
Normal file
@@ -0,0 +1,300 @@
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user