Skip to main content

Business Logic Services

Overview

The business logic services layer in MepApps.Dash.Inv.Batch.MiniMrpOrderCreation encapsulates the core MRP calculation engine, order creation workflows, and inventory management rules. This layer orchestrates complex business operations, enforces business rules, and manages the transformation of data between the UI and data layers.

Key Concepts

  • MRP Calculations: Safety stock and minimum quantity based reorder calculations
  • Order Consolidation: Grouping items by supplier, warehouse, and due date
  • Business Rule Enforcement: Validation and constraint checking
  • Workflow Orchestration: Multi-step order creation process

Implementation Details

Core Business Service - InvOrderingService

The InvOrderingService is the primary business logic service handling MRP calculations and order creation:

// Services/InvOrderingService.cs (lines 8-20)
public class InvOrderingService
{
private readonly ILogger<InvOrderingService> _logger;
private readonly PluginSysproDataContext _sysproDataContext;
private readonly SysproPostService _sysproPost;

public InvOrderingService(
ILogger<InvOrderingService> logger,
PluginSysproDataContext sysproDataContext,
SysproPostService sysproPost)
{
_logger = logger;
_sysproDataContext = sysproDataContext;
_sysproPost = sysproPost;
}
}

MRP Calculation Logic

The system implements sophisticated MRP calculations based on safety stock and minimum quantities:

-- From CG_InventoryOrdering_View.txt (lines 16-25)
-- Safety stock based calculation
IIf(
QtyOnHand - ([QtyInTransit] + [QtyAllocated] + [QtyAllocatedWip]) < SafetyStockQty,
IIf(
ReOrderQty > 0,
ReOrderQty * CEILING((SafetyStockQty - (QtyOnHand - ([QtyInTransit] + [QtyAllocated] + [QtyAllocatedWip]))) / ReOrderQty),
SafetyStockQty - (QtyOnHand - ([QtyInTransit] + [QtyAllocated] + [QtyAllocatedWip]))
),
0
) AS QtyToOrder

Order Summary Creation

Complex business logic for creating order summaries with multiple grouping strategies:

