Skip to main content

SYSPRO Business Objects Pattern

Overview

This pattern describes the standard approach for working with SYSPRO Business Objects, which provide the official API for interacting with SYSPRO's business logic. Business objects ensure data integrity, enforce business rules, and maintain consistency with SYSPRO's core functionality.

Core Concepts

  • Business Object Interface: SYSPRO's XML-based API layer
  • Method Types: Add, Update, Delete, Query operations
  • XML Document Structure: Input and output formats
  • Parameter Handling: Configuration options for operations
  • Response Processing: Error and success handling

Common Business Objects

PORTOI - Purchase Order Take On

Creates and maintains purchase orders:

// Pattern: PORTOI Implementation
public class PurchaseOrderBusinessObject
{
private readonly ISysproBusinessObject _businessObject;
private readonly ILogger<PurchaseOrderBusinessObject> _logger;

public async Task<PurchaseOrderResult> CreatePurchaseOrderAsync(
PurchaseOrderRequest request)
{
// Build input XML
var inputXml = BuildPurchaseOrderXml(request);

// Build parameter XML
var paramXml = BuildParameterXml(new Dictionary<string, string>
{
["ActionType"] = "A", // Add
["ValidateOnly"] = "N",
["IgnoreWarnings"] = request.IgnoreWarnings ? "Y" : "N",
["ApplyIfEntireDocumentValid"] = "Y"
});

// Invoke business object
var result = await _businessObject.InvokeAsync(
"PORTOI",
inputXml,
paramXml);

// Parse response
return ParsePurchaseOrderResponse(result.Response);
}

private string BuildPurchaseOrderXml(PurchaseOrderRequest request)
{
var doc = new XDocument(
new XElement("PostPurchaseOrders",
new XElement("Item",
new XElement("Supplier", request.Supplier),
new XElement("Warehouse", request.Warehouse),
new XElement("OrderDate", request.OrderDate.ToString("yyyy-MM-dd")),
new XElement("DueDate", request.DueDate.ToString("yyyy-MM-dd")),
new XElement("StockLines",
request.Lines.Select(line =>
new XElement("StockLine",
new XElement("StockCode", line.StockCode),
new XElement("OrderQty", line.OrderQty),
new XElement("Price", line.Price),
new XElement("TaxCode", line.TaxCode)
)
)
)
)
)
);

return doc.ToString();
}
}

ARSTRN - AR Transaction Posting

Handles accounts receivable transactions:

// Pattern: ARSTRN Implementation
public class ArTransactionBusinessObject
{
public async Task<PaymentReversalResult> ReversePaymentAsync(
PaymentReversalRequest request)
{
var inputXml = BuildReversalXml(request);

var paramXml = BuildParameterXml(new Dictionary<string, string>
{
["ActionType"] = "A",
["ValidateOnly"] = "N",
["IgnoreWarnings"] = "N",
["PostingPeriod"] = request.PostingPeriod,
["JournalNumber"] = request.JournalNumber
});

var result = await _businessObject.InvokeAsync(
"ARSTRN",
inputXml,
paramXml);

return ParseReversalResponse(result.Response);
}

private string BuildReversalXml(PaymentReversalRequest request)
{
var doc = new XDocument(
new XElement("PostArTransactions",
new XElement("Item",
new XElement("Customer", request.Customer),
new XElement("TransactionType", "C"), // Credit
new XElement("Invoice", request.PaymentNumber),
new XElement("TransactionDate", request.ReversalDate.ToString("yyyy-MM-dd")),
new XElement("TransactionValue", request.Amount),
new XElement("Reference", $"Reversal of {request.PaymentNumber}"),
new XElement("Branch", request.Branch)
)
)
);

return doc.ToString();
}
}

APSTIN - AP Transaction Posting

Processes accounts payable transactions:

