Webiant Logo Webiant Logo
  1. No results found.

    Try your search with a different keyword or use * as a wildcard.

ProductReviewService.cs

using Nop.Core;
using Nop.Core.Domain.Catalog;
using Nop.Core.Domain.Localization;
using Nop.Core.Domain.Orders;
using Nop.Core.Events;
using Nop.Data;
using Nop.Services.Customers;
using Nop.Services.Localization;
using Nop.Services.Logging;
using Nop.Services.Messages;
using Nop.Services.Orders;
using Nop.Services.Security;
using Nop.Services.Stores;

namespace Nop.Services.Catalog;

/// <summary>
/// Product review service
/// </summary>
public partial class ProductReviewService : IProductReviewService
{
    #region Fields

    protected readonly CatalogSettings _catalogSettings;
    protected readonly IAclService _aclService;
    protected readonly ICustomerActivityService _customerActivityService;
    protected readonly ICustomerService _customerService;
    protected readonly IEventPublisher _eventPublisher;
    protected readonly ILocalizationService _localizationService;
    protected readonly IOrderService _orderService;
    protected readonly IProductService _productService;
    protected readonly IRepository<Product> _productRepository;
    protected readonly IRepository<ProductReview> _productReviewRepository;
    protected readonly IRepository<ProductReviewHelpfulness> _productReviewHelpfulnessRepository;
    protected readonly IReviewTypeService _reviewTypeService;
    protected readonly IStoreMappingService _storeMappingService;
    protected readonly IWorkContext _workContext;
    protected readonly IWorkflowMessageService _workflowMessageService;
    protected readonly LocalizationSettings _localizationSettings;

    #endregion

    #region Ctor

    public ProductReviewService(CatalogSettings catalogSettings,
        IAclService aclService,
        ICustomerActivityService customerActivityService,
        ICustomerService customerService,
        IEventPublisher eventPublisher,
        ILocalizationService localizationService,
        IOrderService orderService,
        IProductService productService,
        IRepository<Product> productRepository,
        IRepository<ProductReview> productReviewRepository,
        IRepository<ProductReviewHelpfulness> productReviewHelpfulnessRepository,
        IReviewTypeService reviewTypeService,
        IStoreMappingService storeMappingService,
        IWorkContext workContext,
        IWorkflowMessageService workflowMessageService,
        LocalizationSettings localizationSettings)
    {
        _catalogSettings = catalogSettings;
        _aclService = aclService;
        _customerActivityService = customerActivityService;
        _customerService = customerService;
        _eventPublisher = eventPublisher;
        _localizationService = localizationService;
        _orderService = orderService;
        _productService = productService;
        _productRepository = productRepository;
        _productReviewRepository = productReviewRepository;
        _productReviewHelpfulnessRepository = productReviewHelpfulnessRepository;
        _reviewTypeService = reviewTypeService;
        _storeMappingService = storeMappingService;
        _workContext = workContext;
        _workflowMessageService = workflowMessageService;
        _localizationSettings = localizationSettings;
    }

    #endregion

    #region Utilities

    /// <summary>
    /// Checks if customer has completed orders with specified product
    /// </summary>
    /// <param name="product">Product to check</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the check result
    /// </returns>
    protected virtual async ValueTask<bool> HasCompletedOrdersAsync(Product product)
    {
        var customer = await _workContext.GetCurrentCustomerAsync();
        return (await _orderService.SearchOrdersAsync(customerId: customer.Id,
            productId: product.Id,
            osIds: [(int)OrderStatus.Complete],
            pageSize: 1)).Any();
    }


