From 1ac1f033be18fecba9f6d48a9860190a091b484d Mon Sep 17 00:00:00 2001 From: mgesing Date: Wed, 22 Nov 2023 13:26:51 +0100 Subject: [PATCH] Resolves #896 Add a cart rule for affiliates Added a button to remove the assignment of a customer to an affiliate on customer edit page. --- .../Rules/AffiliateRuleOptionsProvider.cs | 51 +++++++++++++ .../Checkout/Bootstrapping/CheckoutStarter.cs | 2 + .../Checkout/Rules/CartRuleProvider.cs | 72 ++++++++++--------- .../Rules/Impl/CustomerAffiliateRule.cs | 27 +++++++ .../Migrations/SmartDbContextDataSeeder.cs | 5 ++ .../KnownRuleOptionDataSourceNames.cs | 1 + .../Admin/Controllers/CustomerController.cs | 19 ++++- .../Admin/Controllers/OrderController.cs | 2 +- .../Customer/_CreateOrUpdate.Info.cshtml | 9 ++- .../Areas/Admin/Views/Shipment/Edit.cshtml | 4 +- 10 files changed, 153 insertions(+), 39 deletions(-) create mode 100644 src/Smartstore.Core/Checkout/Affiliates/Rules/AffiliateRuleOptionsProvider.cs create mode 100644 src/Smartstore.Core/Checkout/Rules/Impl/CustomerAffiliateRule.cs diff --git a/src/Smartstore.Core/Checkout/Affiliates/Rules/AffiliateRuleOptionsProvider.cs b/src/Smartstore.Core/Checkout/Affiliates/Rules/AffiliateRuleOptionsProvider.cs new file mode 100644 index 0000000000..377b3da602 --- /dev/null +++ b/src/Smartstore.Core/Checkout/Affiliates/Rules/AffiliateRuleOptionsProvider.cs @@ -0,0 +1,51 @@ +using Smartstore.Core.Data; +using Smartstore.Core.Rules; +using Smartstore.Core.Rules.Rendering; + +namespace Smartstore.Core.Checkout.Affiliates.Rules +{ + public partial class AffiliateRuleOptionsProvider : IRuleOptionsProvider + { + private readonly SmartDbContext _db; + + public AffiliateRuleOptionsProvider(SmartDbContext db) + { + _db = db; + } + + public int Order => 0; + + public bool Matches(string dataSource) + { + return dataSource == KnownRuleOptionDataSourceNames.Affiliate; + } + + public async Task GetOptionsAsync(RuleOptionsContext context) + { + if (context.DataSource != KnownRuleOptionDataSourceNames.Affiliate) + { + return null; + } + + var result = new RuleOptionsResult(); + + var pager = _db.Affiliates + .AsNoTracking() + .Include(x => x.Address) + .Where(x => x.Active) + .ToFastPager(); + + while ((await pager.ReadNextPageAsync()).Out(out var affiliates)) + { + result.AddOptions(context, affiliates.Select(x => new RuleValueSelectListOption + { + Value = x.Id.ToString(), + Text = x.Address?.GetFullName()?.NullEmpty() ?? StringExtensions.NotAvailable, + Hint = x.Address?.Email + })); + } + + return result; + } + } +} diff --git a/src/Smartstore.Core/Checkout/Bootstrapping/CheckoutStarter.cs b/src/Smartstore.Core/Checkout/Bootstrapping/CheckoutStarter.cs index 0e1dc69951..b6444e25d8 100644 --- a/src/Smartstore.Core/Checkout/Bootstrapping/CheckoutStarter.cs +++ b/src/Smartstore.Core/Checkout/Bootstrapping/CheckoutStarter.cs @@ -1,4 +1,5 @@ using Autofac; +using Smartstore.Core.Checkout.Affiliates.Rules; using Smartstore.Core.Checkout.Attributes; using Smartstore.Core.Checkout.Cart; using Smartstore.Core.Checkout.GiftCards; @@ -69,6 +70,7 @@ public override void ConfigureContainer(ContainerBuilder builder, IApplicationCo builder.RegisterType().As().InstancePerLifetimeScope(); builder.RegisterType().As().InstancePerLifetimeScope(); builder.RegisterType().As().InstancePerLifetimeScope(); + builder.RegisterType().As().InstancePerLifetimeScope(); } } } diff --git a/src/Smartstore.Core/Checkout/Rules/CartRuleProvider.cs b/src/Smartstore.Core/Checkout/Rules/CartRuleProvider.cs index 1f74d463c4..d995dfc062 100644 --- a/src/Smartstore.Core/Checkout/Rules/CartRuleProvider.cs +++ b/src/Smartstore.Core/Checkout/Rules/CartRuleProvider.cs @@ -236,7 +236,7 @@ protected override Task> LoadDescriptorsAsync() var descriptors = new List { - new CartRuleDescriptor + new() { Name = "Currency", DisplayName = T("Admin.Rules.FilterDescriptor.Currency"), @@ -244,7 +244,7 @@ protected override Task> LoadDescriptorsAsync() ProcessorType = typeof(CurrencyRule), SelectList = new RemoteRuleValueSelectList(KnownRuleOptionDataSourceNames.Currency) { Multiple = true } }, - new CartRuleDescriptor + new() { Name = "Language", DisplayName = T("Admin.Rules.FilterDescriptor.Language"), @@ -252,7 +252,7 @@ protected override Task> LoadDescriptorsAsync() ProcessorType = typeof(LanguageRule), SelectList = new RemoteRuleValueSelectList(KnownRuleOptionDataSourceNames.Language) { Multiple = true } }, - new CartRuleDescriptor + new() { Name = "Store", DisplayName = T("Admin.Rules.FilterDescriptor.Store"), @@ -260,7 +260,7 @@ protected override Task> LoadDescriptorsAsync() ProcessorType = typeof(StoreRule), SelectList = new LocalRuleValueSelectList(stores) { Multiple = true } }, - new CartRuleDescriptor + new() { Name = "IPCountry", DisplayName = T("Admin.Rules.FilterDescriptor.IPCountry"), @@ -268,7 +268,7 @@ protected override Task> LoadDescriptorsAsync() ProcessorType = typeof(IPCountryRule), SelectList = new RemoteRuleValueSelectList(KnownRuleOptionDataSourceNames.Country) { Multiple = true } }, - new CartRuleDescriptor + new() { Name = "Weekday", DisplayName = T("Admin.Rules.FilterDescriptor.Weekday"), @@ -277,7 +277,7 @@ protected override Task> LoadDescriptorsAsync() SelectList = new LocalRuleValueSelectList(WeekdayRule.GetDefaultValues(language)) { Multiple = true } }, - new CartRuleDescriptor + new() { Name = "CustomerRole", DisplayName = T("Admin.Rules.FilterDescriptor.IsInCustomerRole"), @@ -286,7 +286,15 @@ protected override Task> LoadDescriptorsAsync() SelectList = new RemoteRuleValueSelectList(KnownRuleOptionDataSourceNames.CustomerRole) { Multiple = true }, IsComparingSequences = true }, - new CartRuleDescriptor + new() + { + Name = "Affiliate", + DisplayName = T("Admin.Rules.FilterDescriptor.Affiliate"), + RuleType = RuleType.IntArray, + ProcessorType = typeof(CustomerAffiliateRule), + SelectList = new RemoteRuleValueSelectList(KnownRuleOptionDataSourceNames.Affiliate) { Multiple = true } + }, + new() { Name = "CartBillingCountry", DisplayName = T("Admin.Rules.FilterDescriptor.BillingCountry"), @@ -294,7 +302,7 @@ protected override Task> LoadDescriptorsAsync() ProcessorType = typeof(BillingCountryRule), SelectList = new RemoteRuleValueSelectList(KnownRuleOptionDataSourceNames.Country) { Multiple = true } }, - new CartRuleDescriptor + new() { Name = "CartShippingCountry", DisplayName = T("Admin.Rules.FilterDescriptor.ShippingCountry"), @@ -302,7 +310,7 @@ protected override Task> LoadDescriptorsAsync() ProcessorType = typeof(ShippingCountryRule), SelectList = new RemoteRuleValueSelectList(KnownRuleOptionDataSourceNames.Country) { Multiple = true } }, - new CartRuleDescriptor + new() { Name = "CartShippingMethod", DisplayName = T("Admin.Rules.FilterDescriptor.ShippingMethod"), @@ -310,7 +318,7 @@ protected override Task> LoadDescriptorsAsync() ProcessorType = typeof(ShippingMethodRule), SelectList = new RemoteRuleValueSelectList(KnownRuleOptionDataSourceNames.ShippingMethod) { Multiple = true } }, - new CartRuleDescriptor + new() { Name = "CartPaymentMethod", DisplayName = T("Admin.Rules.FilterDescriptor.PaymentMethod"), @@ -319,21 +327,21 @@ protected override Task> LoadDescriptorsAsync() SelectList = new RemoteRuleValueSelectList(KnownRuleOptionDataSourceNames.PaymentMethod) { Multiple = true } }, - new CartRuleDescriptor + new() { Name = "CartTotal", DisplayName = T("Admin.Rules.FilterDescriptor.CartTotal"), RuleType = RuleType.Money, ProcessorType = typeof(CartTotalRule) }, - new CartRuleDescriptor + new() { Name = "CartSubtotal", DisplayName = T("Admin.Rules.FilterDescriptor.CartSubtotal"), RuleType = RuleType.Money, ProcessorType = typeof(CartSubtotalRule) }, - new CartRuleDescriptor + new() { Name = "CartProductCount", DisplayName = T("Admin.Rules.FilterDescriptor.CartProductCount"), @@ -342,7 +350,7 @@ protected override Task> LoadDescriptorsAsync() }, cartItemQuantity, cartItemFromCategoryQuantity, - new CartRuleDescriptor + new() { Name = "ProductInCart", DisplayName = T("Admin.Rules.FilterDescriptor.ProductInCart"), @@ -351,7 +359,7 @@ protected override Task> LoadDescriptorsAsync() SelectList = new RemoteRuleValueSelectList(KnownRuleOptionDataSourceNames.Product) { Multiple = true }, IsComparingSequences = true }, - new CartRuleDescriptor + new() { Name = "ProductFromCategoryInCart", DisplayName = T("Admin.Rules.FilterDescriptor.ProductFromCategoryInCart"), @@ -360,7 +368,7 @@ protected override Task> LoadDescriptorsAsync() SelectList = new RemoteRuleValueSelectList(KnownRuleOptionDataSourceNames.Category) { Multiple = true }, IsComparingSequences = true }, - new CartRuleDescriptor + new() { Name = "ProductFromManufacturerInCart", DisplayName = T("Admin.Rules.FilterDescriptor.ProductFromManufacturerInCart"), @@ -369,7 +377,7 @@ protected override Task> LoadDescriptorsAsync() SelectList = new RemoteRuleValueSelectList(KnownRuleOptionDataSourceNames.Manufacturer) { Multiple = true }, IsComparingSequences = true }, - new CartRuleDescriptor + new() { Name = "ProductWithDeliveryTimeInCart", DisplayName = T("Admin.Rules.FilterDescriptor.ProductWithDeliveryTimeInCart"), @@ -378,7 +386,7 @@ protected override Task> LoadDescriptorsAsync() SelectList = new RemoteRuleValueSelectList(KnownRuleOptionDataSourceNames.DeliveryTime) { Multiple = true }, IsComparingSequences = true }, - new CartRuleDescriptor + new() { Name = "ProductInWishlist", DisplayName = T("Admin.Rules.FilterDescriptor.ProductOnWishlist"), @@ -387,21 +395,21 @@ protected override Task> LoadDescriptorsAsync() SelectList = new RemoteRuleValueSelectList(KnownRuleOptionDataSourceNames.Product) { Multiple = true }, IsComparingSequences = true }, - new CartRuleDescriptor + new() { Name = "ProductReviewCount", DisplayName = T("Admin.Rules.FilterDescriptor.ProductReviewCount"), RuleType = RuleType.Int, ProcessorType = typeof(ProductReviewCountRule) }, - new CartRuleDescriptor + new() { Name = "RewardPointsBalance", DisplayName = T("Admin.Rules.FilterDescriptor.RewardPointsBalance"), RuleType = RuleType.Int, ProcessorType = typeof(RewardPointsBalanceRule) }, - new CartRuleDescriptor + new() { Name = "RuleSet", DisplayName = T("Admin.Rules.FilterDescriptor.RuleSet"), @@ -411,7 +419,7 @@ protected override Task> LoadDescriptorsAsync() SelectList = new RemoteRuleValueSelectList(KnownRuleOptionDataSourceNames.CartRule), }, - new CartRuleDescriptor + new() { Name = "CartOrderCount", DisplayName = T("Admin.Rules.FilterDescriptor.OrderCount"), @@ -419,7 +427,7 @@ protected override Task> LoadDescriptorsAsync() RuleType = RuleType.Int, ProcessorType = typeof(OrderCountRule) }, - new CartRuleDescriptor + new() { Name = "CartSpentAmount", DisplayName = T("Admin.Rules.FilterDescriptor.SpentAmount"), @@ -427,7 +435,7 @@ protected override Task> LoadDescriptorsAsync() RuleType = RuleType.Money, ProcessorType = typeof(SpentAmountRule) }, - new CartRuleDescriptor + new() { Name = "CartPaidBy", DisplayName = T("Admin.Rules.FilterDescriptor.PaidBy"), @@ -437,7 +445,7 @@ protected override Task> LoadDescriptorsAsync() SelectList = new RemoteRuleValueSelectList(KnownRuleOptionDataSourceNames.PaymentMethod) { Multiple = true }, IsComparingSequences = true }, - new CartRuleDescriptor + new() { Name = "CartPurchasedProduct", DisplayName = T("Admin.Rules.FilterDescriptor.PurchasedProduct"), @@ -447,7 +455,7 @@ protected override Task> LoadDescriptorsAsync() SelectList = new RemoteRuleValueSelectList(KnownRuleOptionDataSourceNames.Product) { Multiple = true }, IsComparingSequences = true }, - new CartRuleDescriptor + new() { Name = "CartPurchasedFromManufacturer", DisplayName = T("Admin.Rules.FilterDescriptor.PurchasedFromManufacturer"), @@ -458,7 +466,7 @@ protected override Task> LoadDescriptorsAsync() IsComparingSequences = true }, - new CartRuleDescriptor + new() { Name = "UserAgent.IsMobile", DisplayName = T("Admin.Rules.FilterDescriptor.MobileDevice"), @@ -466,7 +474,7 @@ protected override Task> LoadDescriptorsAsync() RuleType = RuleType.Boolean, ProcessorType = typeof(IsMobileRule) }, - new CartRuleDescriptor + new() { Name = "UserAgent.Device", DisplayName = T("Admin.Rules.FilterDescriptor.DeviceFamily"), @@ -475,7 +483,7 @@ protected override Task> LoadDescriptorsAsync() ProcessorType = typeof(DeviceRule), SelectList = new LocalRuleValueSelectList(DeviceRule.GetDefaultOptions()) { Multiple = true, Tags = true } }, - new CartRuleDescriptor + new() { Name = "UserAgent.OS", DisplayName = T("Admin.Rules.FilterDescriptor.OperatingSystem"), @@ -484,7 +492,7 @@ protected override Task> LoadDescriptorsAsync() ProcessorType = typeof(OSRule), SelectList = new LocalRuleValueSelectList(OSRule.GetDefaultOptions()) { Multiple = true, Tags = true } }, - new CartRuleDescriptor + new() { Name = "UserAgent.Browser", DisplayName = T("Admin.Rules.FilterDescriptor.BrowserName"), @@ -493,7 +501,7 @@ protected override Task> LoadDescriptorsAsync() ProcessorType = typeof(BrowserRule), SelectList = new LocalRuleValueSelectList(BrowserRule.GetDefaultOptions()) { Multiple = true, Tags = true } }, - new CartRuleDescriptor + new() { Name = "UserAgent.BrowserMajorVersion", DisplayName = T("Admin.Rules.FilterDescriptor.BrowserMajorVersion"), @@ -501,7 +509,7 @@ protected override Task> LoadDescriptorsAsync() RuleType = RuleType.Int, ProcessorType = typeof(BrowserMajorVersionRule) }, - new CartRuleDescriptor + new() { Name = "UserAgent.BrowserMinorVersion", DisplayName = T("Admin.Rules.FilterDescriptor.BrowserMinorVersion"), diff --git a/src/Smartstore.Core/Checkout/Rules/Impl/CustomerAffiliateRule.cs b/src/Smartstore.Core/Checkout/Rules/Impl/CustomerAffiliateRule.cs new file mode 100644 index 0000000000..7790ef0e6d --- /dev/null +++ b/src/Smartstore.Core/Checkout/Rules/Impl/CustomerAffiliateRule.cs @@ -0,0 +1,27 @@ +using Smartstore.Core.Data; +using Smartstore.Core.Rules; + +namespace Smartstore.Core.Checkout.Rules.Impl +{ + internal class CustomerAffiliateRule : IRule + { + private readonly SmartDbContext _db; + + public CustomerAffiliateRule(SmartDbContext db) + { + _db = db; + } + + public async Task MatchAsync(CartRuleContext context, RuleExpression expression) + { + var customer = context.Customer; + if (customer != null && !customer.IsSystemAccount && customer.AffiliateId != 0 && expression.HasListMatch(customer.AffiliateId)) + { + var isValidAffiliate = await _db.Affiliates.AnyAsync(x => x.Id == customer.AffiliateId && !x.Deleted && x.Active); + return isValidAffiliate; + } + + return false; + } + } +} diff --git a/src/Smartstore.Core/Migrations/SmartDbContextDataSeeder.cs b/src/Smartstore.Core/Migrations/SmartDbContextDataSeeder.cs index 10f1329353..2f01d20e52 100644 --- a/src/Smartstore.Core/Migrations/SmartDbContextDataSeeder.cs +++ b/src/Smartstore.Core/Migrations/SmartDbContextDataSeeder.cs @@ -22,6 +22,11 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) builder.Delete("Account.ChangePassword.Errors.PasswordIsNotProvided"); builder.AddOrUpdate("Admin.Report.MediaFilesSize", "Media size", "Mediengröße"); + builder.AddOrUpdate("Admin.Rules.FilterDescriptor.Affiliate", "Affiliate", "Partner"); + + builder.AddOrUpdate("Admin.Customers.RemoveAffiliateAssignment", + "Remove assignment to affiliate", + "Zuordnung zum Partner entfernen"); } } } \ No newline at end of file diff --git a/src/Smartstore.Core/Platform/Rules/Rendering/KnownRuleOptionDataSourceNames.cs b/src/Smartstore.Core/Platform/Rules/Rendering/KnownRuleOptionDataSourceNames.cs index 9a13740eaa..73308e5c44 100644 --- a/src/Smartstore.Core/Platform/Rules/Rendering/KnownRuleOptionDataSourceNames.cs +++ b/src/Smartstore.Core/Platform/Rules/Rendering/KnownRuleOptionDataSourceNames.cs @@ -2,6 +2,7 @@ { public static partial class KnownRuleOptionDataSourceNames { + public const string Affiliate = "Affiliate"; public const string AttributeOption = "AttributeOption"; public const string CartRule = "CartRule"; public const string Category = "Category"; diff --git a/src/Smartstore.Web/Areas/Admin/Controllers/CustomerController.cs b/src/Smartstore.Web/Areas/Admin/Controllers/CustomerController.cs index 6ace64f1f3..58909ea33f 100644 --- a/src/Smartstore.Web/Areas/Admin/Controllers/CustomerController.cs +++ b/src/Smartstore.Web/Areas/Admin/Controllers/CustomerController.cs @@ -752,6 +752,23 @@ public async Task ChangePassword(CustomerModel.ChangePasswordMode return PartialView("_ChangePasswordPopup", model); } + [HttpPost] + [FormValueRequired("removeAffiliateAssignment"), ActionName("Edit")] + [Permission(Permissions.Customer.Update)] + public async Task RemoveAffiliateAssignment(int id) + { + var customer = await _db.Customers.FindByIdAsync(id); + if (customer == null) + { + return NotFound(); + } + + customer.AffiliateId = 0; + await _db.SaveChangesAsync(); + + return RedirectToAction(nameof(Edit), customer.Id); + } + [HttpPost] [FormValueRequired("markVatNumberAsValid"), ActionName("Edit")] [Permission(Permissions.Customer.Update)] @@ -764,7 +781,6 @@ public async Task MarkVatNumberAsValid(CustomerModel model) } customer.VatNumberStatusId = (int)VatNumberStatus.Valid; - await _db.SaveChangesAsync(); return RedirectToAction(nameof(Edit), customer.Id); @@ -782,7 +798,6 @@ public async Task MarkVatNumberAsInvalid(CustomerModel model) } customer.VatNumberStatusId = (int)VatNumberStatus.Invalid; - await _db.SaveChangesAsync(); return RedirectToAction(nameof(Edit), customer.Id); diff --git a/src/Smartstore.Web/Areas/Admin/Controllers/OrderController.cs b/src/Smartstore.Web/Areas/Admin/Controllers/OrderController.cs index e02e892316..f5de76f970 100644 --- a/src/Smartstore.Web/Areas/Admin/Controllers/OrderController.cs +++ b/src/Smartstore.Web/Areas/Admin/Controllers/OrderController.cs @@ -1822,7 +1822,7 @@ private async Task PrepareOrderModel(OrderModel model, Order order) .Include(x => x.Address) .FindByIdAsync(order.AffiliateId); - model.AffiliateFullName = affiliate?.Address?.GetFullName() ?? StringExtensions.NotAvailable; + model.AffiliateFullName = affiliate?.Address?.GetFullName()?.NullEmpty() ?? StringExtensions.NotAvailable; } model.OrderSubtotalInclTaxString = Format(order.OrderSubtotalInclTax, true); diff --git a/src/Smartstore.Web/Areas/Admin/Views/Customer/_CreateOrUpdate.Info.cshtml b/src/Smartstore.Web/Areas/Admin/Views/Customer/_CreateOrUpdate.Info.cshtml index e1eb8effb3..e24825649c 100644 --- a/src/Smartstore.Web/Areas/Admin/Views/Customer/_CreateOrUpdate.Info.cshtml +++ b/src/Smartstore.Web/Areas/Admin/Views/Customer/_CreateOrUpdate.Info.cshtml @@ -292,8 +292,13 @@
-
- @Model.AffiliateFullName +
+
+ @Model.AffiliateFullName + +
diff --git a/src/Smartstore.Web/Areas/Admin/Views/Shipment/Edit.cshtml b/src/Smartstore.Web/Areas/Admin/Views/Shipment/Edit.cshtml index a4e9989b9c..b66a2ea161 100644 --- a/src/Smartstore.Web/Areas/Admin/Views/Shipment/Edit.cshtml +++ b/src/Smartstore.Web/Areas/Admin/Views/Shipment/Edit.cshtml @@ -104,7 +104,7 @@ } @if (Model.CanShip) { - @@ -128,7 +128,7 @@ } @if (Model.CanDeliver) { -