Вытащил солюшен на уровень выше, чтобы прощё было дотнетить
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
2025-10-05 14:32:06 +03:00
parent fa87a56ad1
commit aae4b28089
242 changed files with 159 additions and 159 deletions

View File

@@ -0,0 +1,27 @@
using System.ComponentModel.DataAnnotations;
using Domains;
namespace ApplicationLayer.Services.Applicants.Models;
public class AddressModel
{
/// Country part of address
[Required]
[MaxLength(ConfigurationConstraints.CountryNameLength)]
public string Country { get; set; } = null!;
/// City part of address
[Required]
[MaxLength(ConfigurationConstraints.CityNameLength)]
public string City { get; set; } = null!;
/// Street part of address
[Required]
[MaxLength(ConfigurationConstraints.StreetNameLength)]
public string Street { get; set; } = null!;
/// Building part of address
[Required]
[MaxLength(ConfigurationConstraints.BuildingNumberLength)]
public string Building { get; set; } = null!;
}

View File

@@ -0,0 +1,66 @@
using System.ComponentModel.DataAnnotations;
using Domains.ApplicantDomain;
namespace ApplicationLayer.Services.Applicants.Models;
/// Model of <see cref="Applicant" />
public class ApplicantModel
{
/// <inheritdoc cref="Applicant.Name" />
[Required]
public NameModel Name { get; set; } = null!;
/// <inheritdoc cref="Applicant.Passport" />
[Required]
public PassportModel Passport { get; set; } = null!;
/// <inheritdoc cref="Applicant.BirthDate" />
[Required]
public DateTime BirthDate { get; set; }
/// <inheritdoc cref="Applicant.CountryOfBirth" />
[Required]
public string CountryOfBirth { get; set; } = null!;
/// <inheritdoc cref="Applicant.CityOfBirth" />
[Required]
public string CityOfBirth { get; set; } = null!;
/// <inheritdoc cref="Applicant.Citizenship" />
[Required]
public string Citizenship { get; set; } = null!;
/// <inheritdoc cref="Applicant.CitizenshipByBirth" />
[Required]
public string CitizenshipByBirth { get; set; } = null!;
/// <inheritdoc cref="Applicant.Gender" />
[Required]
public Gender Gender { get; set; }
/// <inheritdoc cref="Applicant.MaritalStatus" />
[Required]
public MaritalStatus MaritalStatus { get; set; }
/// <inheritdoc cref="Applicant.FatherName" />
[Required]
public NameModel FatherName { get; set; } = null!;
/// <inheritdoc cref="Applicant.MotherName" />
[Required]
public NameModel MotherName { get; set; } = null!;
/// <inheritdoc cref="Applicant.JobTitle" />
[Required]
public string JobTitle { get; set; } = null!;
/// <inheritdoc cref="Applicant.PlaceOfWork" />
[Required]
public PlaceOfWorkModel PlaceOfWork { get; set; } = null!;
/// <inheritdoc cref="Applicant.IsNonResident" />
[Required]
public bool IsNonResident { get; set; }
public override string ToString() => Name.ToString();
}

View File

@@ -0,0 +1,21 @@
using System.ComponentModel.DataAnnotations;
using Domains;
namespace ApplicationLayer.Services.Applicants.Models;
/// Model of name for presentation layer
public class NameModel
{
[Required]
[MaxLength(ConfigurationConstraints.NameLength)]
public string FirstName { get; set; } = null!;
[Required]
[MaxLength(ConfigurationConstraints.NameLength)]
public string Surname { get; set; } = null!;
[MaxLength(ConfigurationConstraints.NameLength)]
public string? Patronymic { get; set; }
public override string ToString() => $"{FirstName} {Surname} {Patronymic}".TrimEnd();
}

View File

@@ -0,0 +1,26 @@
using System.ComponentModel.DataAnnotations;
using Domains;
namespace ApplicationLayer.Services.Applicants.Models;
/// Model of passport fpr presentation layer
public class PassportModel
{
/// Number of passport
[Required]
[MaxLength(ConfigurationConstraints.PassportNumberLength)]
public string Number { get; set; } = null!;
/// Issuing authority of passport
[Required]
[MaxLength(ConfigurationConstraints.IssuerNameLength)]
public string Issuer { get; set; } = null!;
/// Date of issue
[Required]
public DateTime IssueDate { get; set; }
/// Date when the passport expires
[Required]
public DateTime ExpirationDate { get; set; }
}

View File

@@ -0,0 +1,22 @@
using System.ComponentModel.DataAnnotations;
using Domains;
namespace ApplicationLayer.Services.Applicants.Models;
public class PlaceOfWorkModel
{
/// Name of hirer
[Required]
[MaxLength(ConfigurationConstraints.PlaceOfWorkNameLength)]
public string Name { get; set; } = null!;
/// Address of hirer
[Required]
public AddressModel Address { get; set; } = null!;
/// Phone number of hirer
[Required]
[MaxLength(ConfigurationConstraints.PhoneNumberLength)]
[MinLength(ConfigurationConstraints.PhoneNumberMinLength)]
public string PhoneNum { get; set; } = null!;
}

View File

@@ -0,0 +1,32 @@
using Domains;
using FluentValidation;
namespace ApplicationLayer.Services.Applicants.Models.Validation;
public class NameModelValidator : AbstractValidator<NameModel>
{
public NameModelValidator()
{
RuleFor(m => m.FirstName)
.NotEmpty()
.WithMessage("First Name can not be empty")
.Matches(Constants.EnglishWordRegex)
.WithMessage("First name must be in english characters")
.MaximumLength(ConfigurationConstraints.NameLength)
.WithMessage($"First Name length must be less than {ConfigurationConstraints.NameLength}");
RuleFor(m => m.Surname)
.NotEmpty()
.WithMessage("Surname can not be empty")
.Matches(Constants.EnglishWordRegex)
.WithMessage("Surname must be in english characters")
.MaximumLength(ConfigurationConstraints.NameLength)
.WithMessage($"Surname length must be less than {ConfigurationConstraints.NameLength}");
RuleFor(m => m.Patronymic)
.Matches(Constants.EnglishWordRegex)
.WithMessage("Patronymic must be in english characters")
.MaximumLength(ConfigurationConstraints.NameLength)
.WithMessage($"Patronymic length must be less than {ConfigurationConstraints.NameLength}");
}
}

View File