    /// <summary>
    /// Gets ratio of useful and not useful product reviews 
    /// </summary>
    /// <param name="productReview">Product review</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the result
    /// </returns>
    protected virtual async Task<(int usefulCount, int notUsefulCount)> GetHelpfulnessCountsAsync(ProductReview productReview)
    {
        ArgumentNullException.ThrowIfNull(productReview);

        var productReviewHelpfulness = _productReviewHelpfulnessRepository.Table.Where(prh => prh.ProductReviewId == productReview.Id);

        return (await productReviewHelpfulness.CountAsync(prh => prh.WasHelpful),
            await productReviewHelpfulness.CountAsync(prh => !prh.WasHelpful));
    }

    /// <summary>
    /// Inserts a product review helpfulness record
    /// </summary>
    /// <param name="productReviewHelpfulness">Product review helpfulness record</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    protected virtual async Task InsertProductReviewHelpfulnessAsync(ProductReviewHelpfulness productReviewHelpfulness)
    {
        await _productReviewHelpfulnessRepository.InsertAsync(productReviewHelpfulness);
    }

    #endregion

    #region Methods

    /// <summary>
    /// Update product review totals
    /// </summary>
    /// <param name="product">Product</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task UpdateProductReviewTotalsAsync(Product product)
    {
        ArgumentNullException.ThrowIfNull(product);

        var approvedRatingSum = 0;
        var notApprovedRatingSum = 0;
        var approvedTotalReviews = 0;
        var notApprovedTotalReviews = 0;

        var reviews = _productReviewRepository.Table
            .Where(r => r.ProductId == product.Id)
            .ToAsyncEnumerable();
        await foreach (var pr in reviews)
            if (pr.IsApproved)
            {
                approvedRatingSum += pr.Rating;
                approvedTotalReviews++;
            }
            else
            {
                notApprovedRatingSum += pr.Rating;
                notApprovedTotalReviews++;
            }

        product.ApprovedRatingSum = approvedRatingSum;
        product.NotApprovedRatingSum = notApprovedRatingSum;
        product.ApprovedTotalReviews = approvedTotalReviews;
        product.NotApprovedTotalReviews = notApprovedTotalReviews;
        await _productService.UpdateProductAsync(product);
    }

    /// <summary>
    /// Validate product review availability
    /// </summary>
    /// <param name="product">Product to validate review availability</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the validation error list if found
    /// </returns>
    public virtual async Task<IList<string>> ValidateProductReviewAvailabilityAsync(Product product)
    {
        var error = new List<string>();
        var customer = await _workContext.GetCurrentCustomerAsync();
        if (await _customerService.IsGuestAsync(customer) && !_catalogSettings.AllowAnonymousUsersToReviewProduct)
            error.Add(await _localizationService.GetResourceAsync("Reviews.OnlyRegisteredUsersCanWriteReviews"));

        if (!_catalogSettings.ProductReviewPossibleOnlyAfterPurchasing)
            return error;

        var hasCompletedOrders = product.ProductType == ProductType.SimpleProduct
            ? await HasCompletedOrdersAsync(product)
            : await (await _productService.GetAssociatedProductsAsync(product.Id)).AnyAwaitAsync(HasCompletedOrdersAsync);

        if (!hasCompletedOrders)
            error.Add(await _localizationService.GetResourceAsync("Reviews.ProductReviewPossibleOnlyAfterPurchasing"));

        return error;
    }

