Skip to main content

Business Logic Services Pattern

Overview

This pattern defines how business logic services encapsulate core domain logic across MepDash dashboards. These services orchestrate complex workflows, implement business rules, and manage domain-specific operations while maintaining clean separation from infrastructure and presentation concerns.

Core 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: Tracking entity states through business processes
  • Error Recovery: Resilient processing with compensation logic

Pattern Implementation

Core Business Service Structure

All dashboards implement a primary business service that orchestrates domain operations:

// Pattern: Core Business Service
public interface IBusinessService
{
// Query operations
Task<IEnumerable<DomainEntity>> GetBusinessEntitiesAsync(BusinessCriteria criteria);
Task<DomainEntity> GetBusinessEntityAsync(string id);

// Command operations
Task<ProcessResult> ProcessBusinessOperationAsync(BusinessRequest request);
Task<ValidationResult> ValidateBusinessRulesAsync(BusinessData data);

// Workflow operations
Task<WorkflowResult> ExecuteWorkflowAsync(WorkflowRequest request);
Task<BatchResult> ProcessBatchAsync(IEnumerable<BatchItem> items);
}

public class BusinessService : IBusinessService
{
private readonly ILogger<BusinessService> _logger;
private readonly IDataService _dataService;
private readonly ISysproPostService _sysproService;
private readonly IValidationService _validationService;

public BusinessService(
ILogger<BusinessService> logger,
IDataService dataService,
ISysproPostService sysproService,
IValidationService validationService)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_dataService = dataService ?? throw new ArgumentNullException(nameof(dataService));
_sysproService = sysproService ?? throw new ArgumentNullException(nameof(sysproService));
_validationService = validationService ?? throw new ArgumentNullException(nameof(validationService));
}
}

Workflow Orchestration Pattern

Complex business processes are implemented as orchestrated workflows:

// Pattern: Workflow Orchestration
public class WorkflowOrchestrator
{
private readonly ILogger<WorkflowOrchestrator> _logger;

public async Task<WorkflowResult> ExecuteWorkflowAsync(WorkflowRequest request)
{
var workflow = new WorkflowResult
{
WorkflowId = Guid.NewGuid().ToString(),
StartTime = DateTime.UtcNow
};

try
{
// Step 1: Validation Phase
_logger.LogInformation("Starting workflow validation {@WorkflowContext}",
new { request.WorkflowType, request.RequestId });

workflow.ValidationResult = await ValidateWorkflowRequest(request);
if (!workflow.ValidationResult.IsValid)
{
workflow.Status = WorkflowStatus.ValidationFailed;
return workflow;
}

// Step 2: Preparation Phase
_logger.LogInformation("Preparing workflow data {@WorkflowContext}",
new { request.RequestId });

workflow.PreparedData = await PrepareWorkflowData(request);

// Step 3: Execution Phase
_logger.LogInformation("Executing workflow operations {@WorkflowContext}",
new { request.RequestId });

workflow.ExecutionResult = await ExecuteWorkflowOperations(
workflow.PreparedData);

// Step 4: Post-Processing Phase
if (workflow.ExecutionResult.Success)
{
_logger.LogInformation("Post-processing workflow {@WorkflowContext}",
new { request.RequestId });

workflow.PostProcessResult = await PostProcessWorkflow(
workflow.ExecutionResult);
}

// Step 5: Completion
workflow.Status = WorkflowStatus.Completed;
workflow.EndTime = DateTime.UtcNow;

_logger.LogInformation("Workflow completed successfully {@WorkflowResult}",
workflow);
}
catch (Exception ex)
{
_logger.LogError(ex, "Workflow failed {@ErrorContext}",
new { request.RequestId, workflow.CurrentStep });

// Execute compensation logic
await CompensateWorkflow(workflow);

workflow.Status = WorkflowStatus.Failed;
workflow.Error = ex.Message;
}

return workflow;
}

private async Task CompensateWorkflow(WorkflowResult workflow)
{
// Implement compensation logic to rollback partial changes
if (workflow.ExecutionResult?.PartialResults != null)
{
foreach (var partial in workflow.ExecutionResult.PartialResults.Reverse())
{
await RollbackOperation(partial);
}
}
}
}

Business Rule Validation Pattern

Business rules are validated consistently across all operations:

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

