Business Logic Services
Overview
The business logic services layer encapsulates the core domain logic of the EFT Remittance Dashboard, orchestrating complex workflows for payment processing, email distribution, and remittance management. This layer implements the business rules and processes that drive the application's primary functionality while maintaining clean separation from infrastructure and presentation concerns.
Key Concepts
- Domain-Driven Design: Business logic aligned with domain requirements
- Workflow Orchestration: Multi-step process coordination
- Business Rule Validation: Enforcement of business constraints
- State Management: Payment and remittance status tracking
- Error Recovery: Resilient processing with retry mechanisms
Implementation Details
Core Business Service Implementation
The EftRemittanceService implements the primary business operations:
// MepApps.Dash.Ap.Rpt.EftRemittance/Services/EftRemittanceService.cs
public class EftRemittanceService : IEftRemittanceService
{
private readonly ILogger<EftRemittanceService> _logger;
private readonly PluginSysproDataContext _context;
private readonly ISharedShellInterface _sharedShellInterface;
public EftRemittanceService(
ILogger<EftRemittanceService> logger,
PluginSysproDataContext context,
ISharedShellInterface sharedShellInterface)
{
_logger = logger;
_context = context;
_sharedShellInterface = sharedShellInterface;
}
public async Task QueueMailInServiceAsync(
string supplier,
string paymentNumber,
string emailTo,
string emailBcc,
string emailSubject)
{
try
{
// Business validation
if (string.IsNullOrWhiteSpace(emailTo))
{
_logger.LogWarning("No email address for supplier {Supplier}", supplier);
throw new BusinessException($"Recipient email is required for supplier {supplier}");
}
// Validate email format
if (!IsValidEmail(emailTo))
{
throw new BusinessException($"Invalid email format: {emailTo}");
}
// Create mail queue entry
var mailQueueSql = @"
INSERT INTO MailQueue (
Supplier, PaymentNumber, EmailTo, EmailBcc,
Subject, Status, CreatedDate, CreatedBy
) VALUES (
@Supplier, @PaymentNumber, @EmailTo, @EmailBcc,
@Subject, 'Queued', GETDATE(), @Operator
)";
await _context.Database.Connection.ExecuteAsync(mailQueueSql, new
{
Supplier = supplier,
PaymentNumber = paymentNumber,
EmailTo = emailTo,
EmailBcc = emailBcc,
Subject = emailSubject,
Operator = _sharedShellInterface.CurrentSession.SysproOperator
});
_logger.LogInformation("Email queued for supplier {Supplier}. {@MailContext}",
supplier, new { supplier, paymentNumber, emailTo });
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to queue email. {@MailContext}",
new { supplier, paymentNumber, emailTo });
throw;
}
}
}
Business Workflow Orchestration
Complex business processes are coordinated through service methods:
public async Task<ProcessingResult> ProcessPaymentRemittances(
string paymentNumber,
RemittanceOptions options)
{
var result = new ProcessingResult();
var stopwatch = Stopwatch.StartNew();
try
{
_logger.LogInformation("Starting remittance processing for payment {PaymentNumber}",
paymentNumber);
// Step 1: Load and validate payment details
var payments = await QueryPaymentNumber(paymentNumber, options.CheckFilter);
if (!payments.Any())
{
_logger.LogWarning("No payments found for {PaymentNumber}", paymentNumber);
result.Status = ProcessingStatus.NoDataFound;
return result;
}
_logger.LogDebug("Found {Count} payment records", payments.Count());
// Step 2: Validate business rules
var validationErrors = ValidatePayments(payments, options);
if (validationErrors.Any() && !options.IgnoreValidationErrors)
{
result.Errors = validationErrors;
result.Status = ProcessingStatus.ValidationFailed;
return result;
}
// Step 3: Process each payment
var processingTasks = payments.Select(async payment =>
{
try
{
await ProcessSinglePayment(payment, options);
Interlocked.Increment(ref result.SuccessCount);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to process payment for supplier {Supplier}",
payment.Supplier);
Interlocked.Increment(ref result.FailureCount);
lock (result.Errors)
{
result.Errors.Add($"{payment.Supplier}: {ex.Message}");
}
}
});
// Process in parallel with controlled concurrency
await Task.WhenAll(processingTasks.Take(options.MaxConcurrency));
result.Status = result.FailureCount == 0
? ProcessingStatus.Success
: ProcessingStatus.PartialSuccess;
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "Payment processing workflow failed");
result.Status = ProcessingStatus.Failed;
result.Errors.Add(ex.Message);
return result;
}
finally
{
stopwatch.Stop();
_logger.LogInformation("Remittance processing completed in {ElapsedMs}ms. {@ProcessingResult}",
stopwatch.ElapsedMilliseconds, result);
}
}
Examples
Example 1: Email Distribution with Retry Logic
public async Task<int?> SendMail(
string mailTo,
string subject,
string fileAttachmentPath,
string blindCcRecipients)
{
const int maxRetries = 3;
int attempt = 0;
while (attempt < maxRetries)
{
try
{
attempt++;
// Validate business rules
await ValidateEmailBusinessRules(mailTo);
// Prepare email content
var emailContent = await PrepareEmailContent(fileAttachmentPath);
// Send through email service
var mailId = await _emailService.SendAsync(new EmailMessage
{
To = mailTo,
Subject = subject,
Body = emailContent.Body,
Attachments = new[] { fileAttachmentPath },
Bcc = blindCcRecipients,
Priority = EmailPriority.Normal
});
// Update statistics
await UpdateEmailStatistics(mailTo, subject, true);
_logger.LogInformation("Email sent successfully. MailId: {MailId}", mailId);
return mailId;
}
catch (TransientException ex) when (attempt < maxRetries)
{
var delay = TimeSpan.FromSeconds(Math.Pow(2, attempt)); // Exponential backoff
_logger.LogWarning(ex, "Transient error sending email, retrying in {Delay}s",
delay.TotalSeconds);
await Task.Delay(delay);
}
catch (Exception ex)
{
_logger.LogError(ex, "Email sending failed permanently. {@EmailContext}",
new { mailTo, subject, attempt });
await UpdateEmailStatistics(mailTo, subject, false);
throw;
}
}
return null;
}
Example 2: Payment Validation Rules
public class PaymentBusinessRules
{
private readonly ILogger<PaymentBusinessRules> _logger;
public ValidationResult ValidatePaymentForRemittance(PaymentDetail payment)
{
var errors = new List<string>();
// Rule 1: Payment must have valid supplier
if (string.IsNullOrWhiteSpace(payment.Supplier))
{
errors.Add("Payment must have a valid supplier code");
}
// Rule 2: Payment amount must be positive
if (payment.NetPayValue <= 0)
{
errors.Add($"Payment amount must be positive (current: {payment.NetPayValue:C})");
}
// Rule 3: EFT payments must have email address
if (payment.PaymentType == "E" && string.IsNullOrWhiteSpace(payment.RemitEmail))
{
errors.Add($"EFT payment for supplier {payment.Supplier} requires email address");
}
// Rule 4: Payment date cannot be future
if (payment.PaymentDate > DateTime.Now.Date)
{
errors.Add($"Payment date cannot be in the future ({payment.PaymentDate:yyyy-MM-dd})");
}
// Rule 5: Check for duplicate processing
if (payment.ProcessedDate.HasValue)
{
errors.Add($"Payment already processed on {payment.ProcessedDate:yyyy-MM-dd HH:mm}");
}
var result = new ValidationResult
{
IsValid = !errors.Any(),
Errors = errors
};
if (!result.IsValid)
{
_logger.LogWarning("Payment validation failed. {@ValidationContext}",
new { payment.PaymentNumber, payment.Supplier, errors });
}
return result;
}
}
Example 3: Batch Processing with Progress Reporting
public async Task ProcessBatchWithProgress(
IEnumerable<PaymentDetail> payments,
IProgress<BatchProgress> progress,
CancellationToken cancellationToken)
{
var paymentList = payments.ToList();
var total = paymentList.Count;
var processed = 0;
var errors = new List<string>();
foreach (var payment in paymentList)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
// Report progress
progress?.Report(new BatchProgress
{
Current = processed,
Total = total,
CurrentItem = payment.Supplier,
Status = "Processing"
});
// Process payment
await ProcessSinglePayment(payment);
processed++;
// Log milestone progress
if (processed % 10 == 0)
{
_logger.LogInformation("Batch progress: {Processed}/{Total} payments processed",
processed, total);
}
}
catch (Exception ex)
{
errors.Add($"{payment.Supplier}: {ex.Message}");
_logger.LogError(ex, "Failed to process payment for supplier {Supplier}",
payment.Supplier);
}
}
// Final progress report
progress?.Report(new BatchProgress
{
Current = processed,
Total = total,
Status = "Complete",
Errors = errors
});
}
Business Rule Implementation
Complex Business Logic Example
public async Task<bool> CanSendRemittance(PaymentDetail payment)
{
// Check multiple business conditions
// 1. Supplier must be active
var supplier = await _context.Suppliers.FindAsync(payment.Supplier);
if (supplier?.Status != "Active")
{
_logger.LogWarning("Cannot send remittance to inactive supplier {Supplier}",
payment.Supplier);
return false;
}
// 2. Check credit hold status
if (supplier.OnCreditHold)
{
_logger.LogWarning("Supplier {Supplier} is on credit hold", payment.Supplier);
return false;
}
// 3. Validate payment threshold
var threshold = await GetPaymentThreshold(payment.Supplier);
if (payment.NetPayValue < threshold)
{
_logger.LogDebug("Payment below threshold for supplier {Supplier}: {Amount} < {Threshold}",
payment.Supplier, payment.NetPayValue, threshold);
return false;
}
// 4. Check daily sending limit
var dailySent = await GetDailySentCount(payment.Supplier);
if (dailySent >= MaxDailyRemittancesPerSupplier)
{
_logger.LogWarning("Daily remittance limit reached for supplier {Supplier}",
payment.Supplier);
return false;
}
return true;
}
Service Method Naming Conventions
The service follows consistent naming patterns:
- Query Methods:
QueryXxxorGetXxxfor data retrieval - Command Methods:
UpdateXxx,CreateXxx,DeleteXxxfor modifications - Process Methods:
ProcessXxxfor complex workflows - Validation Methods:
ValidateXxxorCanXxxfor business rule checks - Async Methods: All async methods end with
Async
Error Handling and Recovery
public async Task<Result> ExecuteWithRecovery(Func<Task> operation, string context)
{
try
{
await operation();
return Result.Success();
}
catch (BusinessException ex)
{
// Business rule violations - don't retry
_logger.LogWarning(ex, "Business rule violation in {Context}", context);
return Result.Failure(ex.Message);
}
catch (TransientException ex)
{
// Transient errors - attempt recovery
_logger.LogWarning(ex, "Transient error in {Context}, attempting recovery", context);
await RecoverFromTransientError(ex);
return Result.Retry();
}
catch (Exception ex)
{
// Unexpected errors - log and fail
_logger.LogError(ex, "Unexpected error in {Context}", context);
return Result.Failure("An unexpected error occurred");
}
}
Best Practices
- Separate business logic from infrastructure: Keep business rules independent of data access
- Implement idempotent operations: Ensure operations can be safely retried
- Use domain-specific exceptions: Create custom exceptions for business rule violations
- Log business events: Maintain audit trail of business operations
- Validate early and completely: Check all business rules before processing
- Handle partial failures: Design for resilience in batch operations
- Provide meaningful error messages: Help users understand and resolve issues
Common Pitfalls
- Mixing business logic with data access code
- Not handling concurrent processing correctly
- Insufficient validation of business rules
- Poor error messages that don't guide resolution
- Not considering idempotency in operations
- Missing audit trail for critical operations
Related Documentation
Summary
The business logic services layer is the heart of the EFT Remittance Dashboard, implementing the complex rules and workflows that govern payment processing and remittance distribution. Through careful separation of concerns, comprehensive validation, and robust error handling, the services ensure reliable and auditable business operations while maintaining flexibility for future enhancements. The consistent patterns and clear abstractions make the business logic testable, maintainable, and aligned with domain requirements.