SYSPRO Posting Patterns
Overview
This document details the SYSPRO posting mechanisms used in the AR Payment Reversal dashboard, including transaction posting workflows, validation and pre-checks, error handling for failed posts, rollback and compensation patterns, batch versus real-time posting strategies, and posting status tracking. Understanding these patterns is crucial for ensuring reliable and accurate payment reversal processing.
Key Concepts
- Transaction Atomicity: All-or-nothing posting to maintain data integrity
- Pre-Post Validation: Comprehensive checks before attempting posts
- Journal Creation: Automatic journal generation in SYSPRO
- GL Integration: Automatic general ledger posting
- Audit Trail: Complete tracking of all posting activities
- Error Recovery: Handling and compensating for posting failures
Implementation Details
Posting Workflow Architecture
Complete Reversal Posting Flow
// MepApps.Dash.Ar.Maint.PaymentReversal/Services/ArReversePaymentService.cs (Lines 475-498)
public async Task<ArReversePaymentPostCompletion> ReversePaymentsAsync(
IEnumerable<ArReversePaymentHeader> checks,
IEnumerable<ArReversePaymentDetail> invoices,
string postPeriod)
{
ArReversePaymentPostCompletion completionObject = null;
try
{
// Step 1: Pre-validation
_logger.LogInformation("Starting payment reversal validation");
if (!await ValidatePrePostConditions(checks, invoices, postPeriod))
{
throw new PostingValidationException("Pre-post validation failed");
}
// Step 2: Prepare posting documents
_logger.LogInformation("Preparing posting documents");
string inputXml = _sysproPostService.GetInputXml(checks, invoices);
string paramXml = _sysproPostService.GetParamXml(postPeriod);
// Step 3: Execute SYSPRO post
_logger.LogInformation("Executing SYSPRO post for {Count} payments", checks.Count());
string outputXml = _sysproPostService.PerformBusinessObjectPost(inputXml, paramXml);
// Step 4: Parse and validate results
completionObject = await BuildCompletionObjectAsync(inputXml, paramXml, outputXml);
// Step 5: Post-processing
if (completionObject.PostSucceeded)
{
await HandleSuccessfulPost(completionObject, checks);
}
else
{
await HandleFailedPost(completionObject, checks);
}
return completionObject;
}
catch (Exception ex)
{
_logger.LogError(ex, "Critical error in ReversePaymentsAsync");
await AttemptRecovery(checks, ex);
throw;
}
}
Pre-Post Validation
Comprehensive Validation Framework
public class PostingValidator
{
private readonly ILogger<PostingValidator> _logger;
private readonly PluginSysproDataContext _context;
public async Task<ValidationResult> ValidatePrePostConditions(
IEnumerable<ArReversePaymentHeader> payments,
IEnumerable<ArReversePaymentDetail> invoices,
string postPeriod)
{
var validationTasks = new List<Task<ValidationResult>>
{
ValidatePeriodStatus(postPeriod),
ValidateCustomerStatus(payments),
ValidateInvoiceBalances(invoices),
ValidatePaymentIntegrity(payments, invoices),
ValidateBankAccounts(payments),
ValidateOperatorPermissions(),
ValidateSystemConfiguration()
};
var results = await Task.WhenAll(validationTasks);
var allErrors = results
.Where(r => !r.IsValid)
.SelectMany(r => r.Errors)
.ToList();
if (allErrors.Any())
{
_logger.LogWarning("Pre-post validation failed. Errors: {@ValidationErrors}", allErrors);
return new ValidationResult { IsValid = false, Errors = allErrors };
}
return new ValidationResult { IsValid = true };
}
private async Task<ValidationResult> ValidatePeriodStatus(string postPeriod)
{
var errors = new List<string>();
// Check if period is open
var periodStatus = await _context.Database
.SqlQuery<string>(@"
SELECT PeriodStatus
FROM ArControl
WHERE PeriodId = @p0", postPeriod)
.FirstOrDefaultAsync();
if (periodStatus != "O") // O = Open
{
errors.Add($"Period {postPeriod} is not open for posting");
}
// Check for period lock
var isLocked = await _context.Database
.SqlQuery<bool>(@"
SELECT CASE WHEN PeriodLocked = 'Y' THEN 1 ELSE 0 END
FROM ArPeriodControl
WHERE Period = @p0", postPeriod)
.FirstOrDefaultAsync();
if (isLocked)
{
errors.Add($"Period {postPeriod} is locked");
}
return new ValidationResult { IsValid = !errors.Any(), Errors = errors };
}
private async Task<ValidationResult> ValidateInvoiceBalances(
IEnumerable<ArReversePaymentDetail> invoices)
{
var errors = new List<string>();
foreach (var invoice in invoices)
{
var currentBalance = await _context.ArInvoices
.Where(i => i.Customer == invoice.Customer && i.Invoice == invoice.Invoice)
.Select(i => i.InvoiceBal1)
.FirstOrDefaultAsync();
// Validate that invoice has sufficient balance for reversal
if (currentBalance == 0)
{
errors.Add($"Invoice {invoice.Invoice} is fully paid - cannot reverse");
}
// Check if reversal amount exceeds original payment
if (Math.Abs(invoice.PaymentValue) > Math.Abs(currentBalance))
{
errors.Add($"Reversal amount for invoice {invoice.Invoice} exceeds available balance");
}
}
return new ValidationResult { IsValid = !errors.Any(), Errors = errors };
}
}
Transaction Posting Execution
SYSPRO Business Object Invocation
// MepApps.Dash.Ar.Maint.PaymentReversal/Services/SysproPostService.cs (Lines 129-146)
public string PerformBusinessObjectPost(string inputXml, string paramXml)
{
string outputXml = string.Empty;
var stopwatch = Stopwatch.StartNew();
try
{
// Must execute on UI thread for COM interop
_dispatcher.Invoke(() =>
{
_logger.LogInformation("Invoking SYSPRO business object ARSTPY");
// Log input for audit trail
LogPostingInput(inputXml, paramXml);
// Execute SYSPRO post
outputXml = _sysproEnet.Transaction(
null, // Uses current session
"ARSTPY", // AR Payment Entry business object
paramXml, // Parameters
inputXml // Transaction data
);
stopwatch.Stop();
// Log output for audit trail
LogPostingOutput(outputXml, stopwatch.ElapsedMilliseconds);
});
// Validate output
if (!IsValidPostResponse(outputXml))
{
throw new SysproPostingException("Invalid response from SYSPRO");
}
return outputXml;
}
catch (Exception ex)
{
stopwatch.Stop();
_logger.LogError(ex, "SYSPRO posting failed after {ElapsedMs}ms",
stopwatch.ElapsedMilliseconds);
// Log failure for audit
LogPostingFailure(inputXml, paramXml, ex);
throw new SysproPostingException("Business object posting failed", ex)
{
InputXml = inputXml,
ParamXml = paramXml,
ElapsedMilliseconds = stopwatch.ElapsedMilliseconds
};
}
}
Post-Processing and Journal Creation
Handling Successful Posts
private async Task HandleSuccessfulPost(
ArReversePaymentPostCompletion completion,
IEnumerable<ArReversePaymentHeader> originalPayments)
{
try
{
_logger.LogInformation("Processing successful post. Items: {Items}, Journals: {Journals}",
completion.ItemsProcessed, completion.JournalCount);
// Step 1: Record in history table
await RecordPostingHistory(completion);
// Step 2: Update payment status
await UpdatePaymentStatus(originalPayments, "Posted");
// Step 3: Remove from queue
await RemoveFromQueue(originalPayments);
// Step 4: Update customer balances (handled by SYSPRO)
// Note: SYSPRO automatically updates ArCustomer.Balance
// Step 5: Create audit entries
await CreateAuditEntries(completion);
// Step 6: Send notifications if configured
await SendPostingNotifications(completion);
_logger.LogInformation("Post-processing completed successfully");
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in post-processing, but posting was successful");
// Don't throw - posting succeeded, just log the error
}
}
private async Task RecordPostingHistory(ArReversePaymentPostCompletion completion)
{
using (var context = _serviceProvider.GetService<PluginSysproDataContext>())
{
var history = new CG_ArReversePaymentPostCompletionHistory
{
PostDate = DateTime.Now,
PostedBy = GetCurrentOperator(),
PostSucceeded = completion.PostSucceeded,
ItemsProcessed = completion.ItemsProcessed,
ItemsInvalid = completion.ItemsInvalid,
JournalCount = completion.JournalCount,
PaymentCount = completion.PaymentCount,
PaymentTotal = completion.PaymentTotal,
BusinessObject = completion.BusinessObject,
InputXml = completion.InputXml,
ParamXml = completion.ParamXml,
OutputXml = completion.OutputXml
};
context.C_ArReversePaymentPostCompletionHistory.Add(history);
await context.SaveChangesAsync();
}
}
Error Handling and Recovery
Failed Post Management
private async Task HandleFailedPost(
ArReversePaymentPostCompletion completion,
IEnumerable<ArReversePaymentHeader> originalPayments)
{
_logger.LogError("Handling failed post. Items processed: {Processed}, Invalid: {Invalid}",
completion.ItemsProcessed, completion.ItemsInvalid);
try
{
// Step 1: Identify what failed
var failedItems = IdentifyFailedItems(completion);
// Step 2: Determine if partial success
if (completion.ItemsProcessed > 0)
{
// Some items succeeded - need careful handling
await HandlePartialSuccess(completion, originalPayments, failedItems);
}
else
{
// Complete failure - simpler recovery
await HandleCompleteFailure(originalPayments, completion);
}
// Step 3: Record failure in history
await RecordPostingHistory(completion);
// Step 4: Notify users
await NotifyPostingFailure(completion, failedItems);
}
catch (Exception ex)
{
_logger.LogError(ex, "Critical error during failed post handling");
throw new CriticalPostingException("Unable to handle posting failure", ex);
}
}
private async Task HandlePartialSuccess(
ArReversePaymentPostCompletion completion,
IEnumerable<ArReversePaymentHeader> originalPayments,
IEnumerable<FailedItem> failedItems)
{
_logger.LogWarning("Handling partial posting success");
// Identify successful payments
var successfulPayments = originalPayments
.Where(p => !failedItems.Any(f =>
f.Customer == p.Customer &&
f.CheckNumber == p.CheckNumber))
.ToList();
// Remove successful items from queue
if (successfulPayments.Any())
{
await RemoveFromQueue(successfulPayments);
_logger.LogInformation("Removed {Count} successful payments from queue",
successfulPayments.Count);
}
// Keep failed items in queue for retry
var failedPayments = originalPayments.Except(successfulPayments);
await UpdatePaymentStatus(failedPayments, "Failed");
// Create detailed failure report
await CreateFailureReport(failedItems);
}
Rollback and Compensation
Transaction Reversal Pattern
public class PostingCompensationService
{
private readonly ISysproPostService _postService;
private readonly ILogger<PostingCompensationService> _logger;
public async Task CompensatePosting(
ArReversePaymentPostCompletion failedPosting,
CompensationStrategy strategy)
{
_logger.LogWarning("Starting posting compensation. Strategy: {Strategy}", strategy);
switch (strategy)
{
case CompensationStrategy.ReverseAll:
await ReverseAllPostedTransactions(failedPosting);
break;
case CompensationStrategy.ReversePartial:
await ReversePartialTransactions(failedPosting);
break;
case CompensationStrategy.ManualIntervention:
await FlagForManualReview(failedPosting);
break;
default:
throw new NotSupportedException($"Strategy {strategy} not supported");
}
}
private async Task ReverseAllPostedTransactions(
ArReversePaymentPostCompletion posting)
{
// Parse successful transactions from output
var successfulTransactions = ParseSuccessfulTransactions(posting.OutputXml);
foreach (var transaction in successfulTransactions)
{
try
{
// Create reversal entry (opposite sign)
var reversalXml = CreateReversalXml(transaction);
// Post reversal
var result = _postService.PerformBusinessObjectPost(
reversalXml,
GetReversalParameters());
_logger.LogInformation("Successfully reversed transaction {TransactionId}",
transaction.Id);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to reverse transaction {TransactionId}",
transaction.Id);
// Continue with other reversals
}
}
}
private string CreateReversalXml(PostedTransaction transaction)
{
// Create XML with opposite values to reverse the original
return $@"
<PostArPayment>
<Item>
<Payment>
<Customer>{transaction.Customer}</Customer>
<PaymentValue>{-transaction.Amount}</PaymentValue>
<Reference>REV-{transaction.Reference}</Reference>
<PaymentDate>{DateTime.Now:yyyy-MM-dd}</PaymentDate>
<JournalNotation>Reversal of {transaction.Reference}</JournalNotation>
<Bank>{transaction.Bank}</Bank>
<PaymentType>C</PaymentType>
</Payment>
</Item>
</PostArPayment>";
}
}
Batch vs Real-Time Posting
Batch Processing Strategy
public class BatchPostingService
{
private readonly int _batchSize = 50; // Configurable batch size
private readonly ILogger<BatchPostingService> _logger;
public async Task<BatchPostingResult> ProcessBatchPostings(
IEnumerable<ArReversePaymentHeader> allPayments,
string postPeriod)
{
var result = new BatchPostingResult();
var batches = allPayments.Chunk(_batchSize);
_logger.LogInformation("Processing {TotalCount} payments in {BatchCount} batches",
allPayments.Count(), batches.Count());
foreach (var batch in batches.Select((value, index) => new { value, index }))
{
var batchNumber = batch.index + 1;
try
{
_logger.LogInformation("Processing batch {BatchNumber}/{TotalBatches}",
batchNumber, batches.Count());
// Process batch
var batchResult = await ProcessSingleBatch(
batch.value,
postPeriod,
batchNumber);
result.SuccessfulBatches.Add(batchResult);
// Add delay between batches to avoid overwhelming SYSPRO
if (batchNumber < batches.Count())
{
await Task.Delay(TimeSpan.FromSeconds(2));
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Batch {BatchNumber} failed", batchNumber);
result.FailedBatches.Add(new FailedBatch
{
BatchNumber = batchNumber,
Error = ex.Message,
Payments = batch.value
});
// Determine if we should continue
if (!ShouldContinueAfterFailure(result))
{
_logger.LogWarning("Stopping batch processing due to failure threshold");
break;
}
}
}
return result;
}
private bool ShouldContinueAfterFailure(BatchPostingResult result)
{
// Stop if more than 20% of batches have failed
var failureRate = (double)result.FailedBatches.Count /
(result.SuccessfulBatches.Count + result.FailedBatches.Count);
return failureRate < 0.2;
}
}
public class BatchPostingResult
{
public List<BatchResult> SuccessfulBatches { get; set; } = new();
public List<FailedBatch> FailedBatches { get; set; } = new();
public int TotalItemsProcessed => SuccessfulBatches.Sum(b => b.ItemsProcessed);
public int TotalItemsFailed => FailedBatches.Sum(b => b.Payments.Count());
}
Posting Status Tracking
Real-Time Status Updates
public class PostingStatusTracker
{
private readonly ILogger<PostingStatusTracker> _logger;
public event EventHandler<PostingStatusEventArgs> StatusChanged;
public async Task TrackPosting(
string postingId,
Func<Task<ArReversePaymentPostCompletion>> postingOperation)
{
var status = new PostingStatus
{
Id = postingId,
StartTime = DateTime.Now,
Status = "Initializing"
};
try
{
// Update status: Validating
UpdateStatus(status, "Validating");
// Update status: Posting
UpdateStatus(status, "Posting");
var result = await postingOperation();
// Update status: Processing Results
UpdateStatus(status, "Processing Results");
if (result.PostSucceeded)
{
status.Status = "Completed";
status.ItemsProcessed = result.ItemsProcessed;
status.JournalsCreated = result.JournalCount;
}
else
{
status.Status = "Failed";
status.ErrorMessage = "Posting validation failed";
}
}
catch (Exception ex)
{
status.Status = "Error";
status.ErrorMessage = ex.Message;
_logger.LogError(ex, "Posting {PostingId} failed", postingId);
}
finally
{
status.EndTime = DateTime.Now;
status.Duration = status.EndTime - status.StartTime;
// Final status update
UpdateStatus(status, status.Status);
// Persist final status
await SavePostingStatus(status);
}
}
private void UpdateStatus(PostingStatus status, string newStatus)
{
status.Status = newStatus;
status.LastUpdated = DateTime.Now;
StatusChanged?.Invoke(this, new PostingStatusEventArgs(status));
_logger.LogDebug("Posting {Id} status: {Status}", status.Id, newStatus);
}
}
Journal Integration
GL Journal Creation
public class JournalIntegrationService
{
public async Task<JournalCreationResult> CreateGLJournals(
ArReversePaymentPostCompletion posting)
{
var result = new JournalCreationResult();
// Parse journal information from SYSPRO output
var journals = ParseJournalInfo(posting.OutputXml);
foreach (var journal in journals)
{
try
{
// Verify journal was created in GL
var glJournal = await VerifyGLJournal(
journal.Year,
journal.Period,
journal.JournalNumber);
if (glJournal != null)
{
result.CreatedJournals.Add(new CreatedJournal
{
JournalNumber = journal.JournalNumber,
Period = journal.Period,
Year = journal.Year,
TotalDebits = glJournal.TotalDebits,
TotalCredits = glJournal.TotalCredits,
IsBalanced = glJournal.TotalDebits == glJournal.TotalCredits
});
_logger.LogInformation("GL Journal {Journal} created successfully",
journal.JournalNumber);
}
else
{
_logger.LogWarning("GL Journal {Journal} not found after posting",
journal.JournalNumber);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error verifying GL journal {Journal}",
journal.JournalNumber);
}
}
return result;
}
}
Best Practices
- Always validate before posting to SYSPRO
- Use transactions to ensure atomicity
- Log all posting activities for audit trail
- Implement retry logic for transient failures
- Handle partial success scenarios carefully
- Monitor posting performance and optimize batch sizes
- Test rollback procedures thoroughly
Common Pitfalls
- Not checking period status before posting
- Ignoring partial failures in batch processing
- Missing compensation for failed posts
- Inadequate error logging for troubleshooting
- Not validating GL integration results
Related Documentation
- SYSPRO Integration Overview - Architecture overview
- SYSPRO Business Objects - ARSTPY details
- Business Logic Services - Service implementation
- Integration Services - API communication
Summary
The SYSPRO posting patterns implemented in the AR Payment Reversal dashboard ensure reliable, auditable, and recoverable transaction processing. Through comprehensive validation, careful error handling, and robust compensation mechanisms, the system maintains data integrity even in failure scenarios while providing complete visibility into all posting activities.