public async Task<ValidationResult> ValidateBusinessRulesAsync<T>(
T entity,
ValidationContext context) where T : class
{
var result = new ValidationResult();

// Apply validation rules based on type
var validators = GetValidators<T>(context);

foreach (var validator in validators)
{
var validationOutcome = await validator.ValidateAsync(entity);

if (!validationOutcome.IsValid)
{
result.Errors.AddRange(validationOutcome.Errors);

_logger.LogWarning("Validation failed {@ValidationContext}",
new {
EntityType = typeof(T).Name,
Errors = validationOutcome.Errors,
Context = context
});
}
}

result.IsValid = !result.Errors.Any();
return result;
}

// Example specific validation
public async Task<ValidationResult> ValidatePaymentReversalAsync(
PaymentReversalRequest request)
{
var result = new ValidationResult();

// Business Rule 1: Payment must exist
var payment = await _dataService.GetPaymentAsync(request.PaymentNumber);
if (payment == null)
{
result.AddError("Payment", $"Payment {request.PaymentNumber} not found");
}

// Business Rule 2: Payment must not be already reversed
if (payment?.IsReversed == true)
{
result.AddError("Payment", "Payment has already been reversed");
}

// Business Rule 3: Reversal date must be in valid period
if (!await IsValidReversalPeriod(request.ReversalDate))
{
result.AddError("Period", "Reversal date is not in an open period");
}

// Business Rule 4: User must have permission
if (!await UserHasPermission(request.UserId, "ReversePayment"))
{
result.AddError("Permission", "User lacks permission to reverse payments");
}

return result;
}
}

Calculation Engine Pattern

Complex business calculations are encapsulated in dedicated services:

// Pattern: Business Calculation Engine
public class CalculationEngine
{
private readonly ILogger<CalculationEngine> _logger;
private readonly IDataService _dataService;

// MRP Calculation Example
public async Task<MrpCalculationResult> CalculateMrpRequirementsAsync(
MrpParameters parameters)
{
var result = new MrpCalculationResult();

try
{
// Get current inventory levels
var inventory = await _dataService.GetInventoryLevelsAsync(
parameters.StockCodes);

// Get demand forecast
var demand = await CalculateDemandForecastAsync(parameters);

// Calculate requirements for each item
foreach (var item in inventory)
{
var requirement = new MrpRequirement
{
StockCode = item.StockCode,
CurrentStock = item.QtyOnHand,
SafetyStock = item.SafetyStockQty,
MinimumOrderQty = item.MinOrderQty,
LeadTimeDays = item.LeadTime
};

// Apply MRP formula
var projectedDemand = demand[item.StockCode];
var availableStock = item.QtyOnHand - item.QtyAllocated;
var reorderPoint = item.SafetyStockQty +
(projectedDemand * item.LeadTime / 30);

if (availableStock < reorderPoint)
{
requirement.RequiredQty = Math.Max(
reorderPoint - availableStock,
item.MinOrderQty
);

// Round to order multiple if specified
if (item.OrderMultiple > 0)
{
requirement.RequiredQty = Math.Ceiling(
requirement.RequiredQty / item.OrderMultiple
) * item.OrderMultiple;
}

requirement.SuggestedOrderDate = DateTime.Now;
requirement.RequiredDate = DateTime.Now.AddDays(item.LeadTime);

result.Requirements.Add(requirement);
}
}

_logger.LogInformation("MRP calculation completed {@MrpContext}",
new {
ItemCount = inventory.Count,
RequirementsCount = result.Requirements.Count
});
}
catch (Exception ex)
{
_logger.LogError(ex, "MRP calculation failed {@ErrorContext}",
parameters);
throw;
}

return result;
}
}

Batch Processing Pattern

Efficient processing of multiple items with transaction support:

