SYSPRO Business Objects
Overview
This document provides detailed documentation on the interaction with SYSPRO business objects in the AR Payment Reversal dashboard, focusing primarily on the ARSTPY (AR Payment Entry) business object. It covers XML document generation, business object method calls, parameter preparation, response parsing, error code handling, and transaction boundary management.
Key Concepts
- ARSTPY Business Object: Core SYSPRO component for AR payment processing
- XML Document Structure: Input and parameter XML formats
- ENet API Methods: Transaction posting through COM interface
- Response Processing: Parsing and interpreting SYSPRO output
- Error Handling: Managing business object errors and warnings
- Transaction Boundaries: Ensuring atomic operations
Implementation Details
ARSTPY Business Object Overview
Business Object Characteristics
public class ARSTPYBusinessObject
{
public const string BusinessObjectCode = "ARSTPY";
public const string Description = "AR Payment Entry";
public const string Module = "AR";
public const string Type = "Transaction";
public static class Capabilities
{
public const bool SupportsValidation = true;
public const bool SupportsReversal = true;
public const bool SupportsBatch = true;
public const bool SupportsMultiCurrency = true;
public const bool GeneratesJournals = true;
public const bool UpdatesGL = true;
}
public static class Requirements
{
public const string RequiredModule = "AR";
public const string RequiredPermission = "AR.PAYMENT.POST";
public const bool RequiresOpenPeriod = true;
public const bool RequiresValidCustomer = true;
public const bool RequiresValidBank = true;
}
}
XML Document Generation
Input XML Structure
// MepApps.Dash.Ar.Maint.PaymentReversal/Services/SysproPostService.cs (Lines 29-48)
public string GetInputXml(
IEnumerable<ArReversePaymentHeader> checks,
IEnumerable<ArReversePaymentDetail> invoices)
{
try
{
var xmlDoc = new XDocument(
new XDeclaration("1.0", "utf-8", "yes"),
new XElement("PostArPayment")
);
foreach (ArReversePaymentHeader check in checks)
{
var checkInvoices = invoices.Where(x =>
x.Customer == check.Customer &&
x.CheckNumber == check.CheckNumber &&
x.TrnYear == check.TrnYear &&
x.TrnMonth == check.TrnMonth &&
x.Journal == check.Journal);
if (checkInvoices.Any())
{
xmlDoc.Root.Add(CreatePaymentItem(check, checkInvoices));
}
}
// Validate XML against SYSPRO schema
ValidateInputXml(xmlDoc);
return xmlDoc.ToString();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error generating input XML for ARSTPY");
throw;
}
}
private XElement CreatePaymentItem(
ArReversePaymentHeader check,
IEnumerable<ArReversePaymentDetail> invoices)
{
var item = new XElement("Item",
new XElement("Payment",
// Required fields
new XElement("Customer", check.Customer.Trim()),
new XElement("PaymentValue", FormatCurrency(-1 * check.CheckValue.Value)),
new XElement("Reference", check.CheckNumber.Trim()),
new XElement("PaymentDate", check.PaymentDate.Value.ToString("yyyy-MM-dd")),
// Optional fields
new XElement("JournalNotation", $"Reversal of payment {check.CheckNumber}"),
new XElement("PaymentNarration", "Payment reversal"),
new XElement("Bank", check.Bank),
new XElement("PaymentType", "C"), // C=Check, E=Electronic, O=Other
// Multi-currency fields (if applicable)
ConditionalElement("Currency", check.Currency),
ConditionalElement("ExchangeRate", check.ExchangeRate),
// Invoice applications
invoices.Select(inv => CreateInvoiceApplication(inv))
)
);
return item;
}
private XElement CreateInvoiceApplication(ArReversePaymentDetail invoice)
{
return new XElement("InvoiceToPay",
new XElement("TransactionType", MapDocumentType(invoice.DocumentType)),
new XElement("Invoice", invoice.Invoice.Trim()),
new XElement("GrossPaymentValue",
FormatCurrency(-1 * (invoice.DiscountValue + invoice.PaymentValue))),
new XElement("DiscountValue", FormatCurrency(-1 * invoice.DiscountValue)),
// Optional fields for specific scenarios
ConditionalElement("TaxPortion", invoice.TaxPortion),
ConditionalElement("FreightPortion", invoice.FreightPortion),
ConditionalElement("OtherPortion", invoice.OtherPortion)
);
}
private string MapDocumentType(string sysproDocType)
{
return sysproDocType switch
{
"I" => "Invoice",
"C" => "Credit",
"D" => "Debit",
"F" => "Finance",
_ => "Invoice"
};
}
private string FormatCurrency(decimal value)
{
// SYSPRO expects specific decimal format
return value.ToString("0.00", CultureInfo.InvariantCulture);
}
private XElement ConditionalElement(string name, object value)
{
if (value == null || string.IsNullOrWhiteSpace(value.ToString()))
return null;
return new XElement(name, value);
}
Parameter XML Configuration
// MepApps.Dash.Ar.Maint.PaymentReversal/Services/SysproPostService.cs (Lines 105-127)
public string GetParamXml(string postPeriod)
{
var paramDoc = new XDocument(
new XElement("PostArPayment",
new XElement("Parameters",
// Posting control
new XElement("PostingPeriod", postPeriod),
new XElement("IgnoreWarnings", "Y"),
new XElement("ApplyIfEntireDocumentValid", "Y"),
new XElement("ValidateOnly", "N"),
// Payment control
new XElement("AutoCorrectPaymentValue", "N"),
new XElement("AllowNegativePayments", "Y"), // For reversals
new XElement("CheckForDuplicates", "Y"),
// Branch and area control
new XElement("ApplySpecificBranch", "N"),
new XElement("BranchToUse", string.Empty),
new XElement("AreaToUseForLedgerIntegration", string.Empty),
// Integration control
new XElement("IntegrateToCashBookInDetail", "N"),
new XElement("AutoCalculateTax", "N"),
new XElement("IgnoreAnalysis", "Y"),
// Journal control
new XElement("NextJournal", string.Empty), // Auto-assign
new XElement("ConsolidateGlEntries", "Y"),
// Security
new XElement("ValidatePasswords", "Y"),
new XElement("OperatorPassword", string.Empty), // If required
// Audit trail
new XElement("CreateAuditRecords", "Y"),
new XElement("AuditProgram", "AR_PAYMENT_REVERSAL")
)
)
);
return paramDoc.ToString();
}
// Advanced parameter configuration for specific scenarios
public string GetAdvancedParamXml(PostingConfiguration config)
{
var parameters = new XElement("Parameters");
// Period handling
if (config.UseSpecificPeriod)
{
parameters.Add(new XElement("PostingPeriod", config.Period));
parameters.Add(new XElement("ForceperiodOverride", "Y"));
}
else
{
parameters.Add(new XElement("PostingPeriod", "C")); // Current
}
// Validation mode
if (config.ValidateOnly)
{
parameters.Add(new XElement("ValidateOnly", "Y"));
parameters.Add(new XElement("ReturnValidationErrors", "Y"));
}
// Multi-currency
if (config.IsMultiCurrency)
{
parameters.Add(new XElement("CurrencyProcessing", "M"));
parameters.Add(new XElement("ExchangeRateType", config.ExchangeRateType));
}
// Workflow integration (SYSPRO 8+)
if (config.EnableWorkflow)
{
parameters.Add(new XElement("TriggerWorkflow", "Y"));
parameters.Add(new XElement("WorkflowTemplate", config.WorkflowTemplate));
}
return new XDocument(
new XElement("PostArPayment", parameters)
).ToString();
}
Business Object Method Calls
Transaction Posting
// MepApps.Dash.Ar.Maint.PaymentReversal/Services/SysproPostService.cs (Lines 129-146)
public class BusinessObjectInvoker
{
private readonly ISysproEnet _sysproEnet;
private readonly ILogger<BusinessObjectInvoker> _logger;
public string InvokeBusinessObject(
string businessObject,
string xmlIn,
string xmlParam)
{
var context = new BusinessObjectContext
{
BusinessObject = businessObject,
StartTime = DateTime.Now,
InputSize = xmlIn?.Length ?? 0,
ParamSize = xmlParam?.Length ?? 0
};
try
{
_logger.LogInformation("Invoking {BusinessObject}", businessObject);
// Pre-invocation validation
ValidateInvocationContext(context);
// Execute business object
string xmlOut = ExecuteWithRetry(() =>
_sysproEnet.Transaction(
null, // Session (null = current)
businessObject,
xmlParam,
xmlIn
)
);
context.EndTime = DateTime.Now;
context.OutputSize = xmlOut?.Length ?? 0;
context.Success = true;
// Post-invocation validation
ValidateOutput(xmlOut, businessObject);
// Log metrics
LogInvocationMetrics(context);
return xmlOut;
}
catch (Exception ex)
{
context.EndTime = DateTime.Now;
context.Success = false;
context.Error = ex.Message;
LogInvocationMetrics(context);
throw new BusinessObjectException(
$"Failed to invoke {businessObject}", ex)
{
BusinessObject = businessObject,
InputXml = xmlIn,
ParamXml = xmlParam
};
}
}
private string ExecuteWithRetry(Func<string> operation)
{
int maxRetries = 3;
int retryCount = 0;
while (retryCount < maxRetries)
{
try
{
return operation();
}
catch (COMException ex) when (IsTransientError(ex) && retryCount < maxRetries - 1)
{
retryCount++;
_logger.LogWarning("Transient error, retrying ({Retry}/{Max})",
retryCount, maxRetries);
Thread.Sleep(TimeSpan.FromSeconds(Math.Pow(2, retryCount)));
}
}
throw new MaxRetriesExceededException($"Failed after {maxRetries} attempts");
}
private bool IsTransientError(COMException ex)
{
// Common transient COM errors
var transientHResults = new[]
{
unchecked((int)0x800706BA), // RPC server unavailable
unchecked((int)0x800706BE), // Remote procedure call failed
unchecked((int)0x80010108) // Object invoked has disconnected
};
return transientHResults.Contains(ex.HResult);
}
}
Response Parsing
Output XML Processing
// MepApps.Dash.Ar.Maint.PaymentReversal/Services/SysproPostService.cs (Lines 148-216)
public PostArPayment_Output ParseXmlOutput(string outputXml)
{
var output = new PostArPayment_Output { OutputXml = outputXml };
try
{
// Initial validation
if (string.IsNullOrWhiteSpace(outputXml))
{
output.PostSucceeded = false;
output.ErrorMessage = "Empty response from SYSPRO";
return output;
}
XDocument doc = XDocument.Parse(outputXml);
// Check for error responses
var errorElement = doc.Descendants("ErrorMessage").FirstOrDefault();
if (errorElement != null)
{
output.PostSucceeded = false;
output.ErrorMessage = errorElement.Value;
output.ErrorCode = doc.Descendants("ErrorCode").FirstOrDefault()?.Value;
return output;
}
// Parse successful response
output.PostSucceeded = true;
// Parse posted items
output.Items = ParsePostedItems(doc);
// Parse status information
ParseStatusInformation(doc, output);
// Parse recap totals
ParseRecapTotals(doc, output);
// Parse GL journals created
output.GlJournals = ParseGlJournals(doc);
// Parse warnings (non-fatal)
output.Warnings = ParseWarnings(doc);
_logger.LogInformation("Successfully parsed ARSTPY output. Items: {Items}, Journals: {Journals}",
output.ItemsProcessed, output.JournalsCount);
return output;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error parsing ARSTPY output XML");
output.PostSucceeded = false;
output.ErrorMessage = $"Parse error: {ex.Message}";
return output;
}
}
private List<PostArPayment_OutputItem> ParsePostedItems(XDocument doc)
{
return doc.Descendants("Payment")
.Select(payment => new PostArPayment_OutputItem
{
Customer = payment.Element("Customer")?.Value,
Reference = payment.Element("Reference")?.Value,
TrnYear = ParseInt(payment.Element("Key")?.Element("TrnYear")?.Value),
TrnMonth = ParseInt(payment.Element("Key")?.Element("TrnMonth")?.Value),
Journal = ParseInt(payment.Element("Key")?.Element("Journal")?.Value),
EntryNumber = ParseInt(payment.Element("Key")?.Element("EntryNumber")?.Value),
// Additional details if available
PostedAmount = ParseDecimal(payment.Element("PostedAmount")?.Value),
PostedDate = ParseDateTime(payment.Element("PostedDate")?.Value),
Status = payment.Element("Status")?.Value ?? "Posted"
})
.ToList();
}
private void ParseStatusInformation(XDocument doc, PostArPayment_Output output)
{
var statusElement = doc.Descendants("StatusOfItems").FirstOrDefault();
if (statusElement != null)
{
output.ItemsProcessed = ParseInt(statusElement.Element("ItemsProcessed")?.Value);
output.ItemsInvalid = ParseInt(statusElement.Element("ItemsInvalid")?.Value);
output.ItemsWarnings = ParseInt(statusElement.Element("ItemsWithWarnings")?.Value);
// Parse invalid items details
var invalidItems = statusElement.Descendants("InvalidItem")
.Select(item => new InvalidItem
{
Customer = item.Element("Customer")?.Value,
Reference = item.Element("Reference")?.Value,
Reason = item.Element("Reason")?.Value,
ErrorCode = item.Element("ErrorCode")?.Value
})
.ToList();
output.InvalidItems = invalidItems;
}
}
Error Code Handling
SYSPRO Error Code Mapping
public class SysproErrorCodeHandler
{
private readonly Dictionary<string, ErrorInfo> _errorMappings = new()
{
["1001"] = new ErrorInfo
{
Severity = ErrorSeverity.Fatal,
Message = "Customer does not exist",
Action = "Verify customer code exists in ArCustomer table"
},
["1002"] = new ErrorInfo
{
Severity = ErrorSeverity.Fatal,
Message = "Invoice not found",
Action = "Check invoice number and customer combination"
},
["1003"] = new ErrorInfo
{
Severity = ErrorSeverity.Warning,
Message = "Payment exceeds invoice balance",
Action = "Review payment amount against outstanding balance"
},
["1004"] = new ErrorInfo
{
Severity = ErrorSeverity.Fatal,
Message = "Period is closed",
Action = "Select an open period for posting"
},
["1005"] = new ErrorInfo
{
Severity = ErrorSeverity.Fatal,
Message = "Bank account not valid",
Action = "Verify bank account exists and is active"
},
["1006"] = new ErrorInfo
{
Severity = ErrorSeverity.Warning,
Message = "Duplicate payment reference",
Action = "Check if payment has already been processed"
},
["1007"] = new ErrorInfo
{
Severity = ErrorSeverity.Fatal,
Message = "Customer on hold",
Action = "Remove customer hold before processing"
},
["1008"] = new ErrorInfo
{
Severity = ErrorSeverity.Fatal,
Message = "Invalid document type",
Action = "Use valid document type (Invoice, Credit, Debit)"
},
["1009"] = new ErrorInfo
{
Severity = ErrorSeverity.Warning,
Message = "Exchange rate not found",
Action = "Maintain exchange rate for currency and date"
},
["1010"] = new ErrorInfo
{
Severity = ErrorSeverity.Fatal,
Message = "GL integration failed",
Action = "Check GL account mappings and period status"
}
};
public ErrorHandlingResult HandleError(string errorCode, string context)
{
if (_errorMappings.TryGetValue(errorCode, out var errorInfo))
{
_logger.LogError("SYSPRO Error {Code}: {Message}. Context: {Context}",
errorCode, errorInfo.Message, context);
return new ErrorHandlingResult
{
CanContinue = errorInfo.Severity != ErrorSeverity.Fatal,
UserMessage = errorInfo.Message,
TechnicalMessage = $"Error {errorCode}: {errorInfo.Action}",
SuggestedAction = errorInfo.Action
};
}
// Unknown error
_logger.LogError("Unknown SYSPRO error code: {Code}", errorCode);
return new ErrorHandlingResult
{
CanContinue = false,
UserMessage = $"Unexpected error: {errorCode}",
TechnicalMessage = $"Unmapped SYSPRO error code: {errorCode}",
SuggestedAction = "Contact system administrator"
};
}
}
public class ErrorInfo
{
public ErrorSeverity Severity { get; set; }
public string Message { get; set; }
public string Action { get; set; }
}
public enum ErrorSeverity
{
Warning,
Error,
Fatal
}
Transaction Boundary Management
Ensuring Atomic Operations
public class TransactionBoundaryManager
{
private readonly ISysproEnet _sysproEnet;
private readonly ILogger<TransactionBoundaryManager> _logger;
public async Task<T> ExecuteInTransactionAsync<T>(
Func<Task<T>> operation,
TransactionOptions options = null)
{
options ??= new TransactionOptions
{
IsolationLevel = IsolationLevel.ReadCommitted,
Timeout = TimeSpan.FromSeconds(60)
};
string transactionId = null;
try
{
// Start SYSPRO transaction
transactionId = BeginTransaction(options);
_logger.LogDebug("Started SYSPRO transaction {TransactionId}", transactionId);
// Execute operation
var result = await operation();
// Commit transaction
CommitTransaction(transactionId);
_logger.LogDebug("Committed SYSPRO transaction {TransactionId}", transactionId);
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in transaction {TransactionId}, rolling back",
transactionId);
if (!string.IsNullOrEmpty(transactionId))
{
RollbackTransaction(transactionId);
}
throw;
}
}
private string BeginTransaction(TransactionOptions options)
{
var xmlParam = $@"
<Transaction>
<Begin>Y</Begin>
<IsolationLevel>{options.IsolationLevel}</IsolationLevel>
<Timeout>{options.Timeout.TotalSeconds}</Timeout>
</Transaction>";
return _sysproEnet.Transaction(null, "COMTRN", xmlParam, string.Empty);
}
private void CommitTransaction(string transactionId)
{
var xmlParam = $@"
<Transaction>
<Commit>Y</Commit>
<TransactionId>{transactionId}</TransactionId>
</Transaction>";
_sysproEnet.Transaction(null, "COMTRN", xmlParam, string.Empty);
}
private void RollbackTransaction(string transactionId)
{
try
{
var xmlParam = $@"
<Transaction>
<Rollback>Y</Rollback>
<TransactionId>{transactionId}</TransactionId>
</Transaction>";
_sysproEnet.Transaction(null, "COMTRN", xmlParam, string.Empty);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to rollback transaction {TransactionId}",
transactionId);
}
}
}
Business Object Query Methods
Query Operations
public class BusinessObjectQuery
{
public async Task<CustomerBalance> QueryCustomerBalance(string customer)
{
var xmlIn = $@"
<Query>
<Key>
<Customer>{customer}</Customer>
</Key>
<Option>
<IncludeBalance>Y</IncludeBalance>
<IncludeAging>Y</IncludeAging>
<IncludeCreditInfo>Y</IncludeCreditInfo>
</Option>
</Query>";
var xmlOut = _sysproEnet.Query(null, "ARSQRY", xmlIn);
return ParseCustomerBalance(xmlOut);
}
public async Task<PaymentValidation> ValidatePayment(
string customer,
string invoice,
decimal amount)
{
var xmlIn = $@"
<Validate>
<Payment>
<Customer>{customer}</Customer>
<Invoice>{invoice}</Invoice>
<Amount>{amount}</Amount>
</Payment>
</Validate>";
var xmlParam = @"<ValidatePayment><ReturnDetails>Y</ReturnDetails></ValidatePayment>";
var xmlOut = _sysproEnet.Transaction(null, "ARSTPY", xmlParam, xmlIn);
return ParseValidationResult(xmlOut);
}
}
Best Practices
- Always validate XML before sending to SYSPRO
- Handle all error codes explicitly
- Use transactions for multi-step operations
- Log all business object calls for audit
- Implement retry logic for transient failures
- Parse responses defensively to handle missing elements
- Test with various scenarios including edge cases
Common Pitfalls
- Not escaping XML special characters
- Ignoring warning messages that may indicate issues
- Missing error code handling for specific scenarios
- Not validating parameter combinations
- Assuming element order in response XML
Related Documentation
- SYSPRO Integration Overview - Architecture
- SYSPRO Posting Patterns - Posting workflows
- Integration Services - Service implementation
- SYSPRO Data Models - Entity relationships
Summary
The ARSTPY business object provides comprehensive payment processing capabilities within SYSPRO. Through careful XML document construction, proper error handling, and transaction management, the AR Payment Reversal dashboard achieves reliable integration with SYSPRO's core financial processes while maintaining data integrity and providing detailed audit trails.