@@ -0,0 +1,39 @@
using ApplicationLayer.InfrastructureServicesInterfaces;
using Domains;
using FluentValidation;
namespace ApplicationLayer.Services.Applicants.Models.Validation;
public class PassportModelValidator : AbstractValidator<PassportModel>
{
public PassportModelValidator(IDateTimeProvider dateTimeProvider)
{
RuleFor(r => r.Issuer)
.NotEmpty()
.WithMessage("Passport issuer can not be empty")
.Matches(Constants.EnglishPhraseRegex)
.WithMessage("Passport issuer field can contain only english letters, digits and special symbols")
.MaximumLength(ConfigurationConstraints.IssuerNameLength)
.WithMessage($"Passport issuer length must be less than {ConfigurationConstraints.IssuerNameLength}");
RuleFor(r => r.Number)
.NotEmpty()
.WithMessage("Passport number can not be empty")
.Matches(Constants.EnglishPhraseRegex)
.WithMessage("Passport number field can contain only english letters, digits and special symbols")
.MaximumLength(ConfigurationConstraints.PassportNumberLength)
.WithMessage($"Passport number length must be less than {ConfigurationConstraints.PassportNumberLength}");
RuleFor(r => r.ExpirationDate)
.NotEmpty()
.WithMessage("Passport expiration date can not be empty")
.GreaterThan(dateTimeProvider.Now())
.WithMessage("Can not approve visa for applicants with expired passport");
RuleFor(r => r.IssueDate)
.NotEmpty()
.WithMessage("Passport issue date can not be empty")
.LessThanOrEqualTo(dateTimeProvider.Now())
.WithMessage("Passport issue date must be in past");
}
}

View File

@@ -0,0 +1,66 @@
using Domains;
using FluentValidation;
namespace ApplicationLayer.Services.Applicants.Models.Validation;
public class PlaceOfWorkModelValidator : AbstractValidator<PlaceOfWorkModel>
{
public PlaceOfWorkModelValidator()
{
RuleFor(p => p.Name)
.NotEmpty()
.WithMessage("Place of work name can not be empty")
.Matches(Constants.EnglishPhraseRegex)
.WithMessage("Place of work name field can contain only english letters, digits and special symbols")
.MaximumLength(ConfigurationConstraints.PlaceOfWorkNameLength)
.WithMessage($"Place of work name length must be less than {ConfigurationConstraints.PlaceOfWorkNameLength}");
RuleFor(p => p.PhoneNum)
.NotEmpty()
.WithMessage("Place of work phone number can not be empty")
.Matches(Constants.PhoneNumRegex)
.WithMessage("Place of work phone number field must be valid")
.MaximumLength(ConfigurationConstraints.PhoneNumberLength)
.WithMessage(
$"Phone number length must be in range from {ConfigurationConstraints.PhoneNumberMinLength} to {ConfigurationConstraints.PhoneNumberLength}")
.MinimumLength(ConfigurationConstraints.PhoneNumberMinLength)
.WithMessage(
$"Phone number length must be in range from {ConfigurationConstraints.PhoneNumberMinLength} to {ConfigurationConstraints.PhoneNumberLength}");
RuleFor(p => p.Address)
.NotEmpty()
.WithMessage("Place of work address can not be empty");
RuleFor(p => p.Address.Country)
.NotEmpty()
.WithMessage("Country name of place of work can not be empty")
.Matches(Constants.EnglishPhraseRegex)
.WithMessage("Place of work Country field can contain only english letters, digits and special symbols")
.MaximumLength(ConfigurationConstraints.CountryNameLength)
.WithMessage($"Country name of place of work length must be less than {ConfigurationConstraints.CountryNameLength}");
RuleFor(p => p.Address.City)
.NotEmpty()
.WithMessage("City name of place of work can not be empty")
.Matches(Constants.EnglishPhraseRegex)
.WithMessage("Place of work City field can contain only english letters, digits and special symbols")
.MaximumLength(ConfigurationConstraints.CityNameLength)
.WithMessage($"City name of place of work length must be less than {ConfigurationConstraints.CityNameLength}");
RuleFor(p => p.Address.Street)
.NotEmpty()
.WithMessage("Street name of place of work can not be empty")
.Matches(Constants.EnglishPhraseRegex)
.WithMessage("Place of work Street field can contain only english letters, digits and special symbols")
.MaximumLength(ConfigurationConstraints.StreetNameLength)
.WithMessage($"Street name of place of work length must be less than {ConfigurationConstraints.StreetNameLength}");
RuleFor(p => p.Address.Building)
.NotEmpty()
.WithMessage("Building of place of work can not be empty")
.Matches(Constants.EnglishPhraseRegex)
.WithMessage("Place of work building field can contain only english letters, digits and special symbols")
.MaximumLength(ConfigurationConstraints.BuildingNumberLength)
.WithMessage($"Building of place of work length must be less than {ConfigurationConstraints.BuildingNumberLength}");
}
}

View File

