diff --git a/samples/VirtoCommerce.OrdersModule2.Web/Module.cs b/samples/VirtoCommerce.OrdersModule2.Web/Module.cs index 67a5b7a82..3abc7e044 100644 --- a/samples/VirtoCommerce.OrdersModule2.Web/Module.cs +++ b/samples/VirtoCommerce.OrdersModule2.Web/Module.cs @@ -5,12 +5,14 @@ using Microsoft.Extensions.DependencyInjection; using VirtoCommerce.CoreModule.Core.Common; using VirtoCommerce.OrdersModule.Core.Model; +using VirtoCommerce.OrdersModule.Core.Services; using VirtoCommerce.OrdersModule.Data.Model; using VirtoCommerce.OrdersModule.Data.Repositories; using VirtoCommerce.OrdersModule2.Web.Authorization; using VirtoCommerce.OrdersModule2.Web.Extensions; using VirtoCommerce.OrdersModule2.Web.Model; using VirtoCommerce.OrdersModule2.Web.Repositories; +using VirtoCommerce.OrdersModule2.Web.Services; using VirtoCommerce.Platform.Core.Common; using VirtoCommerce.Platform.Core.Modularity; using VirtoCommerce.Platform.Core.Security; @@ -37,6 +39,7 @@ public void Initialize(IServiceCollection serviceCollection) serviceCollection.AddTransient(); serviceCollection.AddTransient(); + serviceCollection.AddScoped(); serviceCollection.AddValidators(); } @@ -48,8 +51,9 @@ public void PostInitialize(IApplicationBuilder appBuilder) AbstractTypeFactory.RegisterType(); - var permissionsProvider = appBuilder.ApplicationServices.GetRequiredService(); - permissionsProvider.WithAvailabeScopesForPermissions( + var permissionsRegistrar = appBuilder.ApplicationServices.GetRequiredService(); + permissionsRegistrar.RegisterPermissions(ModuleInfo.Id, ModuleInfo.Title, ModuleConstants.Permissions.AllPermissions); + permissionsRegistrar.WithAvailabeScopesForPermissions( [ // Permission list OrdersModule.Core.ModuleConstants.Security.Permissions.Read diff --git a/samples/VirtoCommerce.OrdersModule2.Web/ModuleConstants.cs b/samples/VirtoCommerce.OrdersModule2.Web/ModuleConstants.cs index a062d9f4f..87652bac1 100644 --- a/samples/VirtoCommerce.OrdersModule2.Web/ModuleConstants.cs +++ b/samples/VirtoCommerce.OrdersModule2.Web/ModuleConstants.cs @@ -5,6 +5,18 @@ namespace VirtoCommerce.OrdersModule2.Web { public class ModuleConstants { + public static class Permissions + { + public const string ReadPricesDirect = "order_samples:read_prices_direct"; + public const string ReadPricesIndirect = "order_samples:read_prices_indirect"; + + public static string[] AllPermissions { get; } = + [ + ReadPricesDirect, + ReadPricesIndirect, + ]; + } + public static class Settings { public static class General diff --git a/samples/VirtoCommerce.OrdersModule2.Web/Services/SampleCustomerOrderDataProtectionService.cs b/samples/VirtoCommerce.OrdersModule2.Web/Services/SampleCustomerOrderDataProtectionService.cs new file mode 100644 index 000000000..27e1e096e --- /dev/null +++ b/samples/VirtoCommerce.OrdersModule2.Web/Services/SampleCustomerOrderDataProtectionService.cs @@ -0,0 +1,54 @@ +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using VirtoCommerce.OrdersModule.Core.Model; +using VirtoCommerce.OrdersModule.Core.Search.Indexed; +using VirtoCommerce.OrdersModule.Core.Services; +using VirtoCommerce.OrdersModule.Data.Services; +using VirtoCommerce.Platform.Core.Common; +using VirtoCommerce.Platform.Core.Security; +using VirtoCommerce.StoreModule.Core.Model; +using VirtoCommerce.StoreModule.Core.Services; + +namespace VirtoCommerce.OrdersModule2.Web.Services; + +public sealed class SampleCustomerOrderDataProtectionService( + ICustomerOrderService crudService, + ICustomerOrderSearchService searchService, + IIndexedCustomerOrderSearchService indexedSearchService, + IUserNameResolver userNameResolver, + SignInManager signInManager, + IStoreService storeService, + IOptions jsonOptions) + : CustomerOrderDataProtectionService(crudService, searchService, indexedSearchService, userNameResolver, signInManager) +{ + private readonly MvcNewtonsoftJsonOptions _jsonOptions = jsonOptions.Value; + + protected override async Task CanReadPrices(ClaimsPrincipal user, CustomerOrder order) + { + var canReadPrices = await base.CanReadPrices(user, order); + + if (!canReadPrices) + { + var store = await storeService.GetByIdAsync(order.StoreId); + canReadPrices = store != null && CanReadPricesForStore(user, store); + } + + return canReadPrices; + } + + private bool CanReadPricesForStore(ClaimsPrincipal user, Store store) + { + var isDirectDistributor = store.Name.ContainsIgnoreCase("Direct"); + + var permissionName = isDirectDistributor + ? ModuleConstants.Permissions.ReadPricesDirect + : ModuleConstants.Permissions.ReadPricesIndirect; + + var permission = user.FindPermission(permissionName, _jsonOptions.SerializerSettings); + + return permission != null; + } +} diff --git a/src/VirtoCommerce.OrdersModule.Core/Extensions/OperationExtensions.cs b/src/VirtoCommerce.OrdersModule.Core/Extensions/OperationExtensions.cs new file mode 100644 index 000000000..5107ecb91 --- /dev/null +++ b/src/VirtoCommerce.OrdersModule.Core/Extensions/OperationExtensions.cs @@ -0,0 +1,33 @@ +using System.Collections; +using System.Linq; +using System.Reflection; +using VirtoCommerce.CoreModule.Core.Common; + +namespace VirtoCommerce.OrdersModule.Core.Extensions; + +public static class OperationExtensions +{ + public static void FillChildOperations(this IOperation operation) + { + var properties = operation.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance); + + var childOperations = properties + .Where(x => x.PropertyType.GetInterface(nameof(IOperation)) != null) + .Select(x => (IOperation)x.GetValue(operation)).Where(x => x != null) + .ToList(); + + // Handle collections + var collections = properties + .Where(x => x.Name != nameof(operation.ChildrenOperations) && x.GetIndexParameters().Length == 0) + .Select(x => x.GetValue(operation, index: null)) + .Where(x => x is IEnumerable and not string) + .Cast(); + + foreach (var collection in collections) + { + childOperations.AddRange(collection.OfType()); + } + + operation.ChildrenOperations = childOperations; + } +} diff --git a/src/VirtoCommerce.OrdersModule.Core/Model/CustomerOrder.cs b/src/VirtoCommerce.OrdersModule.Core/Model/CustomerOrder.cs index 20866225d..0dfe68f3f 100644 --- a/src/VirtoCommerce.OrdersModule.Core/Model/CustomerOrder.cs +++ b/src/VirtoCommerce.OrdersModule.Core/Model/CustomerOrder.cs @@ -3,6 +3,7 @@ using System.Linq; using VirtoCommerce.CoreModule.Core.Common; using VirtoCommerce.CoreModule.Core.Tax; +using VirtoCommerce.OrdersModule.Core.Extensions; using VirtoCommerce.Platform.Core.Common; using VirtoCommerce.Platform.Core.Security; using VirtoCommerce.SearchModule.Core.Model; @@ -258,27 +259,33 @@ public class CustomerOrder : OrderOperation, IHasTaxDetalization, ISupportSecuri #endregion - public virtual void ReduceDetails(string responseGroup) + public override void ReduceDetails(string responseGroup) { - //Reduce details according to response group + base.ReduceDetails(responseGroup); + + // Reduce details according to the response group var orderResponseGroup = EnumUtility.SafeParseFlags(responseGroup, CustomerOrderResponseGroup.Full); if (!orderResponseGroup.HasFlag(CustomerOrderResponseGroup.WithItems)) { Items = null; } + if (!orderResponseGroup.HasFlag(CustomerOrderResponseGroup.WithShipments)) { Shipments = null; } + if (!orderResponseGroup.HasFlag(CustomerOrderResponseGroup.WithInPayments)) { InPayments = null; } + if (!orderResponseGroup.HasFlag(CustomerOrderResponseGroup.WithAddresses)) { Addresses = null; } + if (!orderResponseGroup.HasFlag(CustomerOrderResponseGroup.WithDiscounts)) { Discounts = null; @@ -286,63 +293,95 @@ public virtual void ReduceDetails(string responseGroup) if (!orderResponseGroup.HasFlag(CustomerOrderResponseGroup.WithPrices)) { - TaxPercentRate = 0m; - ShippingTotalWithTax = 0m; - PaymentTotalWithTax = 0m; DiscountAmount = 0m; - Total = 0m; - SubTotal = 0m; - SubTotalWithTax = 0m; - ShippingTotal = 0m; - PaymentTotal = 0m; DiscountTotal = 0m; DiscountTotalWithTax = 0m; - TaxTotal = 0m; - Sum = 0m; Fee = 0m; - FeeTotalWithTax = 0m; FeeTotal = 0m; + FeeTotalWithTax = 0m; FeeWithTax = 0m; HandlingTotal = 0m; HandlingTotalWithTax = 0m; + PaymentDiscountTotal = 0m; + PaymentDiscountTotalWithTax = 0m; + PaymentSubTotal = 0m; + PaymentSubTotalWithTax = 0m; + PaymentTaxTotal = 0m; + PaymentTotal = 0m; + PaymentTotalWithTax = 0m; + ShippingDiscountTotal = 0m; + ShippingDiscountTotalWithTax = 0m; + ShippingSubTotal = 0m; + ShippingSubTotalWithTax = 0m; + ShippingTaxTotal = 0m; + ShippingTotal = 0m; + ShippingTotalWithTax = 0m; + SubTotal = 0m; + SubTotalDiscount = 0m; + SubTotalDiscountWithTax = 0m; + SubTotalTaxTotal = 0m; + SubTotalWithTax = 0m; + TaxPercentRate = 0m; + TaxTotal = 0m; + Total = 0m; } foreach (var shipment in Shipments ?? Array.Empty()) { shipment.ReduceDetails(responseGroup); } + foreach (var inPayment in InPayments ?? Array.Empty()) { inPayment.ReduceDetails(responseGroup); } + foreach (var item in Items ?? Array.Empty()) { item.ReduceDetails(responseGroup); } - } - public virtual void RestoreDetails(CustomerOrder order) + public override void RestoreDetails(OrderOperation operation) { - TaxPercentRate = order.TaxPercentRate; - ShippingTotalWithTax = order.ShippingTotalWithTax; - PaymentTotalWithTax = order.PaymentTotalWithTax; + base.RestoreDetails(operation); + + if (operation is not CustomerOrder order) + { + return; + } + DiscountAmount = order.DiscountAmount; - Total = order.Total; - SubTotal = order.SubTotal; - SubTotalWithTax = order.SubTotalWithTax; - ShippingTotal = order.ShippingTotal; - PaymentTotal = order.PaymentTotal; DiscountTotal = order.DiscountTotal; DiscountTotalWithTax = order.DiscountTotalWithTax; - TaxTotal = order.TaxTotal; - Sum = order.Sum; Fee = order.Fee; - FeeTotalWithTax = order.FeeTotalWithTax; FeeTotal = order.FeeTotal; + FeeTotalWithTax = order.FeeTotalWithTax; FeeWithTax = order.FeeWithTax; HandlingTotal = order.HandlingTotal; HandlingTotalWithTax = order.HandlingTotalWithTax; + PaymentDiscountTotal = order.PaymentDiscountTotal; + PaymentDiscountTotalWithTax = order.PaymentDiscountTotalWithTax; + PaymentSubTotal = order.PaymentSubTotal; + PaymentSubTotalWithTax = order.PaymentSubTotalWithTax; + PaymentTaxTotal = order.PaymentTaxTotal; + PaymentTotal = order.PaymentTotal; + PaymentTotalWithTax = order.PaymentTotalWithTax; + ShippingDiscountTotal = order.ShippingDiscountTotal; + ShippingDiscountTotalWithTax = order.ShippingDiscountTotalWithTax; + ShippingSubTotal = order.ShippingSubTotal; + ShippingSubTotalWithTax = order.ShippingSubTotalWithTax; + ShippingTaxTotal = order.ShippingTaxTotal; + ShippingTotal = order.ShippingTotal; + ShippingTotalWithTax = order.ShippingTotalWithTax; + SubTotal = order.SubTotal; + SubTotalDiscount = order.SubTotalDiscount; + SubTotalDiscountWithTax = order.SubTotalDiscountWithTax; + SubTotalTaxTotal = order.SubTotalTaxTotal; + SubTotalWithTax = order.SubTotalWithTax; + TaxPercentRate = order.TaxPercentRate; + TaxTotal = order.TaxTotal; + Total = order.Total; foreach (var shipment in order.Shipments ?? Array.Empty()) { @@ -377,6 +416,8 @@ public override object Clone() result.Discounts = Discounts?.Select(x => x.Clone()).OfType().ToList(); result.FeeDetails = FeeDetails?.Select(x => x.Clone()).OfType().ToList(); + result.FillChildOperations(); + return result; } diff --git a/src/VirtoCommerce.OrdersModule.Core/Model/LineItem.cs b/src/VirtoCommerce.OrdersModule.Core/Model/LineItem.cs index 3c65c4b82..622d34dfc 100644 --- a/src/VirtoCommerce.OrdersModule.Core/Model/LineItem.cs +++ b/src/VirtoCommerce.OrdersModule.Core/Model/LineItem.cs @@ -155,6 +155,7 @@ public class LineItem : AuditableEntity, IHasOuterId, IHasTaxDetalization, ISupp public virtual void ReduceDetails(string responseGroup) { + // Reduce details according to the response group var orderResponseGroup = EnumUtility.SafeParseFlags(responseGroup, CustomerOrderResponseGroup.Full); if (!orderResponseGroup.HasFlag(CustomerOrderResponseGroup.WithDiscounts)) @@ -164,23 +165,43 @@ public virtual void ReduceDetails(string responseGroup) if (!orderResponseGroup.HasFlag(CustomerOrderResponseGroup.WithPrices)) { - Price = 0m; - PriceWithTax = 0m; DiscountAmount = 0m; DiscountAmountWithTax = 0m; - TaxTotal = 0m; + DiscountTotal = 0m; + DiscountTotalWithTax = 0m; + ExtendedPrice = 0m; + ExtendedPriceWithTax = 0m; + Fee = 0m; + FeeWithTax = 0m; + ListTotal = 0m; + ListTotalWithTax = 0m; + PlacedPrice = 0m; + PlacedPriceWithTax = 0m; + Price = 0m; + PriceWithTax = 0m; TaxPercentRate = 0m; + TaxTotal = 0m; } } public virtual void RestoreDetails(LineItem item) { - Price = item.Price; - PriceWithTax = item.PriceWithTax; DiscountAmount = item.DiscountAmount; DiscountAmountWithTax = item.DiscountAmountWithTax; - TaxTotal = item.TaxTotal; + DiscountTotal = item.DiscountTotal; + DiscountTotalWithTax = item.DiscountTotalWithTax; + ExtendedPrice = item.ExtendedPrice; + ExtendedPriceWithTax = item.ExtendedPriceWithTax; + Fee = item.Fee; + FeeWithTax = item.FeeWithTax; + ListTotal = item.ListTotal; + ListTotalWithTax = item.ListTotalWithTax; + PlacedPrice = item.PlacedPrice; + PlacedPriceWithTax = item.PlacedPriceWithTax; + Price = item.Price; + PriceWithTax = item.PriceWithTax; TaxPercentRate = item.TaxPercentRate; + TaxTotal = item.TaxTotal; } #region ICloneable members diff --git a/src/VirtoCommerce.OrdersModule.Core/Model/OrderOperation.cs b/src/VirtoCommerce.OrdersModule.Core/Model/OrderOperation.cs index 4fed5447e..dc7ddc41b 100644 --- a/src/VirtoCommerce.OrdersModule.Core/Model/OrderOperation.cs +++ b/src/VirtoCommerce.OrdersModule.Core/Model/OrderOperation.cs @@ -74,11 +74,31 @@ protected OrderOperation() #endregion + public bool WithPrices { get; set; } = true; + + public virtual void ReduceDetails(string responseGroup) + { + // Reduce details according to the response group + var orderResponseGroup = EnumUtility.SafeParseFlags(responseGroup, CustomerOrderResponseGroup.Full); + + if (!orderResponseGroup.HasFlag(CustomerOrderResponseGroup.WithPrices)) + { + Sum = 0m; + WithPrices = false; + } + } + + public virtual void RestoreDetails(OrderOperation operation) + { + Sum = operation.Sum; + WithPrices = true; + } + #region ICloneable members public virtual object Clone() { - var result = MemberwiseClone() as OrderOperation; + var result = (OrderOperation)MemberwiseClone(); result.DynamicProperties = DynamicProperties?.Select(x => x.Clone()).OfType().ToList(); result.OperationsLog = OperationsLog?.Select(x => x.Clone()).OfType().ToList(); diff --git a/src/VirtoCommerce.OrdersModule.Core/Model/PaymentIn.cs b/src/VirtoCommerce.OrdersModule.Core/Model/PaymentIn.cs index 9fb84b928..3964719cd 100644 --- a/src/VirtoCommerce.OrdersModule.Core/Model/PaymentIn.cs +++ b/src/VirtoCommerce.OrdersModule.Core/Model/PaymentIn.cs @@ -88,52 +88,63 @@ public class PaymentIn : OrderOperation, IHasTaxDetalization, ITaxable, IHasDisc public ICollection Refunds { get; set; } public ICollection Captures { get; set; } - public virtual void ReduceDetails(string responseGroup) + public override void ReduceDetails(string responseGroup) { - //Reduce details according to response group + base.ReduceDetails(responseGroup); + + // Reduce details according to the response group var orderResponseGroup = EnumUtility.SafeParseFlags(responseGroup, CustomerOrderResponseGroup.Full); + if (!orderResponseGroup.HasFlag(CustomerOrderResponseGroup.WithAddresses)) { BillingAddress = null; } + if (!orderResponseGroup.HasFlag(CustomerOrderResponseGroup.WithDiscounts)) { Discounts = null; } + if (!orderResponseGroup.HasFlag(CustomerOrderResponseGroup.WithRefunds)) { Refunds = null; } + if (!orderResponseGroup.HasFlag(CustomerOrderResponseGroup.WithCaptures)) { Captures = null; } + if (!orderResponseGroup.HasFlag(CustomerOrderResponseGroup.WithPrices)) { - Price = 0m; - PriceWithTax = 0m; DiscountAmount = 0m; DiscountAmountWithTax = 0m; + Price = 0m; + PriceWithTax = 0m; + TaxPercentRate = 0m; + TaxTotal = 0m; Total = 0m; TotalWithTax = 0m; - TaxTotal = 0m; - TaxPercentRate = 0m; - Sum = 0m; } - } - public virtual void RestoreDetails(PaymentIn payment) + public override void RestoreDetails(OrderOperation operation) { - Price = payment.Price; - PriceWithTax = payment.PriceWithTax; + base.RestoreDetails(operation); + + if (operation is not PaymentIn payment) + { + return; + } + DiscountAmount = payment.DiscountAmount; DiscountAmountWithTax = payment.DiscountAmountWithTax; + Price = payment.Price; + PriceWithTax = payment.PriceWithTax; + TaxPercentRate = payment.TaxPercentRate; + TaxTotal = payment.TaxTotal; Total = payment.Total; TotalWithTax = payment.TotalWithTax; - TaxTotal = payment.TaxTotal; - TaxPercentRate = payment.TaxPercentRate; - Sum = payment.Sum; } diff --git a/src/VirtoCommerce.OrdersModule.Core/Model/Shipment.cs b/src/VirtoCommerce.OrdersModule.Core/Model/Shipment.cs index 67c80436e..cf993ba75 100644 --- a/src/VirtoCommerce.OrdersModule.Core/Model/Shipment.cs +++ b/src/VirtoCommerce.OrdersModule.Core/Model/Shipment.cs @@ -113,14 +113,18 @@ public class Shipment : OrderOperation, IHasTaxDetalization, ISupportCancellatio #endregion - public virtual void ReduceDetails(string responseGroup) + public override void ReduceDetails(string responseGroup) { - //Reduce details according to response group + base.ReduceDetails(responseGroup); + + // Reduce details according to the response group var orderResponseGroup = EnumUtility.SafeParseFlags(responseGroup, CustomerOrderResponseGroup.Full); + if (!orderResponseGroup.HasFlag(CustomerOrderResponseGroup.WithAddresses)) { DeliveryAddress = null; } + if (!orderResponseGroup.HasFlag(CustomerOrderResponseGroup.WithDiscounts)) { Discounts = null; @@ -128,30 +132,38 @@ public virtual void ReduceDetails(string responseGroup) if (!orderResponseGroup.HasFlag(CustomerOrderResponseGroup.WithPrices)) { - Price = 0m; - PriceWithTax = 0m; DiscountAmount = 0m; DiscountAmountWithTax = 0m; + Fee = 0m; + FeeWithTax = 0m; + Price = 0m; + PriceWithTax = 0m; + TaxPercentRate = 0m; + TaxTotal = 0m; Total = 0m; TotalWithTax = 0m; - TaxTotal = 0m; - TaxPercentRate = 0m; - Sum = 0m; } - } - public virtual void RestoreDetails(Shipment shipment) + public override void RestoreDetails(OrderOperation operation) { - Price = shipment.Price; - PriceWithTax = shipment.PriceWithTax; + base.RestoreDetails(operation); + + if (operation is not Shipment shipment) + { + return; + } + DiscountAmount = shipment.DiscountAmount; DiscountAmountWithTax = shipment.DiscountAmountWithTax; + Fee = shipment.Fee; + FeeWithTax = shipment.FeeWithTax; + Price = shipment.Price; + PriceWithTax = shipment.PriceWithTax; + TaxPercentRate = shipment.TaxPercentRate; + TaxTotal = shipment.TaxTotal; Total = shipment.Total; TotalWithTax = shipment.TotalWithTax; - TaxTotal = shipment.TaxTotal; - TaxPercentRate = shipment.TaxPercentRate; - Sum = shipment.Sum; } #region ICloneable members diff --git a/src/VirtoCommerce.OrdersModule.Core/Services/ICustomerOrderDataProtectionService.cs b/src/VirtoCommerce.OrdersModule.Core/Services/ICustomerOrderDataProtectionService.cs new file mode 100644 index 000000000..59882722a --- /dev/null +++ b/src/VirtoCommerce.OrdersModule.Core/Services/ICustomerOrderDataProtectionService.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using VirtoCommerce.OrdersModule.Core.Model; +using VirtoCommerce.OrdersModule.Core.Search.Indexed; + +namespace VirtoCommerce.OrdersModule.Core.Services; + +public interface ICustomerOrderDataProtectionService : IIndexedCustomerOrderSearchService, ICustomerOrderSearchService, ICustomerOrderService +{ + Task GetByNumberAsync(string number, string responseGroup = null, bool clone = true); +} diff --git a/src/VirtoCommerce.OrdersModule.Data/Authorization/OrderAuthorizationContext.cs b/src/VirtoCommerce.OrdersModule.Data/Authorization/OrderAuthorizationContext.cs new file mode 100644 index 000000000..08eb0fc33 --- /dev/null +++ b/src/VirtoCommerce.OrdersModule.Data/Authorization/OrderAuthorizationContext.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Authorization; +using VirtoCommerce.Platform.Core.Security; + +namespace VirtoCommerce.OrdersModule.Data.Authorization; + +public class OrderAuthorizationContext +{ + public AuthorizationHandlerContext HandlerContext { get; set; } + public OrderAuthorizationRequirement Requirement { get; set; } + + public string UserId { get; set; } + public string MemberId { get; set; } + public string EmployeeId { get; set; } + + public Permission UserPermission { get; set; } + public string[] AllowedStoreIds { get; set; } + public bool HasResponsibleScope { get; set; } +} diff --git a/src/VirtoCommerce.OrdersModule.Data/Authorization/OrderAuthorizationHandler.cs b/src/VirtoCommerce.OrdersModule.Data/Authorization/OrderAuthorizationHandler.cs new file mode 100644 index 000000000..4550549e7 --- /dev/null +++ b/src/VirtoCommerce.OrdersModule.Data/Authorization/OrderAuthorizationHandler.cs @@ -0,0 +1,91 @@ +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using VirtoCommerce.OrdersModule.Core.Model; +using VirtoCommerce.OrdersModule.Core.Model.Search; +using VirtoCommerce.Platform.Core.Common; +using VirtoCommerce.Platform.Core.Security; +using VirtoCommerce.Platform.Security.Authorization; +using static VirtoCommerce.Platform.Core.PlatformConstants.Security.Claims; + +namespace VirtoCommerce.OrdersModule.Data.Authorization; + +public class OrderAuthorizationHandler(IOptions jsonOptions) + : PermissionAuthorizationHandlerBase +{ + private readonly MvcNewtonsoftJsonOptions _jsonOptions = jsonOptions.Value; + + protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, OrderAuthorizationRequirement requirement) + { + await base.HandleRequirementAsync(context, requirement); + + var orderContext = GetOrderAuthorizationContext(context, requirement); + + if (!context.HasSucceeded && orderContext.UserPermission != null) + { + var succeed = false; + + if (context.Resource is CustomerOrder order) + { + succeed = HandleRequirement(order, orderContext); + } + else if (context.Resource is OrderOperationSearchCriteriaBase criteria) + { + succeed = HandleRequirement(criteria, orderContext); + } + + if (succeed) + { + context.Succeed(requirement); + } + } + } + + protected virtual OrderAuthorizationContext GetOrderAuthorizationContext(AuthorizationHandlerContext context, OrderAuthorizationRequirement requirement) + { + var orderContext = AbstractTypeFactory.TryCreateInstance(); + + orderContext.HandlerContext = context; + orderContext.Requirement = requirement; + + orderContext.UserId = context.User.GetUserId().EmptyToNull(); + orderContext.MemberId = context.User.FindFirstValue(MemberIdClaimType).EmptyToNull(); + orderContext.EmployeeId = orderContext.MemberId ?? orderContext.UserId; + + orderContext.UserPermission = context.User.FindPermission(requirement.Permission, _jsonOptions.SerializerSettings); + + if (orderContext.UserPermission != null) + { + orderContext.AllowedStoreIds = orderContext.UserPermission.AssignedScopes.OfType().Select(x => x.StoreId).DistinctIgnoreCase().ToArray(); + orderContext.HasResponsibleScope = orderContext.UserPermission.AssignedScopes.OfType().Any(); + } + + return orderContext; + } + + protected virtual bool HandleRequirement(CustomerOrder order, OrderAuthorizationContext context) + { + return context.AllowedStoreIds.ContainsIgnoreCase(order.StoreId) || + (context.HasResponsibleScope && order.EmployeeId.EqualsIgnoreCase(context.EmployeeId)); + } + + protected virtual bool HandleRequirement(OrderOperationSearchCriteriaBase criteria, OrderAuthorizationContext context) + { + if (!context.AllowedStoreIds.IsNullOrEmpty()) + { + criteria.StoreIds = criteria.StoreIds.IsNullOrEmpty() + ? context.AllowedStoreIds + : context.AllowedStoreIds.Intersect(criteria.StoreIds ?? []).ToArray(); + } + + if (context.HasResponsibleScope) + { + criteria.EmployeeId = context.EmployeeId; + } + + return true; + } +} diff --git a/src/VirtoCommerce.OrdersModule.Data/ExportImport/OrderExportImport.cs b/src/VirtoCommerce.OrdersModule.Data/ExportImport/OrderExportImport.cs index b914faddb..82f580ae8 100644 --- a/src/VirtoCommerce.OrdersModule.Data/ExportImport/OrderExportImport.cs +++ b/src/VirtoCommerce.OrdersModule.Data/ExportImport/OrderExportImport.cs @@ -12,15 +12,13 @@ namespace VirtoCommerce.OrdersModule.Data.ExportImport { public sealed class OrderExportImport { - private readonly ICustomerOrderSearchService _customerOrderSearchService; - private readonly ICustomerOrderService _customerOrderService; + private readonly ICustomerOrderDataProtectionService _customerOrderDataProtectionService; private readonly JsonSerializer _jsonSerializer; private const int _batchSize = 50; - public OrderExportImport(ICustomerOrderSearchService customerOrderSearchService, ICustomerOrderService customerOrderService, JsonSerializer jsonSerializer) + public OrderExportImport(ICustomerOrderDataProtectionService customerOrderDataProtectionService, JsonSerializer jsonSerializer) { - _customerOrderSearchService = customerOrderSearchService; - _customerOrderService = customerOrderService; + _customerOrderDataProtectionService = customerOrderDataProtectionService; _jsonSerializer = jsonSerializer; } @@ -46,7 +44,7 @@ await writer.SerializeArrayWithPagingAsync(_jsonSerializer, _batchSize, async (s searchCriteria.Take = take; searchCriteria.Skip = skip; searchCriteria.WithPrototypes = true; - var searchResult = await _customerOrderSearchService.SearchAsync(searchCriteria); + var searchResult = await _customerOrderDataProtectionService.SearchAsync(searchCriteria); return (GenericSearchResult)searchResult; }, (processedCount, totalCount) => { @@ -72,7 +70,7 @@ public async Task DoImportAsync(Stream inputStream, Action(_jsonSerializer, _batchSize, _customerOrderService.SaveChangesAsync, processedCount => + await reader.DeserializeArrayWithPagingAsync(_jsonSerializer, _batchSize, _customerOrderDataProtectionService.SaveChangesAsync, processedCount => { progressInfo.Description = $"{processedCount} orders have been imported"; progressCallback(progressInfo); diff --git a/src/VirtoCommerce.OrdersModule.Data/Extensions/OperationExtensions.cs b/src/VirtoCommerce.OrdersModule.Data/Extensions/OperationExtensions.cs index dcf92cc3c..688f42d35 100644 --- a/src/VirtoCommerce.OrdersModule.Data/Extensions/OperationExtensions.cs +++ b/src/VirtoCommerce.OrdersModule.Data/Extensions/OperationExtensions.cs @@ -1,43 +1,15 @@ -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; +using System; using VirtoCommerce.CoreModule.Core.Common; +using VirtoCommerce.OrdersModule.Core.Extensions; namespace VirtoCommerce.OrdersModule.Data.Extensions { public static class OperationExtensions { + [Obsolete("Use VirtoCommerce.OrdersModule.Core.Extensions.OperationExtensions.FillChildOperations()", DiagnosticId = "VC0011", UrlFormat = "https://docs.virtocommerce.org/products/products-virto3-versions/")] public static void FillAllChildOperations(this IOperation operation) { - var retVal = new List(); - var objectType = operation.GetType(); - - var properties = objectType.GetProperties(BindingFlags.Public | BindingFlags.Instance); - - var childOperations = properties.Where(x => x.PropertyType.GetInterface(typeof(IOperation).Name) != null) - .Select(x => (IOperation)x.GetValue(operation)).Where(x => x != null).ToList(); - - foreach (var childOperation in childOperations) - { - retVal.Add(childOperation); - } - - //Handle collection and arrays - var collections = properties.Where(p => p.GetIndexParameters().Length == 0) - .Select(x => x.GetValue(operation, null)) - .Where(x => x is IEnumerable && !(x is string)) - .Cast(); - - foreach (var collection in collections) - { - foreach (var childOperation in collection.OfType()) - { - retVal.Add(childOperation); - } - } - - operation.ChildrenOperations = retVal; + operation.FillChildOperations(); } } } diff --git a/src/VirtoCommerce.OrdersModule.Data/Model/OperationEntity.cs b/src/VirtoCommerce.OrdersModule.Data/Model/OperationEntity.cs index 416f80db7..9e5a211c9 100644 --- a/src/VirtoCommerce.OrdersModule.Data/Model/OperationEntity.cs +++ b/src/VirtoCommerce.OrdersModule.Data/Model/OperationEntity.cs @@ -1,8 +1,8 @@ using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using VirtoCommerce.OrdersModule.Core.Extensions; using VirtoCommerce.OrdersModule.Core.Model; -using VirtoCommerce.OrdersModule.Data.Extensions; using VirtoCommerce.Platform.Core.Common; using VirtoCommerce.Platform.Core.Domain; @@ -70,7 +70,8 @@ public virtual OrderOperation ToModel(OrderOperation operation) operation.CancelReason = CancelReason; operation.IsApproved = IsApproved; operation.Sum = Sum; - operation.FillAllChildOperations(); + + operation.FillChildOperations(); return operation; } diff --git a/src/VirtoCommerce.OrdersModule.Data/Services/CustomerOrderDataProtectionService.cs b/src/VirtoCommerce.OrdersModule.Data/Services/CustomerOrderDataProtectionService.cs new file mode 100644 index 000000000..08f37e754 --- /dev/null +++ b/src/VirtoCommerce.OrdersModule.Data/Services/CustomerOrderDataProtectionService.cs @@ -0,0 +1,174 @@ +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using VirtoCommerce.OrdersModule.Core.Model; +using VirtoCommerce.OrdersModule.Core.Model.Search; +using VirtoCommerce.OrdersModule.Core.Search.Indexed; +using VirtoCommerce.OrdersModule.Core.Services; +using VirtoCommerce.Platform.Core.Common; +using VirtoCommerce.Platform.Core.Security; +using static VirtoCommerce.OrdersModule.Core.ModuleConstants.Security.Permissions; + +namespace VirtoCommerce.OrdersModule.Data.Services; + +public class CustomerOrderDataProtectionService( + ICustomerOrderService crudService, + ICustomerOrderSearchService searchService, + IIndexedCustomerOrderSearchService indexedSearchService, + IUserNameResolver userNameResolver, + SignInManager signInManager) + : ICustomerOrderDataProtectionService +{ + public virtual async Task SearchCustomerOrdersAsync(CustomerOrderIndexedSearchCriteria criteria) + { + var searchResult = await indexedSearchService.SearchCustomerOrdersAsync(criteria); + await ReduceDetailsForCurrentUser(searchResult.Results, cloned: true); + + return searchResult; + } + + public virtual async Task SearchAsync(CustomerOrderSearchCriteria criteria, bool clone = true) + { + var searchResult = await searchService.SearchAsync(criteria, clone); + await ReduceDetailsForCurrentUser(searchResult.Results, clone); + + return searchResult; + } + + public virtual async Task GetByNumberAsync(string number, string responseGroup = null, bool clone = true) + { + var searchCriteria = AbstractTypeFactory.TryCreateInstance(); + searchCriteria.Number = number; + searchCriteria.ResponseGroup = responseGroup; + searchCriteria.Take = 1; + + var searchResult = await searchService.SearchAsync(searchCriteria, clone); + await ReduceDetailsForCurrentUser(searchResult.Results, clone); + + return searchResult.Results.FirstOrDefault(); + } + + public virtual async Task> GetAsync(IList ids, string responseGroup = null, bool clone = true) + { + var orders = await crudService.GetAsync(ids, responseGroup, clone); + await ReduceDetailsForCurrentUser(orders, clone); + + return orders; + } + + public virtual async Task> GetByOuterIdsAsync(IList outerIds, string responseGroup = null, bool clone = true) + { + var orders = await crudService.GetByOuterIdsAsync(outerIds, responseGroup, clone); + await ReduceDetailsForCurrentUser(orders, clone); + + return orders; + } + + public virtual async Task SaveChangesAsync(IList orders) + { + var user = await GetCurrentUser(); + + foreach (var order in orders) + { + await RestoreDetailsForUser(user, order); + } + + await crudService.SaveChangesAsync(orders); + } + + public virtual Task DeleteAsync(IList ids, bool softDelete = false) + { + return crudService.DeleteAsync(ids, softDelete); + } + + + protected virtual async Task ReduceDetailsForCurrentUser(IList orders, bool cloned) + { + if (orders.IsNullOrEmpty()) + { + return; + } + + var user = await GetCurrentUser(); + + for (var i = orders.Count - 1; i >= 0; i--) + { + orders[i] = await ReduceDetailsForUser(user, orders[i], cloned); + } + } + + protected virtual async Task ReduceDetailsForUser(ClaimsPrincipal user, CustomerOrder order, bool cloned) + { + if (order is null || await CanReadPrices(user, order)) + { + return order; + } + + // If the order has not been cloned yet, clone it to avoid corrupting the cache + if (!cloned) + { + order = order.CloneTyped(); + } + + await RemovePrices(order); + + return order; + } + + protected virtual async Task RestoreDetailsForUser(ClaimsPrincipal user, CustomerOrder order) + { + if (!await CanReadPrices(user, order)) + { + await RestorePrices(order); + } + } + + protected virtual Task CanReadPrices(ClaimsPrincipal user, CustomerOrder order) + { + if (user is null) + { + return Task.FromResult(false); + } + + var result = user.HasGlobalPermission(ReadPrices); + + return Task.FromResult(result); + } + + protected virtual Task RemovePrices(CustomerOrder order) + { + order.ReduceDetails((CustomerOrderResponseGroup.Full & ~CustomerOrderResponseGroup.WithPrices).ToString()); + + return Task.CompletedTask; + } + + protected virtual async Task RestorePrices(CustomerOrder order) + { + var originalOrder = await crudService.GetByIdAsync(order.Id); + if (originalOrder != null) + { + order.RestoreDetails(originalOrder); + } + } + + protected virtual async Task GetCurrentUser() + { + var userName = userNameResolver.GetCurrentUserName(); + if (userName is null) + { + return null; + } + + var user = await signInManager.UserManager.FindByNameAsync(userName); + if (user is null) + { + return null; + } + + var claimsPrincipal = await signInManager.CreateUserPrincipalAsync(user); + + return claimsPrincipal; + } +} diff --git a/src/VirtoCommerce.OrdersModule.Web/Authorization/OrderAuthorizationHandler.cs b/src/VirtoCommerce.OrdersModule.Web/Authorization/OrderAuthorizationHandler.cs deleted file mode 100644 index 7cf2aa9d3..000000000 --- a/src/VirtoCommerce.OrdersModule.Web/Authorization/OrderAuthorizationHandler.cs +++ /dev/null @@ -1,100 +0,0 @@ -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; -using VirtoCommerce.OrdersModule.Core; -using VirtoCommerce.OrdersModule.Core.Model; -using VirtoCommerce.OrdersModule.Core.Model.Search; -using VirtoCommerce.OrdersModule.Data.Authorization; -using VirtoCommerce.Platform.Core.Common; -using VirtoCommerce.Platform.Core.Security; -using VirtoCommerce.Platform.Security.Authorization; - -namespace VirtoCommerce.OrdersModule.Web.Authorization -{ - public sealed class OrderAuthorizationHandler : PermissionAuthorizationHandlerBase - { - //VP-6222 Fix permission scope "Only for order responsible" - //Copy of PlatformConstants.Security.Claims.MemberIdClaimType. Copied to reduce platform version dependency - public const string MemberIdClaimType = "memberId"; - - private readonly MvcNewtonsoftJsonOptions _jsonOptions; - - public OrderAuthorizationHandler(IOptions jsonOptions) - { - _jsonOptions = jsonOptions.Value; - } - - protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, OrderAuthorizationRequirement requirement) - { - await base.HandleRequirementAsync(context, requirement); - - if (!context.HasSucceeded) - { - var userPermission = context.User.FindPermission(requirement.Permission, _jsonOptions.SerializerSettings); - if (userPermission != null) - { - //Use associated to user memberId and userId as only fall-back value to check "OnlyOrderResponsibleScope" auth rule - var memberId = context.User.FindFirstValue(MemberIdClaimType); - var userId = context.User.GetUserId(); - memberId = string.IsNullOrEmpty(memberId) ? null : memberId; - userId = string.IsNullOrEmpty(userId) ? null : userId; - - var storeSelectedScopes = userPermission.AssignedScopes.OfType(); - var onlyResponsibleScope = userPermission.AssignedScopes.OfType().FirstOrDefault(); - var allowedStoreIds = storeSelectedScopes.Select(x => x.StoreId).Distinct().ToArray(); - - if (context.Resource is OrderOperationSearchCriteriaBase criteria) - { - criteria.StoreIds = allowedStoreIds; - if (onlyResponsibleScope != null) - { - criteria.EmployeeId = memberId ?? userId; - } - - context.Succeed(requirement); - } - - if (context.Resource is CustomerOrder order) - { - var succeed = allowedStoreIds.Contains(order.StoreId); - if (!succeed) - { - succeed = onlyResponsibleScope != null && order.EmployeeId == (memberId ?? userId); - } - if (succeed) - { - context.Succeed(requirement); - } - } - } - } - - //Apply ReadPrices authorization rules for all checks - if (!context.User.HasGlobalPermission(ModuleConstants.Security.Permissions.ReadPrices)) - { - if (context.Resource is OrderOperationSearchCriteriaBase criteria) - { - if (string.IsNullOrEmpty(criteria.ResponseGroup)) - { - criteria.ResponseGroup = CustomerOrderResponseGroup.Full.ToString(); - } - - criteria.ResponseGroup = EnumUtility.SafeRemoveFlagFromEnumString(criteria.ResponseGroup, CustomerOrderResponseGroup.WithPrices); - //Do not allow pass empty response group into services because that can leads to use default response group CustomerOrderResponseGroup.Full - if (string.IsNullOrEmpty(criteria.ResponseGroup)) - { - criteria.ResponseGroup = CustomerOrderResponseGroup.Default.ToString(); - } - } - - if (context.Resource is CustomerOrder order) - { - order.ReduceDetails((CustomerOrderResponseGroup.Full & ~CustomerOrderResponseGroup.WithPrices).ToString()); - } - } - } - } -} diff --git a/src/VirtoCommerce.OrdersModule.Web/Controllers/Api/OrderModuleController.cs b/src/VirtoCommerce.OrdersModule.Web/Controllers/Api/OrderModuleController.cs index 144d31f12..c8e9fdea3 100644 --- a/src/VirtoCommerce.OrdersModule.Web/Controllers/Api/OrderModuleController.cs +++ b/src/VirtoCommerce.OrdersModule.Web/Controllers/Api/OrderModuleController.cs @@ -26,11 +26,9 @@ using VirtoCommerce.OrdersModule.Core.Model; using VirtoCommerce.OrdersModule.Core.Model.Search; using VirtoCommerce.OrdersModule.Core.Notifications; -using VirtoCommerce.OrdersModule.Core.Search.Indexed; using VirtoCommerce.OrdersModule.Core.Services; using VirtoCommerce.OrdersModule.Data.Authorization; using VirtoCommerce.OrdersModule.Data.Caching; -using VirtoCommerce.OrdersModule.Data.Extensions; using VirtoCommerce.PaymentModule.Core.Model; using VirtoCommerce.PaymentModule.Data; using VirtoCommerce.PaymentModule.Model.Requests; @@ -38,7 +36,6 @@ using VirtoCommerce.Platform.Core.ChangeLog; using VirtoCommerce.Platform.Core.Common; using VirtoCommerce.Platform.Core.Json; -using VirtoCommerce.Platform.Core.Security; using VirtoCommerce.Platform.Core.Settings; using VirtoCommerce.StoreModule.Core.Model; using VirtoCommerce.StoreModule.Core.Services; @@ -51,6 +48,7 @@ namespace VirtoCommerce.OrdersModule.Web.Controllers.Api public class OrderModuleController( ICustomerOrderService customerOrderService, ICustomerOrderSearchService searchService, + ICustomerOrderDataProtectionService customerOrderDataProtectionService, IStoreService storeService, ITenantUniqueNumberGenerator numberGenerator, IPlatformMemoryCache platformMemoryCache, @@ -63,7 +61,6 @@ public class OrderModuleController( ICustomerOrderTotalsCalculator totalsCalculator, IAuthorizationService authorizationService, IConverter converter, - IIndexedCustomerOrderSearchService indexedSearchService, IConfiguration configuration, IOptions htmlToPdfOptions, IOptions outputJsonSerializerSettings, @@ -87,9 +84,10 @@ public async Task> SearchCustomerOrder([ return Forbid(); } - var result = await searchService.SearchAsync(criteria); - //It is a important to return serialized data by such way. Instead you have a slow response time for large outputs - //https://github.com/dotnet/aspnetcore/issues/19646 + var result = await customerOrderDataProtectionService.SearchAsync(criteria); + + // It is important to return serialized data in this way. Otherwise, large outputs result in a slow response. + // https://github.com/dotnet/aspnetcore/issues/19646 return Content(JsonConvert.SerializeObject(result, outputJsonSerializerSettings.Value), "application/json"); } @@ -103,18 +101,19 @@ public async Task> SearchCustomerOrder([ [Route("number/{number}")] public async Task> GetByNumber(string number, [SwaggerOptional][FromQuery] string respGroup = null) { - var searchCriteria = AbstractTypeFactory.TryCreateInstance(); - searchCriteria.Number = number; - searchCriteria.ResponseGroup = respGroup; - var authorizationResult = await authorizationService.AuthorizeAsync(User, searchCriteria, new OrderAuthorizationRequirement(ModuleConstants.Security.Permissions.Read)); + var order = await customerOrderDataProtectionService.GetByNumberAsync(number, respGroup); + if (order == null) + { + return NotFound(); + } + + var authorizationResult = await authorizationService.AuthorizeAsync(User, order, new OrderAuthorizationRequirement(ModuleConstants.Security.Permissions.Read)); if (!authorizationResult.Succeeded) { return Forbid(); } - var result = await searchService.SearchAsync(searchCriteria); - var retVal = result.Results.FirstOrDefault(); - return Ok(retVal); + return Ok(order); } /// @@ -127,20 +126,19 @@ public async Task> GetByNumber(string number, [Swagg [Route("{id}")] public async Task> GetById(string id, [SwaggerOptional][FromQuery] string respGroup = null) { - var searchCriteria = AbstractTypeFactory.TryCreateInstance(); - searchCriteria.Ids = [id]; - searchCriteria.ResponseGroup = respGroup; - searchCriteria.WithPrototypes = true; + var order = await customerOrderDataProtectionService.GetByIdAsync(id, respGroup); + if (order == null) + { + return NotFound(); + } - var authorizationResult = await authorizationService.AuthorizeAsync(User, searchCriteria, new OrderAuthorizationRequirement(ModuleConstants.Security.Permissions.Read)); + var authorizationResult = await authorizationService.AuthorizeAsync(User, order, new OrderAuthorizationRequirement(ModuleConstants.Security.Permissions.Read)); if (!authorizationResult.Succeeded) { return Forbid(); } - var result = await searchService.SearchAsync(searchCriteria); - - return Ok(result.Results.FirstOrDefault()); + return Ok(order); } /// @@ -153,19 +151,19 @@ public async Task> GetById(string id, [SwaggerOption [Route("outer/{outerId}")] public async Task> GetByOuterId(string outerId, [SwaggerOptional][FromQuery] string responseGroup = null) { - var searchCriteria = AbstractTypeFactory.TryCreateInstance(); - searchCriteria.OuterIds = [outerId]; - searchCriteria.ResponseGroup = responseGroup; - searchCriteria.WithPrototypes = true; + var order = await customerOrderDataProtectionService.GetByOuterIdAsync(outerId, responseGroup); + if (order == null) + { + return NotFound(); + } - var authorizationResult = await authorizationService.AuthorizeAsync(User, searchCriteria, new OrderAuthorizationRequirement(ModuleConstants.Security.Permissions.Read)); + var authorizationResult = await authorizationService.AuthorizeAsync(User, order, new OrderAuthorizationRequirement(ModuleConstants.Security.Permissions.Read)); if (!authorizationResult.Succeeded) { return Forbid(); } - var result = await searchService.SearchAsync(searchCriteria); - return Ok(result.Results.FirstOrDefault()); + return Ok(order); } /// @@ -187,7 +185,7 @@ public async Task> CalculateTotals([FromBody] Custom }); } totalsCalculator.CalculateTotals(customerOrder); - customerOrder.FillAllChildOperations(); + customerOrder.FillChildOperations(); return Ok(customerOrder); } @@ -336,7 +334,8 @@ public async Task> CreateOrder([FromBody] CustomerOr }); } - await customerOrderService.SaveChangesAsync([customerOrder]); + await customerOrderDataProtectionService.SaveChangesAsync([customerOrder]); + return Ok(customerOrder); } @@ -371,15 +370,9 @@ public async Task UpdateOrder([FromBody] CustomerOrder customerOrd }); } - if (!User.HasGlobalPermission(ModuleConstants.Security.Permissions.ReadPrices)) - { - // Restore prices from order if user has no ReadPrices permission and receive the order without prices - customerOrder.RestoreDetails(order); - } - try { - await customerOrderService.SaveChangesAsync(new[] { customerOrder }); + await customerOrderDataProtectionService.SaveChangesAsync([customerOrder]); } catch (DbUpdateConcurrencyException) { @@ -561,13 +554,7 @@ private async Task GetRequestBody() [SwaggerFileResponse] public async Task GetInvoicePdf(string orderNumber) { - var searchCriteria = AbstractTypeFactory.TryCreateInstance(); - searchCriteria.Number = orderNumber; - searchCriteria.Take = 1; - - var orders = await searchService.SearchAsync(searchCriteria); - var order = orders.Results.FirstOrDefault(); - + var order = await customerOrderDataProtectionService.GetByNumberAsync(orderNumber); if (order == null) { throw new InvalidOperationException($"Cannot find order with number {orderNumber}"); @@ -682,7 +669,8 @@ public async Task> SearchCustomerOrderIn return Forbid(); } - var result = await indexedSearchService.SearchCustomerOrdersAsync(criteria); + var result = await customerOrderDataProtectionService.SearchCustomerOrdersAsync(criteria); + return Content(JsonConvert.SerializeObject(result, outputJsonSerializerSettings.Value), "application/json"); } @@ -708,15 +696,15 @@ private byte[] GeneratePdf(string htmlContent) } /// - /// Partial update for the specified Orde by id + /// Partial update for the specified Order by id /// - /// Orde id + /// Order id /// JsonPatchDocument object with fields to update /// [HttpPatch] [Route("{id}")] [ProducesResponseType(typeof(void), StatusCodes.Status204NoContent)] - public async Task PatcOrder(string id, [FromBody] JsonPatchDocument patchDocument) + public async Task PatchOrder(string id, [FromBody] JsonPatchDocument patchDocument) { if (patchDocument == null) { @@ -754,7 +742,7 @@ public async Task PatcOrder(string id, [FromBody] JsonPatchDocumen try { - await customerOrderService.SaveChangesAsync([order]); + await customerOrderDataProtectionService.SaveChangesAsync([order]); } catch (DbUpdateConcurrencyException) { diff --git a/src/VirtoCommerce.OrdersModule.Web/Module.cs b/src/VirtoCommerce.OrdersModule.Web/Module.cs index 5e72a5d72..0efc76274 100644 --- a/src/VirtoCommerce.OrdersModule.Web/Module.cs +++ b/src/VirtoCommerce.OrdersModule.Web/Module.cs @@ -29,7 +29,6 @@ using VirtoCommerce.OrdersModule.Data.Search.Indexed; using VirtoCommerce.OrdersModule.Data.Services; using VirtoCommerce.OrdersModule.Data.SqlServer; -using VirtoCommerce.OrdersModule.Web.Authorization; using VirtoCommerce.OrdersModule.Web.Extensions; using VirtoCommerce.OrdersModule.Web.JsonConverters; using VirtoCommerce.Platform.Core.Common; @@ -85,6 +84,7 @@ public void Initialize(IServiceCollection serviceCollection) serviceCollection.AddTransient>(provider => () => provider.CreateScope().ServiceProvider.GetRequiredService()); serviceCollection.AddTransient(); serviceCollection.AddTransient(); + serviceCollection.AddScoped(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); @@ -93,7 +93,7 @@ public void Initialize(IServiceCollection serviceCollection) serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); - serviceCollection.AddTransient(); + serviceCollection.AddScoped(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); @@ -241,14 +241,14 @@ public void Uninstall() public Task ExportAsync(Stream outStream, ExportImportOptions options, Action progressCallback, ICancellationToken cancellationToken) { - return _appBuilder.ApplicationServices.GetRequiredService().DoExportAsync(outStream, + return _appBuilder.ApplicationServices.CreateScope().ServiceProvider.GetRequiredService().DoExportAsync(outStream, progressCallback, cancellationToken); } public Task ImportAsync(Stream inputStream, ExportImportOptions options, Action progressCallback, ICancellationToken cancellationToken) { - return _appBuilder.ApplicationServices.GetRequiredService().DoImportAsync(inputStream, + return _appBuilder.ApplicationServices.CreateScope().ServiceProvider.GetRequiredService().DoImportAsync(inputStream, progressCallback, cancellationToken); } } diff --git a/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/capture-detail.js b/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/capture-detail.js index 161f50391..d77d2a082 100644 --- a/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/capture-detail.js +++ b/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/capture-detail.js @@ -7,7 +7,7 @@ angular.module('virtoCommerce.orderModule') 'virtoCommerce.customerModule.members', function ($scope, bladeNavigationService, authService, paymentMethods, members) { var blade = $scope.blade; - blade.isVisiblePrices = authService.checkPermission('order:read_prices'); + blade.isVisiblePrices = blade.currentEntity.withPrices; blade.title = 'orders.blades.capture-details.title'; blade.titleValues = { number: blade.currentEntity.number }; diff --git a/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/customerOrder-detail.js b/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/customerOrder-detail.js index 260cb1bca..c68bf694e 100644 --- a/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/customerOrder-detail.js +++ b/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/customerOrder-detail.js @@ -6,7 +6,7 @@ angular.module('virtoCommerce.orderModule') var blade = $scope.blade; blade.currentEntityId = blade.customerOrder.id; - blade.isVisiblePrices = authService.checkPermission('order:read_prices'); + blade.isVisiblePrices = blade.customerOrder.withPrices; angular.extend(blade, { title: 'orders.blades.customerOrder-detail.title', diff --git a/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/customerOrder-item-detail.js b/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/customerOrder-item-detail.js index 8e9fd25ca..bc31c6e02 100644 --- a/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/customerOrder-item-detail.js +++ b/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/customerOrder-item-detail.js @@ -4,7 +4,7 @@ angular.module('virtoCommerce.orderModule') function ($scope, bladeNavigationService, authService) { var blade = $scope.blade; blade.updatePermission = 'order:update'; - blade.isVisiblePrices = authService.checkPermission('order:read_prices'); + blade.isVisiblePrices = blade.order.withPrices; blade.formScope = null; blade.metaFields = [ { diff --git a/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/customerOrder-items.js b/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/customerOrder-items.js index 507b5f188..b8d4680fe 100644 --- a/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/customerOrder-items.js +++ b/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/customerOrder-items.js @@ -5,7 +5,7 @@ angular.module('virtoCommerce.orderModule') function ($scope, $translate, bladeNavigationService, authService) { var blade = $scope.blade; blade.updatePermission = 'order:update'; - blade.isVisiblePrices = authService.checkPermission('order:read_prices'); + blade.isVisiblePrices = blade.currentEntity.withPrices; $translate('orders.blades.customerOrder-detail.title', { customer: blade.currentEntity.customerName }).then(function (result) { blade.title = 'orders.widgets.customerOrder-items.blade-title'; diff --git a/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/customerOrder-list.js b/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/customerOrder-list.js index 4244828e3..0d2114d99 100644 --- a/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/customerOrder-list.js +++ b/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/customerOrder-list.js @@ -5,8 +5,6 @@ angular.module('virtoCommerce.orderModule') var bladeNavigationService = bladeUtils.bladeNavigationService; $scope.uiGridConstants = uiGridConstants; $scope.useIndexedSearch = false; - - $scope.getPricesVisibility = () => authService.checkPermission('order:read_prices'); $scope.getGridOptions = () => { return { @@ -18,7 +16,7 @@ angular.module('virtoCommerce.orderModule') { name: 'number', displayName: 'orders.blades.customerOrder-list.labels.number', width: '***', displayAlways: true }, { name: 'customerName', displayName: 'orders.blades.customerOrder-list.labels.customer', width: '***' }, { name: 'storeId', displayName: 'orders.blades.customerOrder-list.labels.store', width: '**' }, - { name: 'total', displayName: 'orders.blades.customerOrder-list.labels.total', cellFilter: 'currency | showPrice:' + $scope.getPricesVisibility(), width: '**' }, + { name: 'total', displayName: 'orders.blades.customerOrder-list.labels.total', cellTemplate: 'price.cell.html', width: '**' }, { name: 'currency', displayName: 'orders.blades.customerOrder-list.labels.currency', width: '*' }, { name: 'isApproved', displayName: 'orders.blades.customerOrder-list.labels.confirmed', width: '*', cellClass: '__blue' }, { name: 'status', displayName: 'orders.blades.customerOrder-list.labels.status', cellFilter: 'settingTranslate:"Order.Status"', width: '*' }, @@ -253,7 +251,7 @@ angular.module('virtoCommerce.orderModule') "paymentTotal", "paymentTotalWithTax", "paymentSubTotal", "paymentSubTotalWithTax", "paymentDiscountTotal", "paymentDiscountTotalWithTax", "paymentTaxTotal", "discountTotal", "discountTotalWithTax", "fee", "feeWithTax", "feeTotal", "feeTotalWithTax", "taxTotal", "sum" ], function(name) { - return { name: name, cellFilter: "currency | showPrice:" + $scope.getPricesVisibility(), visible: false }; + return { name: name, cellTemplate: 'price.cell.html', visible: false }; })); $scope.gridOptions = gridOptions; diff --git a/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/customerOrder-list.tpl.html b/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/customerOrder-list.tpl.html index e499610a4..c988f3d61 100644 --- a/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/customerOrder-list.tpl.html +++ b/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/customerOrder-list.tpl.html @@ -1,42 +1,42 @@
-
-
-
-
-
-
- -
-
+
+
+
+
+ +
+
\ No newline at end of file + + diff --git a/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/payment-detail.js b/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/payment-detail.js index 4e68cbaa0..abae31092 100644 --- a/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/payment-detail.js +++ b/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/payment-detail.js @@ -9,7 +9,7 @@ angular.module('virtoCommerce.orderModule') 'virtoCommerce.orderModule.knownOperations', function ($scope, bladeNavigationService, customerOrders, authService, paymentMethods, members, knownOperations) { var blade = $scope.blade; - blade.isVisiblePrices = authService.checkPermission('order:read_prices'); + blade.isVisiblePrices = blade.currentEntity.withPrices; blade.paymentMethods = []; blade.captureStatuses = ['Authorized', 'Paid']; diff --git a/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/refund-detail.js b/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/refund-detail.js index 668426e34..c60c623f2 100644 --- a/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/refund-detail.js +++ b/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/refund-detail.js @@ -8,7 +8,7 @@ angular.module('virtoCommerce.orderModule') 'virtoCommerce.orderModule.refundReasonsService', function ($scope, bladeNavigationService, authService, paymentMethods, members, refundReasonsService) { var blade = $scope.blade; - blade.isVisiblePrices = authService.checkPermission('order:read_prices'); + blade.isVisiblePrices = blade.currentEntity.withPrices; blade.title = 'orders.blades.refund-details.title'; blade.titleValues = { number: blade.currentEntity.number }; diff --git a/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/shipment-detail.js b/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/shipment-detail.js index 9c7b6723c..1cb397807 100644 --- a/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/shipment-detail.js +++ b/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/shipment-detail.js @@ -11,7 +11,7 @@ angular.module('virtoCommerce.orderModule') function ($scope, bladeNavigationService, customerOrders, fulfillments, authService, shippingMethods, members, knownOperations) { var blade = $scope.blade; - blade.isVisiblePrices = authService.checkPermission('order:read_prices'); + blade.isVisiblePrices = blade.currentEntity.withPrices; blade.shippingMethods = []; if (blade.isNew) { diff --git a/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/shipment-items.js b/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/shipment-items.js index f04a0026e..71b25f61b 100644 --- a/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/shipment-items.js +++ b/src/VirtoCommerce.OrdersModule.Web/Scripts/blades/shipment-items.js @@ -2,7 +2,7 @@ angular.module('virtoCommerce.orderModule') .controller('virtoCommerce.orderModule.shipmentItemsController', ['$scope', 'platformWebApp.bladeNavigationService', 'platformWebApp.dialogService', 'platformWebApp.authService', function ($scope, bladeNavigationService, dialogService, authService) { var blade = $scope.blade; blade.updatePermission = 'order:update'; - blade.isVisiblePrices = authService.checkPermission('order:read_prices'); + blade.isVisiblePrices = blade.currentEntity.withPrices; blade.currentEntity.items = blade.currentEntity.items || []; diff --git a/src/VirtoCommerce.OrdersModule.Web/Scripts/order.js b/src/VirtoCommerce.OrdersModule.Web/Scripts/order.js index 9a8f5bb43..494640005 100644 --- a/src/VirtoCommerce.OrdersModule.Web/Scripts/order.js +++ b/src/VirtoCommerce.OrdersModule.Web/Scripts/order.js @@ -405,8 +405,8 @@ angular.module(moduleName, [ template: 'Modules/$(VirtoCommerce.Orders)/Scripts/widgets/customerOrder-address-widget.tpl.html' }; widgetService.registerWidget(customerOrderAddressWidget, 'customerOrderDetailWidgets'); - function checkPermissionToReadPrices() { - return authService.checkPermission('order:read_prices'); + function checkPermissionToReadPrices(blade) { + return blade.customerOrder.withPrices; } function checkPermissionToViewDashboard() { diff --git a/src/VirtoCommerce.OrdersModule.Web/Scripts/widgets/customerOrder-items-widget.tpl.html b/src/VirtoCommerce.OrdersModule.Web/Scripts/widgets/customerOrder-items-widget.tpl.html index f2aec70ff..681e87409 100644 --- a/src/VirtoCommerce.OrdersModule.Web/Scripts/widgets/customerOrder-items-widget.tpl.html +++ b/src/VirtoCommerce.OrdersModule.Web/Scripts/widgets/customerOrder-items-widget.tpl.html @@ -2,6 +2,6 @@
{{operation.items.length | number:0}}
{{'orders.widgets.customerOrder-items.title' | translate}}
-
{{(operation.subTotalWithTax - operation.subTotalDiscountWithTax) | currency:operation.currency}}
+
{{(operation.subTotalWithTax - operation.subTotalDiscountWithTax) | currency:operation.currency}}
diff --git a/src/VirtoCommerce.OrdersModule.Web/Scripts/widgets/operation-tree-widget.tpl.html b/src/VirtoCommerce.OrdersModule.Web/Scripts/widgets/operation-tree-widget.tpl.html index e3051e4e7..cade0ae4f 100644 --- a/src/VirtoCommerce.OrdersModule.Web/Scripts/widgets/operation-tree-widget.tpl.html +++ b/src/VirtoCommerce.OrdersModule.Web/Scripts/widgets/operation-tree-widget.tpl.html @@ -17,7 +17,7 @@ {{node.operation.createdDate | amParse | date}}
-
+
{{node.operation.sum || node.operation.amount | currency:node.operation.currency}}
@@ -46,7 +46,7 @@ {{node.operation.deliveryDate ? (node.operation.deliveryDate | amParse | date) : '-' }}
-
+
{{node.operation.sum || node.operation.amount | currency:node.operation.currency}}
diff --git a/tests/VirtoCommerce.OrdersModule.Tests/CustomerOrderTests.cs b/tests/VirtoCommerce.OrdersModule.Tests/CustomerOrderTests.cs index 165428778..6d69a71f0 100644 --- a/tests/VirtoCommerce.OrdersModule.Tests/CustomerOrderTests.cs +++ b/tests/VirtoCommerce.OrdersModule.Tests/CustomerOrderTests.cs @@ -11,6 +11,31 @@ namespace VirtoCommerce.OrdersModule.Tests { public class CustomerOrderTests { + [Fact] + public void CloneTest() + { + // Arrange + var shipment = new Shipment(); + + var order = new CustomerOrder + { + Shipments = [shipment], + ChildrenOperations = [shipment], + }; + + // Act + var clonedOrder = order.CloneTyped(); + + // Assert + Assert.NotSame(clonedOrder.Shipments, order.Shipments); + Assert.NotSame(clonedOrder.Shipments.First(), order.Shipments.First()); + + Assert.NotSame(clonedOrder.ChildrenOperations, order.ChildrenOperations); + Assert.NotSame(clonedOrder.ChildrenOperations.First(), order.ChildrenOperations.First()); + + Assert.Same(clonedOrder.Shipments.First(), clonedOrder.ChildrenOperations.First()); + } + [Fact] public void FromModel_MultipleShipments_ShipmentItemsLinkedCorrectly() {