Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Provider] Upload the list of employees #1621

Draft
wants to merge 7 commits into
base: develop
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System.ComponentModel.DataAnnotations;

namespace OutOfSchool.BusinessLogic.Models.Individual;

public class UploadEmployeeRequestDto
{
[Required(ErrorMessage = "FirstName is required")]
[MinLength(Constants.MinIndividualNameLength)]
[MaxLength(Constants.MaxIndividualNameLength)]
public string FirstName { get; set; }

[Required(ErrorMessage = "MiddleName is required")]
[MinLength(Constants.MinIndividualNameLength)]
[MaxLength(Constants.MaxIndividualNameLength)]
public string MiddleName { get; set; }

[Required(ErrorMessage = "LastName is required")]
[MinLength(Constants.MinIndividualNameLength)]
[MaxLength(Constants.MaxIndividualNameLength)]
public string LastName { get; set; }

[Required(ErrorMessage = "Rnokpp is required")]
public string Rnokpp { get; set; }

[Required(ErrorMessage = "AssignedRole is required")]
[MaxLength(60)]
public string AssignedRole { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace OutOfSchool.BusinessLogic.Models.Individual;

public class UploadEmployeeResponseDto
{
public int CountOfCreatedIndividuals { get; set; }
public int CountOfCreatedOfficials { get; set; }
public int CountOfCreatedPositions { get; set; }
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc;
using OutOfSchool.BusinessLogic.Models.Individual;
using OutOfSchool.BusinessLogic.Models.Providers;
using OutOfSchool.Common.Enums;
using OutOfSchool.Common.Models;
Expand Down Expand Up @@ -94,4 +95,12 @@ public interface IProviderService
/// <param name="id">Key in the table.</param>
/// <returns>A <see cref="Task{TResult}"/> representing the result of the asynchronous operation.
Task<bool> Exists(Guid id);

/// <summary>
/// Upload employees for provider.
/// </summary>
/// <param name="id">Id of provider that requests upload.</param>
/// <param name="uploadEployees">List of employees to upload.</param>
/// <returns>A <see cref="Task{TResult}"/> representing the result of the asynchronous operation.</returns>
Task<UploadEmployeeResponseDto> UploadEmployeesForProvider(Guid id, UploadEmployeeRequestDto[] data);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Options;
using OutOfSchool.BusinessLogic.Models;
using OutOfSchool.BusinessLogic.Models.Individual;
using OutOfSchool.BusinessLogic.Models.Providers;
using OutOfSchool.BusinessLogic.Services.AverageRatings;
using OutOfSchool.BusinessLogic.Services.SearchString;
Expand All @@ -31,6 +32,9 @@ public class ProviderService : IProviderService, ISensitiveProviderService
private readonly IStringLocalizer<SharedResource> localizer;
private readonly IMapper mapper;
private readonly IEntityRepositorySoftDeleted<long, Address> addressRepository;
private readonly IEntityRepositorySoftDeleted<Guid, Individual> individualRepository;
private readonly IEntityRepositorySoftDeleted<Guid, Official> officialRepository;
private readonly IEntityRepositorySoftDeleted<Guid, Position> positionRepository;
private readonly IWorkshopServicesCombiner workshopServiceCombiner;
private readonly IChangesLogService changesLogService;
private readonly INotificationService notificationService;
Expand Down Expand Up @@ -63,6 +67,9 @@ public class ProviderService : IProviderService, ISensitiveProviderService
/// <param name="localizer">Localizer.</param>
/// <param name="mapper">Mapper.</param>
/// <param name="addressRepository">AddressRepository.</param>
/// <param name="individualRepository">IndividualRepository.</param>
/// <param name="officialRepository">OfficialRepository.</param>
/// <param name="positionRepository">PositionRepository.</param>
/// <param name="workshopServiceCombiner">WorkshopServiceCombiner.</param>
/// <param name="employeeRepository">Employee repository.</param>
/// <param name="providerImagesService">Images service.</param>
Expand All @@ -89,6 +96,9 @@ public ProviderService(
IStringLocalizer<SharedResource> localizer,
IMapper mapper,
IEntityRepositorySoftDeleted<long, Address> addressRepository,
IEntityRepositorySoftDeleted<Guid, Individual> individualRepository,
IEntityRepositorySoftDeleted<Guid, Official> officialRepository,
IEntityRepositorySoftDeleted<Guid, Position> positionRepository,
IWorkshopServicesCombiner workshopServiceCombiner,
IEmployeeRepository employeeRepository,
IImageDependentEntityImagesInteractionService<Provider> providerImagesService,
Expand All @@ -112,6 +122,9 @@ public ProviderService(
this.localizer = localizer ?? throw new ArgumentNullException(nameof(localizer));
this.mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
this.addressRepository = addressRepository ?? throw new ArgumentNullException(nameof(addressRepository));
this.individualRepository = individualRepository ?? throw new ArgumentNullException(nameof(individualRepository));
this.officialRepository = officialRepository ?? throw new ArgumentNullException(nameof(officialRepository));
this.positionRepository = positionRepository ?? throw new ArgumentNullException(nameof(positionRepository));
this.providerRepository = providerRepository ?? throw new ArgumentNullException(nameof(providerRepository));
this.usersRepository = usersRepository ?? throw new ArgumentNullException(nameof(usersRepository));
this.workshopServiceCombiner = workshopServiceCombiner ?? throw new ArgumentNullException(nameof(workshopServiceCombiner));
Expand Down Expand Up @@ -523,6 +536,91 @@ public Task<bool> Exists(Guid id)
return providerRepository.Any(x => x.Id == id);
}

public async Task<UploadEmployeeResponseDto> UploadEmployeesForProvider(Guid id, UploadEmployeeRequestDto[] data)
{
CheckListOfEmployeesForUploading(data);

var result = new UploadEmployeeResponseDto();

// Dictionary for uploading employees
var uploadDictionary = new Dictionary<Guid, UploadEmployeeRequestDto>();
var existingIndividuals = (await individualRepository.GetByFilter(i => data.Select(e => e.Rnokpp).Contains(i.Rnokpp))
.ConfigureAwait(false))
.Select(i => new { i.Rnokpp, i.Id })
.ToDictionary(e => e.Rnokpp);

async Task UploadEmployeesIntoDb()
{
// Circle to add an individual to DB and populate the Dictionary for uploading employees
foreach (var employee in data)
{
if (existingIndividuals.ContainsKey(employee.Rnokpp))
{
uploadDictionary.Add(existingIndividuals[employee.Rnokpp].Id, employee);
}
else // Add an Individual to DB if it has not already existed in DB
{
var newIndividual = await individualRepository.Create(mapper.Map<Individual>(employee));
uploadDictionary.Add(newIndividual.Id, employee);
result.CountOfCreatedIndividuals++;
}
}

// Get dictionary (Lookup) with keys - IndividualId and values - Officials
var existingOfficialsForProvider = (await officialRepository.GetByFilter(o =>
o.Position.ProviderId == id
&& uploadDictionary.Keys.Contains(o.IndividualId)
&& (o.DismissalOrder == null || o.DismissalOrder == string.Empty)
, includeProperties: "Position")
.ConfigureAwait(false))
.ToLookup(o => o.IndividualId, o => o);

//Cycle for filling the database with new employees
foreach (var key in uploadDictionary.Keys)
{
// If this Employee already exists and occupies the same Position
if (existingOfficialsForProvider.Contains(key)
&& existingOfficialsForProvider[key].Select(o => o.Position.FullName).Contains(uploadDictionary[key].AssignedRole))
{
continue;
}

// Create a new Position if it doesn't exist
var position = (await positionRepository.GetByFilter(
p => p.ProviderId == id
&& p.FullName == uploadDictionary[key].AssignedRole
).ConfigureAwait(false))
.FirstOrDefault();

if (position == default)
{
position = await positionRepository.Create(
new Position
{
ProviderId = id,
FullName = uploadDictionary[key].AssignedRole
}).ConfigureAwait(false);
result.CountOfCreatedPositions++;
}

// Create a new Official
await officialRepository.Create(
new Official()
{
IndividualId = key,
PositionId = position.Id
}).ConfigureAwait(false);
result.CountOfCreatedOfficials++;
}
}

await providerRepository.RunInTransaction(UploadEmployeesIntoDb).ConfigureAwait(false);

logger.LogInformation("Upload employees for provider finished successfully.");

return result;
}

private async Task<IEnumerable<string>> GetNotificationsRecipientIds(NotificationAction action, Dictionary<string, string> additionalData, Guid objectId)
{
var recipientIds = new List<string>();
Expand Down Expand Up @@ -1013,4 +1111,35 @@ private async Task<bool> ExistsAnotherProviderWithTheSameEdrpouIpn(ProviderUpdat

return providersWithTheSameEdrpouIpn.Any();
}
}

private void CheckListOfEmployeesForUploading(UploadEmployeeRequestDto[] data)
{
_ = data ?? throw new ArgumentNullException(nameof(data));

logger.LogInformation("Upload employees for provider was started.");

if (data.Length == 0)
{
var errorMessage = "The number of entries to upload should be greater than 0.";
logger.LogError(errorMessage);
throw new InvalidOperationException(errorMessage);
}

if (data.Length > Constants.MaxNumberOfEmployeesToUpload)
{
var errorMessage = $"The number of entries should not exceed {Constants.MaxNumberOfEmployeesToUpload}.";
logger.LogError("The number of entries should not exceed {MaxNumberOfEmployeesToUpload}.", Constants.MaxNumberOfEmployeesToUpload);
throw new InvalidOperationException(errorMessage);
}

var uploadEmployeesRnokpps = data.Select(e => e.Rnokpp).ToList();

// Check if the Rnokpp property values ​​are unique?
if (uploadEmployeesRnokpps.Distinct().Count() != data.Length)
{
var errorMessage = $"The Rnokpp property values are not unique.";
logger.LogError(errorMessage);
throw new InvalidOperationException(errorMessage);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ public ProviderServiceV2(
IStringLocalizer<SharedResource> localizer,
IMapper mapper,
IEntityRepositorySoftDeleted<long, Address> addressRepository,
IEntityRepositorySoftDeleted<Guid, Individual> individualRepository,
IEntityRepositorySoftDeleted<Guid, Official> officialRepository,
IEntityRepositorySoftDeleted<Guid, Position> positionRepository,
IWorkshopServicesCombiner workshopServiceCombiner,
IEmployeeRepository employeeRepository,
IImageDependentEntityImagesInteractionService<Provider> providerImagesService,
Expand All @@ -48,6 +51,9 @@ public ProviderServiceV2(
localizer,
mapper,
addressRepository,
individualRepository,
officialRepository,
positionRepository,
workshopServiceCombiner,
employeeRepository,
providerImagesService,
Expand Down
25 changes: 25 additions & 0 deletions OutOfSchool/OutOfSchool.BusinessLogic/Util/MappingProfile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using OutOfSchool.BusinessLogic.Models.Codeficator;
using OutOfSchool.BusinessLogic.Models.CompetitiveEvent;
using OutOfSchool.BusinessLogic.Models.Geocoding;
using OutOfSchool.BusinessLogic.Models.Individual;
using OutOfSchool.BusinessLogic.Models.Notifications;
using OutOfSchool.BusinessLogic.Models.Providers;
using OutOfSchool.BusinessLogic.Models.Exported;
Expand Down Expand Up @@ -815,6 +816,30 @@ public MappingProfile()
CreateMap<CompetitiveEventDescriptionItem, CompetitiveEventDescriptionItemDto>().ReverseMap();

CreateMap<CompetitiveEventRegistrationDeadline, CompetitiveEventRegistrationDeadlineDto>().ReverseMap();

CreateSoftDeletedMap<UploadEmployeeRequestDto, Individual>()
.ForMember(dest => dest.Id, opt => opt.Ignore())
.ForMember(dest => dest.IsRegistered, opt => opt.Ignore())
.ForMember(dest => dest.ExternalRegistryId, opt => opt.Ignore())
.ForMember(dest => dest.Gender, opt => opt.Ignore())
.ForMember(dest => dest.UserId, opt => opt.Ignore())
.ForMember(dest => dest.User, opt => opt.Ignore())
.ForMember(dest => dest.Officials, opt => opt.Ignore())
.ForMember(dest => dest.Document, opt => opt.Ignore())
.ForMember(dest => dest.File, opt => opt.Ignore())
.ForMember(dest => dest.ActiveFrom, opt => opt.Ignore())
.ForMember(dest => dest.ActiveTo, opt => opt.Ignore())
.ForMember(dest => dest.IsBlocked, opt => opt.Ignore())
.ForMember(dest => dest.IsSystemProtected, opt => opt.Ignore())
.ForMember(dest => dest.CreatedBy, opt => opt.Ignore())
.ForMember(dest => dest.ModifiedBy, opt => opt.Ignore())
.ForMember(dest => dest.DeletedBy, opt => opt.Ignore())
.ForMember(dest => dest.CreatedAt, opt => opt.Ignore())
.ForMember(dest => dest.UpdatedAt, opt => opt.Ignore())
.ForMember(dest => dest.DeleteDate, opt => opt.Ignore());

CreateMap<Individual, UploadEmployeeRequestDto>()
.ForMember(dest => dest.AssignedRole, opt => opt.Ignore());
}

public IMappingExpression<TSource, TDestination> CreateSoftDeletedMap<TSource, TDestination>()
Expand Down
15 changes: 15 additions & 0 deletions OutOfSchool/OutOfSchool.Common/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -184,4 +184,19 @@ public static class Constants
/// Maximum length of keywords.
/// </summary>
public const int MaxKeywordsLength = 200;

/// <summary>
/// Maximum length of first name, middle name, and last name for Individual.
/// </summary>
public const int MaxIndividualNameLength = 50;

/// <summary>
/// Minimum length of first name, middle name, and last name for Individual.
/// </summary>
public const int MinIndividualNameLength = 2;

/// <summary>
/// Maximum number of employees to upload.
/// </summary>
public const int MaxNumberOfEmployeesToUpload = 100;
}
6 changes: 3 additions & 3 deletions OutOfSchool/OutOfSchool.DataAccess/Models/Position.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ public class Position : BusinessEntity
[Required(ErrorMessage = "FullName is required.")]
[MaxLength(Constants.NameMaxLength)]
public string FullName { get; set; }

[MaxLength(Constants.NameMaxLength)]
public string ShortName { get; set; }

[Required(ErrorMessage = "GenitiveName is required.")]
[MaxLength(Constants.NameMaxLength)]
public string GenitiveName { get; set; }
public string GenitiveName { get; set; } = string.Empty;

public bool IsTeachingPosition { get; set; }

Expand All @@ -47,7 +47,7 @@ public class Position : BusinessEntity

[Required(ErrorMessage = "ClassifierType is required.")]
[MaxLength(60)]
public string ClassifierType { get; set; }
public string ClassifierType { get; set; } = string.Empty;

public virtual ICollection<Official> Officials { get; set; }
}
22 changes: 14 additions & 8 deletions OutOfSchool/OutOfSchool.DataAccess/Models/User.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System;
using System.ComponentModel.DataAnnotations;

using Microsoft.AspNetCore.Identity;

namespace OutOfSchool.Services.Models;
Expand All @@ -9,35 +8,42 @@
{
public bool IsDeleted { get; set; }

// TODO: For now it is left here so existing code does not break
[Required(ErrorMessage = "LastName is required")]
[MaxLength(60)]
public string LastName { get; set; }

// TODO: For now it is left here so existing code does not break
[Required(ErrorMessage = "FirstName is required")]
[MaxLength(60)]
public string FirstName { get; set; }

// TODO: For now it is left here so existing code does not break
[MaxLength(60)]
public string MiddleName { get; set; }

// TODO: Should delete CreatingTime property?
[DataType(DataType.DateTime)]
public DateTimeOffset CreatingTime { get; set; }

[DataType(DataType.DateTime)]
public DateTimeOffset LastLogin { get; set; }

[MaxLength(60)]
public string MiddleName { get; set; }

[Required(ErrorMessage = "FirstName is required")]
[MaxLength(60)]
public string FirstName { get; set; }

[MaxLength(50)]
public string Role { get; set; }

// TODO: For now it is left here so existing code does not break
public bool IsRegistered { get; set; }

// TODO: Should delete IsBlocked property?
// If the flag is true, that user can no longer do anything to website.
public bool IsBlocked { get; set; } = false;

// TODO: Should delete IsDerived property?
// for permissions managing at login and check if user is original provider or its admin, temporary field, needs to be removed then
public bool IsDerived { get; set; } = false;

// If it's true then user must change his password before the logging into the system
public bool MustChangePassword { get; set; }

public virtual Individual? Individual { get; set; }

Check warning on line 49 in OutOfSchool/OutOfSchool.DataAccess/Models/User.cs

View workflow job for this annotation

GitHub Actions / SonarCloud

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 49 in OutOfSchool/OutOfSchool.DataAccess/Models/User.cs

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest)

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 49 in OutOfSchool/OutOfSchool.DataAccess/Models/User.cs

View workflow job for this annotation

GitHub Actions / test (windows-latest)

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 49 in OutOfSchool/OutOfSchool.DataAccess/Models/User.cs

View workflow job for this annotation

GitHub Actions / test (macOS-latest)

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
Expand Down
Loading
Loading