Skip to main content

SYSPRO Integration Pattern

Overview

This pattern defines the standard approach for integrating with SYSPRO ERP across MepDash dashboards. The integration leverages SYSPRO's E.net architecture, business objects, and direct database access to provide seamless ERP functionality while maintaining data integrity and business rule compliance.

Core Concepts

  • E.net Architecture: SYSPRO's COM-based integration layer
  • Business Objects: XML-based API for business operations
  • Session Management: Authentication and connection handling
  • Database Integration: Direct access for read operations
  • Transaction Processing: Ensuring data consistency

Pattern Implementation

SYSPRO Session Management

All dashboards must establish and maintain SYSPRO sessions:

// Pattern: SYSPRO Session Management
public interface ISysproSession
{
string SessionId { get; }
string CompanyId { get; }
string OperatorCode { get; }
bool IsValid { get; }

Task<bool> ConnectAsync();
Task DisconnectAsync();
Task<bool> ValidateSessionAsync();
string GetConnectionString();
}

public class SysproSessionManager : ISysproSession
{
private readonly ILogger<SysproSessionManager> _logger;
private readonly ISysproEnet _sysproEnet;
private string _sessionId;
private DateTime _lastActivity;
private readonly TimeSpan _sessionTimeout = TimeSpan.FromMinutes(20);

public async Task<bool> ConnectAsync()
{
try
{
_sessionId = await _sysproEnet.LogonAsync(
CompanyId,
OperatorCode,
OperatorPassword,
LanguageCode);

_lastActivity = DateTime.UtcNow;

_logger.LogInformation("SYSPRO session established {@SessionContext}",
new { SessionId = _sessionId, CompanyId, OperatorCode });

return !string.IsNullOrEmpty(_sessionId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to establish SYSPRO session");
return false;
}
}

public async Task<bool> ValidateSessionAsync()
{
if (string.IsNullOrEmpty(_sessionId))
return false;

// Check for timeout
if (DateTime.UtcNow - _lastActivity > _sessionTimeout)
{
_logger.LogWarning("SYSPRO session timeout detected");
await DisconnectAsync();
return await ConnectAsync();
}

// Validate with SYSPRO
try
{
var isValid = await _sysproEnet.IsSessionValidAsync(_sessionId);
if (isValid)
{
_lastActivity = DateTime.UtcNow;
}
return isValid;
}
catch
{
return false;
}
}
}

Business Object Integration Pattern

Standard pattern for invoking SYSPRO business objects:

// Pattern: Business Object Invocation
public interface ISysproBusinessObject
{
Task<BusinessObjectResult> InvokeAsync(
string businessObject,
string xmlInput,
string xmlParameters);
}

public class SysproBusinessObjectService : ISysproBusinessObject
{
private readonly ILogger<SysproBusinessObjectService> _logger;
private readonly ISysproSession _session;
private readonly ISysproEnet _sysproEnet;

public async Task<BusinessObjectResult> InvokeAsync(
string businessObject,
string xmlInput,
string xmlParameters)
{
// Validate session
if (!await _session.ValidateSessionAsync())
{
throw new SysproSessionException("Invalid SYSPRO session");
}

var result = new BusinessObjectResult
{
BusinessObject = businessObject,
StartTime = DateTime.UtcNow
};

try
{
// Log the invocation
_logger.LogInformation("Invoking SYSPRO business object {@BOContext}",
new {
BusinessObject = businessObject,
SessionId = _session.SessionId,
InputLength = xmlInput?.Length,
ParametersLength = xmlParameters?.Length
});

if (_logger.IsEnabled(LogLevel.Trace))
{
_logger.LogTrace("Business object input {@BOInput}",
new { businessObject, xmlInput, xmlParameters });
}

// Invoke the business object
var response = await _sysproEnet.CallBusinessObjectAsync(
_session.SessionId,
businessObject,
xmlInput,
xmlParameters);

result.Response = response;
result.Success = true;

// Parse for errors
var errors = ParseErrors(response);
if (errors.Any())
{
result.Success = false;
result.Errors = errors;

_logger.LogWarning("Business object returned errors {@BOErrors}",
new { businessObject, errors });
}

result.EndTime = DateTime.UtcNow;

_logger.LogInformation("Business object completed {@BOResult}",
new {
businessObject,
result.Success,
Duration = result.EndTime - result.StartTime
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Business object invocation failed {@BOError}",
new { businessObject });

result.Success = false;
result.ErrorMessage = ex.Message;
throw;
}

return result;
}

private List<string> ParseErrors(string xmlResponse)
{
var errors = new List<string>();

try
{
var doc = XDocument.Parse(xmlResponse);

// Check for error elements
errors.AddRange(doc.Descendants("Error")
.Select(e => e.Value));

// Check for error messages
errors.AddRange(doc.Descendants("ErrorMessage")
.Select(e => e.Value));

// Check status codes
var status = doc.Root?.Element("Status")?.Value;
if (status == "1") // Error status
{
var message = doc.Root?.Element("StatusMessage")?.Value;
if (!string.IsNullOrEmpty(message))
errors.Add(message);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to parse business object response");
}

return errors;
}
}

XML Document Construction Pattern

Building XML documents for SYSPRO business objects:

// Pattern: XML Document Builder
public class SysproXmlBuilder
{
private readonly ILogger<SysproXmlBuilder> _logger;

// Generic XML builder
public XDocument BuildBusinessObjectXml<T>(
string rootElement,
T data,
Action<XElement, T> builder)
{
var doc = new XDocument(
new XDeclaration("1.0", "utf-8", "yes"),
new XElement(rootElement)
);

builder(doc.Root, data);

if (_logger.IsEnabled(LogLevel.Trace))
{
_logger.LogTrace("Generated XML: {Xml}", doc.ToString());
}

return doc;
}

// Example: Purchase Order XML
public string BuildPurchaseOrderXml(PurchaseOrderRequest request)
{
var doc = BuildBusinessObjectXml("PostPurchaseOrders", request,
(root, req) =>
{
var item = new XElement("Item",
new XElement("Supplier", req.Supplier),
new XElement("Warehouse", req.Warehouse),
new XElement("OrderDate", req.OrderDate.ToString("yyyy-MM-dd")),
new XElement("DueDate", req.DueDate.ToString("yyyy-MM-dd"))
);

var stockLines = new XElement("StockLines");
foreach (var line in req.Lines)
{
stockLines.Add(new XElement("StockLine",
new XElement("StockCode", line.StockCode),
new XElement("OrderQty", line.OrderQty),
new XElement("Price", line.Price),
new XElement("TaxCode", line.TaxCode ?? "")
));
}

item.Add(stockLines);
root.Add(item);
});

return doc.ToString();
}

// Parameter XML builder
public string BuildParameterXml(Dictionary<string, string> parameters)
{
var doc = new XDocument(
new XElement("Parameters")
);

foreach (var param in parameters)
{
doc.Root.Add(new XElement(param.Key, param.Value));
}

return doc.ToString();
}
}

Database Integration Pattern

Direct database access for read operations:

// Pattern: SYSPRO Database Access
public class SysproDataAccess
{
private readonly string _connectionString;
private readonly ILogger<SysproDataAccess> _logger;

public SysproDataAccess(ISysproSession session, ILogger<SysproDataAccess> logger)
{
_connectionString = session.GetConnectionString();
_logger = logger;
}

// Query SYSPRO tables directly
public async Task<IEnumerable<T>> QuerySysproTableAsync<T>(
string query,
object parameters = null)
{
using (var connection = new SqlConnection(_connectionString))
{
try
{
var result = await connection.QueryAsync<T>(query, parameters);

_logger.LogDebug("SYSPRO query executed: {RowCount} rows returned",
result.Count());

return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "SYSPRO database query failed");
throw;
}
}
}

// Example: Get customer information
public async Task<Customer> GetCustomerAsync(string customerId)
{
const string query = @"
SELECT
Customer,
Name,
ShortName,
Currency,
CreditLimit,
TermsCode,
TaxStatus
FROM ArCustomer
WHERE Customer = @CustomerId";

var result = await QuerySysproTableAsync<Customer>(
query,
new { CustomerId = customerId });

return result.FirstOrDefault();
}
}

Error Handling Pattern

Comprehensive error handling for SYSPRO operations:

// Pattern: SYSPRO Error Handling
public class SysproErrorHandler
{
private readonly ILogger<SysproErrorHandler> _logger;

public async Task<T> ExecuteWithErrorHandlingAsync<T>(
Func<Task<T>> operation,
string operationName)
{
try
{
return await operation();
}
catch (SysproSessionException ex)
{
_logger.LogError(ex, "SYSPRO session error in {Operation}", operationName);
throw new BusinessException("Session expired. Please reconnect.", ex);
}
catch (SysproBusinessObjectException ex)
{
_logger.LogError(ex, "Business object error in {Operation}", operationName);

// Parse and translate SYSPRO errors
var userMessage = TranslateSysproError(ex.ErrorCode);
throw new BusinessException(userMessage, ex);
}
catch (SqlException ex) when (ex.Number == -2) // Timeout
{
_logger.LogError(ex, "Database timeout in {Operation}", operationName);
throw new BusinessException("Operation timed out. Please try again.", ex);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error in {Operation}", operationName);
throw;
}
}

private string TranslateSysproError(string errorCode)
{
return errorCode switch
{
"1001" => "Invalid customer code",
"1002" => "Customer is on hold",
"2001" => "Stock item not found",
"2002" => "Insufficient stock available",
"3001" => "Period is closed for posting",
"3002" => "Invalid GL account",
"4001" => "User lacks permission for this operation",
_ => $"SYSPRO error: {errorCode}"
};
}
}

Transaction Pattern

Managing SYSPRO transactions:

// Pattern: SYSPRO Transaction Management
public class SysproTransactionManager
{
private readonly ISysproBusinessObject _businessObject;
private readonly ILogger<SysproTransactionManager> _logger;

public async Task<TransactionResult> ExecuteTransactionAsync(
Func<Task<List<BusinessOperation>>> buildOperations)
{
var result = new TransactionResult
{
TransactionId = Guid.NewGuid().ToString(),
StartTime = DateTime.UtcNow
};

try
{
// Build operations
var operations = await buildOperations();
result.TotalOperations = operations.Count;

// Execute each operation
foreach (var operation in operations)
{
try
{
var opResult = await _businessObject.InvokeAsync(
operation.BusinessObject,
operation.InputXml,
operation.ParameterXml);

if (opResult.Success)
{
result.SuccessfulOperations++;
result.Results.Add(opResult);
}
else
{
// Handle based on transaction strategy
if (operation.FailureStrategy == FailureStrategy.Abort)
{
throw new TransactionAbortException(
$"Operation failed: {operation.Name}");
}

result.FailedOperations++;
result.Errors.Add($"{operation.Name}: {opResult.ErrorMessage}");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Operation failed: {Operation}",
operation.Name);

if (operation.FailureStrategy == FailureStrategy.Abort)
throw;

result.FailedOperations++;
result.Errors.Add($"{operation.Name}: {ex.Message}");
}
}

result.Success = result.FailedOperations == 0;
result.EndTime = DateTime.UtcNow;

_logger.LogInformation("Transaction completed {@TransactionResult}",
result);
}
catch (Exception ex)
{
_logger.LogError(ex, "Transaction failed");
result.Success = false;
result.ErrorMessage = ex.Message;
}

return result;
}
}

Dashboard-Specific Implementations

AR Payment Reversal

  • Uses ARSTRN business object for payment reversals
  • Implements queue-based batch processing
  • Direct database updates for queue management
  • See: AR SYSPRO Integration

Inventory Mini MRP

  • Uses PORTOI business object for purchase order creation
  • Direct database queries for MRP calculations
  • Custom views for inventory analysis
  • See: Inventory SYSPRO Integration

AP EFT Remittance

  • Uses APSTIN for transaction posting
  • APSPMT for payment processing
  • GENQRY for flexible data retrieval
  • See: AP SYSPRO Integration

Best Practices

  1. Always validate sessions before business object calls
  2. Use structured XML for business object input
  3. Parse responses thoroughly for errors and warnings
  4. Implement retry logic for transient failures
  5. Log all operations for audit and debugging
  6. Handle SYSPRO-specific errors gracefully
  7. Use transactions for multi-step operations
  8. Cache reference data to reduce database calls

Common Pitfalls

  1. Session timeout - Not checking session validity
  2. XML formatting - Invalid XML structure for business objects
  3. Error parsing - Missing error conditions in responses
  4. Transaction boundaries - Not handling partial failures
  5. Connection pooling - Not managing database connections properly

Examples