    /// <summary>
    /// Gets all product reviews
    /// </summary>
    /// <param name="customerId">Customer identifier (who wrote a review); 0 to load all records</param>
    /// <param name="approved">A value indicating whether to content is approved; null to load all records</param> 
    /// <param name="fromUtc">Item creation from; null to load all records</param>
    /// <param name="toUtc">Item creation to; null to load all records</param>
    /// <param name="message">Search title or review text; null to load all records</param>
    /// <param name="storeId">The store identifier, where a review has been created; pass 0 to load all records</param>
    /// <param name="productId">The product identifier; pass 0 to load all records</param>
    /// <param name="vendorId">The vendor identifier (limit to products of this vendor); pass 0 to load all records</param>
    /// <param name="showHidden">A value indicating whether to show hidden records</param>
    /// <param name="pageIndex">Page index</param>
    /// <param name="pageSize">Page size</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the reviews
    /// </returns>
    public virtual async Task<IPagedList<ProductReview>> GetAllProductReviewsAsync(int customerId = 0, bool? approved = null,
        DateTime? fromUtc = null, DateTime? toUtc = null,
        string message = null, int storeId = 0, int productId = 0, int vendorId = 0, bool showHidden = false,
        int pageIndex = 0, int pageSize = int.MaxValue)
    {
        var productReviews = await _productReviewRepository.GetAllPagedAsync(async query =>
        {
            if (!showHidden)
            {
                var productsQuery = _productRepository.Table.Where(p => p.Published);

                //apply store mapping constraints
                productsQuery = await _storeMappingService.ApplyStoreMapping(productsQuery, storeId);

                //apply ACL constraints
                var customer = await _workContext.GetCurrentCustomerAsync();
                productsQuery = await _aclService.ApplyAcl(productsQuery, customer);

                query = query.Where(review => productsQuery.Any(product => product.Id == review.ProductId));
            }

            if (approved.HasValue)
                query = query.Where(pr => pr.IsApproved == approved);
            if (customerId > 0)
                query = query.Where(pr => pr.CustomerId == customerId);
            if (fromUtc.HasValue)
                query = query.Where(pr => fromUtc.Value <= pr.CreatedOnUtc);
            if (toUtc.HasValue)
                query = query.Where(pr => toUtc.Value >= pr.CreatedOnUtc);
            if (!string.IsNullOrEmpty(message))
                query = query.Where(pr => pr.Title.Contains(message) || pr.ReviewText.Contains(message));
            if (storeId > 0)
                query = query.Where(pr => pr.StoreId == storeId);
            if (productId > 0)
                query = query.Where(pr => pr.ProductId == productId);

            query = from productReview in query
                    join product in _productRepository.Table on productReview.ProductId equals product.Id
                    where
                        (vendorId == 0 || product.VendorId == vendorId) &&
                        //ignore deleted products
                        !product.Deleted
                    select productReview;

            query = _catalogSettings.ProductReviewsSortByCreatedDateAscending
                ? query.OrderBy(pr => pr.CreatedOnUtc).ThenBy(pr => pr.Id)
                : query.OrderByDescending(pr => pr.CreatedOnUtc).ThenBy(pr => pr.Id);

            return query;
        }, pageIndex, pageSize);

        return productReviews;
    }

    /// <summary>
    /// Gets product review
    /// </summary>
    /// <param name="productReviewId">Product review identifier</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the product review
    /// </returns>
    public virtual async Task<ProductReview> GetProductReviewByIdAsync(int productReviewId)
    {
        return await _productReviewRepository.GetByIdAsync(productReviewId, _ => default);
    }

    /// <summary>
    /// Get product reviews by identifiers
    /// </summary>
    /// <param name="productReviewIds">Product review identifiers</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the product reviews
    /// </returns>
    public virtual async Task<IList<ProductReview>> GetProductReviewsByIdsAsync(int[] productReviewIds)
    {
        return await _productReviewRepository.GetByIdsAsync(productReviewIds);
    }

