@@ -10,13 +10,16 @@ public class EmployeeManagementCommandValidator : AbstractValidator<EmployeeMana
1010{
1111 private readonly IBranchQueryRepository _branchQueryRepository ;
1212 private readonly IDepartmentQueryRepository _departmentQueryRepository ;
13+ private readonly IEmployeeQueryRepository _employeeQueryRepository ;
1314
1415 public EmployeeManagementCommandValidator (
1516 IBranchQueryRepository branchQueryRepository ,
16- IDepartmentQueryRepository departmentQueryRepository )
17+ IDepartmentQueryRepository departmentQueryRepository ,
18+ IEmployeeQueryRepository employeeQueryRepository )
1719 {
1820 _branchQueryRepository = branchQueryRepository ;
1921 _departmentQueryRepository = departmentQueryRepository ;
22+ _employeeQueryRepository = employeeQueryRepository ?? throw new ArgumentNullException ( nameof ( employeeQueryRepository ) ) ;
2023
2124 RuleFor ( x => x . TenantID )
2225 . NotEmpty ( ) ;
@@ -31,6 +34,21 @@ public EmployeeManagementCommandValidator(
3134 RuleFor ( x => x . AssignedDepartmentId )
3235 . MustAsync ( ValidateDepartmentAsync )
3336 . WithMessage ( "Department must exist and belong to the same tenant" ) ;
37+
38+ RuleFor ( x => x . ReportsToId )
39+ . MustAsync ( ValidateReportsToIdBasicAsync )
40+ . WithMessage ( "ReportsToId must exist and belong to the same tenant" ) ;
41+
42+ RuleFor ( x => x . ReportsToId )
43+ . Must ( ( command , reportsToId ) =>
44+ reportsToId . Match (
45+ Some : id => id != command . ID ,
46+ None : ( ) => true ) )
47+ . WithMessage ( "An employee cannot report to themselves." ) ;
48+
49+ RuleFor ( x => x . ReportsToId )
50+ . MustAsync ( ValidateReportsToIdNoCyclesAsync )
51+ . WithMessage ( "ReportsToId must exist and belong to the same tenant" ) ;
3452 }
3553
3654 private async Task < bool > ValidateBranchAsync ( EmployeeManagementCommand command , Option < BranchId > assignedBranchId , CancellationToken cancellationToken )
@@ -58,4 +76,39 @@ private async Task<bool> ValidateDepartmentAsync(EmployeeManagementCommand comma
5876 } ,
5977 None : ( ) => Task . FromResult ( true ) ) . ConfigureAwait ( false ) ;
6078 }
79+
80+ private async Task < bool > ValidateReportsToIdBasicAsync ( EmployeeManagementCommand command , Option < EmployeeId > reportsToId , CancellationToken cancellationToken )
81+ {
82+ return await reportsToId . Match (
83+ Some : async id =>
84+ {
85+ var reportsToEmployee = await _employeeQueryRepository . GetOrNoneAsync ( id , cancellationToken ) . ConfigureAwait ( false ) ;
86+ return reportsToEmployee . Match (
87+ Some : x => x . TenantID == command . TenantID ,
88+ None : ( ) => false ) ;
89+ } ,
90+ None : ( ) => Task . FromResult ( true ) ) . ConfigureAwait ( false ) ;
91+ }
92+
93+ private async Task < bool > ValidateReportsToIdNoCyclesAsync ( EmployeeManagementCommand command , Option < EmployeeId > reportsToId , CancellationToken cancellationToken )
94+ {
95+ return await reportsToId . Match (
96+ Some : async id =>
97+ {
98+ var visited = new System . Collections . Generic . HashSet < EmployeeId > { command . ID } ;
99+ return await VisitAsync ( id , visited , cancellationToken ) . ConfigureAwait ( false ) ;
100+ } ,
101+ None : ( ) => Task . FromResult ( true ) ) . ConfigureAwait ( false ) ;
102+
103+ async Task < bool > VisitAsync ( EmployeeId reportsToId , ISet < EmployeeId > visited , CancellationToken cancellationToken )
104+ {
105+ if ( ! visited . Add ( reportsToId ) )
106+ return false ;
107+
108+ var upperManager = await _employeeQueryRepository . GetAsync ( reportsToId , cancellationToken ) . ConfigureAwait ( false ) ;
109+ return await upperManager . ReportsToId . MatchAsync (
110+ Some : upperManagerReportsToId => VisitAsync ( upperManagerReportsToId , visited , cancellationToken ) ,
111+ None : ( ) => true ) . ConfigureAwait ( false ) ;
112+ }
113+ }
61114}
0 commit comments