Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion src/VirtoCommerce.OrdersModule.Core/ModuleConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ public static class General
Name = "Order.Validation.Enable",
GroupName = "Orders|General",
ValueType = SettingValueType.Boolean,
DefaultValue = false
DefaultValue = true
};

public static SettingDescriptor OrderPaidAndOrderSentNotifications { get; } = new SettingDescriptor
Expand Down Expand Up @@ -276,6 +276,14 @@ public static class General
IsPublic = true,
};

public static SettingDescriptor MaxOrderDocumentCount { get; } = new SettingDescriptor
{
Name = "Order.MaxOrderDocumentCount",
GroupName = "Orders|General",
ValueType = SettingValueType.Integer,
DefaultValue = 20,
};

public static IEnumerable<SettingDescriptor> AllSettings
{
get
Expand All @@ -301,6 +309,7 @@ public static IEnumerable<SettingDescriptor> AllSettings
yield return PurchasedProductIndexation;
yield return EventBasedPurchasedProductIndexation;
yield return PurchasedProductStoreFilter;
yield return MaxOrderDocumentCount;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ public class CustomerOrderPaymentService(
IStoreService storeService,
ICustomerOrderService customerOrderService,
ICustomerOrderSearchService customerOrderSearchService,
IValidator<CustomerOrder> customerOrderValidator,
ISettingsManager settingsManager)
IValidator<CustomerOrder> customerOrderValidator)
: ICustomerOrderPaymentService
{
public virtual async Task<PostProcessPaymentRequestResult> PostProcessPaymentAsync(PaymentParameters paymentParameters)
Expand Down Expand Up @@ -117,13 +116,8 @@ protected virtual IList<PaymentIn> GetInPayments(CustomerOrder customerOrder, Pa
.ToList();
}

protected virtual async Task<ValidationResult> ValidateAsync(CustomerOrder customerOrder)
protected virtual Task<ValidationResult> ValidateAsync(CustomerOrder customerOrder)
{
if (await settingsManager.GetValueAsync<bool>(ModuleConstants.Settings.General.CustomerOrderValidation))
{
return await customerOrderValidator.ValidateAsync(customerOrder);
}

return new ValidationResult();
return customerOrderValidator.ValidateAsync(customerOrder);
}
}
462 changes: 240 additions & 222 deletions src/VirtoCommerce.OrdersModule.Data/Services/CustomerOrderService.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using FluentValidation;
using FluentValidation.Results;
using VirtoCommerce.CoreModule.Core.Common;
using VirtoCommerce.OrdersModule.Core;
using VirtoCommerce.OrdersModule.Core.Model;
using VirtoCommerce.Platform.Core.Settings;

namespace VirtoCommerce.OrdersModule.Data.Validators;

