diff --git a/src/Libraries/Nop.Core/Domain/Orders/OrderSettings.cs b/src/Libraries/Nop.Core/Domain/Orders/OrderSettings.cs index 5eb987ac3fd..99564e03c9c 100644 --- a/src/Libraries/Nop.Core/Domain/Orders/OrderSettings.cs +++ b/src/Libraries/Nop.Core/Domain/Orders/OrderSettings.cs @@ -202,4 +202,24 @@ public partial class OrderSettings : ISettings /// Gets or sets a value indicating whether to allow customers to cancel orders /// public bool AllowCustomersCancelOrders { get; set; } + + /// + /// Gets or sets a value indicating whether to automatically cancel unpaid orders + /// + public bool AutoCancelUnpaidOrdersEnabled { get; set; } + + /// + /// Gets or sets the delay (in minutes) after which unpaid orders should be automatically cancelled + /// + public int AutoCancelUnpaidOrdersDelay { get; set; } = 600; + + /// + /// Gets or sets a comma-separated list of payment method system names to ignore when auto-cancelling unpaid orders + /// + public string IgnorePaymentMethods { get; set; } + + /// + /// Gets or sets a value indicating whether to restore shopping cart items after automatic order cancellation + /// + public bool RestoreCartAfterCancellation { get; set; } } \ No newline at end of file diff --git a/src/Libraries/Nop.Services/Installation/InstallRequiredData.cs b/src/Libraries/Nop.Services/Installation/InstallRequiredData.cs index 07bb19965ed..4efe9633bdb 100644 --- a/src/Libraries/Nop.Services/Installation/InstallRequiredData.cs +++ b/src/Libraries/Nop.Services/Installation/InstallRequiredData.cs @@ -3359,6 +3359,15 @@ protected virtual async Task InstallScheduleTasksAsync() Type = "Nop.Services.Gdpr.DeleteInactiveCustomersTask, Nop.Services", Enabled = false, StopOnError = false + }, + new() { + Name = "Cancel unpaid orders", + //60 minutes + Seconds = 3600, + Type = "Nop.Services.Orders.CancelUnpaidOrdersTask, Nop.Services", + Enabled = true, + LastEnabledUtc = lastEnabledUtc, + StopOnError = false } }; diff --git a/src/Libraries/Nop.Services/Orders/CancelUnpaidOrdersTask.cs b/src/Libraries/Nop.Services/Orders/CancelUnpaidOrdersTask.cs new file mode 100644 index 00000000000..fb25bd293a1 --- /dev/null +++ b/src/Libraries/Nop.Services/Orders/CancelUnpaidOrdersTask.cs @@ -0,0 +1,169 @@ +using Nop.Core.Domain.Orders; +using Nop.Core.Domain.Payments; +using Nop.Services.Catalog; +using Nop.Services.Customers; +using Nop.Services.Logging; +using Nop.Services.ScheduleTasks; + +namespace Nop.Services.Orders; + +/// +/// Represents a task for automatically cancelling unpaid orders +/// +public partial class CancelUnpaidOrdersTask : IScheduleTask +{ + #region Fields + + protected readonly ICustomerService _customerService; + protected readonly ILogger _logger; + protected readonly IOrderProcessingService _orderProcessingService; + protected readonly IOrderService _orderService; + protected readonly IProductService _productService; + protected readonly IShoppingCartService _shoppingCartService; + protected readonly OrderSettings _orderSettings; + + #endregion + + #region Ctor + + public CancelUnpaidOrdersTask(ICustomerService customerService, + ILogger logger, + IOrderProcessingService orderProcessingService, + IOrderService orderService, + IProductService productService, + IShoppingCartService shoppingCartService, + OrderSettings orderSettings) + { + _customerService = customerService; + _logger = logger; + _orderProcessingService = orderProcessingService; + _orderService = orderService; + _productService = productService; + _shoppingCartService = shoppingCartService; + _orderSettings = orderSettings; + } + + #endregion + + #region Methods + + /// + /// Executes a task + /// + public virtual async Task ExecuteAsync() + { + // Exit if the feature is disabled + if (!_orderSettings.AutoCancelUnpaidOrdersEnabled) + return; + + // Calculate the cutoff time + var delay = _orderSettings.AutoCancelUnpaidOrdersDelay; + if (delay <= 0) + delay = 600; // Default to 600 minutes if invalid + + var cutoffTimeUtc = DateTime.UtcNow.AddMinutes(-delay); + + // Parse ignored payment methods + var ignoredPaymentMethods = new List(); + if (!string.IsNullOrWhiteSpace(_orderSettings.IgnorePaymentMethods)) + { + ignoredPaymentMethods = _orderSettings.IgnorePaymentMethods + .Split(',', StringSplitOptions.RemoveEmptyEntries) + .Select(x => x.Trim()) + .Where(x => !string.IsNullOrEmpty(x)) + .ToList(); + } + + // Search for orders with pending payment status created before the cutoff time + var pendingOrders = await _orderService.SearchOrdersAsync( + createdToUtc: cutoffTimeUtc, + psIds: new List { (int)PaymentStatus.Pending }, + osIds: null, // Don't filter by order status initially + pageIndex: 0, + pageSize: int.MaxValue); + + var cancelledCount = 0; + var failedCount = 0; + + foreach (var order in pendingOrders) + { + try + { + // Skip already cancelled orders + if (order.OrderStatus == OrderStatus.Cancelled) + continue; + + // Skip orders with ignored payment methods + if (ignoredPaymentMethods.Any() && + !string.IsNullOrWhiteSpace(order.PaymentMethodSystemName) && + ignoredPaymentMethods.Contains(order.PaymentMethodSystemName, StringComparer.OrdinalIgnoreCase)) + continue; + + // Cancel the order and notify the customer + await _orderProcessingService.CancelOrderAsync(order, notifyCustomer: true); + + // Restore shopping cart if enabled + if (_orderSettings.RestoreCartAfterCancellation) + { + await RestoreShoppingCartAsync(order); + } + + cancelledCount++; + } + catch (Exception ex) + { + failedCount++; + await _logger.ErrorAsync($"Error cancelling unpaid order #{order.Id}", ex); + } + } + + // Log summary + if (cancelledCount > 0 || failedCount > 0) + { + await _logger.InformationAsync($"Auto-cancel unpaid orders task completed. Cancelled: {cancelledCount}, Failed: {failedCount}"); + } + } + + /// + /// Restores shopping cart items from a cancelled order + /// + /// The cancelled order + /// A task that represents the asynchronous operation + protected virtual async Task RestoreShoppingCartAsync(Order order) + { + var customer = await _customerService.GetCustomerByIdAsync(order.CustomerId); + if (customer == null) + return; + + var orderItems = await _orderService.GetOrderItemsAsync(order.Id); + + foreach (var orderItem in orderItems) + { + try + { + var product = await _productService.GetProductByIdAsync(orderItem.ProductId); + if (product == null || product.Deleted) + continue; + + // Add the product back to the shopping cart + await _shoppingCartService.AddToCartAsync( + customer: customer, + product: product, + shoppingCartType: ShoppingCartType.ShoppingCart, + storeId: order.StoreId, + attributesXml: orderItem.AttributesXml, + quantity: orderItem.Quantity, + customerEnteredPrice: orderItem.UnitPriceInclTax, + rentalStartDate: orderItem.RentalStartDateUtc, + rentalEndDate: orderItem.RentalEndDateUtc); + } + catch (Exception ex) + { + // Log the error but continue processing other items + await _logger.WarningAsync($"Failed to restore order item #{orderItem.Id} to shopping cart for customer #{customer.Id}", ex); + } + } + } + + #endregion +} diff --git a/src/Presentation/Nop.Web.Framework/Migrations/UpgradeTo510/AutoCancelUnpaidOrdersLocalization.cs b/src/Presentation/Nop.Web.Framework/Migrations/UpgradeTo510/AutoCancelUnpaidOrdersLocalization.cs new file mode 100644 index 00000000000..aea728c0d81 --- /dev/null +++ b/src/Presentation/Nop.Web.Framework/Migrations/UpgradeTo510/AutoCancelUnpaidOrdersLocalization.cs @@ -0,0 +1,39 @@ +using FluentMigrator; +using Nop.Data; +using Nop.Data.Migrations; +using Nop.Web.Framework.Extensions; + +namespace Nop.Web.Framework.Migrations.UpgradeTo510; + +[NopUpdateMigration("2026-04-11 00:00:01", "5.10", UpdateMigrationType.Localization)] +public class AutoCancelUnpaidOrdersLocalization : MigrationBase +{ + /// Collect the UP migration expressions + public override void Up() + { + if (!DataSettingsManager.IsDatabaseInstalled()) + return; + + //add localization resources for auto-cancel unpaid orders feature + this.AddOrUpdateLocaleResource(new Dictionary + { + ["Admin.Configuration.Settings.Order.AutoCancelUnpaidOrdersEnabled"] = "Enable auto-cancel unpaid orders", + ["Admin.Configuration.Settings.Order.AutoCancelUnpaidOrdersEnabled.Hint"] = "Check to enable automatic cancellation of unpaid orders after a specified delay.", + + ["Admin.Configuration.Settings.Order.AutoCancelUnpaidOrdersDelay"] = "Auto-cancel delay (minutes)", + ["Admin.Configuration.Settings.Order.AutoCancelUnpaidOrdersDelay.Hint"] = "Specify the delay in minutes after which unpaid orders should be automatically cancelled. Default is 600 minutes (10 hours).", + + ["Admin.Configuration.Settings.Order.IgnorePaymentMethods"] = "Ignore payment methods", + ["Admin.Configuration.Settings.Order.IgnorePaymentMethods.Hint"] = "Enter comma-separated payment method system names to exclude from auto-cancellation (e.g., \"Payments.CheckMoneyOrder,Payments.Manual\"). Orders using these payment methods will not be automatically cancelled.", + + ["Admin.Configuration.Settings.Order.RestoreCartAfterCancellation"] = "Restore cart after cancellation", + ["Admin.Configuration.Settings.Order.RestoreCartAfterCancellation.Hint"] = "Check to automatically restore shopping cart items when an order is cancelled by the auto-cancel task." + }); + } + + /// Collects the DOWN migration expressions + public override void Down() + { + //nothing + } +} diff --git a/src/Presentation/Nop.Web/App_Data/Localization/defaultResources.nopres.xml b/src/Presentation/Nop.Web/App_Data/Localization/defaultResources.nopres.xml index d17e00c2bae..cbf76dee6fd 100644 --- a/src/Presentation/Nop.Web/App_Data/Localization/defaultResources.nopres.xml +++ b/src/Presentation/Nop.Web/App_Data/Localization/defaultResources.nopres.xml @@ -8425,6 +8425,30 @@ Check to automatically update order totals on editing an order in admin area. IMPORANT: currently this functionality is in BETA testing status. + + Enable auto-cancel unpaid orders + + + Check to enable automatic cancellation of unpaid orders after a specified delay. + + + Auto-cancel delay (minutes) + + + Specify the delay in minutes after which unpaid orders should be automatically cancelled. Default is 600 minutes (10 hours). + + + Ignore payment methods + + + Select payment methods to exclude from auto-cancellation. Orders using these payment methods will not be automatically cancelled. + + + Restore cart after cancellation + + + Check to automatically restore shopping cart items when an order is cancelled by the auto-cancel task. + Checkout diff --git a/src/Presentation/Nop.Web/Areas/Admin/Controllers/SettingController.cs b/src/Presentation/Nop.Web/Areas/Admin/Controllers/SettingController.cs index 27cabb31f45..8af60445197 100644 --- a/src/Presentation/Nop.Web/Areas/Admin/Controllers/SettingController.cs +++ b/src/Presentation/Nop.Web/Areas/Admin/Controllers/SettingController.cs @@ -939,6 +939,11 @@ public virtual async Task Order(OrderSettingsModel model) var orderSettings = await _settingService.LoadSettingAsync(storeScope); orderSettings = model.ToSettings(orderSettings); + //convert selected payment methods to comma-separated string + orderSettings.IgnorePaymentMethods = model.SelectedPaymentMethods != null && model.SelectedPaymentMethods.Any() + ? string.Join(",", model.SelectedPaymentMethods) + : string.Empty; + //we do not clear cache after each setting update. //this behavior can increase performance because cached settings will not be cleared //and loaded from database after each update @@ -968,6 +973,10 @@ public virtual async Task Order(OrderSettingsModel model) await _settingService.SaveSettingOverridablePerStoreAsync(orderSettings, x => x.ExportWithProducts, model.ExportWithProducts_OverrideForStore, storeScope, false); await _settingService.SaveSettingOverridablePerStoreAsync(orderSettings, x => x.AllowAdminsToBuyCallForPriceProducts, model.AllowAdminsToBuyCallForPriceProducts_OverrideForStore, storeScope, false); await _settingService.SaveSettingOverridablePerStoreAsync(orderSettings, x => x.AllowCustomersCancelOrders, model.AllowCustomersCancelOrders_OverrideForStore, storeScope, false); + await _settingService.SaveSettingOverridablePerStoreAsync(orderSettings, x => x.AutoCancelUnpaidOrdersEnabled, model.AutoCancelUnpaidOrdersEnabled_OverrideForStore, storeScope, false); + await _settingService.SaveSettingOverridablePerStoreAsync(orderSettings, x => x.AutoCancelUnpaidOrdersDelay, model.AutoCancelUnpaidOrdersDelay_OverrideForStore, storeScope, false); + await _settingService.SaveSettingOverridablePerStoreAsync(orderSettings, x => x.IgnorePaymentMethods, model.IgnorePaymentMethods_OverrideForStore, storeScope, false); + await _settingService.SaveSettingOverridablePerStoreAsync(orderSettings, x => x.RestoreCartAfterCancellation, model.RestoreCartAfterCancellation_OverrideForStore, storeScope, false); await _settingService.SaveSettingOverridablePerStoreAsync(orderSettings, x => x.ShowProductThumbnailInOrderDetailsPage, model.ShowProductThumbnailInOrderDetailsPage_OverrideForStore, storeScope, false); await _settingService.SaveSettingOverridablePerStoreAsync(orderSettings, x => x.DeleteGiftCardUsageHistory, model.DeleteGiftCardUsageHistory_OverrideForStore, storeScope, false); await _settingService.SaveSettingAsync(orderSettings, x => x.ActivateGiftCardsAfterCompletingOrder, 0, false); diff --git a/src/Presentation/Nop.Web/Areas/Admin/Factories/SettingModelFactory.cs b/src/Presentation/Nop.Web/Areas/Admin/Factories/SettingModelFactory.cs index 7dc757b773d..3246dc6211f 100644 --- a/src/Presentation/Nop.Web/Areas/Admin/Factories/SettingModelFactory.cs +++ b/src/Presentation/Nop.Web/Areas/Admin/Factories/SettingModelFactory.cs @@ -30,6 +30,7 @@ using Nop.Services.Helpers; using Nop.Services.Localization; using Nop.Services.Media; +using Nop.Services.Payments; using Nop.Services.Stores; using Nop.Services.Themes; using Nop.Web.Areas.Admin.Infrastructure.Mapper.Extensions; @@ -65,6 +66,7 @@ public partial class SettingModelFactory : ISettingModelFactory protected readonly IGenericAttributeService _genericAttributeService; protected readonly ILanguageService _languageService; protected readonly ILocalizationService _localizationService; + protected readonly IPaymentPluginManager _paymentPluginManager; protected readonly IPictureService _pictureService; protected readonly IReturnRequestModelFactory _returnRequestModelFactory; protected readonly IReviewTypeModelFactory _reviewTypeModelFactory; @@ -96,6 +98,7 @@ public SettingModelFactory(AppSettings appSettings, IGenericAttributeService genericAttributeService, ILanguageService languageService, ILocalizationService localizationService, + IPaymentPluginManager paymentPluginManager, IPictureService pictureService, IReturnRequestModelFactory returnRequestModelFactory, ISettingService settingService, @@ -123,6 +126,7 @@ public SettingModelFactory(AppSettings appSettings, _genericAttributeService = genericAttributeService; _languageService = languageService; _localizationService = localizationService; + _paymentPluginManager = paymentPluginManager; _pictureService = pictureService; _returnRequestModelFactory = returnRequestModelFactory; _settingService = settingService; @@ -1472,6 +1476,28 @@ public virtual async Task PrepareOrderSettingsModelAsync(Ord model.PrimaryStoreCurrencyCode = (await _currencyService.GetCurrencyByIdAsync(_currencySettings.PrimaryStoreCurrencyId))?.CurrencyCode; model.OrderIdent = await _dataProvider.GetTableIdentAsync(); + //load available payment methods + var paymentMethods = await _paymentPluginManager + .LoadAllPluginsAsync(customer: await _workContext.GetCurrentCustomerAsync()); + + model.AvailablePaymentMethods = paymentMethods + .Select(pm => new SelectListItem + { + Value = pm.PluginDescriptor.SystemName, + Text = pm.PluginDescriptor.FriendlyName + }) + .OrderBy(x => x.Text) + .ToList(); + + //preselect saved values + if (!string.IsNullOrWhiteSpace(orderSettings.IgnorePaymentMethods)) + { + model.SelectedPaymentMethods = orderSettings.IgnorePaymentMethods + .Split(',', StringSplitOptions.RemoveEmptyEntries) + .Select(x => x.Trim()) + .ToList(); + } + //fill in overridden values if (storeId > 0) { diff --git a/src/Presentation/Nop.Web/Areas/Admin/Infrastructure/Mapper/AdminMapperConfiguration.cs b/src/Presentation/Nop.Web/Areas/Admin/Infrastructure/Mapper/AdminMapperConfiguration.cs index 07e5ffe02f8..6f35aa9a8fe 100644 --- a/src/Presentation/Nop.Web/Areas/Admin/Infrastructure/Mapper/AdminMapperConfiguration.cs +++ b/src/Presentation/Nop.Web/Areas/Admin/Infrastructure/Mapper/AdminMapperConfiguration.cs @@ -1288,6 +1288,8 @@ protected virtual void CreateOrdersMaps() .ForMember(model => model.AttachPdfInvoiceToOrderPaidEmail_OverrideForStore, options => options.Ignore()) .ForMember(model => model.AttachPdfInvoiceToOrderPlacedEmail_OverrideForStore, options => options.Ignore()) .ForMember(model => model.AutoUpdateOrderTotalsOnEditingOrder_OverrideForStore, options => options.Ignore()) + .ForMember(model => model.AutoCancelUnpaidOrdersEnabled_OverrideForStore, options => options.Ignore()) + .ForMember(model => model.AutoCancelUnpaidOrdersDelay_OverrideForStore, options => options.Ignore()) .ForMember(model => model.CheckoutDisabled_OverrideForStore, options => options.Ignore()) .ForMember(model => model.CustomOrderNumberMask_OverrideForStore, options => options.Ignore()) .ForMember(model => model.DeleteGiftCardUsageHistory_OverrideForStore, options => options.Ignore()) @@ -1295,6 +1297,7 @@ protected virtual void CreateOrdersMaps() .ForMember(model => model.DisableOrderCompletedPage_OverrideForStore, options => options.Ignore()) .ForMember(model => model.DisplayPickupInStoreOnShippingMethodPage_OverrideForStore, options => options.Ignore()) .ForMember(model => model.ExportWithProducts_OverrideForStore, options => options.Ignore()) + .ForMember(model => model.IgnorePaymentMethods_OverrideForStore, options => options.Ignore()) .ForMember(model => model.IsReOrderAllowed_OverrideForStore, options => options.Ignore()) .ForMember(model => model.MinOrderSubtotalAmountIncludingTax_OverrideForStore, options => options.Ignore()) .ForMember(model => model.MinOrderSubtotalAmount_OverrideForStore, options => options.Ignore()) @@ -1304,6 +1307,7 @@ protected virtual void CreateOrdersMaps() .ForMember(model => model.OnePageCheckoutEnabled_OverrideForStore, options => options.Ignore()) .ForMember(model => model.OrderIdent, options => options.Ignore()) .ForMember(model => model.PrimaryStoreCurrencyCode, options => options.Ignore()) + .ForMember(model => model.RestoreCartAfterCancellation_OverrideForStore, options => options.Ignore()) .ForMember(model => model.ReturnRequestActionSearchModel, options => options.Ignore()) .ForMember(model => model.ReturnRequestNumberMask_OverrideForStore, options => options.Ignore()) .ForMember(model => model.ReturnRequestReasonSearchModel, options => options.Ignore()) @@ -1311,7 +1315,9 @@ protected virtual void CreateOrdersMaps() .ForMember(model => model.ReturnRequestsEnabled_OverrideForStore, options => options.Ignore()) .ForMember(model => model.TermsOfServiceOnOrderConfirmPage_OverrideForStore, options => options.Ignore()) .ForMember(model => model.TermsOfServiceOnShoppingCartPage_OverrideForStore, options => options.Ignore()) - .ForMember(model => model.PrimaryStoreCurrencyCode, options => options.Ignore()); + .ForMember(model => model.PrimaryStoreCurrencyCode, options => options.Ignore()) + .ForMember(model => model.AvailablePaymentMethods, options => options.Ignore()) + .ForMember(model => model.SelectedPaymentMethods, options => options.Ignore()); CreateMap() .ForMember(settings => settings.GeneratePdfInvoiceInCustomerLanguage, options => options.Ignore()) .ForMember(settings => settings.MinimumOrderPlacementInterval, options => options.Ignore()) diff --git a/src/Presentation/Nop.Web/Areas/Admin/Models/Settings/OrderSettingsModel.cs b/src/Presentation/Nop.Web/Areas/Admin/Models/Settings/OrderSettingsModel.cs index 4c71a05896a..454210b937c 100644 --- a/src/Presentation/Nop.Web/Areas/Admin/Models/Settings/OrderSettingsModel.cs +++ b/src/Presentation/Nop.Web/Areas/Admin/Models/Settings/OrderSettingsModel.cs @@ -1,4 +1,5 @@ -using Nop.Web.Areas.Admin.Models.Orders; +using Microsoft.AspNetCore.Mvc.Rendering; +using Nop.Web.Areas.Admin.Models.Orders; using Nop.Web.Framework.Models; using Nop.Web.Framework.Mvc.ModelBinding; @@ -15,6 +16,8 @@ public OrderSettingsModel() { ReturnRequestReasonSearchModel = new ReturnRequestReasonSearchModel(); ReturnRequestActionSearchModel = new ReturnRequestActionSearchModel(); + AvailablePaymentMethods = new List(); + SelectedPaymentMethods = new List(); } #endregion @@ -152,6 +155,25 @@ public OrderSettingsModel() public bool AllowCustomersCancelOrders { get; set; } public bool AllowCustomersCancelOrders_OverrideForStore { get; set; } + [NopResourceDisplayName("Admin.Configuration.Settings.Order.AutoCancelUnpaidOrdersEnabled")] + public bool AutoCancelUnpaidOrdersEnabled { get; set; } + public bool AutoCancelUnpaidOrdersEnabled_OverrideForStore { get; set; } + + [NopResourceDisplayName("Admin.Configuration.Settings.Order.AutoCancelUnpaidOrdersDelay")] + public int AutoCancelUnpaidOrdersDelay { get; set; } + public bool AutoCancelUnpaidOrdersDelay_OverrideForStore { get; set; } + + [NopResourceDisplayName("Admin.Configuration.Settings.Order.IgnorePaymentMethods")] + public string IgnorePaymentMethods { get; set; } + public IList AvailablePaymentMethods { get; set; } + public IList SelectedPaymentMethods { get; set; } + + public bool IgnorePaymentMethods_OverrideForStore { get; set; } + + [NopResourceDisplayName("Admin.Configuration.Settings.Order.RestoreCartAfterCancellation")] + public bool RestoreCartAfterCancellation { get; set; } + public bool RestoreCartAfterCancellation_OverrideForStore { get; set; } + public ReturnRequestReasonSearchModel ReturnRequestReasonSearchModel { get; set; } public ReturnRequestActionSearchModel ReturnRequestActionSearchModel { get; set; } diff --git a/src/Presentation/Nop.Web/Areas/Admin/Views/Setting/_Order.Common.cshtml b/src/Presentation/Nop.Web/Areas/Admin/Views/Setting/_Order.Common.cshtml index 4bc1b2ee4ed..191f02e70a6 100644 --- a/src/Presentation/Nop.Web/Areas/Admin/Views/Setting/_Order.Common.cshtml +++ b/src/Presentation/Nop.Web/Areas/Admin/Views/Setting/_Order.Common.cshtml @@ -88,6 +88,48 @@ +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
diff --git a/src/Tests/Nop.Tests/Nop.Services.Tests/ScheduleTasks/ScheduleTaskServiceTests.cs b/src/Tests/Nop.Tests/Nop.Services.Tests/ScheduleTasks/ScheduleTaskServiceTests.cs index 13ffc0ad61b..7e619c11ea7 100644 --- a/src/Tests/Nop.Tests/Nop.Services.Tests/ScheduleTasks/ScheduleTaskServiceTests.cs +++ b/src/Tests/Nop.Tests/Nop.Services.Tests/ScheduleTasks/ScheduleTaskServiceTests.cs @@ -133,16 +133,38 @@ public async Task GetTaskByTypeAsyncShouldReturnNullIfTypeEmptyOrNotExists() [Test] public async Task CanGetAllTasksAsync() { + // GetAllTasksAsync() should return only enabled tasks var tasks = await _scheduleTaskService.GetAllTasksAsync(); - tasks.Count.Should().Be(5); + tasks.Should().NotBeNull(); + tasks.Count.Should().BeGreaterThan(4); + + // Verify only enabled tasks are returned tasks.Any(p => p.Enabled == false).Should().BeFalse(); + + // Verify the disabled test task is not included tasks.Any(p => p.Id == _task.Id).Should().BeFalse(); - + + // Verify core enabled tasks exist + tasks.Should().Contain(t => t.Type == "Nop.Services.Messages.QueuedMessagesSendTask, Nop.Services"); + tasks.Should().Contain(t => t.Type == "Nop.Services.Common.KeepAliveTask, Nop.Services"); + tasks.Should().Contain(t => t.Type == "Nop.Services.Directory.UpdateExchangeRateTask, Nop.Services"); + tasks.Should().Contain(t => t.Type == "Nop.Services.Orders.CancelUnpaidOrdersTask, Nop.Services"); + + // GetAllTasksAsync(true) should return all tasks including disabled ones tasks = await _scheduleTaskService.GetAllTasksAsync(true); - tasks.Count.Should().Be(9); + tasks.Should().NotBeNull(); + tasks.Count.Should().BeGreaterThan(8); + + // Should contain both enabled and disabled tasks tasks.Any(p => p.Enabled).Should().BeTrue(); + + // Should include the disabled test task tasks.Any(p => p.Id == _task.Id).Should().BeTrue(); + + // Verify disabled tasks are included + tasks.Should().Contain(t => t.Type == "Nop.Services.Caching.ClearCacheTask, Nop.Services"); + tasks.Should().Contain(t => t.Type == "Nop.Services.Logging.ClearLogTask, Nop.Services"); } } \ No newline at end of file