@@ -0,0 +1,17 @@
using ApplicationLayer.InfrastructureServicesInterfaces;
using Domains.ApplicantDomain;
namespace ApplicationLayer.Services.Applicants.NeededServices;
/// Repository pattern for <see cref="Applicant"/>
public interface IApplicantsRepository : IGenericRepository<Applicant>
{
/// Find <see cref="Applicant"/> by Identifier
Task<Applicant> FindByUserIdAsync(Guid userId, CancellationToken cancellationToken);
/// Get identifier of applicant by user identifier
Task<Guid> GetApplicantIdByUserId(Guid userId, CancellationToken cancellationToken);
/// Returns value of NonResident property of applicant
Task<bool> IsApplicantNonResidentByUserId(Guid userId, CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,15 @@
using System.ComponentModel.DataAnnotations;
using Domains;
namespace ApplicationLayer.Services.AuthServices.Common;
public class AuthData
{
[Required]
[MaxLength(ConfigurationConstraints.EmailLength)]
public string Email { get; set; } = null!;
[Required]
[MaxLength(ConfigurationConstraints.PasswordLength)]
public string Password { get; set; } = null!;
}

View File

@@ -0,0 +1,8 @@
using System.ComponentModel.DataAnnotations;
namespace ApplicationLayer.Services.AuthServices.Common;
public class AuthToken
{
[Required] public string Token { get; set; } = null!;
}

View File

@@ -0,0 +1,29 @@
using ApplicationLayer.InfrastructureServicesInterfaces;
using ApplicationLayer.Services.AuthServices.Common;
using ApplicationLayer.Services.AuthServices.LoginService.Exceptions;
using ApplicationLayer.Services.AuthServices.NeededServices;
using ApplicationLayer.Services.AuthServices.Requests;
using Domains.Users;
namespace ApplicationLayer.Services.AuthServices.LoginService;
public class DevelopmentLoginService(IUsersRepository users, ITokenGenerator tokenGenerator) : ILoginService
{
async Task<AuthToken> ILoginService.LoginAsync(LoginRequest request, CancellationToken cancellationToken)
{
if (request.AuthData is { Email: "admin@mail.ru", Password: "admin" })
{
var admin = new User { Role = Role.Admin };
return tokenGenerator.CreateToken(admin);
}
var user = await users.FindByEmailAsync(request.AuthData.Email, cancellationToken);
if (user is null || user.Password != request.AuthData.Password)
{
throw new IncorrectLoginDataException();
}
return tokenGenerator.CreateToken(user);
}
}

View File

@@ -0,0 +1,5 @@
using ApplicationLayer.GeneralExceptions;
namespace ApplicationLayer.Services.AuthServices.LoginService.Exceptions;
public class IncorrectLoginDataException() : ApiException("Incorrect email or password");

View File

@@ -0,0 +1,12 @@
using ApplicationLayer.Services.AuthServices.Common;
using ApplicationLayer.Services.AuthServices.Requests;
namespace ApplicationLayer.Services.AuthServices.LoginService;
/// Handles login requests
public interface ILoginService
{
/// Handle login request
/// <returns>JWT-token</returns>
Task<AuthToken> LoginAsync(LoginRequest request, CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,22 @@
using ApplicationLayer.InfrastructureServicesInterfaces;
using ApplicationLayer.Services.AuthServices.Common;
using ApplicationLayer.Services.AuthServices.LoginService.Exceptions;
using ApplicationLayer.Services.AuthServices.NeededServices;
using ApplicationLayer.Services.AuthServices.Requests;
namespace ApplicationLayer.Services.AuthServices.LoginService;
/// <inheritdoc cref="ILoginService" />
public class LoginService(IUsersRepository users, ITokenGenerator tokenGenerator) : ILoginService
{
async Task<AuthToken> ILoginService.LoginAsync(LoginRequest request, CancellationToken cancellationToken)
{
var user = await users.FindByEmailAsync(request.AuthData.Email, cancellationToken);
if (user is null || user.Password != request.AuthData.Password)
{
throw new IncorrectLoginDataException();
}
return tokenGenerator.CreateToken(user);
}
}

View File

@@ -0,0 +1,20 @@
using ApplicationLayer.InfrastructureServicesInterfaces;
using Domains.Users;
namespace ApplicationLayer.Services.AuthServices.NeededServices;
/// Repository pattern for <see cref="User"/>
public interface IUsersRepository : IGenericRepository<User>
{
/// Find <see cref="User"/> by email
/// <param name="email"><see cref="User"/>'s email</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>User or null if not found</returns>
Task<User?> FindByEmailAsync(string email, CancellationToken cancellationToken);
/// Returns all accounts with specific role
/// <param name="role">role</param>
/// <param name="cancellationToken">cancellation token</param>
/// <returns>list of accounts</returns>
Task<List<User>> GetAllOfRoleAsync(Role role, CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,13 @@
using ApplicationLayer.Services.AuthServices.Requests;
namespace ApplicationLayer.Services.AuthServices.RegisterService;
/// Handles register request
public interface IRegisterService
{
/// Handle <see cref="RegisterApplicantRequest"/>
Task RegisterApplicant(RegisterApplicantRequest request, CancellationToken cancellationToken);
/// Handles <see cref="RegisterRequest"/> and adds approving authority account
Task RegisterAuthority(RegisterRequest request, CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,41 @@
using ApplicationLayer.InfrastructureServicesInterfaces;
using ApplicationLayer.Services.Applicants.NeededServices;
using ApplicationLayer.Services.AuthServices.NeededServices;
using ApplicationLayer.Services.AuthServices.Requests;
using AutoMapper;
using Domains.ApplicantDomain;
using Domains.Users;
namespace ApplicationLayer.Services.AuthServices.RegisterService;
/// <inheritdoc cref="IRegisterService"/>
public class RegisterService(
IUsersRepository users,
IApplicantsRepository applicants,
IUnitOfWork unitOfWork,
IMapper mapper) : IRegisterService
{
async Task IRegisterService.RegisterApplicant(RegisterApplicantRequest request, CancellationToken cancellationToken)
{
var user = mapper.Map<User>(request.RegisterRequest.AuthData);
user.Role = Role.Applicant;
var applicant = mapper.Map<Applicant>(request);
applicant.UserId = user.Id;
await users.AddAsync(user, cancellationToken);
await applicants.AddAsync(applicant, cancellationToken);
await unitOfWork.SaveAsync(cancellationToken);
}
async Task IRegisterService.RegisterAuthority(RegisterRequest request, CancellationToken cancellationToken)
{
var user = mapper.Map<User>(request.AuthData);
user.Role = Role.ApprovingAuthority;
await users.AddAsync(user, cancellationToken);
await unitOfWork.SaveAsync(cancellationToken);
}
}

View File

@@ -0,0 +1,9 @@
using System.ComponentModel.DataAnnotations;
using ApplicationLayer.Services.AuthServices.Common;
namespace ApplicationLayer.Services.AuthServices.Requests;
public class LoginRequest
{
[Required] public AuthData AuthData { get; set; } = null!;
}

View File

@@ -0,0 +1,49 @@
using System.ComponentModel.DataAnnotations;
using ApplicationLayer.Services.Applicants.Models;
using Domains;
using Domains.ApplicantDomain;
namespace ApplicationLayer.Services.AuthServices.Requests;
public record RegisterApplicantRequest
{
[Required] public RegisterRequest RegisterRequest { get; set; } = null!;
[Required] public NameModel ApplicantName { get; set; } = null!;
[Required] public PassportModel Passport { get; set; } = null!;
[Required] public DateTime BirthDate { get; set; }
[Required]
[MaxLength(ConfigurationConstraints.CityNameLength)]
public string CityOfBirth { get; set; } = null!;
[Required]
[MaxLength(ConfigurationConstraints.CountryNameLength)]
public string CountryOfBirth { get; set; } = null!;
[Required]
[MaxLength(ConfigurationConstraints.CitizenshipLength)]
public string Citizenship { get; set; } = null!;
[Required]
[MaxLength(ConfigurationConstraints.CitizenshipLength)]
public string CitizenshipByBirth { get; set; } = null!;
[Required] public Gender Gender { get; set; }
[Required] public MaritalStatus MaritalStatus { get; set; }
[Required] public NameModel FatherName { get; set; } = null!;
[Required] public NameModel MotherName { get; set; } = null!;
[Required]
[MaxLength(ConfigurationConstraints.JobTitleLength)]
public string JobTitle { get; set; } = null!;
[Required] public PlaceOfWorkModel PlaceOfWork { get; set; } = null!;
[Required] public bool IsNonResident { get; set; }
}

View File

@@ -0,0 +1,9 @@
using System.ComponentModel.DataAnnotations;
using ApplicationLayer.Services.AuthServices.Common;
namespace ApplicationLayer.Services.AuthServices.Requests;
public class RegisterRequest
{
[Required] public AuthData AuthData { get; set; } = null!;
}

View File

@@ -0,0 +1,27 @@
using ApplicationLayer.Services.AuthServices.Common;
using Domains;
using FluentValidation;
namespace ApplicationLayer.Services.AuthServices.Requests.Validation;
public class AuthDataValidator : AbstractValidator<AuthData>
{
public AuthDataValidator()
{
RuleFor(d => d.Email)
.NotEmpty()
.WithMessage("Email can not be empty")
.EmailAddress()
.WithMessage("Email must be valid")
.MaximumLength(ConfigurationConstraints.EmailLength)
.WithMessage($"Email length must be less than {ConfigurationConstraints.EmailLength}");
RuleFor(d => d.Password)
.NotEmpty()
.WithMessage("Password can not be empty")
.Matches(Constants.EnglishPhraseRegex)
.WithMessage("Password can contain only english letters, digits and special symbols")
.MaximumLength(ConfigurationConstraints.PasswordLength)
.WithMessage($"Password length must be less than {ConfigurationConstraints.PasswordLength}");
}
}

View File

@@ -0,0 +1,93 @@
using ApplicationLayer.InfrastructureServicesInterfaces;
using ApplicationLayer.Services.Applicants.Models;
using Domains;
using FluentValidation;
namespace ApplicationLayer.Services.AuthServices.Requests.Validation;
public class RegisterApplicantRequestValidator : AbstractValidator<RegisterApplicantRequest>
{
public RegisterApplicantRequestValidator(
IDateTimeProvider dateTimeProvider,
IValidator<NameModel> nameValidator,
IValidator<RegisterRequest> registerRequestValidator,
IValidator<PassportModel> passportValidator,
IValidator<PlaceOfWorkModel> placeOfWorkModelValidator)
{
RuleFor(r => r.RegisterRequest)
.NotEmpty()
.SetValidator(registerRequestValidator);
RuleFor(r => r.ApplicantName)
.NotEmpty()
.SetValidator(nameValidator);
RuleFor(r => r.FatherName)
.NotEmpty()
.SetValidator(nameValidator);
RuleFor(r => r.MotherName)
.NotEmpty()
.SetValidator(nameValidator);
RuleFor(r => r.Passport)
.NotEmpty()
.SetValidator(passportValidator);
RuleFor(r => r.BirthDate)
.NotEmpty()
.WithMessage("Birth date can not be empty")
.LessThanOrEqualTo(dateTimeProvider.Now().AddYears(-ConfigurationConstraints.ApplicantMinAge))
.WithMessage($"Applicant must be older than {ConfigurationConstraints.ApplicantMinAge}");
RuleFor(r => r.CountryOfBirth)
.NotEmpty()
.WithMessage("Country of birth can not be empty")
.Matches(Constants.EnglishPhraseRegex)
.WithMessage("Country of birth field can contain only english letters, digits and special symbols")
.MaximumLength(ConfigurationConstraints.CountryNameLength)
.WithMessage($"Country of birth name length must be less than {ConfigurationConstraints.CountryNameLength}");
RuleFor(r => r.CityOfBirth)
.NotEmpty()
.WithMessage("City of birth can not be empty")
.Matches(Constants.EnglishPhraseRegex)
.WithMessage("City of birth field can contain only english letters, digits and special symbols")
.MaximumLength(ConfigurationConstraints.CityNameLength)
.WithMessage($"City of birth name length must be less than {ConfigurationConstraints.CityNameLength}");
RuleFor(r => r.Citizenship)
.NotEmpty()
.WithMessage("Citizenship can not be empty")
.Matches(Constants.EnglishPhraseRegex)
.WithMessage("Citizenship field can contain only english letters, digits and special symbols")
.MaximumLength(ConfigurationConstraints.CitizenshipLength)
.WithMessage($"Citizenship length must be less than {ConfigurationConstraints.CitizenshipLength}");
RuleFor(r => r.CitizenshipByBirth)
.NotEmpty()
.WithMessage("Citizenship by birth can not be empty")
.Matches(Constants.EnglishPhraseRegex)
.WithMessage("Citizenship by birth field can contain only english letters, digits and special symbols")
.MaximumLength(ConfigurationConstraints.CitizenshipLength)
.WithMessage($"Citizenship by birth length must be less than {ConfigurationConstraints.CitizenshipLength}");
RuleFor(r => r.Gender)
.IsInEnum();
RuleFor(r => r.MaritalStatus)
.IsInEnum();
RuleFor(r => r.JobTitle)
.NotEmpty()
.WithMessage("Title of job can not be empty")
.Matches(Constants.EnglishPhraseRegex)
.WithMessage("Title of job field can contain only english letters, digits and special symbols")
.MaximumLength(ConfigurationConstraints.JobTitleLength)
.WithMessage($"Title of job length must be less than {ConfigurationConstraints.JobTitleLength}");
RuleFor(r => r.PlaceOfWork)
.NotEmpty()
.SetValidator(placeOfWorkModelValidator);
}
}

View File

@@ -0,0 +1,17 @@
using ApplicationLayer.Services.AuthServices.Common;
using ApplicationLayer.Services.AuthServices.NeededServices;
using FluentValidation;
namespace ApplicationLayer.Services.AuthServices.Requests.Validation;
public class RegisterRequestValidator : AbstractValidator<RegisterRequest>
{
public RegisterRequestValidator(IUsersRepository users, IValidator<AuthData> authDataValidator)
{
RuleFor(r => r.AuthData)
.NotEmpty()
.SetValidator(authDataValidator)
.MustAsync(async (authData, ct) => await users.FindByEmailAsync(authData.Email, ct) is null)
.WithMessage("Email already exists");
}
}

View File

@@ -0,0 +1,6 @@
using ApplicationLayer.GeneralExceptions;
using Domains.Users;
namespace ApplicationLayer.Services.Users.Exceptions;
public class WrongRoleException(Guid userId) : EntityNotFoundByIdException<User>(userId);

View File

@@ -0,0 +1,27 @@
using ApplicationLayer.Services.Applicants.Models;
using ApplicationLayer.Services.Users.Models;
using ApplicationLayer.Services.Users.Requests;
namespace ApplicationLayer.Services.Users;
/// user accounts service
public interface IUsersService
{
/// Returns all user accounts with role of approving authority
/// <param name="cancellationToken">Cancellation token</param>
Task<List<UserModel>> GetAuthoritiesAccountsAsync(CancellationToken cancellationToken);
/// Changes authentication data for an authority account
/// <param name="request"> Request object with identifier of user and new authentication data</param>
/// <param name="cancellationToken">Cancellation token</param>
Task ChangeAuthorityAuthDataAsync(ChangeUserAuthDataRequest request, CancellationToken cancellationToken);
/// Removes account of authority
/// <param name="userId">Identifier of account</param>
/// <param name="cancellationToken">Cancellation token</param>
Task RemoveAuthorityAccount(Guid userId, CancellationToken cancellationToken);
/// Get applicant that made request
/// <param name="cancellationToken">cancellation token</param>
Task<ApplicantModel> GetAuthenticatedApplicant(CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,15 @@
using System.ComponentModel.DataAnnotations;
using Domains;
namespace ApplicationLayer.Services.Users.Models;
/// Auth data with nullable password for making change auth data requests
public class ChangeAuthData
{
[Required]
[MaxLength(ConfigurationConstraints.EmailLength)]
public string Email { get; set; } = null!;
[MaxLength(ConfigurationConstraints.PasswordLength)]
public string? Password { get; set; }
}

View File

@@ -0,0 +1,15 @@
using System.ComponentModel.DataAnnotations;
using Domains;
namespace ApplicationLayer.Services.Users.Models;
public class UserModel
{
/// Unique Identifier of user
[Required]
public Guid Id { get; private set; } = Guid.NewGuid();
[Required]
[MaxLength(ConfigurationConstraints.EmailLength)]
public string Email { get; set; } = null!;
}

View File

@@ -0,0 +1,11 @@
using System.ComponentModel.DataAnnotations;
using ApplicationLayer.Services.Users.Models;
namespace ApplicationLayer.Services.Users.Requests;
public class ChangeUserAuthDataRequest(Guid userId, ChangeAuthData newAuthData)
{
[Required] public Guid UserId { get; set; } = userId;
[Required] public ChangeAuthData NewAuthData { get; set; } = newAuthData;
}

View File

@@ -0,0 +1,23 @@
using Domains;
using FluentValidation;
namespace ApplicationLayer.Services.Users.Requests.Validation;
public class ChangeUserAuthDataRequestValidator : AbstractValidator<ChangeUserAuthDataRequest>
{
public ChangeUserAuthDataRequestValidator()
{
RuleFor(r => r.UserId)
.NotEmpty();
RuleFor(r => r.NewAuthData)
.NotEmpty();
RuleFor(r => r.NewAuthData.Email)
.NotEmpty()
.EmailAddress()
.WithMessage("Email should be valid")
.MaximumLength(ConfigurationConstraints.EmailLength)
.WithMessage($"Email address length must be less than {ConfigurationConstraints.EmailLength}");
}
}

View File

@@ -0,0 +1,85 @@
using ApplicationLayer.InfrastructureServicesInterfaces;
using ApplicationLayer.Services.Applicants.Models;
using ApplicationLayer.Services.Applicants.NeededServices;
using ApplicationLayer.Services.AuthServices.NeededServices;
using ApplicationLayer.Services.Users.Exceptions;
using ApplicationLayer.Services.Users.Models;
using ApplicationLayer.Services.Users.Requests;
using AutoMapper;
using Domains.Users;
namespace ApplicationLayer.Services.Users;
public class UsersService(
IMapper mapper,
IUserIdProvider userIdProvider,
IUsersRepository users,
IApplicantsRepository applicants,
IUnitOfWork unitOfWork) : IUsersService
{
async Task<List<UserModel>> IUsersService.GetAuthoritiesAccountsAsync(CancellationToken cancellationToken)
{
var userList = await users.GetAllOfRoleAsync(Role.ApprovingAuthority, cancellationToken);
return mapper.Map<List<UserModel>>(userList);
}
async Task IUsersService.ChangeAuthorityAuthDataAsync(ChangeUserAuthDataRequest request, CancellationToken cancellationToken)
{
var user = await users.GetByIdAsync(request.UserId, cancellationToken);
ValidateRole(user, Role.ApprovingAuthority);
await ChangeAccountAuthDataAsync(user, request.NewAuthData, cancellationToken);
}
async Task IUsersService.RemoveAuthorityAccount(Guid userId, CancellationToken cancellationToken)
{
var user = await users.GetByIdAsync(userId, cancellationToken);
ValidateRole(user, Role.ApprovingAuthority);
await RemoveUserAccount(user, cancellationToken);
}
async Task<ApplicantModel> IUsersService.GetAuthenticatedApplicant(CancellationToken cancellationToken)
{
var applicant = await applicants.FindByUserIdAsync(userIdProvider.GetUserId(), cancellationToken);
return mapper.Map<ApplicantModel>(applicant);
}
/// Updates user account auth data
/// <param name="user">User to remove</param>
/// <param name="authData">New auth data</param>
/// <param name="cancellationToken">Cancellation token</param>
private async Task ChangeAccountAuthDataAsync(User user, ChangeAuthData authData, CancellationToken cancellationToken)
{
user.Email = authData.Email;
user.Password = authData.Password ?? user.Password;
await users.UpdateAsync(user, cancellationToken);
await unitOfWork.SaveAsync(cancellationToken);
}
/// Removes user account from DB
/// <param name="user">User to remove</param>
/// <param name="cancellationToken">Cancellation token</param>
private async Task RemoveUserAccount(User user, CancellationToken cancellationToken)
{
users.Remove(user);
await unitOfWork.SaveAsync(cancellationToken);
}
/// Checks if role of user equals expected
/// <param name="user">User to check</param>
/// <param name="expectedRole">Expected role</param>
/// <exception cref="WrongRoleException">Role is not expected</exception>
private static void ValidateRole(User user, Role expectedRole)
{
if (user.Role != expectedRole)
{
throw new WrongRoleException(user.Id);
}
}
}

View File

@@ -0,0 +1,5 @@
using ApplicationLayer.GeneralExceptions;
namespace ApplicationLayer.Services.VisaApplications.Exceptions;
public class ApplicationAlreadyProcessedException() : ApiException("This application already processed or closed by applicant.");

View File

@@ -0,0 +1,31 @@
using ApplicationLayer.Services.VisaApplications.Models;
using ApplicationLayer.Services.VisaApplications.Requests;
namespace ApplicationLayer.Services.VisaApplications.Handlers;
public interface IVisaApplicationRequestsHandler
{
/// Returns all applications for approving authorities
Task<List<VisaApplicationPreview>> GetPendingAsync(CancellationToken cancellationToken);
/// Returns all applications of one applicant
Task<List<VisaApplicationPreview>> GetForApplicantAsync(CancellationToken cancellationToken);
/// Returns one application with specific id
Task<VisaApplicationModel> GetApplicationForApplicantAsync(Guid id, CancellationToken cancellationToken);
/// Returns one application with specific id
Task<VisaApplicationModel> GetApplicationForAuthorityAsync(Guid id, CancellationToken cancellationToken);
/// Creates application for applicant with specific user identifier
Task HandleCreateRequestAsync(VisaApplicationCreateRequest request, CancellationToken cancellationToken);
/// Sets application status to closed
Task HandleCloseRequestAsync(Guid applicationId, CancellationToken cancellationToken);
/// Sets application status to approved or rejected
Task SetApplicationStatusFromAuthorityAsync(Guid applicationId, AuthorityRequestStatuses status, CancellationToken cancellationToken);
/// Returns stream with file with formatted application data to download
Task<Stream> ApplicationToStreamAsync(Guid applicationId, CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,115 @@
using ApplicationLayer.InfrastructureServicesInterfaces;
using ApplicationLayer.Services.Applicants.Models;
using ApplicationLayer.Services.Applicants.NeededServices;
using ApplicationLayer.Services.VisaApplications.Exceptions;
using ApplicationLayer.Services.VisaApplications.Models;
using ApplicationLayer.Services.VisaApplications.NeededServices;
using ApplicationLayer.Services.VisaApplications.Requests;
using AutoMapper;
using Domains.VisaApplicationDomain;
namespace ApplicationLayer.Services.VisaApplications.Handlers;
/// Handles visa requests
public class VisaApplicationRequestsHandler(
IVisaApplicationsRepository applications,
IApplicantsRepository applicants,
IUnitOfWork unitOfWork,
IMapper mapper,
IDateTimeProvider dateTimeProvider,
IUserIdProvider userIdProvider,
IEntityWriter entityWriter) : IVisaApplicationRequestsHandler
{
async Task<List<VisaApplicationPreview>> IVisaApplicationRequestsHandler.GetPendingAsync(CancellationToken cancellationToken)
{
var applicationsList = await applications.GetPendingApplicationsAsync(cancellationToken);
var applicationModels = mapper.Map<List<VisaApplicationPreview>>(applicationsList);
return applicationModels;
}
async Task<List<VisaApplicationPreview>> IVisaApplicationRequestsHandler.GetForApplicantAsync(CancellationToken cancellationToken)
{
var applicantId = await applicants.GetApplicantIdByUserId(userIdProvider.GetUserId(), cancellationToken);
var visaApplications = await applications.GetOfApplicantAsync(applicantId, cancellationToken);
return mapper.Map<List<VisaApplicationPreview>>(visaApplications);
}
/// <inheritdoc cref="IVisaApplicationRequestsHandler.GetApplicationForApplicantAsync"/>
public async Task<VisaApplicationModel> GetApplicationForApplicantAsync(Guid id, CancellationToken cancellationToken)
{
var applicant = await applicants.FindByUserIdAsync(userIdProvider.GetUserId(), cancellationToken);
var application = await applications.GetByApplicantAndApplicationIdAsync(applicant.Id, id, cancellationToken);
var mapped = mapper.Map<VisaApplicationModel>(application);
mapped.Applicant = mapper.Map<ApplicantModel>(applicant);
return mapped;
}
async Task<VisaApplicationModel> IVisaApplicationRequestsHandler.GetApplicationForAuthorityAsync(Guid id, CancellationToken cancellationToken)
{
var pending = await applications.GetPendingApplicationsAsync(cancellationToken);
var application = pending.SingleOrDefault(a => a.Id == id) ?? throw new ApplicationAlreadyProcessedException();
var mapped = mapper.Map<VisaApplicationModel>(application);
var applicant = await applicants.GetByIdAsync(application.ApplicantId, cancellationToken);
mapped.Applicant = mapper.Map<ApplicantModel>(applicant);
return mapped;
}
async Task IVisaApplicationRequestsHandler.HandleCreateRequestAsync(VisaApplicationCreateRequest request, CancellationToken cancellationToken)
{
var applicant = await applicants.FindByUserIdAsync(userIdProvider.GetUserId(), cancellationToken);
var visaApplication = mapper.Map<VisaApplication>(request);
visaApplication.RequestDate = dateTimeProvider.Now();
visaApplication.ApplicantId = applicant.Id;
await applications.AddAsync(visaApplication, cancellationToken);
await unitOfWork.SaveAsync(cancellationToken);
}
async Task IVisaApplicationRequestsHandler.HandleCloseRequestAsync(Guid applicationId, CancellationToken cancellationToken)
{
var applicantId = await applicants.GetApplicantIdByUserId(userIdProvider.GetUserId(), cancellationToken);
var application = await applications.GetByApplicantAndApplicationIdAsync(applicantId, applicationId, cancellationToken);
if (application.Status is ApplicationStatus.Approved or ApplicationStatus.Rejected)
{
throw new ApplicationAlreadyProcessedException();
}
application.Status = ApplicationStatus.Closed;
await applications.UpdateAsync(application, cancellationToken);
await unitOfWork.SaveAsync(cancellationToken);
}
async Task IVisaApplicationRequestsHandler.SetApplicationStatusFromAuthorityAsync(
Guid applicationId,
AuthorityRequestStatuses status,
CancellationToken cancellationToken)
{
var application = await applications.GetByIdAsync(applicationId, cancellationToken);
if (application.Status != ApplicationStatus.Pending)
{
throw new ApplicationAlreadyProcessedException();
}
var statusToSet = status switch
{
AuthorityRequestStatuses.Approved => ApplicationStatus.Approved,
AuthorityRequestStatuses.Rejected => ApplicationStatus.Rejected,
_ => throw new ArgumentOutOfRangeException(nameof(status), status, null)
};
application.Status = statusToSet;
await applications.UpdateAsync(application, cancellationToken);
await unitOfWork.SaveAsync(cancellationToken);
}
async Task<Stream> IVisaApplicationRequestsHandler.ApplicationToStreamAsync(Guid applicationId, CancellationToken cancellationToken)
{
var application = await GetApplicationForApplicantAsync(applicationId, cancellationToken);
return await entityWriter.WriteEntityToStream(application, cancellationToken);
}
}

View File

@@ -0,0 +1,7 @@
namespace ApplicationLayer.Services.VisaApplications.Models;
public enum AuthorityRequestStatuses
{
Approved,
Rejected
}

View File

@@ -0,0 +1,21 @@
using System.ComponentModel.DataAnnotations;
using Domains;
namespace ApplicationLayer.Services.VisaApplications.Models;
/// Model of past visa for presentation layer
public class PastVisaModel
{
// Date of issue
[Required]
public DateTime IssueDate { get; set; }
/// Name of visa
[MaxLength(ConfigurationConstraints.VisaNameLength)]
[Required]
public string Name { get; set; } = null!;
/// Date when visa expires
[Required]
public DateTime ExpirationDate { get; set; }
}

View File

@@ -0,0 +1,21 @@
using System.ComponentModel.DataAnnotations;
using Domains;
namespace ApplicationLayer.Services.VisaApplications.Models;
/// Model of past visit for presentation layer
public class PastVisitModel
{
/// First day of past visit
[Required]
public DateTime StartDate { get; set; }
/// Last day of past visit
[Required]
public DateTime EndDate { get; set; }
/// Destination country of past visit
[MaxLength(ConfigurationConstraints.CountryNameLength)]
[Required]
public string DestinationCountry { get; set; } = null!;
}

View File

@@ -0,0 +1,17 @@
using System.ComponentModel.DataAnnotations;
using Domains;
namespace ApplicationLayer.Services.VisaApplications.Models;
/// Model of permission to destination country for presentation layer
public class PermissionToDestCountryModel
{
/// Date when permission to destination country expires
[Required]
public DateTime ExpirationDate { get; set; }
/// Issuing authority
[MaxLength(ConfigurationConstraints.IssuerNameLength)]
[Required]
public string Issuer { get; set; } = null!;
}

View File

@@ -0,0 +1,17 @@
using System.ComponentModel.DataAnnotations;
using Domains;
namespace ApplicationLayer.Services.VisaApplications.Models;
/// Model of re-entry permit for presentation layer
public class ReentryPermitModel
{
/// Number of re-entry permit
[MaxLength(ConfigurationConstraints.ReentryPermitNumberLength)]
[Required]
public string Number { get; set; } = null!;
/// Date when re-entry permit expires
[Required]
public DateTime ExpirationDate { get; set; }
}

View File

@@ -0,0 +1,31 @@
using ApplicationLayer.InfrastructureServicesInterfaces;
using Domains;
using FluentValidation;
namespace ApplicationLayer.Services.VisaApplications.Models.Validation;
public class PastVisaModelValidator : AbstractValidator<PastVisaModel>
{
public PastVisaModelValidator(IDateTimeProvider dateTimeProvider)
{
RuleFor(v => v.ExpirationDate)
.NotEmpty()
.WithMessage("Expiration date of past visa can not be empty")
.GreaterThan(v => v.IssueDate)
.WithMessage("Past visa expiration date can not be earlier than issue date");
RuleFor(v => v.IssueDate)
.NotEmpty()
.WithMessage("Issue date of past visa can not be empty")
.LessThan(dateTimeProvider.Now())
.WithMessage("Issue date of past visa must be in past");
RuleFor(v => v.Name)
.NotEmpty()
.WithMessage("Name of past visa can not be empty")
.Matches(Constants.EnglishPhraseRegex)
.WithMessage("Name of past visa can contain only english letters, digits and special symbols")
.MaximumLength(ConfigurationConstraints.VisaNameLength)
.WithMessage($"Past visa name length must be less than {ConfigurationConstraints.VisaNameLength}");
}
}

View File

@@ -0,0 +1,31 @@
using ApplicationLayer.InfrastructureServicesInterfaces;
using Domains;
using FluentValidation;
namespace ApplicationLayer.Services.VisaApplications.Models.Validation;
public class PastVisitModelValidator : AbstractValidator<PastVisitModel>
{
public PastVisitModelValidator(IDateTimeProvider dateTimeProvider)
{
RuleFor(v => v.StartDate)
.NotEmpty()
.WithMessage("Start date of past visit can not be empty")
.LessThan(v => v.EndDate)
.WithMessage("Start date of past visit must be earlier than end date")
.LessThan(dateTimeProvider.Now())
.WithMessage("Start date of past visit must be in past");
RuleFor(v => v.EndDate)
.NotEmpty()
.WithMessage("End date of past visit can not be empty");
RuleFor(v => v.DestinationCountry)
.NotEmpty()
.WithMessage("Destination Country of past visit can not be null")
.Matches(Constants.EnglishPhraseRegex)
.WithMessage("Destination Country of past visit can contain only english letters, digits and special symbols")
.MaximumLength(ConfigurationConstraints.CountryNameLength)
.WithMessage($"Destination Country of past visit length must be less than {ConfigurationConstraints.CountryNameLength}");
}
}

View File

@@ -0,0 +1,25 @@
using ApplicationLayer.InfrastructureServicesInterfaces;
using Domains;
using FluentValidation;
namespace ApplicationLayer.Services.VisaApplications.Models.Validation;
public class PermissionToDestCountryModelValidator : AbstractValidator<PermissionToDestCountryModel?>
{
public PermissionToDestCountryModelValidator(IDateTimeProvider dateTimeProvider)
{
RuleFor(p => p!.ExpirationDate)
.NotEmpty()
.WithMessage("Expiration date of permission to destination Country can not be empty")
.GreaterThan(dateTimeProvider.Now())
.WithMessage("Permission to destination Country must not be expired");
RuleFor(p => p!.Issuer)
.NotEmpty()
.WithMessage("Issuer of permission for destination Country can not be empty")
.Matches(Constants.EnglishPhraseRegex)
.WithMessage("Issuer of permission for destination Country can contain only english letters, digits and special symbols")
.MaximumLength(ConfigurationConstraints.IssuerNameLength)
.WithMessage($"Issuer of permission to destination Country length must be less than {ConfigurationConstraints.IssuerNameLength}");
}
}

View File

@@ -0,0 +1,25 @@
using ApplicationLayer.InfrastructureServicesInterfaces;
using Domains;
using FluentValidation;
namespace ApplicationLayer.Services.VisaApplications.Models.Validation;
public class ReentryPermitModelValidator : AbstractValidator<ReentryPermitModel?>
{
public ReentryPermitModelValidator(IDateTimeProvider dateTimeProvider)
{
RuleFor(p => p!.Number)
.NotEmpty()
.WithMessage("Re-entry permit number can not be empty")
.Matches(Constants.EnglishPhraseRegex)
.WithMessage("Re-entry permit number can contain only english letters, digits and special symbols")
.MaximumLength(ConfigurationConstraints.ReentryPermitNumberLength)
.WithMessage($"Re-entry permit number length must be less than {ConfigurationConstraints.ReentryPermitNumberLength}");
RuleFor(p => p!.ExpirationDate)
.NotEmpty()
.WithMessage("Re-entry permit expiration date can not be empty")
.GreaterThan(dateTimeProvider.Now())
.WithMessage("Re-entry permit must not be expired");
}
}

View File

@@ -0,0 +1,89 @@
using System.ComponentModel.DataAnnotations;
using System.Text;
using ApplicationLayer.Services.Applicants.Models;
using Domains.VisaApplicationDomain;
namespace ApplicationLayer.Services.VisaApplications.Models;
/// Model of <see cref="VisaApplication" /> with applicant property
public class VisaApplicationModel
{
/// <inheritdoc cref="VisaApplication.Id" />
[Required]
public Guid Id { get; set; }
/// Applicant of application
[Required]
public ApplicantModel Applicant { get; set; } = null!;
/// <inheritdoc cref="VisaApplication.Status" />
[Required]
public ApplicationStatus Status { get; set; }
/// <inheritdoc cref="VisaApplication.ReentryPermit" />
public ReentryPermitModel? ReentryPermit { get; set; }
/// <inheritdoc cref="VisaApplication.DestinationCountry" />
[Required]
public string DestinationCountry { get; set; } = null!;
/// <inheritdoc cref="VisaApplication.PastVisas" />
[Required]
public List<PastVisaModel> PastVisas { get; set; } = null!;
/// <inheritdoc cref="VisaApplication.PermissionToDestCountry" />
public PermissionToDestCountryModel? PermissionToDestCountry { get; set; }
[Required] public List<PastVisitModel> PastVisits { get; set; } = null!;
/// <inheritdoc cref="VisaApplication.VisaCategory" />
[Required]
public VisaCategory VisaCategory { get; set; }
/// <inheritdoc cref="VisaApplication.ForGroup" />
[Required]
public bool ForGroup { get; set; }
/// <inheritdoc cref="VisaApplication.RequestedNumberOfEntries" />
[Required]
public RequestedNumberOfEntries RequestedNumberOfEntries { get; set; }
/// <inheritdoc cref="VisaApplication.RequestDate" />
[Required]
public DateTime RequestDate { get; set; }
/// <inheritdoc cref="VisaApplication.ValidDaysRequested" />
[Required]
public int ValidDaysRequested { get; set; }
public string ForGroupToString() => ForGroup ? "For group" : "Individual";
public string PastVisasToString()
{
var stringBuilder = new StringBuilder();
foreach (var visa in PastVisas)
{
stringBuilder.AppendLine($"{visa.Name} issued at {visa.IssueDate.ToShortDateString()} and valid for {visa.ExpirationDate.ToShortDateString()}");
}
return stringBuilder.ToString();
}
public string PastVisitsToString()
{
var stringBuilder = new StringBuilder();
foreach (var visit in PastVisits)
{
stringBuilder.AppendLine($"Visit to {visit.DestinationCountry} started at {visit.StartDate.ToShortDateString()} and ends {visit.EndDate.ToShortDateString()}");
}
return stringBuilder.ToString();
}
public string PermissionToDestCountryToString()
{
return VisaCategory is VisaCategory.Transit
? $"Issued by{PermissionToDestCountry!.Issuer}, expires at {PermissionToDestCountry.ExpirationDate.ToShortDateString()}"
: "Non-transit";
}
}

View File

@@ -0,0 +1,32 @@
using System.ComponentModel.DataAnnotations;
using Domains.VisaApplicationDomain;
namespace ApplicationLayer.Services.VisaApplications.Models;
/// Model of <see cref="VisaApplication" />
public class VisaApplicationPreview
{
/// <inheritdoc cref="VisaApplication.Id" />
[Required]
public Guid Id { get; set; }
/// <inheritdoc cref="VisaApplication.Status" />
[Required]
public ApplicationStatus Status { get; set; }
/// <inheritdoc cref="VisaApplication.DestinationCountry" />
[Required]
public string DestinationCountry { get; set; } = null!;
/// <inheritdoc cref="VisaApplication.VisaCategory" />
[Required]
public VisaCategory VisaCategory { get; set; }
/// <inheritdoc cref="VisaApplication.RequestDate" />
[Required]
public DateTime RequestDate { get; set; }
/// <inheritdoc cref="VisaApplication.ValidDaysRequested" />
[Required]
public int ValidDaysRequested { get; set; }
}

View File

@@ -0,0 +1,7 @@
namespace ApplicationLayer.Services.VisaApplications.NeededServices
{
public interface IEntityWriter
{
Task<Stream> WriteEntityToStream(object entity, CancellationToken cancellationToken);
}
}

View File

@@ -0,0 +1,16 @@
using ApplicationLayer.InfrastructureServicesInterfaces;
using Domains.VisaApplicationDomain;
namespace ApplicationLayer.Services.VisaApplications.NeededServices;
public interface IVisaApplicationsRepository : IGenericRepository<VisaApplication>
{
/// Get applications of one applicant
Task<List<VisaApplication>> GetOfApplicantAsync(Guid applicantId, CancellationToken cancellationToken);
/// Get specific application of specific user
Task<VisaApplication> GetByApplicantAndApplicationIdAsync(Guid applicantId, Guid applicationId, CancellationToken cancellationToken);
/// Returns pending applications for approving authorities
Task<List<VisaApplication>> GetPendingApplicationsAsync(CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,55 @@
using ApplicationLayer.InfrastructureServicesInterfaces;
using ApplicationLayer.Services.Applicants.NeededServices;
using ApplicationLayer.Services.VisaApplications.Models;
using Domains;
using Domains.VisaApplicationDomain;
using FluentValidation;
namespace ApplicationLayer.Services.VisaApplications.Requests.Validation;
public class VisaApplicationCreateRequestValidator : AbstractValidator<VisaApplicationCreateRequest>
{
public VisaApplicationCreateRequestValidator(
IValidator<ReentryPermitModel?> reentryPermitModelValidator,
IValidator<PastVisaModel> pastVisaModelValidator,
IValidator<PermissionToDestCountryModel?> permissionToDestCountryModelValidator,
IValidator<PastVisitModel> pastVisitModelValidator,
IApplicantsRepository applicants,
IUserIdProvider userIdProvider)
{
RuleFor(r => r.PermissionToDestCountry)
.NotEmpty()
.WithMessage("For transit you must provide permission to destination country")
.SetValidator(permissionToDestCountryModelValidator)
.When(r => r.VisaCategory is VisaCategory.Transit);
RuleFor(r => r.ReentryPermit)
.NotEmpty()
.WithMessage("Non-residents must provide re-entry permission")
.SetValidator(reentryPermitModelValidator)
.WhenAsync(async (_, ct) =>
await applicants.IsApplicantNonResidentByUserId(userIdProvider.GetUserId(), ct));
RuleFor(r => r.DestinationCountry)
.NotEmpty()
.WithMessage("Destination country can not be empty");
RuleFor(r => r.VisaCategory)
.IsInEnum();
RuleFor(r => r.RequestedNumberOfEntries)
.IsInEnum();
RuleFor(r => r.ValidDaysRequested)
.GreaterThan(0)
.WithMessage($"Valid days requested should be positive number and less than {ConfigurationConstraints.MaxValidDays}")
.LessThanOrEqualTo(ConfigurationConstraints.MaxValidDays)
.WithMessage($"Valid days requested must be less than or equal to {ConfigurationConstraints.MaxValidDays}");
RuleForEach(r => r.PastVisas)
.SetValidator(pastVisaModelValidator);
RuleForEach(r => r.PastVisits)
.SetValidator(pastVisitModelValidator);
}
}

View File

@@ -0,0 +1,37 @@
using System.ComponentModel.DataAnnotations;
using ApplicationLayer.Services.VisaApplications.Models;
using Domains;
using Domains.VisaApplicationDomain;
namespace ApplicationLayer.Services.VisaApplications.Requests;
/// Model of visa request from user
public class VisaApplicationCreateRequest
{
public ReentryPermitModel? ReentryPermit { get; set; }
[Required]
[MaxLength(ConfigurationConstraints.CountryNameLength)]
public string DestinationCountry { get; set; } = null!;
[Required]
public VisaCategory VisaCategory { get; set; }
[Required]
public bool IsForGroup { get; set; }
[Required]
public RequestedNumberOfEntries RequestedNumberOfEntries { get; set; }
[Required]
[Range(0, ConfigurationConstraints.MaxValidDays)]
public int ValidDaysRequested { get; set; }
[Required]
public PastVisaModel[] PastVisas { get; set; } = null!;
public PermissionToDestCountryModel? PermissionToDestCountry { get; set; }
[Required]
public PastVisitModel[] PastVisits { get; set; } = null!;
}