public class CustomerOrderValidator : AbstractValidator<CustomerOrder>
{
private readonly ISettingsManager _settingsManager;

public CustomerOrderValidator(
ISettingsManager settingsManager,
IEnumerable<IValidator<LineItem>> lineItemValidators,
IEnumerable<IValidator<Shipment>> shipmentValidators,
IValidator<PaymentIn> paymentInValidator,
IEnumerable<IValidator<IOperation>> operationValidators)
{
_settingsManager = settingsManager;

SetDefaultRules();

if (lineItemValidators.Any())
{
RuleForEach(order => order.Items).SetValidator(lineItemValidators.Last(), "default");
}

if (shipmentValidators.Any())
{
RuleForEach(order => order.Shipments).SetValidator(shipmentValidators.Last(), "default");
}

RuleForEach(order => order.InPayments).SetValidator(paymentInValidator);

// Apply all operation-level validators (e.g., document count limits)
foreach (var operationValidator in operationValidators)
{
Include(operationValidator);
}
}

public override ValidationResult Validate(ValidationContext<CustomerOrder> context)
{
// Check if validation is enabled (synchronous version)
var isValidationEnabled = _settingsManager.GetValueAsync<bool>(
ModuleConstants.Settings.General.CustomerOrderValidation)
.GetAwaiter()
.GetResult();

if (!isValidationEnabled)
{
// Skip validation if disabled
return new ValidationResult();
}

// Perform validation if enabled
return base.Validate(context);
}

public override async Task<ValidationResult> ValidateAsync(ValidationContext<CustomerOrder> context, CancellationToken cancellation = default)
{
// Check if validation is enabled
var isValidationEnabled = await _settingsManager.GetValueAsync<bool>(
ModuleConstants.Settings.General.CustomerOrderValidation);

if (!isValidationEnabled)
{
// Skip validation if disabled
return new ValidationResult();
}

// Perform validation if enabled
return await base.ValidateAsync(context, cancellation);
}

protected void SetDefaultRules()
{
#pragma warning disable S109
RuleFor(order => order.Number).NotEmpty().MaximumLength(64);
RuleFor(order => order.CustomerId).NotNull().NotEmpty().MaximumLength(64);
RuleFor(order => order.CustomerName).NotEmpty().MaximumLength(255);
RuleFor(order => order.StoreId).NotNull().NotEmpty().MaximumLength(64);
RuleFor(order => order.StoreName).MaximumLength(255);
RuleFor(order => order.ChannelId).MaximumLength(64);
RuleFor(order => order.OrganizationId).MaximumLength(64);
RuleFor(order => order.OrganizationName).MaximumLength(255);
RuleFor(order => order.EmployeeId).MaximumLength(64);
RuleFor(order => order.EmployeeName).MaximumLength(255);
RuleFor(order => order.SubscriptionId).MaximumLength(64);
RuleFor(order => order.SubscriptionNumber).MaximumLength(64);
RuleFor(order => order.LanguageCode).MaximumLength(16)
.Matches("^[a-z]{2}-[A-Z]{2}$")
.When(order => !string.IsNullOrEmpty(order.LanguageCode));
RuleFor(order => order.ShoppingCartId).MaximumLength(128);
RuleFor(order => order.PurchaseOrderNumber).MaximumLength(128);
#pragma warning restore S109
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
using System.Collections.Generic;
using System.Linq;
using FluentValidation;
using VirtoCommerce.CoreModule.Core.Common;
using VirtoCommerce.OrdersModule.Core;
using VirtoCommerce.OrdersModule.Core.Model;
using VirtoCommerce.Platform.Core.Common;
using VirtoCommerce.Platform.Core.Settings;

namespace VirtoCommerce.OrdersModule.Data.Validators;

/// <summary>
/// Validates that the total number of child documents (operations) per order
/// does not exceed the configured maximum limit.
/// Uses the IOperation.ChildrenOperations tree structure for generic traversal.
/// This ensures system performance, storage optimization, and data consistency.
///
/// This validator works with IOperation interface, making it applicable to any operation type,
/// though it's primarily designed for root-level operations like CustomerOrder.
/// </summary>
public class OrderDocumentCountValidator : AbstractValidator<IOperation>
{
private readonly ISettingsManager _settingsManager;

public OrderDocumentCountValidator(ISettingsManager settingsManager)
{
_settingsManager = settingsManager;

RuleFor(operation => operation)
.CustomAsync(async (operation, context, cancellationToken) =>
{
// Only validate root-level operations (like CustomerOrder) that have children
// Skip validation for child operations to avoid redundant checks
if (!string.IsNullOrEmpty(operation.ParentOperationId))
{
return;
}

var maxDocumentCount = await _settingsManager.GetValueAsync<int>(
ModuleConstants.Settings.General.MaxOrderDocumentCount);

// Get all operations in the tree (excluding the root operation itself)
var allOperations = operation.GetFlatObjectsListWithInterface<IOperation>().ToList();
var childOperations = allOperations.Where(op => op.Id != operation.Id).ToList();

// Total child documents count
var totalDocumentCount = childOperations.Count;

if (totalDocumentCount > maxDocumentCount)
{
var operationBreakdown = GetOperationBreakdown(childOperations);

context.AddFailure(
operation.OperationType,
$"{operation.OperationType} document count ({totalDocumentCount}) exceeds the maximum allowed limit of {maxDocumentCount}. " +
$"Documents breakdown: {operationBreakdown}");
}

// Validate each operation that has children
ValidateOperationChildren(childOperations, maxDocumentCount, context);
});
}

/// <summary>
/// Creates a human-readable breakdown of operations by type
/// </summary>
private static string GetOperationBreakdown(IList<IOperation> operations)
{
var grouped = operations
.GroupBy(op => op.OperationType)
.Select(g => $"{g.Key}={g.Count()}")
.OrderBy(s => s);

return string.Join(", ", grouped);
}

/// <summary>
/// Validates that individual operations do not have too many child operations
/// </summary>
private static void ValidateOperationChildren(
IList<IOperation> allOperations,
int maxDocumentCount,
ValidationContext<IOperation> context)
{
// Group operations by their parent to count children per operation
var operationsByParent = allOperations
.Where(op => !string.IsNullOrEmpty(op.ParentOperationId))
.GroupBy(op => op.ParentOperationId)
.ToDictionary(g => g.Key, g => g.ToList());

// Check each operation's child count
foreach (var operation in allOperations)
{
if (operationsByParent.TryGetValue(operation.Id, out var children))
{
var childCount = children.Count;

if (childCount > maxDocumentCount)
{
var childBreakdown = GetOperationBreakdown(children);

context.AddFailure(
operation.OperationType,
$"{operation.OperationType} '{operation.Number}' has {childCount} child documents ({childBreakdown}), " +
$"which exceeds the maximum allowed limit of {maxDocumentCount}.");
}
}
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using FluentValidation;
using VirtoCommerce.OrdersModule.Core.Model;

namespace VirtoCommerce.OrdersModule.Data.Validators;

public class PaymentInValidator : AbstractValidator<PaymentIn>
{
public PaymentInValidator()
{
SetDefaultRules();
}

protected void SetDefaultRules()
{
RuleFor(payment => payment.OrganizationId).MaximumLength(64);
RuleFor(payment => payment.OrganizationName).MaximumLength(255);
RuleFor(payment => payment.CustomerId).NotNull().NotEmpty().MaximumLength(64);
RuleFor(payment => payment.CustomerName).MaximumLength(255);
RuleFor(payment => payment.Purpose).MaximumLength(1024);
RuleFor(payment => payment.GatewayCode).MaximumLength(64);
RuleFor(payment => payment.TaxType).MaximumLength(64);
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
using FluentValidation;
using Microsoft.Extensions.DependencyInjection;
using VirtoCommerce.CoreModule.Core.Common;
using VirtoCommerce.OrdersModule.Core.Model;
using VirtoCommerce.OrdersModule.Data.Validators;
using VirtoCommerce.OrdersModule.Web.Validation;

namespace VirtoCommerce.OrdersModule.Web.Extensions
{
public static class ServiceCollectionExtensions
{
public static void AddValidators(this IServiceCollection serviceCollection)
{
// Register operation-level validators
serviceCollection.AddTransient<IValidator<IOperation>, OrderDocumentCountValidator>();

// Register entity-specific validators
serviceCollection.AddTransient<IValidator<CustomerOrder>, CustomerOrderValidator>();
serviceCollection.AddTransient<IValidator<PaymentIn>, PaymentInValidator>();
serviceCollection.AddTransient<IValidator<OrderPaymentInfo>, PaymentRequestValidator>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,10 @@
"Order.PurchasedProductStoreFilter.Enable": {
"title": "Aktivierung des Store-Filters für gekaufte Produkte",
"description": "Zeigt den Filter für gekaufte Produkte im Store an."
},
"Order.MaxOrderDocumentCount": {
"title": "Maximale Anzahl von Unterdokumenten pro Bestellung",
"description": "Definiert die maximale Anzahl von Unterdokumenten (Zahlungen, Sendungen, Captures, Rückerstattungen usw.), die pro Bestellung erstellt oder gespeichert werden können. Dies gewährleistet Systemleistung, Speicheroptimierung und Datenkonsistenz. Bei Überschreitung dieser Grenze wird beim Speichern eine Ausnahme ausgelöst"
}
},
"module": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,10 @@
"Order.PurchasedProductStoreFilter.Enable": {
"title": "Enable purchased product store filter",
"description": "Display the purchased product filter for the store"
},
"Order.MaxOrderDocumentCount": {
"title": "Maximum number of child documents per order",
"description": "Defines the maximum number of child documents (payments, shipments, captures, refunds, etc.) that can be created or stored per order. This ensures system performance, storage optimization, and data consistency. An exception will be thrown on save if this limit is exceeded"
}
},
"module": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,10 @@
"Order.PurchasedProductStoreFilter.Enable": {
"title": "Habilitar el filtro de productos comprados en la tienda",
"description": "Muestra el filtro de productos comprados en la tienda."
},
"Order.MaxOrderDocumentCount": {
"title": "Número máximo de documentos secundarios por pedido",
"description": "Define el número máximo de documentos secundarios (pagos, envíos, capturas, reembolsos, etc.) que se pueden crear o almacenar por pedido. Esto garantiza el rendimiento del sistema, la optimización del almacenamiento y la consistencia de los datos. Se generará una excepción al guardar si se excede este límite"
}
},
"module": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,10 @@
"Order.PurchasedProductStoreFilter.Enable": {
"title": "Activer le filtre de produits achetés en magasin",
"description": "Affiche le filtre de produits achetés pour le magasin."
},
"Order.MaxOrderDocumentCount": {
"title": "Nombre maximum de documents enfants par commande",
"description": "Définit le nombre maximum de documents enfants (paiements, expéditions, captures, remboursements, etc.) qui peuvent être créés ou stockés par commande. Cela garantit les performances du système, l'optimisation du stockage et la cohérence des données. Une exception sera levée lors de la sauvegarde si cette limite est dépassée"
}
},
"module": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,10 @@
"Order.PurchasedProductStoreFilter.Enable": {
"title": "Abilita il filtro dei prodotti acquistati nel negozio",
"description": "Visualizza il filtro dei prodotti acquistati per il negozio."
},
"Order.MaxOrderDocumentCount": {
"title": "Numero massimo di documenti secondari per ordine",
"description": "Definisce il numero massimo di documenti secondari (pagamenti, spedizioni, acquisizioni, rimborsi, ecc.) che possono essere creati o memorizzati per ordine. Ciò garantisce le prestazioni del sistema, l'ottimizzazione dello storage e la coerenza dei dati. Verrà generata un'eccezione durante il salvataggio se questo limite viene superato"
}
},
"module": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,10 @@
"Order.PurchasedProductStoreFilter.Enable": {
"title": "購入済み商品ストアフィルターを有効にする",
"description": "ストアで購入済み商品フィルターを表示します。"
},
"Order.MaxOrderDocumentCount": {
"title": "注文あたりの子ドキュメントの最大数",
"description": "注文ごとに作成または保存できる子ドキュメント(支払い、配送、キャプチャ、返金など)の最大数を定義します。これにより、システムパフォーマンス、ストレージの最適化、データの整合性が確保されます。この制限を超えた場合、保存時に例外がスローされます"
}
},
"module": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,10 @@
"Order.PurchasedProductStoreFilter.Enable": {
"title": "Włącz filtr zakupionych produktów w sklepie",
"description": "Wyświetla filtr zakupionych produktów w sklepie."
},
"Order.MaxOrderDocumentCount": {
"title": "Maksymalna liczba dokumentów podrzędnych na zamówienie",
"description": "Określa maksymalną liczbę dokumentów podrzędnych (płatności, przesyłki, przechwycenia, zwroty itp.), które można utworzyć lub przechowywać dla jednego zamówienia. Zapewnia to wydajność systemu, optymalizację przechowywania i spójność danych. Jeśli ten limit zostanie przekroczony, podczas zapisywania zostanie zgłoszony wyjątek"
}
},
"module": {
Expand Down
Loading
Loading