// Pattern: APSTIN Implementation
public class ApTransactionBusinessObject
{
public async Task<InvoicePostResult> PostSupplierInvoiceAsync(
SupplierInvoiceRequest request)
{
var inputXml = new XDocument(
new XElement("PostApInvoices",
new XElement("Item",
new XElement("Supplier", request.Supplier),
new XElement("InvoiceNumber", request.InvoiceNumber),
new XElement("InvoiceDate", request.InvoiceDate.ToString("yyyy-MM-dd")),
new XElement("DueDate", request.DueDate.ToString("yyyy-MM-dd")),
new XElement("InvoiceValue", request.TotalAmount),
new XElement("TaxValue", request.TaxAmount),
new XElement("DiscountValue", request.DiscountAmount),
new XElement("Lines",
request.Lines.Select(line =>
new XElement("Line",
new XElement("GlCode", line.GlCode),
new XElement("Amount", line.Amount),
new XElement("TaxCode", line.TaxCode),
new XElement("Description", line.Description)
)
)
)
)
)
).ToString();

var result = await _businessObject.InvokeAsync("APSTIN", inputXml, paramXml);
return ParseInvoiceResponse(result.Response);
}
}

GENQRY - Generic Query

Flexible data retrieval from SYSPRO:

// Pattern: GENQRY Implementation
public class GenericQueryBusinessObject
{
public async Task<QueryResult> ExecuteQueryAsync(QueryRequest request)
{
var inputXml = new XDocument(
new XElement("Query",
new XElement("TableName", request.TableName),
new XElement("Columns", string.Join(",", request.Columns)),
new XElement("Where", request.WhereClause),
new XElement("OrderBy", request.OrderBy),
new XElement("MaxRows", request.MaxRows)
)
).ToString();

var paramXml = new XDocument(
new XElement("Parameters",
new XElement("ReturnFormat", "XML"),
new XElement("IncludeHeaders", "Y")
)
).ToString();

var result = await _businessObject.InvokeAsync("GENQRY", inputXml, paramXml);
return ParseQueryResponse(result.Response);
}
}

Response Processing Pattern

Standardized response handling for all business objects:

// Pattern: Response Processing
public class BusinessObjectResponseProcessor
{
private readonly ILogger<BusinessObjectResponseProcessor> _logger;

public T ProcessResponse<T>(string xmlResponse) where T : BusinessObjectResponse, new()
{
var response = new T();

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

// Check for errors
var errors = ExtractErrors(doc);
if (errors.Any())
{
response.Success = false;
response.Errors = errors;
return response;
}

// Check for warnings
var warnings = ExtractWarnings(doc);
response.Warnings = warnings;

// Extract success data
response.Success = true;
ExtractSuccessData(doc, response);

return response;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to process business object response");
response.Success = false;
response.Errors = new List<string> { ex.Message };
return response;
}
}

private List<string> ExtractErrors(XDocument doc)
{
var errors = new List<string>();

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

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

// Check status
var status = doc.Root?.Element("Status")?.Value;
if (status == "1" || status == "ERROR")
{
var message = doc.Root?.Element("StatusMessage")?.Value;
if (!string.IsNullOrEmpty(message))
errors.Add(message);
}

return errors;
}

private List<string> ExtractWarnings(XDocument doc)
{
return doc.Descendants("Warning")
.Select(w => w.Value)
.Where(v => !string.IsNullOrEmpty(v))
.ToList();
}

private void ExtractSuccessData<T>(XDocument doc, T response)
where T : BusinessObjectResponse
{
// Extract common success fields
response.TransactionNumber = doc.Root?.Element("TransactionNumber")?.Value;
response.JournalNumber = doc.Root?.Element("Journal")?.Value;
response.DocumentNumber = doc.Root?.Element("DocumentNumber")?.Value;

// Type-specific extraction
if (response is PurchaseOrderResponse poResponse)
{
poResponse.OrderNumber = doc.Root?.Element("PurchaseOrder")?.Value;
poResponse.LinesAdded = int.Parse(doc.Root?.Element("LinesAdded")?.Value ?? "0");
poResponse.TotalValue = decimal.Parse(doc.Root?.Element("TotalValue")?.Value ?? "0");
}
}
}

Validation Pattern

Pre-validation before business object invocation:

