using Nop.Core; using Nop.Core.Domain.Customers; using Nop.Core.Domain.Orders; using Nop.Data; using Nop.Services.Helpers; using Nop.Services.Localization; namespace Nop.Services.Orders; /// /// Reward point service /// public partial class RewardPointService : IRewardPointService { #region Fields protected readonly IDateTimeHelper _dateTimeHelper; protected readonly ILocalizationService _localizationService; protected readonly IRepository _rewardPointsHistoryRepository; protected readonly RewardPointsSettings _rewardPointsSettings; #endregion #region Ctor public RewardPointService(IDateTimeHelper dateTimeHelper, ILocalizationService localizationService, IRepository rewardPointsHistoryRepository, RewardPointsSettings rewardPointsSettings) { _dateTimeHelper = dateTimeHelper; _localizationService = localizationService; _rewardPointsHistoryRepository = rewardPointsHistoryRepository; _rewardPointsSettings = rewardPointsSettings; } #endregion #region Utilities /// /// Get query to load reward points history /// /// Customer identifier; pass 0 to load all records /// Store identifier; pass null to load all records /// Whether to load reward points that did not yet activated /// /// A task that represents the asynchronous operation /// The task result contains the query to load reward points history /// protected virtual async Task> GetRewardPointsQueryAsync(int customerId, int? storeId, bool showNotActivated = false) { var query = _rewardPointsHistoryRepository.Table; //filter by customer if (customerId > 0) query = query.Where(historyEntry => historyEntry.CustomerId == customerId); //filter by store if (!_rewardPointsSettings.PointsAccumulatedForAllStores && storeId > 0) query = query.Where(historyEntry => historyEntry.StoreId == storeId); //whether to show only the points that already activated if (!showNotActivated) query = query.Where(historyEntry => historyEntry.CreatedOnUtc < DateTime.UtcNow); //update points balance await UpdateRewardPointsBalanceAsync(query); return query; } /// /// Update reward points balance if necessary /// /// Input query /// A task that represents the asynchronous operation protected virtual async Task UpdateRewardPointsBalanceAsync(IQueryable query) { //get expired points var nowUtc = DateTime.UtcNow; var expiredPoints = query .Where(historyEntry => historyEntry.EndDateUtc < nowUtc && historyEntry.ValidPoints > 0) .OrderBy(historyEntry => historyEntry.CreatedOnUtc).ThenBy(historyEntry => historyEntry.Id).ToList(); //reduce the balance for these points foreach (var historyEntry in expiredPoints) { await InsertRewardPointsHistoryEntryAsync(new RewardPointsHistory { CustomerId = historyEntry.CustomerId, StoreId = historyEntry.StoreId, Points = -historyEntry.ValidPoints.Value, Message = string.Format(await _localizationService.GetResourceAsync("RewardPoints.Expired"), await _dateTimeHelper.ConvertToUserTimeAsync(historyEntry.CreatedOnUtc, DateTimeKind.Utc)), CreatedOnUtc = historyEntry.EndDateUtc.Value }); historyEntry.ValidPoints = 0; await UpdateRewardPointsHistoryEntryAsync(historyEntry); } //get has not yet activated points, but it's time to do it var notActivatedPoints = query .Where(historyEntry => !historyEntry.PointsBalance.HasValue && historyEntry.CreatedOnUtc < nowUtc) .OrderBy(historyEntry => historyEntry.CreatedOnUtc).ThenBy(historyEntry => historyEntry.Id).ToList(); if (!notActivatedPoints.Any()) return; //get current points balance //LINQ to entities does not support Last method, thus order by desc and use First one var currentPointsBalance = (await query .OrderByDescending(historyEntry => historyEntry.CreatedOnUtc).ThenByDescending(historyEntry => historyEntry.Id) .FirstOrDefaultAsync(historyEntry => historyEntry.PointsBalance.HasValue)) ?.PointsBalance ?? 0; //update appropriate records foreach (var historyEntry in notActivatedPoints) { currentPointsBalance += historyEntry.Points; historyEntry.PointsBalance = currentPointsBalance; await UpdateRewardPointsHistoryEntryAsync(historyEntry); } } /// /// Insert the reward point history entry /// /// Reward point history entry /// A task that represents the asynchronous operation protected virtual async Task InsertRewardPointsHistoryEntryAsync(RewardPointsHistory rewardPointsHistory) { await _rewardPointsHistoryRepository.InsertAsync(rewardPointsHistory); } #endregion #region Methods /// /// Load reward point history records /// /// Customer identifier; 0 to load all records /// Store identifier; pass null to load all records /// A value indicating whether to show reward points that did not yet activated /// Order Guid; pass null to load all record /// Page index /// Page size /// /// A task that represents the asynchronous operation /// The task result contains the reward point history records /// public virtual async Task> GetRewardPointsHistoryAsync(int customerId = 0, int? storeId = null, bool showNotActivated = false, Guid? orderGuid = null, int pageIndex = 0, int pageSize = int.MaxValue) { var query = await GetRewardPointsQueryAsync(customerId, storeId, showNotActivated); if (orderGuid.HasValue) query = query.Where(historyEntry => historyEntry.UsedWithOrder == orderGuid.Value); query = query.OrderByDescending(historyEntry => historyEntry.CreatedOnUtc) .ThenByDescending(historyEntry => historyEntry.Id); //return paged reward points history return await query.ToPagedListAsync(pageIndex, pageSize); } /// /// Gets reward points balance /// /// Customer identifier /// Store identifier /// /// A task that represents the asynchronous operation /// The task result contains the balance /// public virtual async Task GetRewardPointsBalanceAsync(int customerId, int storeId) { var query = (await GetRewardPointsQueryAsync(customerId, storeId)) .OrderByDescending(historyEntry => historyEntry.CreatedOnUtc).ThenByDescending(historyEntry => historyEntry.Id); //return point balance of the first actual history entry return (await query.FirstOrDefaultAsync())?.PointsBalance ?? 0; } /// /// Add reward points history record /// /// Customer /// Number of points to add /// Store identifier /// Message /// The order for which points were redeemed (spent) as a payment /// Used amount /// Date and time of activating reward points; pass null to immediately activating /// Date and time when the reward points will no longer be valid; pass null to add date termless points /// /// A task that represents the asynchronous operation /// The task result contains the reward points history entry identifier /// public virtual async Task AddRewardPointsHistoryEntryAsync(Customer customer, int points, int storeId, string message = "", Order usedWithOrder = null, decimal usedAmount = 0M, DateTime? activatingDate = null, DateTime? endDate = null) { ArgumentNullException.ThrowIfNull(customer); if (storeId == 0) throw new ArgumentException("Store ID should be valid"); if (points < 0 && endDate.HasValue) throw new ArgumentException("End date is available only for positive points amount"); //insert new history entry var newHistoryEntry = new RewardPointsHistory { CustomerId = customer.Id, StoreId = storeId, Points = points, PointsBalance = activatingDate.HasValue ? null : (int?)(await GetRewardPointsBalanceAsync(customer.Id, storeId) + points), UsedAmount = usedAmount, Message = message, CreatedOnUtc = activatingDate ?? DateTime.UtcNow, EndDateUtc = endDate, ValidPoints = points > 0 ? (int?)points : null, UsedWithOrder = usedWithOrder?.OrderGuid }; await InsertRewardPointsHistoryEntryAsync(newHistoryEntry); //reduce valid points of previous entries if (points >= 0) return newHistoryEntry.Id; var withValidPoints = (await GetRewardPointsQueryAsync(customer.Id, storeId)) .Where(historyEntry => historyEntry.ValidPoints > 0) .OrderBy(historyEntry => historyEntry.CreatedOnUtc).ThenBy(historyEntry => historyEntry.Id).ToList(); foreach (var historyEntry in withValidPoints) { points += historyEntry.ValidPoints.Value; historyEntry.ValidPoints = Math.Max(points, 0); await UpdateRewardPointsHistoryEntryAsync(historyEntry); if (points >= 0) break; } return newHistoryEntry.Id; } /// /// Gets a reward point history entry /// /// Reward point history entry identifier /// /// A task that represents the asynchronous operation /// The task result contains the reward point history entry /// public virtual async Task GetRewardPointsHistoryEntryByIdAsync(int rewardPointsHistoryId) { return await _rewardPointsHistoryRepository.GetByIdAsync(rewardPointsHistoryId); } /// /// Update the reward point history entry /// /// Reward point history entry /// A task that represents the asynchronous operation public virtual async Task UpdateRewardPointsHistoryEntryAsync(RewardPointsHistory rewardPointsHistory) { await _rewardPointsHistoryRepository.UpdateAsync(rewardPointsHistory); } /// /// Delete the reward point history entry /// /// Reward point history entry /// A task that represents the asynchronous operation public virtual async Task DeleteRewardPointsHistoryEntryAsync(RewardPointsHistory rewardPointsHistory) { await _rewardPointsHistoryRepository.DeleteAsync(rewardPointsHistory); } #endregion }