Persisting Data in Acumatica Events – Part 4: Complex Scenarios
In the first three parts of this series, we explored the foundations, advanced techniques, and performance optimization strategies for persisting data in Acumatica events. In this final part, we'll tackle complex real-world scenarios and production-ready patterns that combine all the techniques we've learned.
⚡ Key Insight: Production-ready persistence requires combining multiple techniques: event handling, validation, transaction management, and performance optimization into cohesive, maintainable solutions.
Scenario 1: Complex Validation with Dependencies
When validation logic spans multiple records, tables, and business rules, you need a structured approach that maintains performance and provides clear feedback.
The Problem
You need to validate that an order's total discount doesn't exceed the customer's maximum discount, the line quantities don't exceed available inventory, and the order total doesn't exceed the customer's credit limit—all in a single save operation.
public class OrderValidationService
{
private readonly SOOrderEntry _graph;
private readonly List _validationErrors = new List();
private readonly List _validationWarnings = new List();
public OrderValidationService(SOOrderEntry graph)
{
_graph = graph;
}
public bool ValidateOrder(SOOrder order, PXCache cache)
{
_validationErrors.Clear();
_validationWarnings.Clear();
// 1. Validate discount against customer maximum
ValidateDiscount(order);
// 2. Validate inventory availability
ValidateInventory(order);
// 3. Validate credit limit
ValidateCreditLimit(order);
// 4. Check for errors and warnings
if (_validationErrors.Any())
{
var errorMsg = string.Join(Environment.NewLine, _validationErrors);
throw new PXException(errorMsg);
}
foreach (var warning in _validationWarnings)
{
cache.RaiseExceptionHandling(
order,
null,
new PXSetPropertyException(warning, PXErrorLevel.Warning));
}
return !_validationErrors.Any();
}
private void ValidateDiscount(SOOrder order)
{
var customer = PXSelect<Customer,
Where<Customer.customerID, Equal<Required<Customer.customerID>>>>
.Select(_graph, order.CustomerID);
if (customer?.MaxDiscountPct > 0 && order.DiscountPercent > customer.MaxDiscountPct)
{
_validationErrors.Add(
$"Order discount {order.DiscountPercent}% exceeds customer maximum {customer.MaxDiscountPct}%");
}
}
private void ValidateInventory(SOOrder order)
{
// Use bulk query for performance
var lines = _graph.Transactions.Select()
.Cast<SOLine>()
.Where(l => l.OrderNbr == order.OrderNbr)
.ToList();
if (!lines.Any()) return;
var inventoryIDs = lines.Select(l => l.InventoryID).Distinct().ToList();
var inventoryData = PXSelect<InventoryItem,
Where<InventoryItem.inventoryID, In<Required<InventoryItem.inventoryID>>>>
.Select(_graph, inventoryIDs)
.Cast<InventoryItem>()
.ToDictionary(i => i.InventoryID, i => i);
foreach (var line in lines)
{
if (inventoryData.TryGetValue(line.InventoryID, out var item))
{
if (line.Quantity > item.AvailableQty)
{
_validationWarnings.Add(
$"Line {line.LineNbr}: {item.InventoryCD} has only {item.AvailableQty} available. " +
$"Requested {line.Quantity}. Order will be processed with available quantity.");
}
}
}
}
private void ValidateCreditLimit(SOOrder order)
{
var customer = PXSelect<Customer,
Where<Customer.customerID, Equal<Required<Customer.customerID>>>>
.Select(_graph, order.CustomerID);
if (customer?.CreditLimit > 0)
{
// Get total open order amount
var openOrdersTotal = GetOpenOrdersTotal(order.CustomerID);
var totalWithCurrent = openOrdersTotal + order.TotalAmount;
if (totalWithCurrent > customer.CreditLimit)
{
_validationErrors.Add(
$"Order total {order.TotalAmount:C} would exceed credit limit. " +
$"Available credit: {(customer.CreditLimit - openOrdersTotal):C}");
}
}
}
private decimal GetOpenOrdersTotal(int customerID)
{
return PXSelect<SOOrder,
Where<SOOrder.customerID, Equal<Required<SOOrder.customerID>,
And<SOOrder.status, NotEqual<SOOrderStatus.completed>>>>
.Select(_graph, customerID)
.Cast<SOOrder>()
.Sum(o => o.TotalAmount);
}
}
💡 Pro Tip: Separate validation logic into a dedicated service class. This makes your code more testable, maintainable, and reusable across multiple graphs.
Scenario 2: Cross-Graph Data Synchronization
When data changes in one graph need to be reflected in other graphs or systems, you need a reliable synchronization pattern that handles failures gracefully.
The Problem
When a sales order is approved, you need to create a corresponding fulfillment record and update inventory reservations, all while maintaining data consistency.
public class OrderApprovalHandler
{
private readonly SOOrderEntry _orderGraph;
private readonly Dictionary<string, object> _syncContext = new Dictionary<string, object>();
public OrderApprovalHandler(SOOrderEntry orderGraph)
{
_orderGraph = orderGraph;
}
[PXOverride]
public void Persist(Action baseMethod)
{
// Capture modified orders before persistence
var modifiedOrders = GetModifiedOrders();
// Create synchronization context
BuildSyncContext(modifiedOrders);
try
{
// Base persist
baseMethod();
// Synchronize after successful persist
SynchronizeAfterPersist();
}
catch (Exception ex)
{
// Log but don't rollback - the main transaction is already committed
PXTrace.WriteError($"Synchronization failed: {ex.Message}");
throw new PXException($"Order approved but synchronization failed: {ex.Message}");
}
}
private List GetModifiedOrders()
{
var orders = _orderGraph.Orders.Cache.Updated
.Cast()
.Where(o => o.Status == SOOrderStatus.Approved)
.ToList();
// Also check new orders
orders.AddRange(_orderGraph.Orders.Cache.Inserted
.Cast()
.Where(o => o.Status == SOOrderStatus.Approved));
return orders.DistinctBy(o => o.OrderNbr).ToList();
}
private void BuildSyncContext(List orders)
{
foreach (var order in orders)
{
var orderLines = _orderGraph.Transactions.Select()
.Cast()
.Where(l => l.OrderNbr == order.OrderNbr)
.ToList();
_syncContext[order.OrderNbr] = new
{
Order = order,
Lines = orderLines
};
}
}
private void SynchronizeAfterPersist()
{
foreach (var entry in _syncContext.Values)
{
var order = entry.GetType().GetProperty("Order")?.GetValue(entry) as SOOrder;
var lines = entry.GetType().GetProperty("Lines")?.GetValue(entry) as List;
if (order == null) continue;
// Create fulfillment record
CreateFulfillment(order, lines);
// Update inventory reservations
UpdateInventoryReservations(order, lines);
// Send notifications
SendOrderApprovalNotifications(order);
}
_syncContext.Clear();
}
private void CreateFulfillment(SOOrder order, List lines)
{
using var fulfillmentGraph = PXGraph.CreateInstance<SOFulfillmentEntry>();
var fulfillment = new SOFulfillment
{
OrderNbr = order.OrderNbr,
CustomerID = order.CustomerID,
Status = "New"
};
fulfillment = fulfillmentGraph.Fulfillments.Insert(fulfillment);
foreach (var line in lines)
{
var fline = new SOFulfillmentLine
{
OrderNbr = order.OrderNbr,
InventoryID = line.InventoryID,
Quantity = line.Quantity,
UnitPrice = line.UnitPrice
};
fulfillmentGraph.FulfillmentLines.Insert(fline);
}
fulfillmentGraph.Actions.PressSave();
}
private void UpdateInventoryReservations(SOOrder order, List lines)
{
var inventoryGraph = PXGraph.CreateInstance<InventoryMaint>();
// Use PXDatabase for bulk update
var sql = new StringBuilder("UPDATE InventoryItem SET ReservedQty = ReservedQty + ");
var parameters = new List