396 lines
15 KiB
C#
396 lines
15 KiB
C#
using Nop.Core;
|
|
using Nop.Core.Domain.Catalog;
|
|
using Nop.Data;
|
|
using Nop.Plugin.Misc.Zettle.Domain;
|
|
|
|
namespace Nop.Plugin.Misc.Zettle.Services;
|
|
|
|
/// <summary>
|
|
/// Represents the service to manage synchronization records
|
|
/// </summary>
|
|
public class ZettleRecordService
|
|
{
|
|
#region Fields
|
|
|
|
protected readonly IRepository<Category> _categoryRepository;
|
|
protected readonly IRepository<ProductAttributeCombination> _productAttributeCombinationRepository;
|
|
protected readonly IRepository<ProductCategory> _productCategoryRepository;
|
|
protected readonly IRepository<Product> _productRepository;
|
|
protected readonly IRepository<ZettleRecord> _repository;
|
|
protected readonly ZettleSettings _zettleSettings;
|
|
|
|
#endregion
|
|
|
|
#region Ctor
|
|
|
|
public ZettleRecordService(IRepository<Category> categoryRepository,
|
|
IRepository<ProductAttributeCombination> productAttributeCombinationRepository,
|
|
IRepository<ProductCategory> productCategoryRepository,
|
|
IRepository<Product> productRepository,
|
|
IRepository<ZettleRecord> repository,
|
|
ZettleSettings zettleSettings)
|
|
{
|
|
_categoryRepository = categoryRepository;
|
|
_productAttributeCombinationRepository = productAttributeCombinationRepository;
|
|
_productCategoryRepository = productCategoryRepository;
|
|
_productRepository = productRepository;
|
|
_repository = repository;
|
|
_zettleSettings = zettleSettings;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Utilities
|
|
|
|
/// <summary>
|
|
/// Prepare records to add
|
|
/// </summary>
|
|
/// <param name="productIds">Product identifiers</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the prepared records; the number of products that were not added
|
|
/// </returns>
|
|
protected async Task<(List<ZettleRecord> Records, int InvalidProducts)> PrepareRecordsToAddAsync(List<int> 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<ZettleRecord> { productRecord }.Union(combinationsRecords);
|
|
}).ToListAsync();
|
|
|
|
return (records, invalidProducts);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Methods
|
|
|
|
/// <summary>
|
|
/// Get a record by the identifier
|
|
/// </summary>
|
|
/// <param name="id">Record identifier</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the record for synchronization
|
|
/// </returns>
|
|
public async Task<ZettleRecord> GetRecordByIdAsync(int id)
|
|
{
|
|
return await _repository.GetByIdAsync(id, null);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Insert the record
|
|
/// </summary>
|
|
/// <param name="record">Record</param>
|
|
/// <returns>A task that represents the asynchronous operation</returns>
|
|
public async Task InsertRecordAsync(ZettleRecord record)
|
|
{
|
|
await _repository.InsertAsync(record, false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Insert records
|
|
/// </summary>
|
|
/// <param name="records">Records</param>
|
|
/// <returns>A task that represents the asynchronous operation</returns>
|
|
public async Task InsertRecordsAsync(List<ZettleRecord> records)
|
|
{
|
|
await _repository.InsertAsync(records, false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update the record
|
|
/// </summary>
|
|
/// <param name="record">Record</param>
|
|
/// <returns>A task that represents the asynchronous operation</returns>
|
|
public async Task UpdateRecordAsync(ZettleRecord record)
|
|
{
|
|
await _repository.UpdateAsync(record, false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update records
|
|
/// </summary>
|
|
/// <param name="records">Records</param>
|
|
/// <returns>A task that represents the asynchronous operation</returns>
|
|
public async Task UpdateRecordsAsync(List<ZettleRecord> records)
|
|
{
|
|
await _repository.UpdateAsync(records, false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Delete the record
|
|
/// </summary>
|
|
/// <param name="record">Record</param>
|
|
/// <returns>A task that represents the asynchronous operation</returns>
|
|
public async Task DeleteRecordAsync(ZettleRecord record)
|
|
{
|
|
await _repository.DeleteAsync(record, false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Delete records
|
|
/// </summary>
|
|
/// <param name="ids">Records identifiers</param>
|
|
/// <returns>A task that represents the asynchronous operation</returns>
|
|
public async Task DeleteRecordsAsync(List<int> ids)
|
|
{
|
|
await _repository.DeleteAsync(record => ids.Contains(record.Id));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clear all records
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation</returns>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get all records for synchronization
|
|
/// </summary>
|
|
/// <param name="productOnly">Whether to load only product records</param>
|
|
/// <param name="active">Whether to load only active records; true - active only, false - inactive only, null - all records</param>
|
|
/// <param name="operationTypes">Operation types; pass null to load all records</param>
|
|
/// <param name="productUuid">Product unique identifier; pass null to load all 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 records for synchronization
|
|
/// </returns>
|
|
public async Task<IPagedList<ZettleRecord>> GetAllRecordsAsync(bool productOnly = false,
|
|
bool? active = null, List<OperationType> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create or update a record for synchronization
|
|
/// </summary>
|
|
/// <param name="operationType">Operation type</param>
|
|
/// <param name="productId">Product identifier</param>
|
|
/// <param name="attributeCombinationId">Product attribute combination identifier</param>
|
|
/// <returns>A task that represents the asynchronous operation</returns>
|
|
public async Task CreateOrUpdateRecordAsync(OperationType operationType, int productId, int attributeCombinationId = 0)
|
|
{
|
|
if (productId == 0 && attributeCombinationId == 0)
|
|
return;
|
|
|
|
var existingRecord = _repository.Table.
|
|
FirstOrDefault(record => record.ProductId == productId && record.CombinationId == attributeCombinationId);
|
|
|
|
if (existingRecord is null)
|
|
{
|
|
if (operationType != OperationType.Create)
|
|
return;
|
|
|
|
if (!_zettleSettings.AutoAddRecordsEnabled)
|
|
return;
|
|
|
|
if (attributeCombinationId == 0 || _repository.Table.FirstOrDefault(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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add records for synchronization
|
|
/// </summary>
|
|
/// <param name="productIds">Product identifiers</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation
|
|
/// The task result contains the number of products that were not added
|
|
/// </returns>
|
|
public async Task<int> AddRecordsAsync(List<int> productIds)
|
|
{
|
|
if (!productIds?.Any() ?? true)
|
|
return 0;
|
|
|
|
var newProductIds = productIds.Except(_repository.Table.Select(record => record.ProductId)).ToList();
|
|
|
|
if (!newProductIds.Any())
|
|
return 0;
|
|
|
|
var (records, invalidProducts) = await PrepareRecordsToAddAsync(newProductIds);
|
|
await InsertRecordsAsync(records);
|
|
|
|
return invalidProducts;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Prepare records for synchronization
|
|
/// </summary>
|
|
/// <param name="records">Records</param>
|
|
/// <returns>Records ready for synchronization</returns>
|
|
public List<ProductToSync> PrepareToSyncRecords(List<ZettleRecord> 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
|
|
} |