انتقل إلى المحتوى الرئيسي

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:

  1. Priority 1: Use explicitly provided SalespersonId (from frontend/API request)
  2. Priority 2: Use logged-in employee's EmployeeId (from IdentityService)
  3. 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/request
  • validateSalesperson: Whether to validate that the ID corresponds to an active salesperson
  • cancellationToken: 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

  1. DRY Principle: Single implementation reused across all command handlers
  2. Consistency: Same resolution logic everywhere
  3. Testability: Easy to unit test in isolation
  4. Maintainability: Changes to logic only need to happen in one place
  5. Flexibility: Supports multiple business scenarios without code duplication
  6. 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).