// Pattern: Business Object Validation
public class BusinessObjectValidator
{
private readonly IDataService _dataService;
private readonly ILogger<BusinessObjectValidator> _logger;

public async Task<ValidationResult> ValidateBeforePostingAsync<T>(T request)
{
var result = new ValidationResult();

// Generic validation
if (request == null)
{
result.AddError("Request cannot be null");
return result;
}

// Type-specific validation
switch (request)
{
case PurchaseOrderRequest po:
await ValidatePurchaseOrder(po, result);
break;

case PaymentReversalRequest pr:
await ValidatePaymentReversal(pr, result);
break;

case SupplierInvoiceRequest si:
await ValidateSupplierInvoice(si, result);
break;
}

return result;
}

private async Task ValidatePurchaseOrder(
PurchaseOrderRequest request,
ValidationResult result)
{
// Validate supplier
if (!await _dataService.SupplierExistsAsync(request.Supplier))
{
result.AddError($"Supplier {request.Supplier} not found");
}

// Validate warehouse
if (!await _dataService.WarehouseExistsAsync(request.Warehouse))
{
result.AddError($"Warehouse {request.Warehouse} not found");
}

// Validate stock items
foreach (var line in request.Lines)
{
if (!await _dataService.StockItemExistsAsync(line.StockCode))
{
result.AddError($"Stock item {line.StockCode} not found");
}

if (line.OrderQty <= 0)
{
result.AddError($"Invalid quantity for {line.StockCode}");
}
}

// Validate dates
if (request.DueDate < request.OrderDate)
{
result.AddError("Due date cannot be before order date");
}
}
}

Error Recovery Pattern

Handling and recovering from business object failures:

// Pattern: Error Recovery
public class BusinessObjectErrorRecovery
{
private readonly ILogger<BusinessObjectErrorRecovery> _logger;
private readonly int _maxRetries = 3;
private readonly TimeSpan _retryDelay = TimeSpan.FromSeconds(2);

public async Task<T> ExecuteWithRetryAsync<T>(
Func<Task<T>> operation,
Func<T, bool> isSuccess,
string operationName) where T : class
{
for (int attempt = 1; attempt <= _maxRetries; attempt++)
{
try
{
var result = await operation();

if (isSuccess(result))
{
return result;
}

// Check if error is retryable
if (IsRetryableError(result))
{
_logger.LogWarning(
"Retryable error in {Operation}, attempt {Attempt}/{Max}",
operationName, attempt, _maxRetries);

if (attempt < _maxRetries)
{
await Task.Delay(_retryDelay * attempt);
continue;
}
}

return result; // Non-retryable error
}
catch (Exception ex) when (IsRetryableException(ex))
{
_logger.LogWarning(ex,
"Retryable exception in {Operation}, attempt {Attempt}/{Max}",
operationName, attempt, _maxRetries);

if (attempt == _maxRetries)
throw;

await Task.Delay(_retryDelay * attempt);
}
}

return null;
}

private bool IsRetryableError<T>(T result)
{
if (result is BusinessObjectResponse response)
{
return response.Errors?.Any(e =>
e.Contains("locked") ||
e.Contains("timeout") ||
e.Contains("deadlock")) ?? false;
}
return false;
}

private bool IsRetryableException(Exception ex)
{
return ex is SqlException sqlEx &&
(sqlEx.Number == 1205 || // Deadlock
sqlEx.Number == -2); // Timeout
}
}

Best Practices

  1. Always validate input before invoking business objects
  2. Use parameter XML to control operation behavior
  3. Parse responses thoroughly for errors, warnings, and data
  4. Implement retry logic for transient failures
  5. Log all invocations with input and output for audit
  6. Cache business object instances where appropriate
  7. Handle partial success in batch operations
  8. Version XML schemas for compatibility

Common Pitfalls

  1. Invalid XML structure - Malformed input XML
  2. Missing required fields - Incomplete request data
  3. Ignoring warnings - May indicate data issues
  4. No error handling - Unhandled business object exceptions
  5. Session issues - Invalid or expired sessions

Examples