diff --git a/features/project/server/Garnet.Projects.AcceptanceTests/FakeServices/ProjectTaskClosedEventFakeConsumer.cs b/features/project/server/Garnet.Projects.AcceptanceTests/FakeServices/ProjectTaskClosedEventFakeConsumer.cs new file mode 100644 index 000000000..b862534ae --- /dev/null +++ b/features/project/server/Garnet.Projects.AcceptanceTests/FakeServices/ProjectTaskClosedEventFakeConsumer.cs @@ -0,0 +1,20 @@ +using Garnet.Common.Application.MessageBus; +using Garnet.Projects.Events.ProjectTask; + +namespace Garnet.Projects.AcceptanceTests.FakeServices; + +public class ProjectTaskClosedEventFakeConsumer : IMessageBusConsumer +{ + private ProjectTaskClosedEvent? _message; + + public Task Consume(ProjectTaskClosedEvent message) + { + _message = message; + return Task.CompletedTask; + } + + public ProjectTaskClosedEvent GetMessage() + { + return _message!; + } +} \ No newline at end of file diff --git a/features/project/server/Garnet.Projects.AcceptanceTests/Features/ProjectParticipantsGetRating/ProjectParticipantsGetRating.feature b/features/project/server/Garnet.Projects.AcceptanceTests/Features/ProjectParticipantsGetRating/ProjectParticipantsGetRating.feature new file mode 100644 index 000000000..19086d296 --- /dev/null +++ b/features/project/server/Garnet.Projects.AcceptanceTests/Features/ProjectParticipantsGetRating/ProjectParticipantsGetRating.feature @@ -0,0 +1,32 @@ +Функция: +Я, как пользователь-участник проекта +Хочу получать рейтинг за выполненную задачу +Чтобы другие пользователи могли оценить меня как специалиста по определенным навыкам + + Контекст: + Допустим существует пользователь 'Вася' + И существует пользователь 'Маша' + И существует пользователь 'Дима' + И существует проект 'FooBar' с владельцем 'Дима' + И существует команда 'DreamTeam' + И команда 'DreamTeam' является участником проекта 'FooBar' + И пользователь 'Вася' является участником команды 'DreamTeam' + И пользователь 'Маша' является участником команды 'DreamTeam' + И существует задача 'DoSomething' в проекте 'FooBar' с тегами 'C#, SQL' + И команда 'DreamTeam' является исполнителем задачи 'DoSomething' + И пользователь 'Вася' является ответственным по задаче 'DoSomething' + И пользователь 'Вася' является исполнителем задачи 'DoSomething' + И пользователь 'Маша' является исполнителем задачи 'DoSomething' + + Сценарий: Закрытие задачи с расчетом рейтинга + Когда пользователь 'Вася' закрывает задачу с названием 'DoSomething' + Тогда в системе существует задача с названием 'DoSomething' и статусом 'Close' + И у пользователя 'Вася' общий рейтинг равен '3,8' а рейтинг каждого из навыков 'C#, SQL' равен '1,9' + И у команды 'DreamTeam' общий рейтинг равен '1,9' + И у пользователя 'Дима' общий рейтинг равен '0,5' + + Сценарий: Повторное закрытие задачи без расчета рейтинга + Допустим задача 'DoSomething' повторно открыта + Когда пользователь 'Вася' закрывает задачу с названием 'DoSomething' + Тогда в системе существует задача с названием 'DoSomething' и статусом 'Close' + И в событии нет расчета рейтинга \ No newline at end of file diff --git a/features/project/server/Garnet.Projects.AcceptanceTests/Features/ProjectParticipantsGetRating/ProjectParticipantsGetRatingSteps.cs b/features/project/server/Garnet.Projects.AcceptanceTests/Features/ProjectParticipantsGetRating/ProjectParticipantsGetRatingSteps.cs new file mode 100644 index 000000000..d0779820e --- /dev/null +++ b/features/project/server/Garnet.Projects.AcceptanceTests/Features/ProjectParticipantsGetRating/ProjectParticipantsGetRatingSteps.cs @@ -0,0 +1,108 @@ +using FluentAssertions; +using Garnet.Common.AcceptanceTests.Fakes; +using Garnet.Projects.AcceptanceTests.FakeServices; +using Garnet.Projects.AcceptanceTests.Support; +using Garnet.Projects.Application.ProjectTask.Args; +using Garnet.Projects.Infrastructure.MongoDb.ProjectTask; +using MongoDB.Driver; +using TechTalk.SpecFlow; + +namespace Garnet.Projects.AcceptanceTests.Features.ProjectParticipantsGetRating; + +[Binding] +public class ProjectParticipantsGetRatingSteps : BaseSteps +{ + private readonly FilterDefinitionBuilder _f = + Builders.Filter; + + private readonly UpdateDefinitionBuilder _u = + Builders.Update; + + private readonly ProjectTaskClosedEventFakeConsumer _taskClosedFake; + private readonly CurrentUserProviderFake _currentUserProviderFake; + + public ProjectParticipantsGetRatingSteps(StepsArgs args, ProjectTaskClosedEventFakeConsumer taskClosedFake, + CurrentUserProviderFake currentUserProviderFake) : + base(args) + { + _taskClosedFake = taskClosedFake; + _currentUserProviderFake = currentUserProviderFake; + } + + [Given(@"задача '([^']*)' повторно открыта")] + public async Task GivenЗадачаПовторноОткрыта(string taskName) + { + await Db.ProjectTasks.UpdateOneAsync( + _f.Eq(x => x.Name, taskName), + _u.Set(x => x.Status, ProjectTaskStatuses.Open) + .Set(x => x.Reopened, true)); + } + + [Given(@"существует задача '([^']*)' в проекте '([^']*)' с тегами '([^']*)'")] + public async Task GivenСуществуетЗадачаВПроектеСТегами(string taskName, string projectName, string tags) + { + var tagList = tags.Split(", "); + var project = await Db.Projects.Find(x => x.ProjectName == projectName).FirstAsync(); + var task = GiveMe.ProjectTask().WithName(taskName).WithProjectId(project.Id).WithTags(tagList).Build(); + await Db.ProjectTasks.InsertOneAsync(task); + } + + [Given(@"пользователь '(.*)' является исполнителем задачи '(.*)'")] + public async Task GivenПользовательЯвляетсяИсполнителемЗадачи(string username, string taskName) + { + var user = await Db.ProjectUsers.Find(x => x.UserName == username).FirstAsync(); + await Db.ProjectTasks.UpdateOneAsync( + _f.Eq(x => x.Name, taskName), + _u.AddToSet(x => x.UserExecutorIds, user.Id)); + } + + [Then(@"в системе существует задача с названием '([^']*)' и статусом '([^']*)'")] + public async Task ThenВСистемеСуществуетЗадачаСоСтатусом(string taskName, string status) + { + var task = await Db.ProjectTasks.Find(x => x.Name == taskName).FirstOrDefaultAsync(); + task.Status.Should().Be(status); + } + + [Then( + @"у пользователя '([^']*)' общий рейтинг равен '([^']*)' а рейтинг каждого из навыков '([^']*)' равен '([^']*)'")] + public void ThenУПользователяОбщийРейтингИРейтингНавыков(string username, float totalScore, string tags, + float skillScore) + { + var tagList = tags.Split(", "); + + _taskClosedFake.GetMessage().RatingCalculation!.UserExecutorIds + .Contains(_currentUserProviderFake.GetUserIdByUsername(username)).Should().BeTrue(); + + _taskClosedFake.GetMessage().RatingCalculation!.UserTotalScore.Should().BeApproximately(totalScore, 0.01f); + + + var skills = _taskClosedFake.GetMessage().RatingCalculation!.SkillScorePerUser; + foreach (var tag in tagList) + { + skills[tag].Should().BeApproximately(skillScore, 0.01f); + } + } + + [Then(@"у команды '(.*)' общий рейтинг равен '(.*)'")] + public async void ThenУКомандыОбщийРейтинг(string teamName, float totalScore) + { + var team = await Db.ProjectTeams.Find(x => x.TeamName == teamName).FirstAsync(); + _taskClosedFake.GetMessage().RatingCalculation!.TeamsTotalScore[team.Id].Should() + .BeApproximately(totalScore, 0.01f); + } + + [Then(@"у пользователя '([^']*)' общий рейтинг равен '([^']*)'")] + public void ThenУПользователяОбщийРейтинг(string username, float totalScore) + { + _taskClosedFake.GetMessage().RatingCalculation!.ProjectOwnerId.Should() + .Be(_currentUserProviderFake.GetUserIdByUsername(username)); + _taskClosedFake.GetMessage().RatingCalculation!.ProjectOwnerTotalScore.Should() + .BeApproximately(totalScore, 0.01f); + } + + [Then(@"в событии нет расчета рейтинга")] + public void ThenВСобытииНетРасчетаРейтинга() + { + _taskClosedFake.GetMessage().RatingCalculation.Should().BeNull(); + } +} \ No newline at end of file diff --git a/features/project/server/Garnet.Projects.AcceptanceTests/Features/ProjectTaskCreate/ProjectTaskCreateSteps.cs b/features/project/server/Garnet.Projects.AcceptanceTests/Features/ProjectTaskCreate/ProjectTaskCreateSteps.cs index d03a0261e..cb1e061e1 100644 --- a/features/project/server/Garnet.Projects.AcceptanceTests/Features/ProjectTaskCreate/ProjectTaskCreateSteps.cs +++ b/features/project/server/Garnet.Projects.AcceptanceTests/Features/ProjectTaskCreate/ProjectTaskCreateSteps.cs @@ -64,7 +64,7 @@ public async Task WhenПользовательСоздаетВПроектеЗа } } - [Then(@"в системе существует задача с названием '(.*)'")] + [Then(@"в системе существует задача с названием '([^']*)'")] public async Task ThenВСистемеСуществуетЗадача(string taskName) { var task = await Db.ProjectTasks.Find(x => x.Name == taskName).FirstOrDefaultAsync(); diff --git a/features/project/server/Garnet.Projects.AcceptanceTests/Features/ProjectTaskEditName/ProjectTaskEditNameSteps.cs b/features/project/server/Garnet.Projects.AcceptanceTests/Features/ProjectTaskEditName/ProjectTaskEditNameSteps.cs index 64b0a84f4..005aa1073 100644 --- a/features/project/server/Garnet.Projects.AcceptanceTests/Features/ProjectTaskEditName/ProjectTaskEditNameSteps.cs +++ b/features/project/server/Garnet.Projects.AcceptanceTests/Features/ProjectTaskEditName/ProjectTaskEditNameSteps.cs @@ -21,7 +21,7 @@ public ProjectTaskEditNameSteps(StepsArgs args, CurrentUserProviderFake currentU _errorStepContext = errorStepContext; } - [Given(@"существует задача '(.*)' в проекте '(.*)'")] + [Given(@"существует задача '([^']*)' в проекте '([^']*)'")] public async Task GivenСуществуетЗадачаВПроекте(string taskName, string projectName) { var project = await Db.Projects.Find(x => x.ProjectName == projectName).FirstAsync(); diff --git a/features/project/server/Garnet.Projects.AcceptanceTests/Startup.cs b/features/project/server/Garnet.Projects.AcceptanceTests/Startup.cs index 1534897a6..0b18f0109 100644 --- a/features/project/server/Garnet.Projects.AcceptanceTests/Startup.cs +++ b/features/project/server/Garnet.Projects.AcceptanceTests/Startup.cs @@ -13,6 +13,8 @@ using SolidToken.SpecFlow.DependencyInjection; using Garnet.Project.AcceptanceTests.FakeServices.NotificationFake; using Garnet.Notifications.Events; +using Garnet.Projects.AcceptanceTests.FakeServices; +using Garnet.Projects.Events.ProjectTask; namespace Garnet.Projects.AcceptanceTests; @@ -72,6 +74,7 @@ private static void AddMessageBus(IServiceCollection services) services.AddGarnetMessageBus(Uuid.NewGuid(), o => { o.RegisterConsumer(); + o.RegisterConsumer(); }); } } \ No newline at end of file diff --git a/features/project/server/Garnet.Projects.AcceptanceTests/Support/ProjectTaskDocumentBuilder.cs b/features/project/server/Garnet.Projects.AcceptanceTests/Support/ProjectTaskDocumentBuilder.cs index 648dec1e2..1acab25ee 100644 --- a/features/project/server/Garnet.Projects.AcceptanceTests/Support/ProjectTaskDocumentBuilder.cs +++ b/features/project/server/Garnet.Projects.AcceptanceTests/Support/ProjectTaskDocumentBuilder.cs @@ -19,6 +19,7 @@ public class ProjectTaskDocumentBuilder private string[] _userExecutorIds = Array.Empty(); private string[] _tags = Array.Empty(); private string[] _labels = Array.Empty(); + private bool _reopened = false; private AuditInfoDocument _auditInfo = new(DateTime.UtcNow, "CreatedByUser", DateTime.UtcNow, "UpdatedByUser", 0); @@ -97,8 +98,7 @@ public ProjectTaskDocumentBuilder WithCreatedBy(string username) public ProjectTaskDocument Build() { return ProjectTaskDocument.Create(_id, _taskNumber, _projectId, _responsibleUserId, _name, _description, - _status, - _teamExecutorIds, _userExecutorIds, _tags, _labels) + _status, _teamExecutorIds, _userExecutorIds, _tags, _labels, _reopened) with { AuditInfo = _auditInfo diff --git a/features/project/server/Garnet.Projects.Application/ProjectTask/Commands/ProjectTaskCloseCommand.cs b/features/project/server/Garnet.Projects.Application/ProjectTask/Commands/ProjectTaskCloseCommand.cs index 3149691f0..266282a03 100644 --- a/features/project/server/Garnet.Projects.Application/ProjectTask/Commands/ProjectTaskCloseCommand.cs +++ b/features/project/server/Garnet.Projects.Application/ProjectTask/Commands/ProjectTaskCloseCommand.cs @@ -5,6 +5,8 @@ using Garnet.Projects.Application.Project.Errors; using Garnet.Projects.Application.ProjectTask.Args; using Garnet.Projects.Application.ProjectTask.Errors; +using Garnet.Projects.Application.ProjectTeamParticipant; +using Garnet.Projects.Events.ProjectTask; namespace Garnet.Projects.Application.ProjectTask.Commands; @@ -14,17 +16,20 @@ public class ProjectTaskCloseCommand private readonly IProjectTaskRepository _projectTaskRepository; private readonly IProjectRepository _projectRepository; private readonly IMessageBus _messageBus; + private readonly IProjectTeamParticipantRepository _projectTeamParticipantRepository; public ProjectTaskCloseCommand( ICurrentUserProvider currentUserProvider, IProjectTaskRepository projectTaskRepository, IMessageBus messageBus, - IProjectRepository projectRepository) + IProjectRepository projectRepository, + IProjectTeamParticipantRepository projectTeamParticipantRepository) { _currentUserProvider = currentUserProvider; _projectTaskRepository = projectTaskRepository; _messageBus = messageBus; _projectRepository = projectRepository; + _projectTeamParticipantRepository = projectTeamParticipantRepository; } public async Task> Execute(CancellationToken ct, string taskId) @@ -53,9 +58,42 @@ public async Task> Execute(CancellationToken ct, strin return Result.Fail(new ProjectTaskAlreadySetThisStatusError(task.Status)); } + RatingCalculation? ratingCalculation = null; + + if (task.Reopened is false) + { + var totalScore = (float)Math.Round(9.5 / task.UserExecutorIds.Length * 0.8, 2, + MidpointRounding.AwayFromZero); + var skillScore = (float)Math.Round(totalScore / task.Tags.Length, 2, MidpointRounding.AwayFromZero); + var teamScorePerUser = (float)Math.Round(9.5 / task.UserExecutorIds.Length * 0.2, 2, + MidpointRounding.AwayFromZero); + + var tags = task.Tags; + var skillScorePerUser = tags.ToDictionary(x => x, x => skillScore); + + var teams = await _projectTeamParticipantRepository.GetProjectTeamParticipantsByProjectId(ct, project.Id); + var teamExecutors = teams.Where(x => task.TeamExecutorIds.Contains(x.TeamId)).ToList(); + + Dictionary teamsScore = new(); + + foreach (var team in teamExecutors) + { + var users = team.UserParticipants.Where(x => task.UserExecutorIds.Contains(x.Id)).ToList(); + teamsScore[team.TeamId] = users.Count * teamScorePerUser; + } + + ratingCalculation = new RatingCalculation( + project.OwnerUserId, + (float)0.5, + task.UserExecutorIds, + totalScore, + skillScorePerUser, + teamsScore); + } + task = await _projectTaskRepository.CloseProjectTask(ct, taskId, ProjectTaskStatuses.Close); - await _messageBus.Publish(task.ToClosedEvent()); + await _messageBus.Publish(task.ToClosedEvent() with { RatingCalculation = ratingCalculation }); return Result.Ok(task); } } \ No newline at end of file diff --git a/features/project/server/Garnet.Projects.Application/ProjectTask/ProjectTaskEntity.cs b/features/project/server/Garnet.Projects.Application/ProjectTask/ProjectTaskEntity.cs index 2ec196c4a..a05cee5d5 100644 --- a/features/project/server/Garnet.Projects.Application/ProjectTask/ProjectTaskEntity.cs +++ b/features/project/server/Garnet.Projects.Application/ProjectTask/ProjectTaskEntity.cs @@ -15,6 +15,7 @@ public record ProjectTaskEntity( string[] UserExecutorIds, string[] Tags, string[] Labels, + bool Reopened, AuditInfo AuditInfo ); @@ -24,27 +25,27 @@ public static ProjectTaskCreatedEvent ToCreatedEvent(this ProjectTaskEntity doc) { return new ProjectTaskCreatedEvent(doc.Id, doc.TaskNumber, doc.ProjectId, doc.ResponsibleUserId, doc.Name, doc.Description, - doc.Status, doc.TeamExecutorIds, doc.UserExecutorIds, doc.Tags, doc.Labels); + doc.Status, doc.TeamExecutorIds, doc.UserExecutorIds, doc.Tags, doc.Labels, doc.Reopened); } public static ProjectTaskUpdatedEvent ToUpdatedEvent(this ProjectTaskEntity doc) { return new ProjectTaskUpdatedEvent(doc.Id, doc.TaskNumber, doc.ProjectId, doc.ResponsibleUserId, doc.Name, doc.Description, - doc.Status, doc.TeamExecutorIds, doc.UserExecutorIds, doc.Tags, doc.Labels); + doc.Status, doc.TeamExecutorIds, doc.UserExecutorIds, doc.Tags, doc.Labels, doc.Reopened); } public static ProjectTaskDeletedEvent ToDeletedEvent(this ProjectTaskEntity doc) { return new ProjectTaskDeletedEvent(doc.Id, doc.TaskNumber, doc.ProjectId, doc.ResponsibleUserId, doc.Name, doc.Description, - doc.Status, doc.TeamExecutorIds, doc.UserExecutorIds, doc.Tags, doc.Labels); + doc.Status, doc.TeamExecutorIds, doc.UserExecutorIds, doc.Tags, doc.Labels, doc.Reopened); } public static ProjectTaskClosedEvent ToClosedEvent(this ProjectTaskEntity doc) { return new ProjectTaskClosedEvent(doc.Id, doc.TaskNumber, doc.ProjectId, doc.ResponsibleUserId, doc.Name, doc.Description, - doc.Status, doc.TeamExecutorIds, doc.UserExecutorIds, doc.Tags, doc.Labels); + doc.Status, doc.TeamExecutorIds, doc.UserExecutorIds, doc.Tags, doc.Labels, doc.Reopened, null); } } \ No newline at end of file diff --git a/features/project/server/Garnet.Projects.Events/ProjectTask/ProjectTaskClosedEvent.cs b/features/project/server/Garnet.Projects.Events/ProjectTask/ProjectTaskClosedEvent.cs index 83de57807..969c42120 100644 --- a/features/project/server/Garnet.Projects.Events/ProjectTask/ProjectTaskClosedEvent.cs +++ b/features/project/server/Garnet.Projects.Events/ProjectTask/ProjectTaskClosedEvent.cs @@ -11,5 +11,16 @@ public record ProjectTaskClosedEvent( string[] TeamExecutorIds, string[] UserExecutorIds, string[] Tags, - string[] Labels - ); \ No newline at end of file + string[] Labels, + bool Reopened, + RatingCalculation? RatingCalculation +); + +public record RatingCalculation( + string ProjectOwnerId, + float ProjectOwnerTotalScore, + string[] UserExecutorIds, + float UserTotalScore, + Dictionary SkillScorePerUser, + Dictionary TeamsTotalScore +); \ No newline at end of file diff --git a/features/project/server/Garnet.Projects.Events/ProjectTask/ProjectTaskCreatedEvent.cs b/features/project/server/Garnet.Projects.Events/ProjectTask/ProjectTaskCreatedEvent.cs index 0ac6a1fcf..120c38a79 100644 --- a/features/project/server/Garnet.Projects.Events/ProjectTask/ProjectTaskCreatedEvent.cs +++ b/features/project/server/Garnet.Projects.Events/ProjectTask/ProjectTaskCreatedEvent.cs @@ -11,5 +11,6 @@ public record ProjectTaskCreatedEvent( string[] TeamExecutorIds, string[] UserExecutorIds, string[] Tags, - string[] Labels + string[] Labels, + bool Reopened ); \ No newline at end of file diff --git a/features/project/server/Garnet.Projects.Events/ProjectTask/ProjectTaskDeletedEvent.cs b/features/project/server/Garnet.Projects.Events/ProjectTask/ProjectTaskDeletedEvent.cs index bc4069450..70ae76225 100644 --- a/features/project/server/Garnet.Projects.Events/ProjectTask/ProjectTaskDeletedEvent.cs +++ b/features/project/server/Garnet.Projects.Events/ProjectTask/ProjectTaskDeletedEvent.cs @@ -11,5 +11,6 @@ public record ProjectTaskDeletedEvent( string[] TeamExecutorIds, string[] UserExecutorIds, string[] Tags, - string[] Labels + string[] Labels, + bool Reopened ); \ No newline at end of file diff --git a/features/project/server/Garnet.Projects.Events/ProjectTask/ProjectTaskUpdatedEvent.cs b/features/project/server/Garnet.Projects.Events/ProjectTask/ProjectTaskUpdatedEvent.cs index 2cad42bc5..2d933e888 100644 --- a/features/project/server/Garnet.Projects.Events/ProjectTask/ProjectTaskUpdatedEvent.cs +++ b/features/project/server/Garnet.Projects.Events/ProjectTask/ProjectTaskUpdatedEvent.cs @@ -11,5 +11,6 @@ public record ProjectTaskUpdatedEvent( string[] TeamExecutorIds, string[] UserExecutorIds, string[] Tags, - string[] Labels + string[] Labels, + bool Reopened ); \ No newline at end of file diff --git a/features/project/server/Garnet.Projects.Infrastructure/MongoDb/ProjectTask/ProjectTaskDocument.cs b/features/project/server/Garnet.Projects.Infrastructure/MongoDb/ProjectTask/ProjectTaskDocument.cs index 0bb77ccb7..3c61f9abd 100644 --- a/features/project/server/Garnet.Projects.Infrastructure/MongoDb/ProjectTask/ProjectTaskDocument.cs +++ b/features/project/server/Garnet.Projects.Infrastructure/MongoDb/ProjectTask/ProjectTaskDocument.cs @@ -16,6 +16,7 @@ public record ProjectTaskDocument : DocumentBase public string[] UserExecutorIds { get; init; } = null!; public string[] Tags { get; init; } = null!; public string[] Labels { get; init; } = null!; + public bool Reopened { get; init; } = false; public static ProjectTaskDocument Create( string id, @@ -28,7 +29,8 @@ public static ProjectTaskDocument Create( string[] teamExecutorIds, string[] userExecutorIds, string[] tags, - string[] labels + string[] labels, + bool reopened ) { return new ProjectTaskDocument @@ -43,7 +45,8 @@ string[] labels TeamExecutorIds = teamExecutorIds, UserExecutorIds = userExecutorIds, Tags = tags, - Labels = labels + Labels = labels, + Reopened = reopened }; } @@ -62,6 +65,7 @@ public static ProjectTaskEntity ToDomain(ProjectTaskDocument doc) doc.UserExecutorIds, doc.Tags, doc.Labels, + doc.Reopened, auditInfo ); } diff --git a/features/project/server/Garnet.Projects.Infrastructure/MongoDb/ProjectTask/ProjectTaskRepository.cs b/features/project/server/Garnet.Projects.Infrastructure/MongoDb/ProjectTask/ProjectTaskRepository.cs index d66f6cc6d..9909e85d7 100644 --- a/features/project/server/Garnet.Projects.Infrastructure/MongoDb/ProjectTask/ProjectTaskRepository.cs +++ b/features/project/server/Garnet.Projects.Infrastructure/MongoDb/ProjectTask/ProjectTaskRepository.cs @@ -39,7 +39,8 @@ public async Task CreateProjectTask(CancellationToken ct, str args.TeamExecutorIds, args.UserExecutorIds, args.Tags, - args.Labels); + args.Labels, + false); task = await InsertOneDocument( ct, @@ -184,7 +185,9 @@ public async Task OpenProjectTask(CancellationToken ct, strin { var db = _dbFactory.Create(); var filter = _f.Eq(x => x.Id, taskId); - var update = _u.Set(x => x.Status, status); + var update = _u + .Set(x => x.Status, status) + .Set(x => x.Reopened, true); var task = await FindOneAndUpdateDocument( ct, diff --git a/features/team/server/Garnet.Teams.AcceptanceTests/Support/TeamDocumentBuilder.cs b/features/team/server/Garnet.Teams.AcceptanceTests/Support/TeamDocumentBuilder.cs index 29eb65f44..438771b5d 100644 --- a/features/team/server/Garnet.Teams.AcceptanceTests/Support/TeamDocumentBuilder.cs +++ b/features/team/server/Garnet.Teams.AcceptanceTests/Support/TeamDocumentBuilder.cs @@ -13,6 +13,7 @@ public class TeamDocumentBuilder private readonly string _ownerUserId = Uuid.NewMongo(); private readonly string _avatarUrl = string.Empty; private readonly string[] _tags = Array.Empty(); + private readonly float _totalScore = 0; public TeamDocumentBuilder WithName(string name) { @@ -22,7 +23,7 @@ public TeamDocumentBuilder WithName(string name) public TeamDocument Build() { - var document = TeamDocument.Create(_id, _name, _description, _ownerUserId, _avatarUrl, _tags); + var document = TeamDocument.Create(_id, _name, _description, _ownerUserId, _avatarUrl, _tags, _totalScore); return document with { AuditInfo = _auditInfo }; } diff --git a/features/team/server/Garnet.Teams.Application/Team/ITeamRepository.cs b/features/team/server/Garnet.Teams.Application/Team/ITeamRepository.cs index 259427225..7479295a8 100644 --- a/features/team/server/Garnet.Teams.Application/Team/ITeamRepository.cs +++ b/features/team/server/Garnet.Teams.Application/Team/ITeamRepository.cs @@ -14,6 +14,7 @@ public interface ITeamRepository Task EditTeamAvatar(CancellationToken ct, string teamId, string avatarUrl); Task EditTeamOwner(CancellationToken ct, string teamId, string newOwnerUserId); Task GetTeamsById(CancellationToken ct, string[] teamIds, TeamsListArgs args); + Task EditTeamTotalScore(CancellationToken ct, string teamId, float teamTotalScore); Task CreateIndexes(CancellationToken ct); } } \ No newline at end of file diff --git a/features/team/server/Garnet.Teams.Application/Team/TeamEntity.cs b/features/team/server/Garnet.Teams.Application/Team/TeamEntity.cs index 2ab8dd4ac..cda09367d 100644 --- a/features/team/server/Garnet.Teams.Application/Team/TeamEntity.cs +++ b/features/team/server/Garnet.Teams.Application/Team/TeamEntity.cs @@ -10,6 +10,7 @@ public record TeamEntity( string OwnerUserId, string AvatarUrl, string[] Tags, + float TotalScore, AuditInfo AuditInfo ); @@ -17,17 +18,20 @@ public static class TeamEntityExtensions { public static TeamCreatedEvent ToCreatedEvent(this TeamEntity entity) { - return new TeamCreatedEvent(entity.Id, entity.Name, entity.OwnerUserId, entity.Description, entity.AvatarUrl, entity.Tags); + return new TeamCreatedEvent(entity.Id, entity.Name, entity.OwnerUserId, entity.Description, + entity.AvatarUrl, entity.Tags); } public static TeamDeletedEvent ToDeletedEvent(this TeamEntity entity) { - return new TeamDeletedEvent(entity.Id, entity.Name, entity.OwnerUserId, entity.Description, entity.AvatarUrl, entity.Tags); + return new TeamDeletedEvent(entity.Id, entity.Name, entity.OwnerUserId, entity.Description, + entity.AvatarUrl, entity.Tags, entity.TotalScore); } public static TeamUpdatedEvent ToUpdatedEvent(this TeamEntity entity) { - return new TeamUpdatedEvent(entity.Id, entity.Name, entity.OwnerUserId, entity.Description, entity.AvatarUrl, entity.Tags); + return new TeamUpdatedEvent(entity.Id, entity.Name, entity.OwnerUserId, entity.Description, + entity.AvatarUrl, entity.Tags, entity.TotalScore); } } } \ No newline at end of file diff --git a/features/team/server/Garnet.Teams.Events/Team/TeamDeletedEvent.cs b/features/team/server/Garnet.Teams.Events/Team/TeamDeletedEvent.cs index 9a0b8d799..2cb620fe3 100644 --- a/features/team/server/Garnet.Teams.Events/Team/TeamDeletedEvent.cs +++ b/features/team/server/Garnet.Teams.Events/Team/TeamDeletedEvent.cs @@ -6,6 +6,7 @@ public record TeamDeletedEvent( string OwnerUserId, string Description, string? AvatarUrl, - string[] Tags + string[] Tags, + float TotalScore ); } \ No newline at end of file diff --git a/features/team/server/Garnet.Teams.Events/Team/TeamUpdatedEvent.cs b/features/team/server/Garnet.Teams.Events/Team/TeamUpdatedEvent.cs index fa7811c07..29cbce401 100644 --- a/features/team/server/Garnet.Teams.Events/Team/TeamUpdatedEvent.cs +++ b/features/team/server/Garnet.Teams.Events/Team/TeamUpdatedEvent.cs @@ -6,6 +6,7 @@ public record TeamUpdatedEvent( string OwnerUserId, string Description, string? AvatarUrl, - string[] Tags + string[] Tags, + float TotalScore ); } \ No newline at end of file diff --git a/features/team/server/Garnet.Teams.Infrastructure/EventHandlers/Project/ProjectTaskClosedEventConsumer.cs b/features/team/server/Garnet.Teams.Infrastructure/EventHandlers/Project/ProjectTaskClosedEventConsumer.cs new file mode 100644 index 000000000..8c81f85a4 --- /dev/null +++ b/features/team/server/Garnet.Teams.Infrastructure/EventHandlers/Project/ProjectTaskClosedEventConsumer.cs @@ -0,0 +1,26 @@ +using Garnet.Common.Application.MessageBus; +using Garnet.Projects.Events.ProjectTask; +using Garnet.Teams.Application.Team; + +namespace Garnet.Teams.Infrastructure.EventHandlers.Project; + +public class ProjectTaskClosedEventConsumer : IMessageBusConsumer +{ + private readonly ITeamRepository _teamRepository; + + public ProjectTaskClosedEventConsumer(ITeamRepository teamRepository) + { + _teamRepository = teamRepository; + } + + public async Task Consume(ProjectTaskClosedEvent message) + { + if (message.RatingCalculation is not null) + { + foreach (var team in message.RatingCalculation.TeamsTotalScore) + { + await _teamRepository.EditTeamTotalScore(CancellationToken.None, team.Key, team.Value); + } + } + } +} \ No newline at end of file diff --git a/features/team/server/Garnet.Teams.Infrastructure/MongoDb/Team/TeamDocument.cs b/features/team/server/Garnet.Teams.Infrastructure/MongoDb/Team/TeamDocument.cs index 770acf9d8..fd409124c 100644 --- a/features/team/server/Garnet.Teams.Infrastructure/MongoDb/Team/TeamDocument.cs +++ b/features/team/server/Garnet.Teams.Infrastructure/MongoDb/Team/TeamDocument.cs @@ -9,10 +9,12 @@ public record TeamDocument : DocumentBase public string Name { get; init; } = null!; public string Description { get; init; } = null!; public string OwnerUserId { get; init; } = null!; - public string[] Tags {get;init;} = null!; + public string[] Tags { get; init; } = null!; public string AvatarUrl { get; init; } = string.Empty; + public float TotalScore { get; init; } = 0; - public static TeamDocument Create(string id, string name, string description, string ownerUserId, string avatarUrl, string[] tags) + public static TeamDocument Create(string id, string name, string description, string ownerUserId, + string avatarUrl, string[] tags, float totalScore) { return new TeamDocument { @@ -21,14 +23,16 @@ public static TeamDocument Create(string id, string name, string description, st Description = description, OwnerUserId = ownerUserId, AvatarUrl = avatarUrl, - Tags = tags + Tags = tags, + TotalScore = totalScore }; } public static TeamEntity ToDomain(TeamDocument doc) { var audit = AuditInfoDocument.ToDomain(doc.AuditInfo); - return new TeamEntity(doc.Id, doc.Name, doc.Description, doc.OwnerUserId, doc.AvatarUrl, doc.Tags, audit); + return new TeamEntity(doc.Id, doc.Name, doc.Description, doc.OwnerUserId, doc.AvatarUrl, doc.Tags, + doc.TotalScore, audit); } } } \ No newline at end of file diff --git a/features/team/server/Garnet.Teams.Infrastructure/MongoDb/Team/TeamRepository.cs b/features/team/server/Garnet.Teams.Infrastructure/MongoDb/Team/TeamRepository.cs index c187dc8f4..fee313ff8 100644 --- a/features/team/server/Garnet.Teams.Infrastructure/MongoDb/Team/TeamRepository.cs +++ b/features/team/server/Garnet.Teams.Infrastructure/MongoDb/Team/TeamRepository.cs @@ -31,7 +31,8 @@ public async Task CreateTeam(CancellationToken ct, string ownerUserI args.Description, ownerUserId!, string.Empty, - args.Tags + args.Tags, + 0 ); team = await InsertOneDocument(ct, db.Teams, team); @@ -62,7 +63,9 @@ public async Task FilterTeams(CancellationToken ct, TeamFilterArgs var searchFilter = args.Search is null ? _f.Empty - : _f.Where(x => x.Description.ToLower().Contains(args.Search.ToLower()) || x.Name.ToLower().Contains(args.Search.ToLower())); + : _f.Where(x => + x.Description.ToLower().Contains(args.Search.ToLower()) || + x.Name.ToLower().Contains(args.Search.ToLower())); var tagsFilter = args.Tags.Length > 0 ? _f.All(o => o.Tags, args.Tags) @@ -172,5 +175,17 @@ public async Task GetTeamsById(CancellationToken ct, string[] team return teams.Select(x => TeamDocument.ToDomain(x)).ToArray(); } + + public async Task EditTeamTotalScore(CancellationToken ct, string teamId, float teamTotalScore) + { + var db = _dbFactory.Create(); + + await FindOneAndUpdateDocument( + ct, + db.Teams, + _f.Eq(x => x.Id, teamId), + _u.Inc(x => x.TotalScore, teamTotalScore) + ); + } } } \ No newline at end of file diff --git a/features/team/server/Garnet.Teams/Startup.cs b/features/team/server/Garnet.Teams/Startup.cs index 4c9d776d7..3c721444e 100644 --- a/features/team/server/Garnet.Teams/Startup.cs +++ b/features/team/server/Garnet.Teams/Startup.cs @@ -43,6 +43,7 @@ using Garnet.Teams.Events.TeamParticipant; using Garnet.Teams.Application.TeamParticipant.Commands; using Garnet.Notifications.Events; +using Garnet.Projects.Events.ProjectTask; using Garnet.Teams.Application.TeamUser.Commands; using Garnet.Teams.Infrastructure.MongoDb.ProjectTeamParticipant; using Garnet.Teams.Application.ProjectTeamParticipant.Commands; @@ -94,6 +95,7 @@ public static void AddGarnetTeamsMessageBus(this IServiceCollection services, st o.RegisterConsumer(); o.RegisterConsumer(); o.RegisterConsumer(); + o.RegisterConsumer(); o.RegisterMessage(); o.RegisterMessage(); o.RegisterMessage(); diff --git a/features/user/server/Garnet.Users.AcceptanceTests/Support/UserDocumentBuilder.cs b/features/user/server/Garnet.Users.AcceptanceTests/Support/UserDocumentBuilder.cs index 10ad8bc81..36d8eb952 100644 --- a/features/user/server/Garnet.Users.AcceptanceTests/Support/UserDocumentBuilder.cs +++ b/features/user/server/Garnet.Users.AcceptanceTests/Support/UserDocumentBuilder.cs @@ -13,6 +13,8 @@ public class UserDocumentBuilder private string _description = "Description"; private string _avatarUrl = ""; private List _tags = new(); + private float _totalScore = 0; + private Dictionary _skillScore = new(); public UserDocumentBuilder WithId(string id) { @@ -43,11 +45,24 @@ public UserDocumentBuilder WithAvatarUrl(string avatarUrl) _avatarUrl = avatarUrl; return this; } - + + public UserDocumentBuilder WithTotalScore(float totalScore) + { + _totalScore = totalScore; + return this; + } + + public UserDocumentBuilder WithSkillScore(Dictionary skillScore) + { + _skillScore = skillScore; + return this; + } + public UserDocument Build() { var document = UserDocument.Create( - new UserDocumentCreateArgs(_id, _userName, _description, _avatarUrl, _tags.ToArray()) + new UserDocumentCreateArgs(_id, _userName, _description, _avatarUrl, _tags.ToArray(), _totalScore, + _skillScore) ); return document with { AuditInfo = _auditInfo }; } diff --git a/features/user/server/Garnet.Users.Application/IUsersRepository.cs b/features/user/server/Garnet.Users.Application/IUsersRepository.cs index 30a0642a0..77bdd75f8 100644 --- a/features/user/server/Garnet.Users.Application/IUsersRepository.cs +++ b/features/user/server/Garnet.Users.Application/IUsersRepository.cs @@ -11,5 +11,7 @@ public interface IUsersRepository Task EditUserAvatar(string userId, string avatarUrl); Task CreateUser(string identityId, string username); Task FilterUsers(UserFilterArgs args); + Task EditUserTotalScore(string userId, float totalScore); + Task EditUserSkillScore(string userId, Dictionary skillScorePerUser); Task CreateIndexes(); } \ No newline at end of file diff --git a/features/user/server/Garnet.Users.Application/User.cs b/features/user/server/Garnet.Users.Application/User.cs index 3bb68ab79..404924040 100644 --- a/features/user/server/Garnet.Users.Application/User.cs +++ b/features/user/server/Garnet.Users.Application/User.cs @@ -7,14 +7,16 @@ public record User( string UserName, string Description, string AvatarUrl, - string[] Tags + string[] Tags, + float TotalScore, + Dictionary SkillScore ); public static class UserDocumentExtensions { public static UserUpdatedEvent ToUpdatedEvent(this User doc) { - return new UserUpdatedEvent(doc.Id, doc.UserName, doc.Description, doc.AvatarUrl, doc.Tags); + return new UserUpdatedEvent(doc.Id, doc.UserName, doc.Description, doc.AvatarUrl, doc.Tags, doc.TotalScore, doc.SkillScore); } public static UserCreatedEvent ToCreatedEvent(this User doc) diff --git a/features/user/server/Garnet.Users.Events/UserUpdatedEvent.cs b/features/user/server/Garnet.Users.Events/UserUpdatedEvent.cs index 2d73f7d1f..dc7fbb01e 100644 --- a/features/user/server/Garnet.Users.Events/UserUpdatedEvent.cs +++ b/features/user/server/Garnet.Users.Events/UserUpdatedEvent.cs @@ -5,5 +5,7 @@ public record UserUpdatedEvent( string UserName, string Description, string AvatarUrl, - string[] Tags + string[] Tags, + float TotalScore, + Dictionary SkillScore ); \ No newline at end of file diff --git a/features/user/server/Garnet.Users.Infrastructure/EventHandlers/ProjectTaskClosedEventConsumer.cs b/features/user/server/Garnet.Users.Infrastructure/EventHandlers/ProjectTaskClosedEventConsumer.cs new file mode 100644 index 000000000..223b6b375 --- /dev/null +++ b/features/user/server/Garnet.Users.Infrastructure/EventHandlers/ProjectTaskClosedEventConsumer.cs @@ -0,0 +1,31 @@ +using Garnet.Common.Application.MessageBus; +using Garnet.Projects.Events.ProjectTask; +using Garnet.Users.Application; + +namespace Garnet.Users.Infrastructure.EventHandlers; + +public class ProjectTaskClosedEventConsumer : IMessageBusConsumer +{ + private readonly IUsersRepository _usersRepository; + + public ProjectTaskClosedEventConsumer(IUsersRepository usersRepository) + { + _usersRepository = usersRepository; + } + + public async Task Consume(ProjectTaskClosedEvent message) + { + if (message.RatingCalculation is not null) + { + foreach (var userId in message.RatingCalculation.UserExecutorIds) + { + await _usersRepository.EditUserTotalScore(userId, message.RatingCalculation.UserTotalScore); + await _usersRepository.EditUserSkillScore(userId, message.RatingCalculation.SkillScorePerUser); + } + + await _usersRepository.EditUserTotalScore( + message.RatingCalculation.ProjectOwnerId, + message.RatingCalculation.ProjectOwnerTotalScore); + } + } +} \ No newline at end of file diff --git a/features/user/server/Garnet.Users.Infrastructure/Garnet.Users.Infrastructure.csproj b/features/user/server/Garnet.Users.Infrastructure/Garnet.Users.Infrastructure.csproj index d685bf64b..19f88766d 100644 --- a/features/user/server/Garnet.Users.Infrastructure/Garnet.Users.Infrastructure.csproj +++ b/features/user/server/Garnet.Users.Infrastructure/Garnet.Users.Infrastructure.csproj @@ -8,6 +8,7 @@ + diff --git a/features/user/server/Garnet.Users.Infrastructure/MongoDb/UserDocument.cs b/features/user/server/Garnet.Users.Infrastructure/MongoDb/UserDocument.cs index cac9963f0..6335d89f4 100644 --- a/features/user/server/Garnet.Users.Infrastructure/MongoDb/UserDocument.cs +++ b/features/user/server/Garnet.Users.Infrastructure/MongoDb/UserDocument.cs @@ -10,6 +10,8 @@ public record UserDocument : DocumentBase public string Description { get; init; } = null!; public string AvatarUrl { get; init; } = null!; public string[] Tags { get; init; } = Array.Empty(); + public float TotalScore { get; init; } = 0; + public Dictionary SkillScore { get; init; } = new(); public static UserDocument Create(UserDocumentCreateArgs args) { @@ -19,12 +21,14 @@ public static UserDocument Create(UserDocumentCreateArgs args) UserName = args.UserName, Description = args.Description, AvatarUrl = args.AvatarUrl, - Tags = args.Tags + Tags = args.Tags, + TotalScore = args.TotalScore, + SkillScore = args.SkillScore }; } public static User ToDomain(UserDocument doc) { - return new User(doc.Id, doc.UserName, doc.Description, doc.AvatarUrl, doc.Tags); + return new User(doc.Id, doc.UserName, doc.Description, doc.AvatarUrl, doc.Tags, doc.TotalScore, doc.SkillScore); } } \ No newline at end of file diff --git a/features/user/server/Garnet.Users.Infrastructure/MongoDb/UserDocumentCreateArgs.cs b/features/user/server/Garnet.Users.Infrastructure/MongoDb/UserDocumentCreateArgs.cs index 3c798d3e9..ef9c4a8b4 100644 --- a/features/user/server/Garnet.Users.Infrastructure/MongoDb/UserDocumentCreateArgs.cs +++ b/features/user/server/Garnet.Users.Infrastructure/MongoDb/UserDocumentCreateArgs.cs @@ -5,4 +5,6 @@ public record UserDocumentCreateArgs( string UserName, string Description, string AvatarUrl, - string[] Tags); \ No newline at end of file + string[] Tags, + float TotalScore, + Dictionary SkillScore); \ No newline at end of file diff --git a/features/user/server/Garnet.Users.Infrastructure/MongoDb/UsersRepository.cs b/features/user/server/Garnet.Users.Infrastructure/MongoDb/UsersRepository.cs index ced0b8e88..825e3daaf 100644 --- a/features/user/server/Garnet.Users.Infrastructure/MongoDb/UsersRepository.cs +++ b/features/user/server/Garnet.Users.Infrastructure/MongoDb/UsersRepository.cs @@ -68,7 +68,9 @@ public async Task CreateUser(string identityId, string username) username, string.Empty, string.Empty, - Array.Empty() + Array.Empty(), + 0, + new Dictionary() ) ); var userDocument = await InsertOneDocument(_ct, db.Users, user); @@ -120,6 +122,36 @@ public async Task EditUsername(string userId, string username) return UserDocument.ToDomain(user); } + public async Task EditUserTotalScore(string userId, float totalScore) + { + var db = _dbFactory.Create(); + + await FindOneAndUpdateDocument( + _ct, + db.Users, + _f.Eq(x => x.Id, userId), + _u.Inc(x => x.TotalScore, totalScore) + ); + } + + public async Task EditUserSkillScore(string userId, Dictionary skillScorePerUser) + { + var db = _dbFactory.Create(); + var user = db.Users.Find(x => x.Id == userId).FirstOrDefault(); + + foreach (var skill in skillScorePerUser) + { + user.SkillScore[skill.Key] += skill.Value; + } + + await FindOneAndUpdateDocument( + _ct, + db.Users, + _f.Eq(x => x.Id, userId), + _u.Set(x => x, user) + ); + } + public async Task CreateIndexes() { var db = _dbFactory.Create(); diff --git a/features/user/server/Garnet.Users/Startup.cs b/features/user/server/Garnet.Users/Startup.cs index 7e79968b5..22b8dc756 100644 --- a/features/user/server/Garnet.Users/Startup.cs +++ b/features/user/server/Garnet.Users/Startup.cs @@ -7,11 +7,13 @@ using Garnet.Common.Infrastructure.MongoDb.Migrations; using Garnet.Common.Infrastructure.S3; using Garnet.Common.Infrastructure.Support; +using Garnet.Projects.Events.ProjectTask; using Garnet.Users.Application; using Garnet.Users.Application.Commands; using Garnet.Users.Application.Queries; using Garnet.Users.Events; using Garnet.Users.Infrastructure.Api; +using Garnet.Users.Infrastructure.EventHandlers; using Garnet.Users.Infrastructure.MongoDb; using Garnet.Users.Infrastructure.MongoDb.Migrations; using HotChocolate.Execution.Configuration; @@ -53,6 +55,7 @@ public static void AddGarnetUsersMessageBus(this IServiceCollection services, st { o.RegisterMessage(); o.RegisterMessage(); + o.RegisterConsumer(); }); }