using Nop.Core; using Nop.Core.Domain.Catalog; using Nop.Data; using Nop.Plugin.Misc.Zettle.Domain; namespace Nop.Plugin.Misc.Zettle.Services; /// /// Represents the service to manage synchronization records /// public class ZettleRecordService { #region Fields protected readonly IRepository _categoryRepository; protected readonly IRepository _productAttributeCombinationRepository; protected readonly IRepository _productCategoryRepository; protected readonly IRepository _productRepository; protected readonly IRepository _repository; protected readonly ZettleSettings _zettleSettings; #endregion #region Ctor public ZettleRecordService(IRepository categoryRepository, IRepository productAttributeCombinationRepository, IRepository productCategoryRepository, IRepository productRepository, IRepository repository, ZettleSettings zettleSettings) { _categoryRepository = categoryRepository; _productAttributeCombinationRepository = productAttributeCombinationRepository; _productCategoryRepository = productCategoryRepository; _productRepository = productRepository; _repository = repository; _zettleSettings = zettleSettings; } #endregion #region Utilities /// /// Prepare records to add /// /// Product identifiers /// /// A task that represents the asynchronous operation /// The task result contains the prepared records; the number of products that were not added /// protected async Task<(List Records, int InvalidProducts)> PrepareRecordsToAddAsync(List productIds) { var products = await _productRepository.GetByIdsAsync(productIds, null, false); var productsWithSku = products.Where(product => !string.IsNullOrEmpty(product.Sku)).ToList(); var invalidProducts = products.Where(product => string.IsNullOrEmpty(product.Sku)).Count(); var records = await productsWithSku.SelectManyAwait(async product => { var uuid = GuidGenerator.GenerateTimeBasedGuid().ToString(); var productRecord = new ZettleRecord { Active = _zettleSettings.SyncEnabled, ProductId = product.Id, Uuid = uuid, VariantUuid = GuidGenerator.GenerateTimeBasedGuid().ToString(), PriceSyncEnabled = _zettleSettings.PriceSyncEnabled, ImageSyncEnabled = _zettleSettings.ImageSyncEnabled, InventoryTrackingEnabled = _zettleSettings.InventoryTrackingEnabled, OperationType = OperationType.Create }; var combinations = await _productAttributeCombinationRepository .GetAllAsync(query => query.Where(combination => combination.ProductId == product.Id && !string.IsNullOrEmpty(combination.Sku)), null); var combinationsRecords = combinations.Select(combination => new ZettleRecord { Active = _zettleSettings.SyncEnabled, ProductId = product.Id, CombinationId = combination.Id, Uuid = uuid, VariantUuid = GuidGenerator.GenerateTimeBasedGuid().ToString(), PriceSyncEnabled = _zettleSettings.PriceSyncEnabled, ImageSyncEnabled = _zettleSettings.ImageSyncEnabled, InventoryTrackingEnabled = _zettleSettings.InventoryTrackingEnabled, OperationType = OperationType.Create }).ToList(); return new List { productRecord }.Union(combinationsRecords); }).ToListAsync(); return (records, invalidProducts); } #endregion #region Methods /// /// Get a record by the identifier /// /// Record identifier /// /// A task that represents the asynchronous operation /// The task result contains the record for synchronization /// public async Task GetRecordByIdAsync(int id) { return await _repository.GetByIdAsync(id, null); } /// /// Insert the record /// /// Record /// A task that represents the asynchronous operation public async Task InsertRecordAsync(ZettleRecord record) { await _repository.InsertAsync(record, false); } /// /// Insert records /// /// Records /// A task that represents the asynchronous operation public async Task InsertRecordsAsync(List records) { await _repository.InsertAsync(records, false); } /// /// Update the record /// /// Record /// A task that represents the asynchronous operation public async Task UpdateRecordAsync(ZettleRecord record) { await _repository.UpdateAsync(record, false); } /// /// Update records /// /// Records /// A task that represents the asynchronous operation public async Task UpdateRecordsAsync(List records) { await _repository.UpdateAsync(records, false); } /// /// Delete the record /// /// Record /// A task that represents the asynchronous operation public async Task DeleteRecordAsync(ZettleRecord record) { await _repository.DeleteAsync(record, false); } /// /// Delete records /// /// Records identifiers /// A task that represents the asynchronous operation public async Task DeleteRecordsAsync(List ids) { await _repository.DeleteAsync(record => ids.Contains(record.Id)); } /// /// Clear all records /// /// A task that represents the asynchronous operation public async Task ClearRecordsAsync() { if (_zettleSettings.ClearRecordsOnChangeCredentials) await _repository.TruncateAsync(); else { var records = (await GetAllRecordsAsync()).ToList(); foreach (var record in records) { record.ImageUrl = string.Empty; record.UpdatedOnUtc = null; record.OperationType = OperationType.Create; } await UpdateRecordsAsync(records); } } /// /// Get all records for synchronization /// /// Whether to load only product records /// Whether to load only active records; true - active only, false - inactive only, null - all records /// Operation types; pass null to load all records /// Product unique identifier; pass null to load all records /// Page index /// Page size /// /// A task that represents the asynchronous operation /// The task result contains the records for synchronization /// public async Task> GetAllRecordsAsync(bool productOnly = false, bool? active = null, List operationTypes = null, string productUuid = null, int pageIndex = 0, int pageSize = int.MaxValue) { return await _repository.GetAllPagedAsync(query => { if (productOnly) query = query.Where(record => record.ProductId > 0 && record.CombinationId == 0); if (active.HasValue) query = query.Where(record => record.Active == active.Value); if (operationTypes?.Any() ?? false) query = query.Where(record => operationTypes.Contains((OperationType)record.OperationTypeId)); if (!string.IsNullOrEmpty(productUuid)) query = query.Where(record => record.Uuid == productUuid); query = query.OrderBy(record => record.Id); return query; }, pageIndex, pageSize); } /// /// Create or update a record for synchronization /// /// Operation type /// Product identifier /// Product attribute combination identifier /// A task that represents the asynchronous operation public async Task CreateOrUpdateRecordAsync(OperationType operationType, int productId, int attributeCombinationId = 0) { if (productId == 0 && attributeCombinationId == 0) return; var existingRecord = await _repository.Table. FirstOrDefaultAsync(record => record.ProductId == productId && record.CombinationId == attributeCombinationId); if (existingRecord is null) { if (operationType != OperationType.Create) return; if (!_zettleSettings.AutoAddRecordsEnabled) return; if (attributeCombinationId == 0 || (await _repository.Table.FirstOrDefaultAsync(record => record.ProductId == productId)) is not ZettleRecord productRecord) { var (records, _) = await PrepareRecordsToAddAsync([productId]); await InsertRecordsAsync(records); } else { await InsertRecordAsync(new() { Active = _zettleSettings.SyncEnabled, ProductId = productId, CombinationId = attributeCombinationId, Uuid = productRecord.Uuid, VariantUuid = GuidGenerator.GenerateTimeBasedGuid().ToString(), PriceSyncEnabled = _zettleSettings.PriceSyncEnabled, ImageSyncEnabled = _zettleSettings.ImageSyncEnabled, InventoryTrackingEnabled = _zettleSettings.InventoryTrackingEnabled, OperationType = operationType }); } return; } switch (existingRecord.OperationType) { case OperationType.Create: if (operationType == OperationType.Delete) await DeleteRecordAsync(existingRecord); return; case OperationType.Update: if (operationType == OperationType.Delete) { existingRecord.OperationType = OperationType.Delete; await UpdateRecordAsync(existingRecord); } return; case OperationType.Delete: if (operationType == OperationType.Create) { existingRecord.OperationType = OperationType.Update; await UpdateRecordAsync(existingRecord); } return; case OperationType.ImageChanged: case OperationType.None: existingRecord.OperationType = operationType; await UpdateRecordAsync(existingRecord); return; } } /// /// Add records for synchronization /// /// Product identifiers /// /// A task that represents the asynchronous operation /// The task result contains the number of products that were not added /// public async Task AddRecordsAsync(List productIds) { if (!productIds?.Any() ?? true) return 0; var newProductIds = productIds.Except(await _repository.Table.Select(record => record.ProductId).ToListAsync()).ToList(); if (!newProductIds.Any()) return 0; var (records, invalidProducts) = await PrepareRecordsToAddAsync(newProductIds); await InsertRecordsAsync(records); return invalidProducts; } /// /// Prepare records for synchronization /// /// Records /// Records ready for synchronization public List PrepareToSyncRecords(List records) { var recordIds = records.Select(record => record.Id).ToList(); var productToSync = _repository.Table .Where(record => recordIds.Contains(record.Id)) .Join(_productCategoryRepository.Table, record => record.ProductId, pc => pc.ProductId, (record, pc) => new { Record = record, ProductCategory = pc }) .Join(_productRepository.Table, item => item.ProductCategory.ProductId, product => product.Id, (item, product) => new { Product = product, Record = item.Record, ProductCategory = item.ProductCategory }) .Join(_categoryRepository.Table, item => item.ProductCategory.CategoryId, category => category.Id, (item, category) => new { Category = category, Product = item.Product, Record = item.Record, ProductCategory = item.ProductCategory }) .Select(item => new { Id = item.Product.Id, Uuid = item.Record.Uuid, VariantUuid = item.Record.VariantUuid, Name = item.Product.Name, Sku = item.Product.Sku, Description = item.Product.ShortDescription, Price = item.Product.Price, ProductCost = item.Product.ProductCost, CategoryName = item.Category.Name, ImageUrl = item.Record.ImageUrl, ImageSyncEnabled = item.Record.ImageSyncEnabled, PriceSyncEnabled = item.Record.PriceSyncEnabled, ProductCategoryId = item.ProductCategory.Id, ProductCategoryDisplayOrder = item.ProductCategory.DisplayOrder }) .GroupBy(item => item.Id) .Select(group => new ProductToSync { Id = group.Key, Uuid = group.FirstOrDefault().Uuid, VariantUuid = group.FirstOrDefault().VariantUuid, Name = group.FirstOrDefault().Name, Sku = group.FirstOrDefault().Sku, Description = group.FirstOrDefault().Description, Price = group.FirstOrDefault().Price, ProductCost = group.FirstOrDefault().ProductCost, CategoryName = group .OrderBy(item => item.ProductCategoryDisplayOrder) .ThenBy(item => item.ProductCategoryId) .Select(item => item.CategoryName) .FirstOrDefault(), ImageUrl = group.FirstOrDefault().ImageUrl, ImageSyncEnabled = group.FirstOrDefault().ImageSyncEnabled, PriceSyncEnabled = group.FirstOrDefault().PriceSyncEnabled }) .ToList(); return productToSync; } #endregion }