Salesperson Resolution Service
Overview
The SalespersonResolutionService is a centralized service that implements a hybrid approach for determining the salesperson responsible for a transaction. This service eliminates code duplication across command handlers and provides consistent salesperson resolution logic throughout the application.
Location
- Interface:
AccountsReceivable.API/Application/Services/Abstractions/ISalespersonResolutionService.cs - Implementation:
AccountsReceivable.API/Application/Services/SalespersonResolutionService.cs
Hybrid Resolution Strategy
The service follows a priority-based approach to determine the salesperson:
- Priority 1: Use explicitly provided
SalespersonId(from frontend/API request) - Priority 2: Use logged-in employee's
EmployeeId(fromIdentityService) - Priority 3: Return
null(no salesperson assigned)
Key Methods
ResolveSalespersonIdAsync
Main method that implements the hybrid resolution logic.
Task<Guid?> ResolveSalespersonIdAsync(
Guid? providedSalespersonId,
bool validateSalesperson = false,
CancellationToken cancellationToken = default)
Parameters:
providedSalespersonId: The explicitly provided salesperson ID from command/requestvalidateSalesperson: Whether to validate that the ID corresponds to an active salespersoncancellationToken: Cancellation token
Returns: The resolved salesperson ID, or null if no salesperson applies.
GetCurrentSalespersonIdAsync
Gets the current logged-in employee's ID if they are a salesperson.
Task<Guid?> GetCurrentSalespersonIdAsync(
bool validateSalesperson = false,
CancellationToken cancellationToken = default)
ValidateSalespersonAsync
Validates that a given ID corresponds to an active salesperson registered in a commission sales group.
Task<bool> ValidateSalespersonAsync(
Guid salespersonId,
CancellationToken cancellationToken = default)
Usage Examples
Example 1: CreateSalesInvoiceCommand
public class CreateSalesInvoiceCommand : IRequest<SalesInvoiceHeaderViewModel>
{
// ... existing properties ...
/// <summary>
/// Salesperson responsible for this sale.
/// If null, will use the logged-in employee's ID if they are a salesperson.
/// </summary>
public Guid? SalespersonId { get; set; }
}
Command Handler:
public class CreateSalesInvoiceCommandHandler : IRequestHandler<CreateSalesInvoiceCommand, SalesInvoiceHeaderViewModel>
{
private readonly ISalesInvoiceHeaderRepository _salesInvoiceHeaderRepository;
private readonly ISalespersonResolutionService _salespersonResolution;
private readonly ILogger<CreateSalesInvoiceCommandHandler> _logger;
public CreateSalesInvoiceCommandHandler(
ISalesInvoiceHeaderRepository salesInvoiceHeaderRepository,
ISalespersonResolutionService salespersonResolution,
ILogger<CreateSalesInvoiceCommandHandler> logger)
{
_salesInvoiceHeaderRepository = salesInvoiceHeaderRepository;
_salespersonResolution = salespersonResolution;
_logger = logger;
}
public async Task<SalesInvoiceHeaderViewModel> Handle(
CreateSalesInvoiceCommand command,
CancellationToken cancellationToken)
{
_logger.LogInformation("Creating sales invoice {InvoiceNumber}", command.InvoiceNumber);
// Resolve the salesperson ID using the hybrid approach
var salespersonId = await _salespersonResolution.ResolveSalespersonIdAsync(
command.SalespersonId,
validateSalesperson: false, // Set to true if you want strict validation
cancellationToken);
if (salespersonId.HasValue)
{
_logger.LogInformation("Sales invoice will be assigned to salesperson: {SalespersonId}", salespersonId);
}
else
{
_logger.LogInformation("Sales invoice will not have an assigned salesperson");
}
var invoice = new SalesInvoiceHeader(
command.InvoiceNumber,
command.LocationId,
command.CustomerAccountId,
command.InvoiceDate,
command.DueDate,
command.CurrencyCode,
command.CashDiscountPercent,
command.SalesOriginId,
command.PaymentScheduleId,
command.PaymentMethodId,
command.OffsetAccountTypeId,
command.OffsetAccountId,
command.TaxGroupId,
command.IncludeTax,
null);
// Set the resolved salesperson ID
if (salespersonId.HasValue)
{
invoice.SalespersonId = salespersonId.Value;
}
// ... rest of handler logic ...
_salesInvoiceHeaderRepository.Add(invoice);
await _salesInvoiceHeaderRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
return invoice.ToSalesInvoiceHeaderViewModel();
}
}
Example 2: Validation in Command Validator
Add validation to ensure the provided salesperson is valid:
public class CreateSalesInvoiceCommandValidator : AbstractValidator<CreateSalesInvoiceCommand>
{
public CreateSalesInvoiceCommandValidator(ISalespersonResolutionService salespersonResolution)
{
// Validate that if SalespersonId is provided, it exists and is active
When(x => x.SalespersonId.HasValue, () =>
{
RuleFor(x => x.SalespersonId!.Value)
.MustAsync(async (id, cancellation) =>
await salespersonResolution.ValidateSalespersonAsync(id, cancellation))
.WithMessage("Specified salesperson does not exist or is not an active sales representative.");
});
// ... other validation rules ...
}
}
Example 3: Checking Current User is Salesperson
Use this when you need to check if the current logged-in user is a salesperson:
public async Task<IActionResult> GetMySalesMetrics()
{
var currentSalespersonId = await _salespersonResolution.GetCurrentSalespersonIdAsync(
validateSalesperson: true);
if (!currentSalespersonId.HasValue)
{
return BadRequest("You must be registered as a salesperson to view sales metrics.");
}
// Fetch metrics for the salesperson...
}
Use Cases
1. Sales Clerk Creating Invoice for Salesperson
Frontend sends: { SalespersonId: "guid-of-john-salesperson" }
System resolves: John's ID (Priority 1)
2. Salesperson Creating Their Own Invoice
Frontend sends: { SalespersonId: null }
Logged-in user: Employee with ID "guid-of-mary-salesperson"
System resolves: Mary's ID (Priority 2)
3. Generic Invoice (No Salesperson)
Frontend sends: { SalespersonId: null }
Logged-in user: Not a salesperson (e.g., accountant)
System resolves: null (Priority 3 - no commission)
4. Bulk Import / API Integration
API request: { SalespersonId: "guid-from-external-system" }
validateSalesperson: true (to ensure data integrity)
System validates and uses the provided ID
Validation Options
The validateSalesperson parameter controls whether the service validates that the resolved ID corresponds to an active salesperson:
false(default): Trust the provided ID or employee ID (faster, less strict)true: Validate against CommissionSalesGroup repository (slower, more strict)
Use validateSalesperson: true when:
- Processing external/untrusted input
- Creating financial transactions that affect commissions
- Enforcing strict business rules
Use validateSalesperson: false when:
- High-performance scenarios (bulk operations)
- The ID has already been validated upstream
- Validation is handled separately in FluentValidation validators
Benefits of This Approach
- DRY Principle: Single implementation reused across all command handlers
- Consistency: Same resolution logic everywhere
- Testability: Easy to unit test in isolation
- Maintainability: Changes to logic only need to happen in one place
- Flexibility: Supports multiple business scenarios without code duplication
- Auditability: Centralized logging of salesperson resolution decisions
Registration
Register the service in your dependency injection container:
services.AddScoped<ISalespersonResolutionService, SalespersonResolutionService>();
This is typically done in the module's startup configuration (e.g., AccountsReceivableModule.cs).