// Services/InvOrderingService.cs (lines 147-237)
public IEnumerable<SummaryOrder> CreateOrderSummary(
IEnumerable<InvOrderingOrderSummaryListItem> items,
string buyer,
bool orderByWarehouse,
bool orderByDueDate)
{
var summary = new List<SummaryOrder>();
try
{
// Dynamic grouping based on business rules
var supplierGroups = orderByWarehouse ?
orderByDueDate ?
// Group by Supplier, Warehouse, and Due Date
items.GroupBy(x => new {
x.SelectedSupplier.Supplier,
x.SelectedSupplier.SupplierName,
x.DefaultWarehouse,
x.SelectedDueDate
})
.Select(x => new
{
x.Key.Supplier,
x.Key.SupplierName,
x.Key.DefaultWarehouse,
x.Key.SelectedDueDate,
OrderLines = x.ToList()
}) :
// Group by Supplier and Warehouse only
items.GroupBy(x => new {
x.SelectedSupplier.Supplier,
x.SelectedSupplier.SupplierName,
x.DefaultWarehouse,
SelectedDueDate = (DateTime?)null
})
.Select(x => new
{
x.Key.Supplier,
x.Key.SupplierName,
x.Key.DefaultWarehouse,
x.Key.SelectedDueDate,
OrderLines = x.ToList()
}) :
// Additional grouping combinations...

foreach (var supplierGroup in supplierGroups)
{
var newOrder = new SummaryOrder
{
Supplier = supplierGroup.Supplier,
SupplierName = supplierGroup.SupplierName,
Warehouse = supplierGroup.DefaultWarehouse,
DueDate = supplierGroup.SelectedDueDate,
Buyer = buyer,
OrderLines = new List<SummaryOrderLine>()
};

foreach (var line in supplierGroup.OrderLines)
{
newOrder.OrderLines.Add(new SummaryOrderLine
{
Warehouse = line.DefaultWarehouse,
StockCode = line.StockCode,
StockDescription = line.StockDescription,
OrderQty = line.OrderQty,
OrderUom = line.OrderUom,
Price = line.Price,
PriceUom = line.PriceUom,
DueDate = line.SelectedDueDate,
TaxCode = line.TaxCode
});
}

summary.Add(newOrder);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in CreateOrderSummary");
throw;
}
return summary;
}

Order Creation Workflow

The multi-step order creation process with error handling and status tracking:

// Services/InvOrderingService.cs (lines 241-293)
public async Task<CreateOrdersResult> CreateOrders(
IEnumerable<SummaryOrder> orderSummaries,
string selectedBuyer)
{
var postResult = new CreateOrdersResult(selectedBuyer);
try
{
// Step 1: Create orders in SYSPRO
postResult.Orders = _sysproPost.CreateOrders(orderSummaries).ToList();

foreach (var order in postResult.Orders)
{
// Step 2: Validate order creation
order.PostFailed = order.OrderNumber == "FAILED" ||
string.IsNullOrWhiteSpace(order.OrderNumber);

if (order.PostFailed)
{
_logger.LogInformation(
"Order creation failed for {Supplier} {Warehouse} {DueDate}",
order.Supplier, order.Warehouse, order.DueDate);
continue;
}

_logger.LogInformation("Purchase order {OrderNumber} created",
order.OrderNumber);

// Step 3: Retrieve created order details
order.PurchaseOrderHeader = await QueryPurchaseOrderHeaderAsync(
order.OrderNumber).ConfigureAwait(true);

// Step 4: Get order lines
List<PurchaseOrderLineLookup> poLines =
await GetPurchaseOrderLinesForPuchaseOrder(
order.OrderNumber).ConfigureAwait(true);

order.LineCount = poLines?.Count ?? 0;

if (poLines == null || order.OrderLines == null)
{
_logger.LogInformation(
"No lines found for purchase order {OrderNumber}",
order.OrderNumber);
continue;
}

// Step 5: Match and update line details
foreach (SummaryOrderLine orderLine in order.OrderLines)
{
var poLine = poLines.FirstOrDefault(
x => x.MStockCode == orderLine.StockCode);

if (poLine != null)
{
orderLine.Line = poLine.Line ?? 0;
orderLine.MStockingUom = poLine.MStockingUom;
orderLine.MOrderQty = poLine.MOrderQty ?? 0;
orderLine.MPrice = poLine.MPrice ?? 0;
orderLine.MCompleteFlag = poLine.MCompleteFlag;
}
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error creating orders {@Orders}", orderSummaries);
throw;
}
return postResult;
}

Business Rule Validation

Implementing complex business validation rules:

public class OrderValidationService
{
public ValidationResult ValidateOrderCreation(
IEnumerable<InvOrderingOrderSummaryListItem> items,
BuyerListItem buyer)
{
var errors = new List<string>();

// Validate buyer selection
if (buyer == null)
{
errors.Add("A buyer must be selected before creating orders");
}

// Validate items
if (!items?.Any() ?? true)
{
errors.Add("At least one item must be selected");
}

// Validate supplier consistency
var suppliers = items.Select(x => x.SelectedSupplier).Distinct();
foreach (var supplier in suppliers)
{
if (supplier.OnHold == "Y")
{
errors.Add($"Supplier {supplier.Supplier} is on hold");
}

if (!supplier.PurchaseOrdersAllowed)
{
errors.Add($"Purchase orders not allowed for supplier {supplier.Supplier}");
}
}

// Validate stock items
foreach (var item in items)
{
if (item.StockOnHold)
{
errors.Add($"Stock item {item.StockCode} is on hold");
}

if (item.OrderQty <= 0)
{
errors.Add($"Invalid order quantity for {item.StockCode}");
}

// Check minimum order quantities
if (item.OrderQty < item.MinOrderQty)
{
errors.Add($"Order quantity for {item.StockCode} is below minimum");
}
}

return new ValidationResult
{
IsValid = !errors.Any(),
Errors = errors
};
}
}

Allocation Query Services

Querying sales order and WIP allocations:

// Services/InvOrderingService.cs
public IEnumerable<AttachedSalesOrderJobMaterial> QueryForSalesOrdersJobMaterials(
string stockCode,
string warehouse,
string demandType)
{
try
{
var query = from demand in _sysproDataContext.CG_v_SalesOrderJobDemand
where demand.StockCode == stockCode &&
demand.Warehouse == warehouse &&
demand.DemandType == demandType
select new AttachedSalesOrderJobMaterial
{
JobSalesOrder = demand.JobSalesOrder,
SalesOrderLine = demand.SalesOrderLine,
StockCode = demand.StockCode,
Warehouse = demand.Warehouse,
QtyRequired = demand.QtyRequired ?? 0,
QtyIssued = demand.QtyIssued ?? 0,
QtyAllocated = demand.QtyAllocated ?? 0,
DueDate = demand.DueDate,
Customer = demand.Customer,
CustomerName = demand.CustomerName,
DemandType = demand.DemandType
};

return query.ToList();
}
catch (Exception ex)
{
_logger.LogError(ex,
"Error querying sales order materials for {StockCode} in {Warehouse}",
stockCode, warehouse);
throw;
}
}

Settings-Driven Business Logic

Business logic configured through settings:

// From WarehouseOrderingViewModel.cs (line 371)
var orderSummaries = _invOrderingService.CreateOrderSummary(
SelectedItems,
SelectedBuyer.Buyer,
_mepSettingsService.GetBool("OrderByWarehouse"), // Settings-driven grouping
_mepSettingsService.GetBool("OrderByDueDate")); // Settings-driven grouping

Examples

Example 1: MRP Calculation Implementation

public decimal CalculateReorderQuantity(InventoryItem item)
{
decimal availableQty = item.QtyOnHand -
(item.QtyAllocated + item.QtyAllocatedWip);

// Safety stock based calculation
if (availableQty < item.SafetyStockQty)
{
decimal shortfall = item.SafetyStockQty - availableQty;

if (item.ReOrderQty > 0)
{
// Round up to reorder quantity multiple
int multiples = (int)Math.Ceiling(shortfall / item.ReOrderQty);
return multiples * item.ReOrderQty;
}
else
{
// Exact quantity needed
return shortfall;
}
}

// Minimum quantity based calculation
decimal projectedQty = availableQty + item.QtyOnOrder;
if (projectedQty < item.MinimumQty)
{
return item.MinimumQty - projectedQty;
}

return 0; // No reorder needed
}

Example 2: Order Consolidation Logic

public SummaryOrder ConsolidateOrders(IEnumerable<OrderLine> lines)
{
var summary = new SummaryOrder
{
Supplier = lines.First().Supplier,
Warehouse = lines.First().Warehouse,
OrderDate = DateTime.Today,
DueDate = lines.Min(l => l.DueDate),
OrderLines = new List<SummaryOrderLine>()
};

// Group by stock code for consolidation
var groupedLines = lines.GroupBy(l => l.StockCode);

foreach (var group in groupedLines)
{
var consolidatedLine = new SummaryOrderLine
{
StockCode = group.Key,
OrderQty = group.Sum(l => l.OrderQty),
Price = group.First().Price,
DueDate = group.Min(l => l.DueDate),
TaxCode = group.First().TaxCode
};

summary.OrderLines.Add(consolidatedLine);
}

// Apply business rules
ApplyVolumeDiscounts(summary);
ValidateMinimumOrderValue(summary);

return summary;
}

Example 3: Workflow State Management

public class OrderWorkflowService
{
public async Task<WorkflowResult> ProcessOrderWorkflow(
OrderRequest request)
{
var workflow = new WorkflowResult();

try
{
// Step 1: Validate request
workflow.ValidationResult = ValidateOrderRequest(request);
if (!workflow.ValidationResult.IsValid)
return workflow;

// Step 2: Calculate requirements
workflow.Requirements = await CalculateRequirements(request);

// Step 3: Create order summary
workflow.OrderSummary = CreateOrderSummary(
workflow.Requirements,
request.Settings);

// Step 4: Submit to SYSPRO
workflow.PostResult = await PostToSyspro(workflow.OrderSummary);

// Step 5: Update local records
if (workflow.PostResult.IsSuccess)
{
await UpdateLocalRecords(workflow.PostResult);
}

workflow.IsComplete = true;
}
catch (Exception ex)
{
workflow.Error = ex.Message;
_logger.LogError(ex, "Workflow failed at step {Step}",
workflow.CurrentStep);
}

return workflow;
}
}

Best Practices

Business Rule Management

  • Centralize business rules in service layer
  • Make rules configurable through settings
  • Document complex calculations clearly
  • Version business rule changes

Error Handling and Recovery

  • Implement compensation logic for failed operations
  • Log business rule violations separately
  • Provide clear feedback on validation failures
  • Allow partial success where appropriate

Performance Optimization

  • Cache frequently used business data
  • Batch operations where possible
  • Use async/await for I/O operations
  • Profile and optimize calculation-heavy methods

Testing Strategies

  • Unit test all business calculations
  • Test edge cases and boundary conditions
  • Mock external dependencies
  • Verify business rule enforcement

Common Pitfalls

Business Logic in Wrong Layer

// BAD: Business logic in ViewModel
public class WarehouseOrderingViewModel
{
private decimal CalculateReorderQty() // Should be in service
{
// Complex business calculation
}
}

// GOOD: Business logic in service
public class InvOrderingService
{
public decimal CalculateReorderQuantity(InventoryItem item)
{
// Business calculation here
}
}

Inconsistent Business Rules

// BAD: Different rules in different places
if (item.QtyOnHand < 10) // Magic number in one place
if (item.QtyOnHand < settings.MinQty) // Different rule elsewhere

// GOOD: Centralized rule
public bool NeedsReorder(InventoryItem item)
{
return item.QtyOnHand < CalculateReorderPoint(item);
}

Missing Transaction Boundaries

// BAD: No transaction management
await CreateOrderHeader(order);
await CreateOrderLines(order); // Could fail, leaving partial order

// GOOD: Proper transaction
using var transaction = await BeginTransactionAsync();
try
{
await CreateOrderHeader(order);
await CreateOrderLines(order);
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}

Summary

The business logic services layer provides the core intelligence of the MiniMrpOrderCreation system, implementing sophisticated MRP calculations, order consolidation strategies, and workflow orchestration. Through proper separation of concerns, configurable business rules, and robust error handling, the services ensure accurate and reliable order creation while maintaining flexibility for changing business requirements.