From 319274bf602c221b63b1e06c1e96c498cd74a44f Mon Sep 17 00:00:00 2001 From: Oleg Zhuk Date: Mon, 22 Dec 2025 17:49:02 +0200 Subject: [PATCH 1/4] VCST-3989: Add support for order search by promotion ID(s) feat: Enable filtering customer orders by single or multiple promotion IDs. Promotion IDs from discounts in orders, line items, payments, and shipments are now indexed and filterable. Updated search criteria, indexing, and query logic to support both indexed and database search for orders associated with specified promotions. --- .../Search/CustomerOrderSearchCriteria.cs | 25 +++++++++++++++++++ .../Indexed/CustomerOrderDocumentBuilder.cs | 20 +++++++++++++++ .../CustomerOrderSearchRequestBuilder.cs | 5 ++++ .../Services/CustomerOrderSearchService.cs | 18 +++++++++++++ 4 files changed, 68 insertions(+) diff --git a/src/VirtoCommerce.OrdersModule.Core/Model/Search/CustomerOrderSearchCriteria.cs b/src/VirtoCommerce.OrdersModule.Core/Model/Search/CustomerOrderSearchCriteria.cs index b25271254..27bed1fa2 100644 --- a/src/VirtoCommerce.OrdersModule.Core/Model/Search/CustomerOrderSearchCriteria.cs +++ b/src/VirtoCommerce.OrdersModule.Core/Model/Search/CustomerOrderSearchCriteria.cs @@ -89,5 +89,30 @@ public string[] OrganizationIds /// Search orders with a certain product /// public string ProductId { get; set; } + + /// + /// Search orders with a certain promotion + /// + public string PromotionId { get; set; } + + private string[] _promotionIds; + /// + /// Search orders with given promotions + /// + public string[] PromotionIds + { + get + { + if (_promotionIds == null && !string.IsNullOrEmpty(PromotionId)) + { + _promotionIds = new[] { PromotionId }; + } + return _promotionIds; + } + set + { + _promotionIds = value; + } + } } } diff --git a/src/VirtoCommerce.OrdersModule.Data/Search/Indexed/CustomerOrderDocumentBuilder.cs b/src/VirtoCommerce.OrdersModule.Data/Search/Indexed/CustomerOrderDocumentBuilder.cs index 9c9d22e33..86e54cbbb 100644 --- a/src/VirtoCommerce.OrdersModule.Data/Search/Indexed/CustomerOrderDocumentBuilder.cs +++ b/src/VirtoCommerce.OrdersModule.Data/Search/Indexed/CustomerOrderDocumentBuilder.cs @@ -54,6 +54,7 @@ public Task BuildSchemaAsync(IndexDocument schema) schema.AddFilterableString("OuterId"); schema.AddFilterableString("Status"); schema.AddFilterableString("Currency"); + schema.AddFilterableString("PromotionId"); schema.AddFilterableDecimal("Total"); schema.AddFilterableDecimal("SubTotal"); @@ -125,6 +126,8 @@ protected virtual async Task CreateDocument(CustomerOrder order) document.AddFilterableBoolean("IsCancelled", order.IsCancelled); document.AddFilterableBoolean("IsPrototype", order.IsPrototype); + IndexDiscounts(order.Discounts, document); + foreach (var address in order.Addresses ?? Enumerable.Empty
()) { IndexAddress(address, document); @@ -132,12 +135,14 @@ protected virtual async Task CreateDocument(CustomerOrder order) foreach (var lineItem in order.Items ?? Enumerable.Empty()) { + IndexDiscounts(lineItem.Discounts, document); document.AddContentString(lineItem.Comment); } foreach (var payment in order.InPayments ?? Enumerable.Empty()) { IndexAddress(payment.BillingAddress, document); + IndexDiscounts(payment.Discounts, document); document.AddContentString(payment.Number); document.AddContentString(payment.Comment); } @@ -145,6 +150,7 @@ protected virtual async Task CreateDocument(CustomerOrder order) foreach (var shipment in order.Shipments ?? Enumerable.Empty()) { IndexAddress(shipment.DeliveryAddress, document); + IndexDiscounts(shipment.Discounts, document); document.AddContentString(shipment.Number); document.AddContentString(shipment.Comment); @@ -171,5 +177,19 @@ protected virtual void IndexAddress(Address address, IndexDocument document) document.AddContentString($"{address.AddressType} {address}"); } } + + protected virtual void IndexDiscounts(ICollection discounts, IndexDocument document) + { + if(discounts!=null) + { + foreach (var discount in discounts) + { + if (discount != null && !string.IsNullOrEmpty(discount.PromotionId)) + { + document.AddFilterableString("PromotionId", discount.PromotionId); + } + } + } + } } } diff --git a/src/VirtoCommerce.OrdersModule.Data/Search/Indexed/CustomerOrderSearchRequestBuilder.cs b/src/VirtoCommerce.OrdersModule.Data/Search/Indexed/CustomerOrderSearchRequestBuilder.cs index d4ac36b52..cb5748c1e 100644 --- a/src/VirtoCommerce.OrdersModule.Data/Search/Indexed/CustomerOrderSearchRequestBuilder.cs +++ b/src/VirtoCommerce.OrdersModule.Data/Search/Indexed/CustomerOrderSearchRequestBuilder.cs @@ -173,6 +173,11 @@ protected virtual IList GetPermanentFilters(CustomerOrderSearchCriteria result.Add(FilterHelper.CreateTermFilter("isprototype", "false")); } + if (!criteria.PromotionIds.IsNullOrEmpty()) + { + result.Add(FilterHelper.CreateTermFilter("promotionid", criteria.PromotionIds)); + } + return result; } diff --git a/src/VirtoCommerce.OrdersModule.Data/Services/CustomerOrderSearchService.cs b/src/VirtoCommerce.OrdersModule.Data/Services/CustomerOrderSearchService.cs index 1dbcedb9d..2ca9dfa45 100644 --- a/src/VirtoCommerce.OrdersModule.Data/Services/CustomerOrderSearchService.cs +++ b/src/VirtoCommerce.OrdersModule.Data/Services/CustomerOrderSearchService.cs @@ -81,6 +81,8 @@ protected override IQueryable BuildQuery(IRepository reposi query = query.Where(o => o.Items.Any(i => i.ProductId == criteria.ProductId)); } + query = WithPromotionConditions(query, criteria); + return query; } @@ -175,5 +177,21 @@ private static IQueryable WithSubscriptionConditions(IQuery return query; } + + private static IQueryable WithPromotionConditions(IQueryable query, CustomerOrderSearchCriteria criteria) + { + if (!criteria.PromotionIds.IsNullOrEmpty()) + { + // Check if any discount in the order, shipments, payments, or line items has the promotion ID + query = query.Where(o => + (o.Discounts != null && o.Discounts.Any(d => criteria.PromotionIds.Contains(d.PromotionId))) || + (o.Shipments != null && o.Shipments.Any(s => s.Discounts != null && s.Discounts.Any(d => criteria.PromotionIds.Contains(d.PromotionId)))) || + (o.InPayments != null && o.InPayments.Any(p => p.Discounts != null && p.Discounts.Any(d => criteria.PromotionIds.Contains(d.PromotionId)))) || + (o.Items != null && o.Items.Any(i => i.Discounts != null && i.Discounts.Any(d => criteria.PromotionIds.Contains(d.PromotionId)))) + ); + } + + return query; + } } } From 884c9a6f240a389d08a2fc2a0f511e05f9438ddd Mon Sep 17 00:00:00 2001 From: Oleg Zhuk Date: Mon, 22 Dec 2025 17:56:54 +0200 Subject: [PATCH 2/4] code review --- .../Search/Indexed/CustomerOrderDocumentBuilder.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/VirtoCommerce.OrdersModule.Data/Search/Indexed/CustomerOrderDocumentBuilder.cs b/src/VirtoCommerce.OrdersModule.Data/Search/Indexed/CustomerOrderDocumentBuilder.cs index 86e54cbbb..14b4d8bb7 100644 --- a/src/VirtoCommerce.OrdersModule.Data/Search/Indexed/CustomerOrderDocumentBuilder.cs +++ b/src/VirtoCommerce.OrdersModule.Data/Search/Indexed/CustomerOrderDocumentBuilder.cs @@ -182,12 +182,9 @@ protected virtual void IndexDiscounts(ICollection d != null && !string.IsNullOrEmpty(d.PromotionId))) { - if (discount != null && !string.IsNullOrEmpty(discount.PromotionId)) - { - document.AddFilterableString("PromotionId", discount.PromotionId); - } + document.AddFilterableString("PromotionId", discount.PromotionId); } } } From 4f271379a10fd47a94410e59fe68d0bca536d8c5 Mon Sep 17 00:00:00 2001 From: Oleg Zhuk Date: Tue, 23 Dec 2025 11:30:05 +0200 Subject: [PATCH 3/4] feat: Add unified operation discounts widget and blade --- .../Scripts/blades/operation-discounts.js | 21 ++++++++++++++++ .../blades/operation-discounts.tpl.html | 25 +++++++++++++++++++ .../Scripts/order.js | 13 ++++++---- .../widgets/operation-discounts-widget.js | 21 ++++++++++++++++ .../operation-discounts-widget.tpl.html | 8 ++++++ 5 files changed, 83 insertions(+), 5 deletions(-) create mode 100644 src/VirtoCommerce.OrdersModule.Web/Scripts/blades/operation-discounts.js create mode 100644 src/VirtoCommerce.OrdersModule.Web/Scripts/blades/operation-discounts.tpl.html create mode 100644 src/VirtoCommerce.OrdersModule.Web/Scripts/widgets/operation-discounts-widget.js create mode 100644 src/VirtoCommerce.OrdersModule.Web/Scripts/widgets/operation-discounts-widget.tpl.html diff --git a/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/operation-discounts.js b/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/operation-discounts.js new file mode 100644 index 000000000..3bc3fe427 --- /dev/null +++ b/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/operation-discounts.js @@ -0,0 +1,21 @@ +angular.module('virtoCommerce.orderModule') + .controller('virtoCommerce.orderModule.operationDiscountsController', [ + '$scope', + 'platformWebApp.uiGridHelper', + function ($scope, uiGridHelper) { + var blade = $scope.blade; + + blade.title = 'orders.blades.customerOrder-item-discounts.title'; + blade.headIcon = 'fa fa-area-chart'; + + $scope.setGridOptions = function (gridOptions) { + uiGridHelper.initialize($scope, gridOptions, function (gridApi) { + $scope.gridApi = gridApi; + }); + }; + + blade.isLoading = false; + } + ]); + + diff --git a/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/operation-discounts.tpl.html b/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/operation-discounts.tpl.html new file mode 100644 index 000000000..1fdfdb8a5 --- /dev/null +++ b/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/operation-discounts.tpl.html @@ -0,0 +1,25 @@ +
+
+
+
+
+
+
{{ 'orders.blades.customerOrder-item-discounts.descr.no-discounts' | translate }}
+
+
+
+ + + + diff --git a/src/VirtoCommerce.OrdersModule.Web/Scripts/order.js b/src/VirtoCommerce.OrdersModule.Web/Scripts/order.js index 9a8f5bb43..1db700313 100644 --- a/src/VirtoCommerce.OrdersModule.Web/Scripts/order.js +++ b/src/VirtoCommerce.OrdersModule.Web/Scripts/order.js @@ -24,7 +24,7 @@ angular.module(moduleName, [ var foundTemplate = knownOperations.getOperation('CustomerOrder'); if (foundTemplate) { var newBlade = angular.copy(foundTemplate.detailBlade); - newBlade.id = 'orderDetail'; + newBlade.id = 'orders'; newBlade.customerOrder = { id: orderId, customerName: 'Customer' }; newBlade.isClosingDisabled = true; bladeNavigationService.showBlade(newBlade); @@ -851,11 +851,14 @@ angular.module(moduleName, [ } }); - var customerOrderItemDiscountWidget = { - controller: 'virtoCommerce.orderModule.customerOrderItemDiscountWidgetController', - template: 'Modules/$(VirtoCommerce.Orders)/Scripts/widgets/customerOrder-item-discounts-widget.tpl.html' + var operationDiscountsWidget = { + controller: 'virtoCommerce.orderModule.operationDiscountWidgetController', + template: 'Modules/$(VirtoCommerce.Orders)/Scripts/widgets/operation-discounts-widget.tpl.html' }; - widgetService.registerWidget(customerOrderItemDiscountWidget, 'customerOrderItemDetailWidgets'); + widgetService.registerWidget(operationDiscountsWidget, 'customerOrderDetailWidgets'); + widgetService.registerWidget(operationDiscountsWidget, 'paymentDetailWidgets'); + widgetService.registerWidget(operationDiscountsWidget, 'shipmentDetailWidgets'); + widgetService.registerWidget(operationDiscountsWidget, 'customerOrderItemDetailWidgets'); var customerOrderItemConfigurationWidget = { controller: 'virtoCommerce.orderModule.customerOrderItemConfigurationWidgetController', diff --git a/src/VirtoCommerce.OrdersModule.Web/Scripts/widgets/operation-discounts-widget.js b/src/VirtoCommerce.OrdersModule.Web/Scripts/widgets/operation-discounts-widget.js new file mode 100644 index 000000000..284f629b0 --- /dev/null +++ b/src/VirtoCommerce.OrdersModule.Web/Scripts/widgets/operation-discounts-widget.js @@ -0,0 +1,21 @@ +angular.module('virtoCommerce.orderModule') + .controller('virtoCommerce.orderModule.operationDiscountWidgetController', [ + '$scope', + 'platformWebApp.bladeNavigationService', + function ($scope, bladeNavigationService) { + var blade = $scope.blade; + + $scope.openBlade = function () { + var newBlade = { + id: "operationDiscounts", + controller: 'virtoCommerce.orderModule.operationDiscountsController', + template: 'Modules/$(VirtoCommerce.Orders)/Scripts/blades/operation-discounts.tpl.html', + currentEntity: blade.currentEntity, + }; + + bladeNavigationService.showBlade(newBlade, blade); + }; + } + ]); + + diff --git a/src/VirtoCommerce.OrdersModule.Web/Scripts/widgets/operation-discounts-widget.tpl.html b/src/VirtoCommerce.OrdersModule.Web/Scripts/widgets/operation-discounts-widget.tpl.html new file mode 100644 index 000000000..feced26cc --- /dev/null +++ b/src/VirtoCommerce.OrdersModule.Web/Scripts/widgets/operation-discounts-widget.tpl.html @@ -0,0 +1,8 @@ +
+
+
{{(blade.currentEntity.discounts || []).length | number:0}}
+
{{ 'orders.widgets.customerOrder-item-discounts.title' | translate }}
+
+
+ + From 9a956c724b3a2171f5ddf62bcfb17b1ae7e3bdfb Mon Sep 17 00:00:00 2001 From: Oleg Zhuk Date: Thu, 15 Jan 2026 16:38:25 +0200 Subject: [PATCH 4/4] Improve PromotionIds getter null/empty check --- .../Model/Search/CustomerOrderSearchCriteria.cs | 2 +- src/VirtoCommerce.OrdersModule.Web/Scripts/order.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/VirtoCommerce.OrdersModule.Core/Model/Search/CustomerOrderSearchCriteria.cs b/src/VirtoCommerce.OrdersModule.Core/Model/Search/CustomerOrderSearchCriteria.cs index 27bed1fa2..8157f5830 100644 --- a/src/VirtoCommerce.OrdersModule.Core/Model/Search/CustomerOrderSearchCriteria.cs +++ b/src/VirtoCommerce.OrdersModule.Core/Model/Search/CustomerOrderSearchCriteria.cs @@ -103,7 +103,7 @@ public string[] PromotionIds { get { - if (_promotionIds == null && !string.IsNullOrEmpty(PromotionId)) + if (_promotionIds.IsNullOrEmpty() && !string.IsNullOrEmpty(PromotionId)) { _promotionIds = new[] { PromotionId }; } diff --git a/src/VirtoCommerce.OrdersModule.Web/Scripts/order.js b/src/VirtoCommerce.OrdersModule.Web/Scripts/order.js index 1db700313..923b3e8b8 100644 --- a/src/VirtoCommerce.OrdersModule.Web/Scripts/order.js +++ b/src/VirtoCommerce.OrdersModule.Web/Scripts/order.js @@ -853,7 +853,7 @@ angular.module(moduleName, [ var operationDiscountsWidget = { controller: 'virtoCommerce.orderModule.operationDiscountWidgetController', - template: 'Modules/$(VirtoCommerce.Orders)/Scripts/widgets/operation-discounts-widget.tpl.html' + template: 'Modules/$(VirtoCommerce.Orders)/Scripts/widgets/operation-discounts-widget.tpl.html' }; widgetService.registerWidget(operationDiscountsWidget, 'customerOrderDetailWidgets'); widgetService.registerWidget(operationDiscountsWidget, 'paymentDetailWidgets');