// Pattern: Batch Processing
public class BatchProcessor
{
private readonly ILogger<BatchProcessor> _logger;
private readonly IDataService _dataService;
private readonly IValidationService _validationService;

public async Task<BatchResult> ProcessBatchAsync<T>(
IEnumerable<T> items,
Func<T, Task<ProcessResult>> processor,
BatchOptions options = null) where T : class
{
options ??= new BatchOptions();
var batchResult = new BatchResult
{
BatchId = Guid.NewGuid().ToString(),
StartTime = DateTime.UtcNow,
TotalItems = items.Count()
};

var validItems = new List<T>();

// Validation phase
foreach (var item in items)
{
var validation = await _validationService.ValidateAsync(item);
if (validation.IsValid)
{
validItems.Add(item);
}
else
{
batchResult.FailedItems.Add(new FailedItem
{
Item = item,
Errors = validation.Errors
});
}
}

// Processing phase
if (options.UseTransaction)
{
await ProcessWithTransactionAsync(validItems, processor, batchResult);
}
else
{
await ProcessWithoutTransactionAsync(validItems, processor, batchResult);
}

batchResult.EndTime = DateTime.UtcNow;
batchResult.SuccessCount = batchResult.ProcessedItems.Count;
batchResult.FailureCount = batchResult.FailedItems.Count;

_logger.LogInformation("Batch processing completed {@BatchResult}",
batchResult);

return batchResult;
}

private async Task ProcessWithTransactionAsync<T>(
List<T> items,
Func<T, Task<ProcessResult>> processor,
BatchResult batchResult) where T : class
{
using (var transaction = await _dataService.BeginTransactionAsync())
{
try
{
foreach (var item in items)
{
var result = await processor(item);

if (result.Success)
{
batchResult.ProcessedItems.Add(item);
}
else
{
// Fail entire batch on error
throw new BatchProcessingException(
$"Item processing failed: {result.ErrorMessage}");
}
}

await transaction.CommitAsync();
}
catch (Exception ex)
{
await transaction.RollbackAsync();
_logger.LogError(ex, "Batch transaction failed");

// Move all items to failed
batchResult.FailedItems.AddRange(
items.Select(i => new FailedItem
{
Item = i,
Errors = new[] { ex.Message }
}));
batchResult.ProcessedItems.Clear();
}
}
}
}

State Management Pattern

Managing entity states through business processes:

// Pattern: State Management
public class StateManager<TEntity> where TEntity : IStateful
{
private readonly ILogger<StateManager<TEntity>> _logger;
private readonly IDataService _dataService;

public async Task<StateTransitionResult> TransitionStateAsync(
TEntity entity,
string targetState,
StateContext context)
{
var result = new StateTransitionResult
{
FromState = entity.CurrentState,
TargetState = targetState
};

try
{
// Validate transition
if (!IsValidTransition(entity.CurrentState, targetState))
{
result.Success = false;
result.Error = $"Invalid transition from {entity.CurrentState} to {targetState}";
return result;
}

// Execute pre-transition actions
await ExecutePreTransitionActions(entity, targetState, context);

// Update state
var previousState = entity.CurrentState;
entity.CurrentState = targetState;
entity.StateChangedDate = DateTime.UtcNow;
entity.StateChangedBy = context.UserId;

// Persist change
await _dataService.UpdateAsync(entity);

// Execute post-transition actions
await ExecutePostTransitionActions(entity, previousState, context);

result.Success = true;
result.NewState = targetState;

_logger.LogInformation("State transition completed {@StateContext}",
new {
EntityId = entity.Id,
FromState = previousState,
ToState = targetState
});
}
catch (Exception ex)
{
_logger.LogError(ex, "State transition failed {@ErrorContext}",
new { entity.Id, targetState });

result.Success = false;
result.Error = ex.Message;
}

return result;
}
}

Dashboard-Specific Implementations

AR Payment Reversal

  • ArReversePaymentService orchestrates payment reversal workflows
  • Queue management for staging reversals
  • Multi-step validation and posting process
  • See: AR Business Logic

Inventory Mini MRP

  • InvOrderingService implements MRP calculations
  • Order consolidation by supplier and warehouse
  • Safety stock and minimum quantity calculations
  • See: Inventory Business Logic

AP EFT Remittance

  • EftRemittanceService manages remittance processing
  • Email distribution workflow
  • Payment selection and validation
  • See: AP Business Logic

Best Practices

  1. Separate business logic from infrastructure

    • Keep domain logic independent of data access
    • Use interfaces for external dependencies
  2. Implement comprehensive validation

    • Validate early in the process
    • Provide clear, actionable error messages
  3. Use structured logging

    • Log business events with context
    • Distinguish business errors from technical errors
  4. Design for failure recovery

    • Implement compensation logic
    • Support partial success scenarios
  5. Optimize batch operations

    • Process in chunks for large datasets
    • Use transactions appropriately
  6. Document business rules

    • Make implicit rules explicit
    • Version business logic changes

Common Pitfalls

  1. Mixing concerns - Business logic in UI or data layers
  2. Missing validation - Not checking business constraints
  3. No compensation - Unable to recover from failures
  4. Poor error messages - Technical errors exposed to users
  5. Synchronous processing - Blocking on long operations

Examples