acuamitca.com logo ACUAMITCA
~/blog/article

Persisting Data in Acumatica Events – Part 4: Complex Scenarios

Acumatica Customization 10 mins read August 25, 2026
 

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();

        for (int i = 0; i < lines.Count; i++)
        {
            var line = lines[i];
            if (i > 0) sql.Append(", ");
            sql.Append($"CASE WHEN InventoryID = @inv_{i} THEN @qty_{i} ELSE 0 END");
            parameters.Add(new PXDataField($"@inv_{i}", line.InventoryID));
            parameters.Add(new PXDataField($"@qty_{i}", line.Quantity));
        }
        sql.Append(" WHERE InventoryID IN ({0})");

        PXDatabase.Execute(
            string.Format(sql.ToString(), string.Join(",", lines.Select((l, i) => $"@inv_{i}"))),
            parameters.ToArray());
    }

    private void SendOrderApprovalNotifications(SOOrder order)
    {
        // Send email or push notification
        // This could use Acumatica's notification system
    }
}

        

Scenario 3: Bulk Import with Error Handling

Processing large data imports requires careful error handling, performance optimization, and user feedback.

The Problem

You need to import thousands of orders from a CSV file, validate each one, and process them efficiently while providing detailed error reporting.

public class BulkOrderImportProcessor
{
    private readonly Dictionary _errors = new Dictionary();
    private readonly Dictionary _warnings = new Dictionary();
    private int _successCount = 0;
    private int _failCount = 0;

    public BulkOrderImportResult ProcessOrders(List orders)
    {
        // Use batch processing for performance
        const int batchSize = 50;
        var results = new BulkOrderImportResult();

        for (int i = 0; i < orders.Count; i += batchSize)
        {
            var batch = orders.Skip(i).Take(batchSize).ToList();
            ProcessBatch(batch, i);
        }

        return new BulkOrderImportResult
        {
            TotalRecords = orders.Count,
            Successful = _successCount,
            Failed = _failCount,
            Errors = _errors,
            Warnings = _warnings
        };
    }

    private void ProcessBatch(List batch, int startIndex)
    {
        using var graph = PXGraph.CreateInstance();
        
        foreach (var importOrder in batch)
        {
            try
            {
                ProcessSingleOrder(importOrder, graph);
                _successCount++;
            }
            catch (PXException ex)
            {
                _errors[startIndex + batch.IndexOf(importOrder)] = ex.Message;
                _failCount++;
            }
            catch (Exception ex)
            {
                _errors[startIndex + batch.IndexOf(importOrder)] = $"Unexpected error: {ex.Message}";
                _failCount++;
                // Rollback any partial changes for this order
                graph.Actions.PressDiscard();
            }
        }
    }

    private void ProcessSingleOrder(ImportOrder importOrder, SOOrderEntry graph)
    {
        // Validate before creating
        ValidateImportOrder(importOrder);

        // Create order
        var order = new SOOrder
        {
            CustomerID = importOrder.CustomerID,
            OrderNbr = importOrder.OrderNbr,
            OrderDate = importOrder.OrderDate,
            Description = importOrder.Description
        };
        order = graph.Orders.Insert(order);

        // Create lines
        foreach (var item in importOrder.Items)
        {
            var line = new SOLine
            {
                OrderNbr = order.OrderNbr,
                InventoryID = item.InventoryID,
                Quantity = item.Quantity,
                UnitPrice = item.UnitPrice
            };
            graph.Transactions.Insert(line);
        }

        // Save this single order
        graph.Actions.PressSave();
    }

    private void ValidateImportOrder(ImportOrder order)
    {
        var errors = new List();

        if (string.IsNullOrEmpty(order.OrderNbr))
            errors.Add("Order number is required");

        if (order.CustomerID <= 0)
            errors.Add("Customer ID is required");

        if (!order.Items.Any())
            errors.Add("Order must have at least one line");

        if (errors.Any())
            throw new PXException(string.Join("; ", errors));
    }
}

public class BulkOrderImportResult
{
    public int TotalRecords { get; set; }
    public int Successful { get; set; }
    public int Failed { get; set; }
    public Dictionary Errors { get; set; } = new();
    public Dictionary Warnings { get; set; } = new();
}

Scenario 4: Historical Data Tracking (Audit Trail)

Many enterprises require tracking changes to critical data for compliance and audit purposes.

The Problem

You need to track all changes to order status, including who made the change, when it was made, and what the previous status was.

public class OrderAuditTracker : PXGraphExtension
{
    private readonly Dictionary _statusHistory = new();

    protected virtual void _(Events.RowPersisting e)
    {
        if (e.Operation != PXDBOperation.Update) return;

        var order = e.Row;
        var oldOrder = e.OldRow;

        // Check if status has changed
        if (oldOrder.Status != order.Status)
        {
            // Record the change for after persistence
            _statusHistory[order.OrderNbr] = oldOrder.Status;
        }
    }

    protected virtual void _(Events.RowPersisted e)
    {
        if (e.TranStatus != PXTranStatus.Completed) return;

        var order = e.Row;
        if (!_statusHistory.TryGetValue(order.OrderNbr, out var oldStatus)) return;

        // Create audit record
        var audit = new OrderStatusAudit
        {
            OrderNbr = order.OrderNbr,
            PreviousStatus = oldStatus,
            NewStatus = order.Status,
            ChangedBy = Base.Accessinfo.UserID,
            ChangedDate = DateTime.Now
        };

        using var auditGraph = PXGraph.CreateInstance();
        auditGraph.AuditRecords.Insert(audit);
        auditGraph.Actions.PressSave();

        _statusHistory.Remove(order.OrderNbr);
    }
}

Production-Ready Checklist

✅ Error Handling

Always handle exceptions gracefully and provide meaningful error messages to users. Use PXException for business rule violations.

✅ Logging

Use PXTrace to log critical operations, errors, and performance metrics for debugging and monitoring.

✅ Transaction Boundaries

Be aware of where transactions begin and end. Don't create new records in RowPersisting unless absolutely necessary.

✅ Performance

Use batch operations, avoid unnecessary cache operations, and profile your code for performance bottlenecks.

✅ Upgrade Safety

Follow Acumatica best practices for customizations to ensure they survive version upgrades. Use PXOverride instead of direct method overriding.

✅ Testability

Design your persistence logic to be testable. Use dependency injection and separate business logic from persistence code.


Series Conclusion

📌 Series Summary: This four-part series covered everything you need to know about persisting data in Acumatica events:

  • Part 1: PXCache foundations, data modification methods, and the persistence flow
  • Part 2: Persisting within specific events, PXDatabase, overriding Persist, and avoiding recursion
  • Part 3: Validation strategies, cross-table persistence, transactions, and performance optimization
  • Part 4: Complex scenarios, production patterns, and real-world solutions

⚡ Final Thought: Mastering data persistence in Acumatica is not just about knowing the syntax—it's about understanding the framework's design philosophy and applying the right pattern for each scenario. The techniques covered in this series will serve as a solid foundation for building robust, maintainable Acumatica customizations.

🔍 Further Reading: Continue your learning journey with the Acumatica Developer Network (ADN) resources, official documentation, and community forums. Practice implementing these patterns in your development environment to build confidence before applying them in production.