Skip to main content

SYSPRO Posting Patterns

Overview

This pattern describes the standard approaches for posting transactions to SYSPRO, including validation, error handling, audit trails, and rollback strategies. Proper posting patterns ensure data integrity and consistency when updating SYSPRO data.

Core Concepts

  • Transaction Posting: Writing data to SYSPRO tables
  • Validation Rules: Pre-posting checks
  • Batch Processing: Handling multiple transactions
  • Audit Trails: Transaction history tracking
  • Error Recovery: Rollback and compensation

Pattern Implementation

Pre-Posting Validation

Comprehensive validation before posting ensures data integrity:

// Pattern: Pre-Posting Validation
public class PostingValidator
{
private readonly IDataService _dataService;
private readonly ILogger<PostingValidator> _logger;

public async Task<ValidationResult> ValidateBeforePostingAsync<T>(
T postingRequest) where T : IPostingRequest
{
var result = new ValidationResult();

try
{
// Step 1: Validate posting period
if (!await ValidatePostingPeriodAsync(postingRequest.PostingDate))
{
result.AddError("Posting period is closed or invalid");
}

// Step 2: Validate user permissions
if (!await ValidateUserPermissionsAsync(postingRequest.UserId, postingRequest.TransactionType))
{
result.AddError("User lacks permission for this transaction type");
}

// Step 3: Validate business rules
await ValidateBusinessRulesAsync(postingRequest, result);

// Step 4: Validate reference data
await ValidateReferenceDataAsync(postingRequest, result);

// Step 5: Check for duplicates
if (await CheckForDuplicatesAsync(postingRequest))
{
result.AddWarning("Possible duplicate transaction detected");
}

_logger.LogInformation("Validation completed {@ValidationContext}",
new {
RequestType = typeof(T).Name,
IsValid = result.IsValid,
ErrorCount = result.Errors.Count,
WarningCount = result.Warnings.Count
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Validation failed for posting request");
result.AddError($"Validation error: {ex.Message}");
}

return result;
}

private async Task<bool> ValidatePostingPeriodAsync(DateTime postingDate)
{
var period = await _dataService.GetPostingPeriodAsync(postingDate);
return period != null && period.IsOpen && !period.IsLocked;
}

private async Task ValidateBusinessRulesAsync<T>(T request, ValidationResult result)
where T : IPostingRequest
{
// Type-specific business rules
switch (request)
{
case PurchaseOrderPosting po:
await ValidatePurchaseOrderRules(po, result);
break;

case PaymentReversalPosting pr:
await ValidatePaymentReversalRules(pr, result);
break;

case InvoicePosting inv:
await ValidateInvoiceRules(inv, result);
break;
}
}
}

Posting Workflow Pattern

Standard workflow for posting transactions:

// Pattern: Posting Workflow
public class PostingWorkflow
{
private readonly IPostingValidator _validator;
private readonly ISysproBusinessObject _businessObject;
private readonly IAuditService _auditService;
private readonly ILogger<PostingWorkflow> _logger;

public async Task<PostingResult> ExecutePostingAsync<T>(
T postingRequest) where T : IPostingRequest
{
var result = new PostingResult
{
RequestId = Guid.NewGuid().ToString(),
StartTime = DateTime.UtcNow
};

try
{
// Phase 1: Validation
_logger.LogInformation("Starting posting validation {@PostingContext}",
new { RequestType = typeof(T).Name });

var validation = await _validator.ValidateBeforePostingAsync(postingRequest);
if (!validation.IsValid)
{
result.Status = PostingStatus.ValidationFailed;
result.Errors = validation.Errors;
return result;
}

// Phase 2: Prepare posting data
_logger.LogInformation("Preparing posting data");
var postingData = await PreparePostingDataAsync(postingRequest);

// Phase 3: Execute posting
_logger.LogInformation("Executing posting to SYSPRO");
var businessObjectResult = await PostToSysproAsync(postingData);

if (!businessObjectResult.Success)
{
result.Status = PostingStatus.PostingFailed;
result.Errors = businessObjectResult.Errors;

// Attempt rollback if partial success
if (businessObjectResult.PartialSuccess)
{
await RollbackPartialPostingAsync(businessObjectResult);
}

return result;
}

// Phase 4: Post-processing
_logger.LogInformation("Executing post-processing");
await ExecutePostProcessingAsync(businessObjectResult, postingRequest);

// Phase 5: Audit logging
await _auditService.LogPostingAsync(new PostingAudit
{
RequestId = result.RequestId,
TransactionType = postingRequest.TransactionType,
PostingDate = postingRequest.PostingDate,
UserId = postingRequest.UserId,
Status = "Success",
Details = businessObjectResult.ToJson()
});

result.Status = PostingStatus.Success;
result.TransactionNumber = businessObjectResult.TransactionNumber;
result.JournalNumber = businessObjectResult.JournalNumber;

_logger.LogInformation("Posting completed successfully {@PostingResult}",
result);
}
catch (Exception ex)
{
_logger.LogError(ex, "Posting failed {@ErrorContext}",
new { RequestId = result.RequestId });

result.Status = PostingStatus.SystemError;
result.ErrorMessage = ex.Message;

// Log failure to audit
await _auditService.LogPostingFailureAsync(result.RequestId, ex);
}
finally
{
result.EndTime = DateTime.UtcNow;
}

return result;
}
}

Batch Posting Pattern

Efficient processing of multiple transactions:

// Pattern: Batch Posting
public class BatchPostingService
{
private readonly IPostingWorkflow _postingWorkflow;
private readonly ILogger<BatchPostingService> _logger;

public async Task<BatchPostingResult> ProcessBatchAsync<T>(
IEnumerable<T> items,
BatchPostingOptions options) where T : IPostingRequest
{
var batchResult = new BatchPostingResult
{
BatchId = Guid.NewGuid().ToString(),
StartTime = DateTime.UtcNow,
TotalItems = items.Count()
};

try
{
if (options.UseTransaction)
{
await ProcessWithTransactionAsync(items, batchResult, options);
}
else
{
await ProcessWithoutTransactionAsync(items, batchResult, options);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Batch posting failed {@BatchContext}",
new { BatchId = batchResult.BatchId });

batchResult.Status = BatchStatus.Failed;
batchResult.ErrorMessage = ex.Message;
}
finally
{
batchResult.EndTime = DateTime.UtcNow;
LogBatchSummary(batchResult);
}

return batchResult;
}

private async Task ProcessWithTransactionAsync<T>(
IEnumerable<T> items,
BatchPostingResult batchResult,
BatchPostingOptions options) where T : IPostingRequest
{
var processedItems = new List<PostingResult>();

try
{
foreach (var item in items)
{
var result = await _postingWorkflow.ExecutePostingAsync(item);

if (result.Status == PostingStatus.Success)
{
processedItems.Add(result);
batchResult.SuccessCount++;
}
else
{
// Rollback all successful items
await RollbackBatchAsync(processedItems);

batchResult.Status = BatchStatus.RolledBack;
batchResult.FailureCount = items.Count();

throw new BatchPostingException(
$"Batch failed at item {batchResult.SuccessCount + 1}");
}
}

batchResult.Status = BatchStatus.Success;
}
catch (Exception ex)
{
_logger.LogError(ex, "Batch transaction failed, rolling back");
throw;
}
}

private async Task ProcessWithoutTransactionAsync<T>(
IEnumerable<T> items,
BatchPostingResult batchResult,
BatchPostingOptions options) where T : IPostingRequest
{
var tasks = new List<Task<PostingResult>>();
var semaphore = new SemaphoreSlim(options.MaxConcurrency);

foreach (var item in items)
{
await semaphore.WaitAsync();

tasks.Add(Task.Run(async () =>
{
try
{
return await _postingWorkflow.ExecutePostingAsync(item);
}
finally
{
semaphore.Release();
}
}));
}

var results = await Task.WhenAll(tasks);

batchResult.SuccessCount = results.Count(r => r.Status == PostingStatus.Success);
batchResult.FailureCount = results.Count(r => r.Status != PostingStatus.Success);
batchResult.Results = results.ToList();

batchResult.Status = batchResult.FailureCount == 0
? BatchStatus.Success
: BatchStatus.PartialSuccess;
}
}

Transaction Rollback Pattern

Handling rollback and compensation:

// Pattern: Transaction Rollback
public class TransactionRollbackService
{
private readonly ISysproBusinessObject _businessObject;
private readonly IAuditService _auditService;
private readonly ILogger<TransactionRollbackService> _logger;

public async Task<RollbackResult> RollbackTransactionAsync(
PostingResult originalPosting)
{
var rollbackResult = new RollbackResult
{
OriginalTransactionId = originalPosting.TransactionNumber,
StartTime = DateTime.UtcNow
};

try
{
_logger.LogInformation("Starting rollback for transaction {TransactionNumber}",
originalPosting.TransactionNumber);

// Determine rollback strategy
var strategy = DetermineRollbackStrategy(originalPosting);

switch (strategy)
{
case RollbackStrategy.Reversal:
rollbackResult = await ExecuteReversalAsync(originalPosting);
break;

case RollbackStrategy.Compensation:
rollbackResult = await ExecuteCompensationAsync(originalPosting);
break;

case RollbackStrategy.Deletion:
rollbackResult = await ExecuteDeletionAsync(originalPosting);
break;

default:
throw new NotSupportedException($"Rollback strategy {strategy} not supported");
}

// Audit the rollback
await _auditService.LogRollbackAsync(new RollbackAudit
{
OriginalTransaction = originalPosting.TransactionNumber,
RollbackTransaction = rollbackResult.RollbackTransactionNumber,
Strategy = strategy.ToString(),
Status = rollbackResult.Success ? "Success" : "Failed",
UserId = originalPosting.UserId,
Timestamp = DateTime.UtcNow
});

_logger.LogInformation("Rollback completed {@RollbackResult}", rollbackResult);
}
catch (Exception ex)
{
_logger.LogError(ex, "Rollback failed for transaction {TransactionNumber}",
originalPosting.TransactionNumber);

rollbackResult.Success = false;
rollbackResult.ErrorMessage = ex.Message;
}

return rollbackResult;
}

private async Task<RollbackResult> ExecuteReversalAsync(PostingResult original)
{
// Create reversal transaction
var reversalXml = BuildReversalXml(original);

var result = await _businessObject.InvokeAsync(
original.BusinessObject,
reversalXml,
BuildReversalParameters());

return new RollbackResult
{
Success = result.Success,
RollbackTransactionNumber = result.TransactionNumber,
Strategy = RollbackStrategy.Reversal
};
}

private async Task<RollbackResult> ExecuteCompensationAsync(PostingResult original)
{
// Create compensating transaction (opposite effect)
var compensationSteps = BuildCompensationSteps(original);

foreach (var step in compensationSteps)
{
var result = await _businessObject.InvokeAsync(
step.BusinessObject,
step.InputXml,
step.ParameterXml);

if (!result.Success)
{
throw new CompensationException(
$"Compensation failed at step {step.Name}");
}
}

return new RollbackResult
{
Success = true,
Strategy = RollbackStrategy.Compensation
};
}
}

Audit Trail Pattern

Comprehensive audit logging for all postings:

// Pattern: Posting Audit Trail
public class PostingAuditService : IAuditService
{
private readonly IDataService _dataService;
private readonly ILogger<PostingAuditService> _logger;

public async Task LogPostingAsync(PostingAudit audit)
{
try
{
// Create audit record
var auditRecord = new AuditRecord
{
Id = Guid.NewGuid(),
TransactionType = audit.TransactionType,
TransactionNumber = audit.TransactionNumber,
PostingDate = audit.PostingDate,
UserId = audit.UserId,
Status = audit.Status,
Details = audit.Details,
Timestamp = DateTime.UtcNow,
MachineName = Environment.MachineName,
ApplicationVersion = GetApplicationVersion()
};

// Store in database
await _dataService.CreateAuditRecordAsync(auditRecord);

// Log to structured logging
_logger.LogInformation("Posting audit recorded {@AuditContext}",
new {
auditRecord.TransactionType,
auditRecord.TransactionNumber,
auditRecord.Status
});

// Optional: Send to external audit system
await SendToExternalAuditSystemAsync(auditRecord);
}
catch (Exception ex)
{
// Audit logging should not fail the posting
_logger.LogError(ex, "Failed to log posting audit");
}
}

public async Task<IEnumerable<AuditRecord>> GetAuditTrailAsync(
string transactionNumber)
{
return await _dataService.GetAuditRecordsAsync(
r => r.TransactionNumber == transactionNumber ||
r.RelatedTransactionNumber == transactionNumber);
}
}

Error Handling Pattern

Comprehensive error handling for posting failures:

// Pattern: Posting Error Handler
public class PostingErrorHandler
{
private readonly ILogger<PostingErrorHandler> _logger;
private readonly INotificationService _notificationService;

public async Task<ErrorHandlingResult> HandlePostingErrorAsync(
Exception error,
IPostingRequest request,
PostingContext context)
{
var result = new ErrorHandlingResult();

try
{
// Classify the error
var errorType = ClassifyError(error);

// Log the error with appropriate level
LogError(error, errorType, request, context);

// Determine recovery action
result.RecoveryAction = DetermineRecoveryAction(errorType);

// Execute recovery if possible
if (result.RecoveryAction != RecoveryAction.None)
{
result.RecoverySuccessful = await ExecuteRecoveryAsync(
result.RecoveryAction,
request,
context);
}

// Notify appropriate parties
await NotifyErrorAsync(error, errorType, request, context);

// Create user-friendly message
result.UserMessage = CreateUserMessage(errorType, error);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error handler failed");
result.UserMessage = "An unexpected error occurred. Please contact support.";
}

return result;
}

private ErrorType ClassifyError(Exception error)
{
return error switch
{
ValidationException _ => ErrorType.Validation,
BusinessRuleException _ => ErrorType.BusinessRule,
SysproSessionException _ => ErrorType.Session,
SqlException sqlEx when sqlEx.Number == -2 => ErrorType.Timeout,
SqlException sqlEx when sqlEx.Number == 1205 => ErrorType.Deadlock,
UnauthorizedAccessException _ => ErrorType.Permission,
_ => ErrorType.System
};
}

private RecoveryAction DetermineRecoveryAction(ErrorType errorType)
{
return errorType switch
{
ErrorType.Timeout => RecoveryAction.Retry,
ErrorType.Deadlock => RecoveryAction.Retry,
ErrorType.Session => RecoveryAction.Reconnect,
ErrorType.Validation => RecoveryAction.None,
ErrorType.BusinessRule => RecoveryAction.None,
_ => RecoveryAction.Alert
};
}
}

Dashboard-Specific Implementations

AR Payment Reversal

  • Queue-based batch posting for reversals
  • Two-phase validation (queue then posting)
  • Comprehensive audit trail for compliance
  • See: AR Posting Patterns

Inventory Mini MRP

  • PORTOI posting for purchase orders
  • Batch order creation with supplier grouping
  • Partial success handling for multi-line orders
  • See: Inventory Posting Patterns

AP EFT Remittance

  • Direct database updates for custom tables
  • APSTIN for supplier invoice posting
  • Email notification after successful posting
  • See: AP Posting Patterns

Best Practices

  1. Always validate before posting to prevent errors
  2. Use transactions for multi-step operations
  3. Implement comprehensive audit trails for compliance
  4. Handle partial failures gracefully in batch operations
  5. Provide clear error messages to users
  6. Implement retry logic for transient failures
  7. Test rollback scenarios thoroughly
  8. Monitor posting performance and optimize as needed

Common Pitfalls

  1. No validation - Posting invalid data
  2. Missing audit trail - No record of transactions
  3. No rollback strategy - Unable to recover from errors
  4. Synchronous batch processing - Poor performance
  5. Ignoring warnings - May indicate data issues

Examples