    /// <summary>
    /// Inserts a product review
    /// </summary>
    /// <param name="productReview">Product review</param>
    /// <param name="productReviewReviewTypeMappings"></param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task InsertProductReviewAsync(ProductReview productReview, IList<ProductReviewReviewTypeMapping> productReviewReviewTypeMappings = null)
    {
        await _productReviewRepository.InsertAsync(productReview);

        //add product review and review type mapping
        if (productReviewReviewTypeMappings != null && productReviewReviewTypeMappings.Any())
        {
            foreach (var additionalProductReview in productReviewReviewTypeMappings)
            {
                additionalProductReview.ProductReviewId = productReview.Id;
                await _reviewTypeService.InsertProductReviewReviewTypeMappingsAsync(additionalProductReview);
            }
        }

        //update product totals
        var product = await _productService.GetProductByIdAsync(productReview.ProductId);
        await UpdateProductReviewTotalsAsync(product);

        //notify store owner
        if (_catalogSettings.NotifyStoreOwnerAboutNewProductReviews)
            await _workflowMessageService.SendProductReviewStoreOwnerNotificationMessageAsync(productReview, _localizationSettings.DefaultAdminLanguageId);

        //activity log
        await _customerActivityService.InsertActivityAsync("PublicStore.AddProductReview",
            string.Format(await _localizationService.GetResourceAsync("ActivityLog.PublicStore.AddProductReview"), product.Name), product);

        //raise event
        if (productReview.IsApproved)
            await _eventPublisher.PublishAsync(new ProductReviewApprovedEvent(productReview));
    }

    /// <summary>
    /// Deletes a product review
    /// </summary>
    /// <param name="productReview">Product review</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task DeleteProductReviewAsync(ProductReview productReview)
    {
        await _productReviewRepository.DeleteAsync(productReview);
    }

    /// <summary>
    /// Deletes product reviews
    /// </summary>
    /// <param name="productReviews">Product reviews</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task DeleteProductReviewsAsync(IList<ProductReview> productReviews)
    {
        await _productReviewRepository.DeleteAsync(productReviews);
    }

    /// <summary>
    /// Sets or create a product review helpfulness record
    /// </summary>
    /// <param name="productReview">Product review</param>
    /// <param name="helpfulness">Value indicating whether a review a helpful</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task SetProductReviewHelpfulnessAsync(ProductReview productReview, bool helpfulness)
    {
        ArgumentNullException.ThrowIfNull(productReview);

        var customer = await _workContext.GetCurrentCustomerAsync();
        var prh = await _productReviewHelpfulnessRepository.Table
            .SingleOrDefaultAsync(h => h.ProductReviewId == productReview.Id && h.CustomerId == customer.Id);

        if (prh is null)
        {
            //insert new helpfulness
            prh = new ProductReviewHelpfulness
            {
                ProductReviewId = productReview.Id,
                CustomerId = customer.Id,
                WasHelpful = helpfulness,
            };

            await InsertProductReviewHelpfulnessAsync(prh);
        }
        else
        {
            //existing one
            prh.WasHelpful = helpfulness;

            await _productReviewHelpfulnessRepository.UpdateAsync(prh);
        }
    }

    /// <summary>
    /// Updates a product review
    /// </summary>
    /// <param name="productReview">Product review</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public virtual async Task UpdateProductReviewAsync(ProductReview productReview)
    {
        await _productReviewRepository.UpdateAsync(productReview);
    }

    /// <summary>
    /// Updates a totals helpfulness count for product review
    /// </summary>
    /// <param name="productReview">Product review</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the result
    /// </returns>
    public virtual async Task UpdateProductReviewHelpfulnessTotalsAsync(ProductReview productReview)
    {
        ArgumentNullException.ThrowIfNull(productReview);

        (productReview.HelpfulYesTotal, productReview.HelpfulNoTotal) = await GetHelpfulnessCountsAsync(productReview);

        await _productReviewRepository.UpdateAsync(productReview);
    }

    /// <summary>
    /// Check possibility added review for current customer
    /// </summary>
    /// <param name="productId">Current product</param>
    /// <param name="storeId">The store identifier; pass 0 to load all records</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the 
    /// </returns>
    public virtual async Task<bool> CanAddReviewAsync(int productId, int storeId = 0)
    {
        var customer = await _workContext.GetCurrentCustomerAsync();

        if (_catalogSettings.OneReviewPerProductFromCustomer)
            return (await GetAllProductReviewsAsync(customerId: customer.Id, productId: productId, storeId: storeId)).TotalCount == 0;

        return true;
    }

    #endregion
}