using Microsoft.AspNetCore.Mvc; using Nop.Core; using Nop.Core.Domain.Catalog; using Nop.Core.Domain.Directory; using Nop.Core.Domain.Tax; using Nop.Plugin.Misc.Zettle.Domain; using Nop.Plugin.Misc.Zettle.Models; using Nop.Plugin.Misc.Zettle.Services; using Nop.Services.Catalog; using Nop.Services.Configuration; using Nop.Services.Directory; using Nop.Services.Helpers; using Nop.Services.Localization; using Nop.Services.Messages; using Nop.Services.ScheduleTasks; using Nop.Services.Security; using Nop.Web.Areas.Admin.Factories; using Nop.Web.Areas.Admin.Models.Catalog; using Nop.Web.Framework; using Nop.Web.Framework.Controllers; using Nop.Web.Framework.Models.Extensions; using Nop.Web.Framework.Mvc; using Nop.Web.Framework.Mvc.Filters; namespace Nop.Plugin.Misc.Zettle.Controllers; [Area(AreaNames.ADMIN)] [AuthorizeAdmin] [AutoValidateAntiforgeryToken] public class ZettleAdminController : BasePluginController { #region Fields protected readonly CurrencySettings _currencySettings; protected readonly IBaseAdminModelFactory _baseAdminModelFactory; protected readonly ICurrencyService _currencyService; protected readonly IDateTimeHelper _dateTimeHelper; protected readonly ILocalizationService _localizationService; protected readonly INotificationService _notificationService; protected readonly IPermissionService _permissionService; protected readonly IProductService _productService; protected readonly IScheduleTaskService _scheduleTaskService; protected readonly ISettingService _settingService; protected readonly IStoreContext _storeContext; protected readonly TaxSettings _taxSettings; protected readonly ZettleRecordService _zettleRecordService; protected readonly ZettleService _zettleService; protected readonly ZettleSettings _zettleSettings; #endregion #region Ctor public ZettleAdminController(CurrencySettings currencySettings, IBaseAdminModelFactory baseAdminModelFactory, ICurrencyService currencyService, IDateTimeHelper dateTimeHelper, ILocalizationService localizationService, INotificationService notificationService, IPermissionService permissionService, IProductService productService, IScheduleTaskService scheduleTaskService, ISettingService settingService, IStoreContext storeContext, TaxSettings taxSettings, ZettleRecordService zettleRecordService, ZettleService zettleService, ZettleSettings zettleSettings) { _currencySettings = currencySettings; _baseAdminModelFactory = baseAdminModelFactory; _currencyService = currencyService; _dateTimeHelper = dateTimeHelper; _localizationService = localizationService; _notificationService = notificationService; _permissionService = permissionService; _productService = productService; _scheduleTaskService = scheduleTaskService; _settingService = settingService; _storeContext = storeContext; _taxSettings = taxSettings; _zettleRecordService = zettleRecordService; _zettleService = zettleService; _zettleSettings = zettleSettings; } #endregion #region Methods #region Configuration [CheckPermission(StandardPermission.Configuration.MANAGE_PLUGINS)] public async Task Configure() { var model = new ConfigurationModel { ClientId = _zettleSettings.ClientId, ApiKey = _zettleSettings.ApiKey, DisconnectOnUninstall = _zettleSettings.DisconnectOnUninstall, AutoSyncEnabled = _zettleSettings.AutoSyncEnabled, AutoSyncPeriod = _zettleSettings.AutoSyncPeriod, DeleteBeforeImport = _zettleSettings.DeleteBeforeImport, SyncEnabled = _zettleSettings.SyncEnabled, PriceSyncEnabled = _zettleSettings.PriceSyncEnabled, ImageSyncEnabled = _zettleSettings.ImageSyncEnabled, InventoryTrackingEnabled = _zettleSettings.InventoryTrackingEnabled, DefaultTaxEnabled = _zettleSettings.DefaultTaxEnabled, DiscountSyncEnabled = _zettleSettings.DiscountSyncEnabled, }; if (ZettleService.IsConfigured(_zettleSettings)) { //account info var (accountInfo, error) = await _zettleService.GetAccountInfoAsync(); if (!string.IsNullOrEmpty(error) || accountInfo is null) { var locale = await _localizationService.GetResourceAsync("Plugins.Misc.Zettle.Configuration.Error"); var errorMessage = string.Format(locale, error, Url.Action("List", "Log")); _notificationService.ErrorNotification(errorMessage, false); return View("~/Plugins/Misc.Zettle/Views/Configure.cshtml", model); } model.Connected = true; model.Account.Name = accountInfo.Name; model.Account.CustomerStatus = accountInfo.CustomerStatus?.ToLower() ?? "undefined"; model.Account.Accepted = string.Equals(accountInfo.CustomerStatus, "ACCEPTED", StringComparison.InvariantCultureIgnoreCase); //ensure the same currencies are used var storeCurrency = await _currencyService.GetCurrencyByIdAsync(_currencySettings.PrimaryStoreCurrencyId); if (string.IsNullOrEmpty(accountInfo.Currency) || !accountInfo.Currency.Equals(storeCurrency.CurrencyCode, StringComparison.InvariantCultureIgnoreCase)) { var locale = await _localizationService.GetResourceAsync("Plugins.Misc.Zettle.Account.Fields.Currency.Warning"); var warning = string.Format(locale, storeCurrency.CurrencyCode, accountInfo.Currency, Url.Action("List", "Currency")); _notificationService.WarningNotification(warning, false); } model.Account.Currency = accountInfo.Currency; //ensure the same tax types are used var taxNone = string.Equals(accountInfo.TaxationType, "NONE", StringComparison.InvariantCultureIgnoreCase); var taxVat = string.Equals(accountInfo.TaxationType, "VAT", StringComparison.InvariantCultureIgnoreCase); var taxRates = string.Equals(accountInfo.TaxationType, "SALES_TAX", StringComparison.InvariantCultureIgnoreCase); if (_taxSettings.EuVatEnabled != taxVat) { var locale = await _localizationService.GetResourceAsync("Plugins.Misc.Zettle.Account.Fields.TaxationType.Vat.Warning"); var warning = string.Format(locale, Url.Action("Tax", "Setting")); _notificationService.WarningNotification(warning, false); } model.Account.TaxationType = accountInfo.TaxationType?.Replace('_', ' '); if (taxVat) { _zettleSettings.DefaultTaxEnabled = true; if (accountInfo.VatPercentage.HasValue) model.Account.TaxationType = $"{model.Account.TaxationType} ({accountInfo.VatPercentage}%)"; } if (taxRates) { var (percentage, _) = await _zettleService.GetDefaultTaxRateAsync(); if (percentage.HasValue) model.Account.TaxationType = $"{model.Account.TaxationType} ({percentage}% by default)"; else { var warning = await _localizationService.GetResourceAsync("Plugins.Misc.Zettle.Account.Fields.TaxationType.SalesTax.Warning"); _notificationService.WarningNotification(warning); } } //ensure the same price types are used var pricesIncludeTax = string.Equals(accountInfo.TaxationMode, "INCLUSIVE", StringComparison.InvariantCultureIgnoreCase); if (_taxSettings.PricesIncludeTax != pricesIncludeTax || (_taxSettings.PricesIncludeTax && taxNone)) { var locale = await _localizationService.GetResourceAsync("Plugins.Misc.Zettle.Account.Fields.TaxationMode.Warning"); var warning = string.Format(locale, Url.Action("Tax", "Setting")); _notificationService.WarningNotification(warning, false); } model.Account.TaxationMode = accountInfo.TaxationMode; //ensure the webhook is created var store = await _storeContext.GetCurrentStoreAsync(); var webhookUrl = $"{store.Url.TrimEnd('/')}{Url.RouteUrl(ZettleDefaults.WebhookRouteName)}".ToLowerInvariant(); var (webhook, _) = await _zettleService.CreateWebhookAsync(webhookUrl); _zettleSettings.WebhookUrl = webhook?.Destination; _zettleSettings.WebhookKey = webhook?.SigningKey; await _settingService.SaveSettingAsync(_zettleSettings); if (string.IsNullOrEmpty(_zettleSettings.WebhookUrl)) { var locale = await _localizationService.GetResourceAsync("Plugins.Misc.Zettle.Configuration.Webhook.Warning"); var warning = string.Format(locale, Url.Action("List", "Log")); _notificationService.WarningNotification(warning, false); } //last import details if (!string.IsNullOrEmpty(_zettleSettings.ImportId)) { var (import, _) = await _zettleService.GetImportAsync(); if (import is not null) { model.Import.StartDate = import.Created; model.Import.EndDate = import.Finished; model.Import.State = import.State?.Replace('_', ' '); model.Import.Items = import.Items?.ToString(); model.Import.Active = string.Equals(import.State, "IMPORTING", StringComparison.InvariantCultureIgnoreCase); } } } var scheduleTask = await _scheduleTaskService.GetTaskByTypeAsync(ZettleDefaults.SynchronizationTask.Type); if (scheduleTask is not null) { model.AutoSyncEnabled = scheduleTask.Enabled; model.AutoSyncPeriod = scheduleTask.Seconds / 60; } return View("~/Plugins/Misc.Zettle/Views/Configure.cshtml", model); } [HttpPost, ActionName("Configure")] [FormValueRequired("credentials")] [CheckPermission(StandardPermission.Configuration.MANAGE_PLUGINS)] public async Task SaveCredentials(ConfigurationModel model) { if (!ModelState.IsValid) return await Configure(); if (!model.ClientId?.Equals(_zettleSettings.ClientId) ?? true) { //credentials are changed await _zettleRecordService.ClearRecordsAsync(); _zettleSettings.WebhookUrl = string.Empty; _zettleSettings.WebhookKey = string.Empty; _zettleSettings.ImportId = string.Empty; if (ZettleService.IsConfigured(_zettleSettings)) { if (_zettleSettings.DisconnectOnUninstall) await _zettleService.DisconnectAsync(); else if (!string.IsNullOrEmpty(_zettleSettings.WebhookUrl)) await _zettleService.DeleteWebhookAsync(); } } _zettleSettings.ClientId = model.ClientId; _zettleSettings.ApiKey = model.ApiKey; _zettleSettings.DisconnectOnUninstall = model.DisconnectOnUninstall; await _settingService.SaveSettingAsync(_zettleSettings); _notificationService.SuccessNotification(await _localizationService.GetResourceAsync("Admin.Plugins.Saved")); return await Configure(); } [HttpPost, ActionName("Configure")] [FormValueRequired("sync")] [CheckPermission(StandardPermission.Configuration.MANAGE_PLUGINS)] public async Task SaveSync(ConfigurationModel model) { if (!ModelState.IsValid) return await Configure(); _zettleSettings.AutoSyncEnabled = model.AutoSyncEnabled; _zettleSettings.AutoSyncPeriod = model.AutoSyncPeriod; _zettleSettings.DeleteBeforeImport = model.DeleteBeforeImport; _zettleSettings.SyncEnabled = model.SyncEnabled; _zettleSettings.PriceSyncEnabled = model.PriceSyncEnabled; _zettleSettings.ImageSyncEnabled = model.ImageSyncEnabled; _zettleSettings.InventoryTrackingEnabled = model.InventoryTrackingEnabled; _zettleSettings.DefaultTaxEnabled = model.DefaultTaxEnabled; _zettleSettings.DiscountSyncEnabled = model.DiscountSyncEnabled; await _settingService.SaveSettingAsync(_zettleSettings); var scheduleTask = await _scheduleTaskService.GetTaskByTypeAsync(ZettleDefaults.SynchronizationTask.Type); if (scheduleTask is not null) { if (!scheduleTask.Enabled && _zettleSettings.AutoSyncEnabled) scheduleTask.LastEnabledUtc = DateTime.UtcNow; scheduleTask.Enabled = _zettleSettings.AutoSyncEnabled; scheduleTask.Seconds = _zettleSettings.AutoSyncPeriod * 60; await _scheduleTaskService.UpdateTaskAsync(scheduleTask); } _notificationService.SuccessNotification(await _localizationService.GetResourceAsync("Admin.Plugins.Saved")); return await Configure(); } [HttpPost, ActionName("Configure")] [FormValueRequired("revoke")] [CheckPermission(StandardPermission.Configuration.MANAGE_PLUGINS)] public async Task RevokeAccess() { if (!ZettleService.IsConfigured(_zettleSettings)) return await Configure(); if (!string.IsNullOrEmpty(_zettleSettings.WebhookUrl)) await _zettleService.DeleteWebhookAsync(); var (_, error) = await _zettleService.DisconnectAsync(); if (!string.IsNullOrEmpty(error)) { var locale = await _localizationService.GetResourceAsync("Plugins.Misc.Zettle.Configuration.Error"); var errorMessage = string.Format(locale, error, Url.Action("List", "Log")); _notificationService.ErrorNotification(errorMessage, false); return await Configure(); } await _zettleRecordService.ClearRecordsAsync(); _zettleSettings.ClientId = string.Empty; _zettleSettings.ApiKey = string.Empty; _zettleSettings.WebhookUrl = string.Empty; _zettleSettings.WebhookKey = string.Empty; _zettleSettings.ImportId = string.Empty; await _settingService.SaveSettingAsync(_zettleSettings); _notificationService.SuccessNotification(await _localizationService.GetResourceAsync("Plugins.Misc.Zettle.Credentials.AccessRevoked")); return await Configure(); } #endregion #region Synchronization [HttpPost] [CheckPermission(StandardPermission.Configuration.MANAGE_PLUGINS)] public async Task SyncRecordList(SyncRecordSearchModel searchModel) { var records = await _zettleRecordService.GetAllRecordsAsync(productOnly: true, pageIndex: searchModel.Page - 1, pageSize: searchModel.PageSize); var products = await _productService.GetProductsByIdsAsync(records.Select(record => record.ProductId).Distinct().ToArray()); var model = await new SyncRecordListModel().PrepareToGridAsync(searchModel, records, () => { return records.SelectAwait(async record => new SyncRecordModel { Id = record.Id, Active = record.Active, ProductId = record.ProductId, ProductName = products.FirstOrDefault(product => product.Id == record.ProductId)?.Name ?? "Not found", PriceSyncEnabled = record.PriceSyncEnabled, ImageSyncEnabled = record.ImageSyncEnabled, InventoryTrackingEnabled = record.InventoryTrackingEnabled, UpdatedDate = record.UpdatedOnUtc.HasValue ? await _dateTimeHelper.ConvertToUserTimeAsync(record.UpdatedOnUtc.Value, DateTimeKind.Utc) : null }); }); return Json(model); } [HttpPost] [CheckPermission(StandardPermission.Configuration.MANAGE_PLUGINS)] public async Task SyncRecordUpdate(SyncRecordModel model) { var productRecord = await _zettleRecordService.GetRecordByIdAsync(model.Id) ?? throw new ArgumentException("No record found"); var records = (await _zettleRecordService.GetAllRecordsAsync(productUuid: productRecord.Uuid)).ToList(); foreach (var record in records) { record.Active = model.Active; record.PriceSyncEnabled = model.PriceSyncEnabled; record.ImageSyncEnabled = model.ImageSyncEnabled; record.InventoryTrackingEnabled = model.InventoryTrackingEnabled; record.OperationType = OperationType.Update; } await _zettleRecordService.UpdateRecordsAsync(records); return new NullJsonResult(); } [HttpPost] [CheckPermission(StandardPermission.Configuration.MANAGE_PLUGINS)] public async Task SyncRecordDelete(ICollection selectedIds) { if (!selectedIds?.Any() ?? true) return NoContent(); foreach (var id in selectedIds) { var productRecord = await _zettleRecordService.GetRecordByIdAsync(id); if (productRecord is null) continue; var records = (await _zettleRecordService.GetAllRecordsAsync(productUuid: productRecord.Uuid)).ToList(); await _zettleRecordService.DeleteRecordsAsync(records.Select(record => record.Id).ToList()); } return new NullJsonResult(); } [CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)] [CheckPermission(StandardPermission.Configuration.MANAGE_PLUGINS)] public async Task ProductToSync() { var model = new AddProductToSyncSearchModel(); await _baseAdminModelFactory.PrepareProductTypesAsync(model.AvailableProductTypes); await _baseAdminModelFactory.PrepareCategoriesAsync(model.AvailableCategories); await _baseAdminModelFactory.PrepareManufacturersAsync(model.AvailableManufacturers); await _baseAdminModelFactory.PrepareStoresAsync(model.AvailableStores); await _baseAdminModelFactory.PrepareVendorsAsync(model.AvailableVendors); model.SetPopupGridPageSize(); return View("~/Plugins/Misc.Zettle/Views/ProductToSync.cshtml", model); } [HttpPost] [CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)] [CheckPermission(StandardPermission.Configuration.MANAGE_PLUGINS)] public async Task ProductListToSync(AddProductToSyncSearchModel searchModel) { var products = await _productService.SearchProductsAsync(showHidden: true, keywords: searchModel.SearchProductName, productType: searchModel.SearchProductTypeId > 0 ? (ProductType?)searchModel.SearchProductTypeId : null, categoryIds: new List { searchModel.SearchCategoryId }, manufacturerIds: new List { searchModel.SearchManufacturerId }, storeId: searchModel.SearchStoreId, vendorId: searchModel.SearchVendorId, pageIndex: searchModel.Page - 1, pageSize: searchModel.PageSize); var model = new AddProductToSyncListModel().PrepareToGrid(searchModel, products, () => { return products.Select(product => new ProductModel { Id = product.Id, Name = product.Name, Sku = product.Sku, Price = product.Price, Published = product.Published }).ToList(); }); return Json(model); } [HttpPost] [FormValueRequired("save")] [CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)] [CheckPermission(StandardPermission.Configuration.MANAGE_PLUGINS)] public async Task ProductToSync(AddProductToSyncModel model) { var invalidProducts = await _zettleRecordService.AddRecordsAsync(model.SelectedProductIds?.ToList()); if (invalidProducts > 0) { var warning = await _localizationService.GetResourceAsync("Plugins.Misc.Zettle.Sync.AddProduct.Warning"); _notificationService.WarningNotification(string.Format(warning, invalidProducts)); } else _notificationService.SuccessNotification(await _localizationService.GetResourceAsync("Plugins.Misc.Zettle.Sync.AddProduct.Success")); ViewBag.RefreshPage = true; return View("~/Plugins/Misc.Zettle/Views/ProductToSync.cshtml", new AddProductToSyncSearchModel()); } [CheckPermission(StandardPermission.Configuration.MANAGE_PLUGINS)] public async Task SyncStart() { await _zettleService.ImportAsync(); return new NullJsonResult(); } [CheckPermission(StandardPermission.Configuration.MANAGE_PLUGINS)] public async Task SyncUpdate() { if (string.IsNullOrEmpty(_zettleSettings.ImportId)) return ErrorJson("Synchronization not found"); var (import, _) = await _zettleService.GetImportAsync(); if (string.IsNullOrEmpty(import?.State)) return ErrorJson("Synchronization error"); if (string.Equals(import.State, "IMPORTING", StringComparison.InvariantCultureIgnoreCase)) return Json(new { StartDate = import.Created, Items = import.Items?.ToString(), State = import.State }); return Json(new { Completed = true }); } #endregion #endregion }