Skip to content

Commit a25c481

Browse files
authored
Merge pull request #750 from fossa-app/employee-reports-to
Employee reports to
2 parents f8a43c5 + 3d5bae3 commit a25c481

29 files changed

Lines changed: 229 additions & 77 deletions

src/API.Core/Entities/EmployeeEntity.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ public record EmployeeEntity(
99
CompanyId CompanyId,
1010
Option<BranchId> AssignedBranchId,
1111
Option<DepartmentId> AssignedDepartmentId,
12+
Option<EmployeeId> ReportsToId,
1213
string FirstName,
1314
string LastName,
1415
string FullName)

src/API.Core/Messages/Commands/EmployeeCreationCommandHandler.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,9 @@ private async Task CreateEmployeeAsync(
6666
request.TenantID,
6767
request.UserID,
6868
companyEntity.ID,
69-
None,
70-
None,
69+
AssignedBranchId: None,
70+
AssignedDepartmentId: None,
71+
ReportsToId: None,
7172
request.FirstName,
7273
request.LastName,
7374
request.FullName);

src/API.Core/Messages/Commands/EmployeeManagementCommand.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ public record EmployeeManagementCommand(
77
Guid TenantID,
88
Guid UserID,
99
Option<BranchId> AssignedBranchId,
10-
Option<DepartmentId> AssignedDepartmentId)
10+
Option<DepartmentId> AssignedDepartmentId,
11+
Option<EmployeeId> ReportsToId)
1112
: EntityTenantUserCommand<EmployeeEntity, EmployeeId, Guid, Guid>(TenantID, UserID)
1213
{
1314
public override IEnumerable<EmployeeId> AffectingTenantEntitiesIdentities

src/API.Core/Messages/Commands/EmployeeManagementCommandHandler.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,16 @@ public async Task<Unit> Handle(
2020
CancellationToken cancellationToken)
2121
{
2222
var entity = await _employeeQueryRepository.GetAsync(request.ID, cancellationToken).ConfigureAwait(false);
23+
2324
entity = entity with
2425
{
2526
AssignedBranchId = request.AssignedBranchId,
26-
AssignedDepartmentId = request.AssignedDepartmentId
27+
AssignedDepartmentId = request.AssignedDepartmentId,
28+
ReportsToId = request.ReportsToId
2729
};
30+
2831
await _employeeRepository.UpdateAsync(entity, cancellationToken).ConfigureAwait(false);
32+
2933
return Unit.Value;
3034
}
3135
}

src/API.Core/Messages/Queries/EmployeePagingQuery.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ public record EmployeePagingQuery(
77
Guid TenantID,
88
Guid UserID,
99
string Search,
10+
Option<EmployeeId> ReportsToId,
11+
bool TopLevelOnly,
1012
Page Page)
1113
: EntityTenantQuery<EmployeeEntity, EmployeeId, Guid, PageResult<EmployeeEntity>>(TenantID)
1214
, IPagingQuery<EmployeeEntity>
Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,35 @@
11
using Fossa.API.Core.Entities;
22
using Fossa.API.Core.Repositories;
33
using TIKSN.Data;
4+
using TIKSN.Mapping;
45

56
namespace Fossa.API.Core.Messages.Queries;
67

78
public class EmployeePagingQueryHandler : IRequestHandler<EmployeePagingQuery, PageResult<EmployeeEntity>>
89
{
10+
private readonly IMapper<EmployeeId, long> _employeeModelToDataIdentityMapper;
911
private readonly IEmployeeQueryRepository _employeeQueryRepository;
1012

11-
public EmployeePagingQueryHandler(IEmployeeQueryRepository employeeQueryRepository)
13+
public EmployeePagingQueryHandler(
14+
IEmployeeQueryRepository employeeQueryRepository,
15+
IMapper<EmployeeId, long> employeeModelToDataIdentityMapper)
1216
{
1317
_employeeQueryRepository =
1418
employeeQueryRepository ?? throw new ArgumentNullException(nameof(employeeQueryRepository));
19+
_employeeModelToDataIdentityMapper = employeeModelToDataIdentityMapper ?? throw new ArgumentNullException(nameof(employeeModelToDataIdentityMapper));
1520
}
1621

1722
public Task<PageResult<EmployeeEntity>> Handle(
1823
EmployeePagingQuery request,
1924
CancellationToken cancellationToken)
2025
{
2126
return _employeeQueryRepository.PageAsync(
22-
new TenantEmployeePageQuery(request.TenantID, request.Search, request.Page),
27+
new TenantEmployeePageQuery(
28+
request.TenantID,
29+
request.Search,
30+
request.Page,
31+
request.ReportsToId.Map(_employeeModelToDataIdentityMapper.Map),
32+
request.TopLevelOnly),
2333
cancellationToken);
2434
}
2535
}

src/API.Core/Repositories/IEmployeeQueryRepository.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ public interface IEmployeeQueryRepository
88
, IDependencyQueryRepository<CompanyId>
99
, IDependencyQueryRepository<BranchId>
1010
, IDependencyQueryRepository<DepartmentId>
11+
, IDependencyQueryRepository<EmployeeId>
1112
{
13+
Task<int> CountAllAsync(
14+
CompanyId companyId,
15+
CancellationToken cancellationToken);
16+
1217
Task<Option<EmployeeEntity>> FindByUserIdAsync(
1318
Guid userId,
1419
CancellationToken cancellationToken);
@@ -20,8 +25,4 @@ Task<EmployeeEntity> GetByUserIdAsync(
2025
Task<PageResult<EmployeeEntity>> PageAsync(
2126
TenantEmployeePageQuery pageQuery,
2227
CancellationToken cancellationToken);
23-
24-
Task<int> CountAllAsync(
25-
CompanyId companyId,
26-
CancellationToken cancellationToken);
2728
}

src/API.Core/Repositories/TenantEmployeePageQuery.cs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,21 @@ namespace Fossa.API.Core.Repositories;
44

55
public class TenantEmployeePageQuery : PageQuery
66
{
7-
public Guid TenantId { get; }
8-
public string Search { get; }
9-
107
public TenantEmployeePageQuery(
118
Guid tenantId,
129
string search,
13-
Page page) : base(page, estimateTotalItems: true)
10+
Page page,
11+
Option<long> reportsToId,
12+
bool topLevelOnly) : base(page, estimateTotalItems: true)
1413
{
1514
TenantId = tenantId;
1615
Search = search;
16+
ReportsToId = reportsToId;
17+
TopLevelOnly = topLevelOnly;
1718
}
19+
20+
public Option<long> ReportsToId { get; }
21+
public string Search { get; }
22+
public Guid TenantId { get; }
23+
public bool TopLevelOnly { get; }
1824
}

src/API.Core/Validators/EmployeeManagementCommandValidator.cs

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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
}

src/API.Persistence/Mongo/Entities/EmployeeMongoEntity.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ public class EmployeeMongoEntity : IEntity<long>
2121

2222
public long? AssignedDepartmentId { get; set; }
2323

24+
public long? ReportsToId { get; set; }
25+
2426
public string? FirstName { get; set; }
2527

2628
public string? LastName { get; set; }

0 commit comments

Comments
 (0)