Compare commits

...

7 Commits

Author SHA1 Message Date
5fc9308ada
add inheritance and enums configuration to in memory db context
All checks were successful
/ tests (push) Successful in 31s
/ build (push) Successful in 7m9s
/ build-docker (push) Successful in 5m34s
2025-05-20 10:33:35 +03:00
674130c52a
add ticket group creation 2025-05-20 10:32:39 +03:00
5982fa7285
add employee management 2025-05-16 15:22:44 +03:00
f4611f029f
add companies management 2025-05-15 19:18:52 +03:00
74dc7ceff3
rename user setings services 2025-05-14 17:48:48 +03:00
b1aceac750
add currency converter service and integrated it with vehicle enrollment management 2025-05-14 17:43:10 +03:00
5ee8c9c5df
add vehicle enrollments management 2025-05-11 10:51:19 +03:00
246 changed files with 10952 additions and 202 deletions

View File

@ -9,7 +9,7 @@ public class AddAddressCommandValidator : AbstractValidator<AddAddressCommand>
{
public AddAddressCommandValidator(
IStringLocalizer localizer,
CultureService cultureService)
SessionCultureService cultureService)
{
RuleFor(v => v.Name)
.NotEmpty()

View File

@ -8,7 +8,7 @@ public class UpdateAddressCommandValidator : AbstractValidator<UpdateAddressComm
{
public UpdateAddressCommandValidator(
IStringLocalizer localizer,
CultureService cultureService)
SessionCultureService cultureService)
{
RuleFor(v => v.Guid)
.NotEmpty()

View File

@ -8,7 +8,7 @@ public class GetAddressesPageQueryValidator : AbstractValidator<GetAddressesPage
{
public GetAddressesPageQueryValidator(
IStringLocalizer localizer,
CultureService cultureService)
SessionCultureService cultureService)
{
RuleFor(v => v.PageNumber)
.GreaterThanOrEqualTo(1)

View File

@ -13,11 +13,16 @@ public sealed class AircraftDto : IMapFrom<Aircraft>
public short Capacity { get; set; }
public Guid CompanyUuid { get; set; }
public void Mapping(MappingProfile profile)
{
profile.CreateMap<Aircraft, AircraftDto>()
.ForMember(
d => d.Uuid,
opt => opt.MapFrom(s => s.Guid));
opt => opt.MapFrom(s => s.Guid))
.ForMember(
d => d.CompanyUuid,
opt => opt.MapFrom(s => s.Company.Guid));
}
}

View File

@ -9,4 +9,6 @@ public record AddAircraftCommand : IRequest<AircraftDto>
public string Model { get; set; }
public short Capacity { get; set; }
public Guid CompanyGuid { get; set; }
}

View File

@ -33,11 +33,23 @@ public class AddAircraftCommandHandler :
"Aircraft with given number already exists.");
}
var parentEntity = await _unitOfWork.CompanyRepository.GetOneAsync(
e => e.Guid == request.CompanyGuid, cancellationToken);
if (parentEntity == null)
{
throw new NotFoundException(
$"Parent entity with Guid: {request.CompanyGuid} not found.");
}
entity = new Aircraft()
{
Number = request.Number,
Model = request.Model,
Capacity = request.Capacity
Capacity = request.Capacity,
CompanyId = parentEntity.Id,
Company = parentEntity
};
entity = await _unitOfWork.AircraftRepository.AddOneAsync(

View File

@ -8,7 +8,7 @@ public class AddAircraftCommandValidator : AbstractValidator<AddAircraftCommand>
{
public AddAircraftCommandValidator(
IStringLocalizer localizer,
CultureService cultureService)
SessionCultureService cultureService)
{
RuleFor(v => v.Number)
.NotEmpty()
@ -33,5 +33,9 @@ public class AddAircraftCommandValidator : AbstractValidator<AddAircraftCommand>
RuleFor(v => v.Capacity)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
RuleFor(v => v.CompanyGuid)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
}
}

View File

@ -11,4 +11,6 @@ public record UpdateAircraftCommand : IRequest<AircraftDto>
public string Model { get; set; }
public short Capacity { get; set; }
public Guid CompanyGuid { get; set; }
}

View File

@ -32,7 +32,7 @@ public class UpdateAircraftCommandHandler :
}
var duplicateEntity = await _unitOfWork.AircraftRepository.GetOneAsync(
e => e.Number == request.Number,
e => e.Number == request.Number && e.Guid != request.Guid,
cancellationToken);
if (duplicateEntity != null)
@ -41,9 +41,20 @@ public class UpdateAircraftCommandHandler :
"Aircraft with given number already exists.");
}
var parentEntity = await _unitOfWork.CompanyRepository.GetOneAsync(
e => e.Guid == request.CompanyGuid, cancellationToken);
if (parentEntity == null)
{
throw new NotFoundException(
$"Parent entity with Guid: {request.CompanyGuid} not found.");
}
entity.Number = request.Number;
entity.Model = request.Model;
entity.Capacity = request.Capacity;
entity.CompanyId = parentEntity.Id;
entity.Company = parentEntity;
entity = await _unitOfWork.AircraftRepository.UpdateOneAsync(
entity, cancellationToken);

View File

@ -8,7 +8,7 @@ public class UpdateAircraftCommandValidator : AbstractValidator<UpdateAircraftCo
{
public UpdateAircraftCommandValidator(
IStringLocalizer localizer,
CultureService cultureService)
SessionCultureService cultureService)
{
RuleFor(v => v.Guid)
.NotEmpty()
@ -37,5 +37,9 @@ public class UpdateAircraftCommandValidator : AbstractValidator<UpdateAircraftCo
RuleFor(v => v.Capacity)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
RuleFor(v => v.CompanyGuid)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
}
}

View File

@ -24,7 +24,8 @@ public class GetAircraftQueryHandler :
CancellationToken cancellationToken)
{
var entity = await _unitOfWork.AircraftRepository.GetOneAsync(
e => e.Guid == request.Guid, cancellationToken);
e => e.Guid == request.Guid, e => e.Company,
cancellationToken);
_unitOfWork.Dispose();

View File

@ -13,11 +13,9 @@ public record GetAircraftsPageQuery : IRequest<PaginatedList<AircraftDto>>
public string Sort { get; set; } = String.Empty;
public string? Number { get; set; }
public Guid? CompanyGuid { get; set; }
public string? Model { get; set; }
public short? CapacityGreaterThanOrEqualTo { get; set; }
public short? CapacityGreaterOrEqualThan { get; set; }
public short? CapacityLessOrEqualThan { get; set; }
public short? CapacityLessThanOrEqualTo { get; set; }
}

View File

@ -28,12 +28,16 @@ public class GetAircraftsPageQueryHandler :
e =>
(e.Number.ToLower().Contains(request.Search.ToLower()) ||
e.Model.ToLower().Contains(request.Search.ToLower())) &&
(request.CapacityGreaterOrEqualThan != null
? e.Capacity >= request.CapacityGreaterOrEqualThan
(request.CompanyGuid != null
? e.Company.Guid == request.CompanyGuid
: true) &&
(request.CapacityLessOrEqualThan != null
? e.Capacity <= request.CapacityLessOrEqualThan
(request.CapacityGreaterThanOrEqualTo != null
? e.Capacity >= request.CapacityGreaterThanOrEqualTo
: true) &&
(request.CapacityLessThanOrEqualTo != null
? e.Capacity <= request.CapacityLessThanOrEqualTo
: true),
e => e.Company,
request.PageNumber, request.PageSize,
cancellationToken);

View File

@ -8,7 +8,7 @@ public class GetAircraftsPageQueryValidator : AbstractValidator<GetAircraftsPage
{
public GetAircraftsPageQueryValidator(
IStringLocalizer localizer,
CultureService cultureService)
SessionCultureService cultureService)
{
RuleFor(v => v.PageNumber)
.GreaterThanOrEqualTo(1)

View File

@ -7,4 +7,6 @@ public sealed class AddAircraftViewModel
public string Model { get; set; }
public short Capacity { get; set; }
public Guid CompanyUuid { get; set; }
}

View File

@ -2,14 +2,12 @@ namespace cuqmbr.TravelGuide.Application.Aircrafts.ViewModels;
public sealed class GetAircraftsPageFilterViewModel
{
public string? Number { get; set; }
public string? Model { get; set; }
public Guid? CompanyUuid { get; set; }
// TODO: Consider adding strict equals rule although it is not
// necessarily needed to filter with exact capacity
public short? CapacityGreaterOrEqualThan { get; set; }
public short? CapacityGreaterThanOrEqualTo { get; set; }
public short? CapacityLessOrEqualThan { get; set; }
public short? CapacityLessThanOrEqualTo { get; set; }
}

View File

@ -7,4 +7,6 @@ public sealed class UpdateAircraftViewModel
public string Model { get; set; }
public short Capacity { get; set; }
public Guid CompanyUuid { get; set; }
}

View File

@ -13,11 +13,16 @@ public sealed class BusDto : IMapFrom<Bus>
public short Capacity { get; set; }
public Guid CompanyUuid { get; set; }
public void Mapping(MappingProfile profile)
{
profile.CreateMap<Bus, BusDto>()
.ForMember(
d => d.Uuid,
opt => opt.MapFrom(s => s.Guid));
opt => opt.MapFrom(s => s.Guid))
.ForMember(
d => d.CompanyUuid,
opt => opt.MapFrom(s => s.Company.Guid));
}
}

View File

@ -9,4 +9,6 @@ public record AddBusCommand : IRequest<BusDto>
public string Model { get; set; }
public short Capacity { get; set; }
public Guid CompanyGuid { get; set; }
}

View File

@ -33,11 +33,22 @@ public class AddBusCommandHandler :
"Bus with given number already exists.");
}
var parentEntity = await _unitOfWork.CompanyRepository.GetOneAsync(
e => e.Guid == request.CompanyGuid, cancellationToken);
if (parentEntity == null)
{
throw new NotFoundException(
$"Parent entity with Guid: {request.CompanyGuid} not found.");
}
entity = new Bus()
{
Number = request.Number,
Model = request.Model,
Capacity = request.Capacity
Capacity = request.Capacity,
CompanyId = parentEntity.Id,
Company = parentEntity
};
entity = await _unitOfWork.BusRepository.AddOneAsync(

View File

@ -8,7 +8,7 @@ public class AddBusCommandValidator : AbstractValidator<AddBusCommand>
{
public AddBusCommandValidator(
IStringLocalizer localizer,
CultureService cultureService)
SessionCultureService cultureService)
{
RuleFor(v => v.Number)
.NotEmpty()
@ -33,5 +33,9 @@ public class AddBusCommandValidator : AbstractValidator<AddBusCommand>
RuleFor(v => v.Capacity)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
RuleFor(v => v.CompanyGuid)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
}
}

View File

@ -11,4 +11,6 @@ public record UpdateBusCommand : IRequest<BusDto>
public string Model { get; set; }
public short Capacity { get; set; }
public Guid CompanyGuid { get; set; }
}

View File

@ -32,7 +32,7 @@ public class UpdateBusCommandHandler :
}
var duplicateEntity = await _unitOfWork.BusRepository.GetOneAsync(
e => e.Number == request.Number,
e => e.Number == request.Number && e.Guid != request.Guid,
cancellationToken);
if (duplicateEntity != null)
@ -41,9 +41,20 @@ public class UpdateBusCommandHandler :
"Bus with given number already exists.");
}
var parentEntity = await _unitOfWork.CompanyRepository.GetOneAsync(
e => e.Guid == request.CompanyGuid, cancellationToken);
if (parentEntity == null)
{
throw new NotFoundException(
$"Parent entity with Guid: {request.CompanyGuid} not found.");
}
entity.Number = request.Number;
entity.Model = request.Model;
entity.Capacity = request.Capacity;
entity.CompanyId = parentEntity.Id;
entity.Company = parentEntity;
entity = await _unitOfWork.BusRepository.UpdateOneAsync(
entity, cancellationToken);

View File

@ -8,7 +8,7 @@ public class UpdateBusCommandValidator : AbstractValidator<UpdateBusCommand>
{
public UpdateBusCommandValidator(
IStringLocalizer localizer,
CultureService cultureService)
SessionCultureService cultureService)
{
RuleFor(v => v.Guid)
.NotEmpty()
@ -37,5 +37,9 @@ public class UpdateBusCommandValidator : AbstractValidator<UpdateBusCommand>
RuleFor(v => v.Capacity)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
RuleFor(v => v.CompanyGuid)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
}
}

View File

@ -24,7 +24,8 @@ public class GetBusQueryHandler :
CancellationToken cancellationToken)
{
var entity = await _unitOfWork.BusRepository.GetOneAsync(
e => e.Guid == request.Guid, cancellationToken);
e => e.Guid == request.Guid, e => e.Company,
cancellationToken);
_unitOfWork.Dispose();

View File

@ -13,11 +13,9 @@ public record GetBusesPageQuery : IRequest<PaginatedList<BusDto>>
public string Sort { get; set; } = String.Empty;
public string? Number { get; set; }
public Guid? CompanyGuid { get; set; }
public string? Model { get; set; }
public short? CapacityGreaterThanOrEqualTo { get; set; }
public short? CapacityGreaterOrEqualThan { get; set; }
public short? CapacityLessOrEqualThan { get; set; }
public short? CapacityLessThanOrEqualTo { get; set; }
}

View File

@ -28,12 +28,16 @@ public class GetBusesPageQueryHandler :
e =>
(e.Number.ToLower().Contains(request.Search.ToLower()) ||
e.Model.ToLower().Contains(request.Search.ToLower())) &&
(request.CapacityGreaterOrEqualThan != null
? e.Capacity >= request.CapacityGreaterOrEqualThan
(request.CompanyGuid != null
? e.Company.Guid == request.CompanyGuid
: true) &&
(request.CapacityLessOrEqualThan != null
? e.Capacity <= request.CapacityLessOrEqualThan
(request.CapacityGreaterThanOrEqualTo != null
? e.Capacity >= request.CapacityGreaterThanOrEqualTo
: true) &&
(request.CapacityLessThanOrEqualTo != null
? e.Capacity <= request.CapacityLessThanOrEqualTo
: true),
e => e.Company,
request.PageNumber, request.PageSize,
cancellationToken);

View File

@ -8,7 +8,7 @@ public class GetBusesPageQueryValidator : AbstractValidator<GetBusesPageQuery>
{
public GetBusesPageQueryValidator(
IStringLocalizer localizer,
CultureService cultureService)
SessionCultureService cultureService)
{
RuleFor(v => v.PageNumber)
.GreaterThanOrEqualTo(1)

View File

@ -7,4 +7,6 @@ public sealed class AddBusViewModel
public string Model { get; set; }
public short Capacity { get; set; }
public Guid CompanyUuid { get; set; }
}

View File

@ -2,14 +2,9 @@ namespace cuqmbr.TravelGuide.Application.Buses.ViewModels;
public sealed class GetBusesPageFilterViewModel
{
public string? Number { get; set; }
public Guid? CompanyUuid { get; set; }
public string? Model { get; set; }
public short? CapacityGreaterThanOrEqualTo { get; set; }
// TODO: Consider adding strict equals rule although it is not
// necessarily needed to filter with exact capacity
public short? CapacityGreaterOrEqualThan { get; set; }
public short? CapacityLessOrEqualThan { get; set; }
public short? CapacityLessThanOrEqualTo { get; set; }
}

View File

@ -7,4 +7,6 @@ public sealed class UpdateBusViewModel
public string Model { get; set; }
public short Capacity { get; set; }
public Guid CompanyUuid { get; set; }
}

View File

@ -8,7 +8,7 @@ public class AddCityCommandValidator : AbstractValidator<AddCityCommand>
{
public AddCityCommandValidator(
IStringLocalizer localizer,
CultureService cultureService)
SessionCultureService cultureService)
{
RuleFor(v => v.Name)
.NotEmpty()

View File

@ -8,7 +8,7 @@ public class UpdateCityCommandValidator : AbstractValidator<UpdateCityCommand>
{
public UpdateCityCommandValidator(
IStringLocalizer localizer,
CultureService cultureService)
SessionCultureService cultureService)
{
RuleFor(v => v.Guid)
.NotEmpty()

View File

@ -8,7 +8,7 @@ public class GetCitiesPageQueryValidator : AbstractValidator<GetCitiesPageQuery>
{
public GetCitiesPageQueryValidator(
IStringLocalizer localizer,
CultureService cultureService)
SessionCultureService cultureService)
{
RuleFor(v => v.PageNumber)
.GreaterThanOrEqualTo(1)

View File

@ -0,0 +1,55 @@
using FluentValidation;
namespace cuqmbr.TravelGuide.Application.Common.FluentValidation;
public static class CustomValidators
{
// According to RFC 5321.
public static IRuleBuilderOptions<T, string> IsEmail<T>(
this IRuleBuilder<T, string> ruleBuilder)
{
return
ruleBuilder
.Matches(@"^[\w\.-]{1,64}@[\w\.-]{1,251}\.\w{2,4}$");
}
// According to ITU-T E.164, no spaces.
public static IRuleBuilderOptions<T, string> IsPhoneNumber<T>(
this IRuleBuilder<T, string> ruleBuilder)
{
return
ruleBuilder
.Matches(@"^\+[0-9]{7,15}$");
}
public static IRuleBuilderOptions<T, ICollection<TSource>>
IsUnique<T, TSource, TResult>(
this IRuleBuilder<T, ICollection<TSource>> ruleBuilder,
Func<TSource, TResult> selector)
{
if (selector == null)
{
throw new ArgumentNullException(
nameof(selector),
"Cannot pass a null selector.");
}
return
ruleBuilder
.Must(x => x.IsDistinct(selector));
}
public static bool IsDistinct<TSource, TResult>(
this IEnumerable<TSource> elements, Func<TSource, TResult> selector)
{
var hashSet = new HashSet<TResult>();
foreach (var element in elements.Select(selector))
{
if (!hashSet.Contains(element))
hashSet.Add(element);
else
return false;
}
return true;
}
}

View File

@ -0,0 +1,6 @@
using cuqmbr.TravelGuide.Domain.Entities;
namespace cuqmbr.TravelGuide.Application.Common.Interfaces
.Persistence.Repositories;
public interface CompanyRepository : BaseRepository<Company> { }

View File

@ -0,0 +1,6 @@
using cuqmbr.TravelGuide.Domain.Entities;
namespace cuqmbr.TravelGuide.Application.Common.Interfaces
.Persistence.Repositories;
public interface EmployeeRepository : BaseRepository<Employee> { }

View File

@ -0,0 +1,7 @@
using cuqmbr.TravelGuide.Domain.Entities;
namespace cuqmbr.TravelGuide.Application.Common.Interfaces
.Persistence.Repositories;
public interface RouteAddressDetailRepository :
BaseRepository<RouteAddressDetail> { }

View File

@ -0,0 +1,7 @@
using cuqmbr.TravelGuide.Domain.Entities;
namespace cuqmbr.TravelGuide.Application.Common.Interfaces
.Persistence.Repositories;
public interface RouteAddressRepository :
BaseRepository<RouteAddress> { }

View File

@ -0,0 +1,6 @@
using cuqmbr.TravelGuide.Domain.Entities;
namespace cuqmbr.TravelGuide.Application.Common.Interfaces
.Persistence.Repositories;
public interface TicketGroupRepository : BaseRepository<TicketGroup> { }

View File

@ -0,0 +1,6 @@
using cuqmbr.TravelGuide.Domain.Entities;
namespace cuqmbr.TravelGuide.Application.Common.Interfaces
.Persistence.Repositories;
public interface TicketRepository : BaseRepository<Ticket> { }

View File

@ -0,0 +1,7 @@
using cuqmbr.TravelGuide.Domain.Entities;
namespace cuqmbr.TravelGuide.Application.Common.Interfaces
.Persistence.Repositories;
public interface VehicleEnrollmentRepository :
BaseRepository<VehicleEnrollment> { }

View File

@ -22,6 +22,20 @@ public interface UnitOfWork : IDisposable
TrainRepository TrainRepository { get; }
VehicleEnrollmentRepository VehicleEnrollmentRepository { get; }
RouteAddressRepository RouteAddressRepository { get; }
CompanyRepository CompanyRepository { get; }
EmployeeRepository EmployeeRepository { get; }
TicketGroupRepository TicketGroupRepository { get; }
TicketRepository TicketRepository { get; }
RouteAddressDetailRepository RouteAddressDetailRepository { get; }
int Save();
Task<int> SaveAsync(CancellationToken cancellationToken);

View File

@ -0,0 +1,12 @@
using cuqmbr.TravelGuide.Domain.Enums;
namespace cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
public interface CurrencyConverterService
{
Task<decimal> ConvertAsync(decimal amount, Currency from, Currency to,
CancellationToken cancellationToken);
Task<decimal> ConvertAsync(decimal amount, Currency from, Currency to,
DateTimeOffset time, CancellationToken cancellationToken);
}

View File

@ -0,0 +1,8 @@
using cuqmbr.TravelGuide.Domain.Enums;
namespace cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
public interface SessionCurrencyService
{
public Currency Currency { get; }
}

View File

@ -1,6 +1,6 @@
namespace cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
public interface TimeZoneService
public interface SessionTimeZoneService
{
public TimeZoneInfo TimeZone { get; }
}

View File

@ -2,7 +2,7 @@ using System.Globalization;
namespace cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
public interface CultureService
public interface SessionCultureService
{
public CultureInfo Culture { get; }
}

View File

@ -6,9 +6,9 @@ namespace cuqmbr.TravelGuide.Application.Common.Mappings.Resolvers;
public class DateTimeOffsetToLocalResolver :
IMemberValueResolver<object, object, DateTimeOffset, DateTimeOffset>
{
private readonly TimeZoneService _timeZoneService;
private readonly SessionTimeZoneService _timeZoneService;
public DateTimeOffsetToLocalResolver(TimeZoneService timeZoneService)
public DateTimeOffsetToLocalResolver(SessionTimeZoneService timeZoneService)
{
_timeZoneService = timeZoneService;
}

View File

@ -0,0 +1,14 @@
using MediatR;
namespace cuqmbr.TravelGuide.Application.Companies.Commands.AddCompany;
public record AddCompanyCommand : IRequest<CompanyDto>
{
public string Name { get; set; }
public string LegalAddress { get; set; }
public string ContactEmail { get; set; }
public string ContactPhoneNumber { get; set; }
}

View File

@ -0,0 +1,31 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using cuqmbr.TravelGuide.Application.Common.Models;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Companies.Commands.AddCompany;
public class AddCompanyCommandAuthorizer :
AbstractRequestAuthorizer<AddCompanyCommand>
{
private readonly SessionUserService _sessionUserService;
public AddCompanyCommandAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(AddCompanyCommand request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInRolesRequirement
{
RequiredRoles = [IdentityRole.Administrator],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,52 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence;
using cuqmbr.TravelGuide.Domain.Entities;
using AutoMapper;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
namespace cuqmbr.TravelGuide.Application.Companies.Commands.AddCompany;
public class AddCompanyCommandHandler :
IRequestHandler<AddCompanyCommand, CompanyDto>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public AddCompanyCommandHandler(
UnitOfWork unitOfWork,
IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
public async Task<CompanyDto> Handle(
AddCompanyCommand request,
CancellationToken cancellationToken)
{
var entity = await _unitOfWork.CompanyRepository.GetOneAsync(
e => e.Name == request.Name, cancellationToken);
if (entity != null)
{
throw new DuplicateEntityException(
"Company with given name already exists.");
}
entity = new Company()
{
Name = request.Name,
LegalAddress = request.LegalAddress,
ContactEmail = request.ContactEmail,
ContactPhoneNumber = request.ContactPhoneNumber
};
entity = await _unitOfWork.CompanyRepository.AddOneAsync(
entity, cancellationToken);
await _unitOfWork.SaveAsync(cancellationToken);
_unitOfWork.Dispose();
return _mapper.Map<CompanyDto>(entity);
}
}

View File

@ -0,0 +1,58 @@
using cuqmbr.TravelGuide.Application.Common.FluentValidation;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Companies.Commands.AddCompany;
public class AddCompanyCommandValidator : AbstractValidator<AddCompanyCommand>
{
public AddCompanyCommandValidator(
IStringLocalizer localizer,
SessionCultureService cultureService)
{
RuleFor(v => v.Name)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.MaximumLength(64)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
64));
RuleFor(v => v.LegalAddress)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.MaximumLength(256)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
256));
RuleFor(v => v.ContactEmail)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.IsEmail()
.WithMessage(localizer["FluentValidation.IsEmail"])
.MaximumLength(256)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
256));
RuleFor(v => v.ContactPhoneNumber)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.IsPhoneNumber()
.WithMessage(localizer["FluentValidation.IsPhoneNumber"])
.MaximumLength(64)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
64));
}
}

View File

@ -0,0 +1,8 @@
using MediatR;
namespace cuqmbr.TravelGuide.Application.Companies.Commands.DeleteCompany;
public record DeleteCompanyCommand : IRequest
{
public Guid Guid { get; set; }
}

View File

@ -0,0 +1,31 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using cuqmbr.TravelGuide.Application.Common.Models;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Companies.Commands.DeleteCompany;
public class DeleteCompanyCommandAuthorizer :
AbstractRequestAuthorizer<DeleteCompanyCommand>
{
private readonly SessionUserService _sessionUserService;
public DeleteCompanyCommandAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(DeleteCompanyCommand request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInRolesRequirement
{
RequiredRoles = [IdentityRole.Administrator],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,34 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
namespace cuqmbr.TravelGuide.Application.Companies.Commands.DeleteCompany;
public class DeleteCompanyCommandHandler : IRequestHandler<DeleteCompanyCommand>
{
private readonly UnitOfWork _unitOfWork;
public DeleteCompanyCommandHandler(UnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public async Task Handle(
DeleteCompanyCommand request,
CancellationToken cancellationToken)
{
var entity = await _unitOfWork.CompanyRepository.GetOneAsync(
e => e.Guid == request.Guid, cancellationToken);
if (entity == null)
{
throw new NotFoundException();
}
await _unitOfWork.CompanyRepository.DeleteOneAsync(
entity, cancellationToken);
await _unitOfWork.SaveAsync(cancellationToken);
_unitOfWork.Dispose();
}
}

View File

@ -0,0 +1,14 @@
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Companies.Commands.DeleteCompany;
public class DeleteCompanyCommandValidator : AbstractValidator<DeleteCompanyCommand>
{
public DeleteCompanyCommandValidator(IStringLocalizer localizer)
{
RuleFor(v => v.Guid)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
}
}

View File

@ -0,0 +1,16 @@
using MediatR;
namespace cuqmbr.TravelGuide.Application.Companies.Commands.UpdateCompany;
public record UpdateCompanyCommand : IRequest<CompanyDto>
{
public Guid Guid { get; set; }
public string Name { get; set; }
public string LegalAddress { get; set; }
public string ContactEmail { get; set; }
public string ContactPhoneNumber { get; set; }
}

View File

@ -0,0 +1,31 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using cuqmbr.TravelGuide.Application.Common.Models;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Companies.Commands.UpdateCompany;
public class UpdateCompanyCommandAuthorizer :
AbstractRequestAuthorizer<UpdateCompanyCommand>
{
private readonly SessionUserService _sessionUserService;
public UpdateCompanyCommandAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(UpdateCompanyCommand request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInRolesRequirement
{
RequiredRoles = [IdentityRole.Administrator],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,47 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence;
using AutoMapper;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
namespace cuqmbr.TravelGuide.Application.Companies.Commands.UpdateCompany;
public class UpdateCompanyCommandHandler :
IRequestHandler<UpdateCompanyCommand, CompanyDto>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public UpdateCompanyCommandHandler(
UnitOfWork unitOfWork,
IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
public async Task<CompanyDto> Handle(
UpdateCompanyCommand request,
CancellationToken cancellationToken)
{
var entity = await _unitOfWork.CompanyRepository.GetOneAsync(
e => e.Guid == request.Guid, cancellationToken);
if (entity == null)
{
throw new NotFoundException();
}
entity.Name = request.Name;
entity.LegalAddress = request.LegalAddress;
entity.ContactEmail = request.ContactEmail;
entity.ContactPhoneNumber = request.ContactPhoneNumber;
entity = await _unitOfWork.CompanyRepository.UpdateOneAsync(
entity, cancellationToken);
await _unitOfWork.SaveAsync(cancellationToken);
_unitOfWork.Dispose();
return _mapper.Map<CompanyDto>(entity);
}
}

View File

@ -0,0 +1,62 @@
using cuqmbr.TravelGuide.Application.Common.FluentValidation;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Companies.Commands.UpdateCompany;
public class UpdateCompanyCommandValidator : AbstractValidator<UpdateCompanyCommand>
{
public UpdateCompanyCommandValidator(
IStringLocalizer localizer,
SessionCultureService cultureService)
{
RuleFor(v => v.Guid)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
RuleFor(v => v.Name)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.MaximumLength(64)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
64));
RuleFor(v => v.LegalAddress)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.MaximumLength(256)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
256));
RuleFor(v => v.ContactEmail)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.IsEmail()
.WithMessage(localizer["FluentValidation.IsEmail"])
.MaximumLength(256)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
256));
RuleFor(v => v.ContactPhoneNumber)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.IsPhoneNumber()
.WithMessage(localizer["FluentValidation.IsPhoneNumber"])
.MaximumLength(64)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
64));
}
}

View File

@ -0,0 +1,25 @@
using cuqmbr.TravelGuide.Application.Common.Mappings;
using cuqmbr.TravelGuide.Domain.Entities;
namespace cuqmbr.TravelGuide.Application.Companies;
public sealed class CompanyDto : IMapFrom<Company>
{
public Guid Uuid { get; set; }
public string Name { get; set; }
public string LegalAddress { get; set; }
public string ContactEmail { get; set; }
public string ContactPhoneNumber { get; set; }
public void Mapping(MappingProfile profile)
{
profile.CreateMap<Company, CompanyDto>()
.ForMember(
d => d.Uuid,
opt => opt.MapFrom(s => s.Guid));
}
}

View File

@ -0,0 +1,15 @@
using cuqmbr.TravelGuide.Application.Common.Models;
using MediatR;
namespace cuqmbr.TravelGuide.Application.Companies.Queries.GetCompaniesPage;
public record GetCompaniesPageQuery : IRequest<PaginatedList<CompanyDto>>
{
public int PageNumber { get; set; } = 1;
public int PageSize { get; set; } = 10;
public string Search { get; set; } = String.Empty;
public string Sort { get; set; } = String.Empty;
}

View File

@ -0,0 +1,31 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using cuqmbr.TravelGuide.Application.Common.Models;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Companies.Queries.GetCompaniesPage;
public class GetCompaniesPageQueryAuthorizer :
AbstractRequestAuthorizer<GetCompaniesPageQuery>
{
private readonly SessionUserService _sessionUserService;
public GetCompaniesPageQueryAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(GetCompaniesPageQuery request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInRolesRequirement
{
RequiredRoles = [IdentityRole.Administrator],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,49 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence;
using AutoMapper;
using cuqmbr.TravelGuide.Application.Common.Models;
using cuqmbr.TravelGuide.Application.Common.Extensions;
namespace cuqmbr.TravelGuide.Application.Companies.Queries.GetCompaniesPage;
public class GetCompaniesPageQueryHandler :
IRequestHandler<GetCompaniesPageQuery, PaginatedList<CompanyDto>>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public GetCompaniesPageQueryHandler(
UnitOfWork unitOfWork,
IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
public async Task<PaginatedList<CompanyDto>> Handle(
GetCompaniesPageQuery request,
CancellationToken cancellationToken)
{
var paginatedList = await _unitOfWork.CompanyRepository.GetPageAsync(
e =>
e.Name.ToLower().Contains(request.Search.ToLower()) ||
e.LegalAddress.ToLower().Contains(request.Search.ToLower()) ||
e.ContactEmail.ToLower().Contains(request.Search.ToLower()) ||
e.ContactPhoneNumber.ToLower().Contains(request.Search.ToLower()),
request.PageNumber, request.PageSize,
cancellationToken);
var mappedItems = _mapper
.ProjectTo<CompanyDto>(paginatedList.Items.AsQueryable());
mappedItems = QueryableExtension<CompanyDto>
.ApplySort(mappedItems, request.Sort);
_unitOfWork.Dispose();
return new PaginatedList<CompanyDto>(
mappedItems.ToList(),
paginatedList.TotalCount, request.PageNumber,
request.PageSize);
}
}

View File

@ -0,0 +1,43 @@
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Companies.Queries.GetCompaniesPage;
public class GetCompaniesPageQueryValidator : AbstractValidator<GetCompaniesPageQuery>
{
public GetCompaniesPageQueryValidator(
IStringLocalizer localizer,
SessionCultureService cultureService)
{
RuleFor(v => v.PageNumber)
.GreaterThanOrEqualTo(1)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.GreaterThanOrEqualTo"],
1));
RuleFor(v => v.PageSize)
.GreaterThanOrEqualTo(1)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.GreaterThanOrEqualTo"],
1))
.LessThanOrEqualTo(50)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.LessThanOrEqualTo"],
50));
RuleFor(v => v.Search)
.MaximumLength(64)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
64));
}
}

View File

@ -0,0 +1,8 @@
using MediatR;
namespace cuqmbr.TravelGuide.Application.Companies.Queries.GetCompany;
public record GetCompanyQuery : IRequest<CompanyDto>
{
public Guid Guid { get; set; }
}

View File

@ -0,0 +1,31 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using cuqmbr.TravelGuide.Application.Common.Models;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Companies.Queries.GetCompany;
public class GetCompanyQueryAuthorizer :
AbstractRequestAuthorizer<GetCompanyQuery>
{
private readonly SessionUserService _sessionUserService;
public GetCompanyQueryAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(GetCompanyQuery request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInRolesRequirement
{
RequiredRoles = [IdentityRole.Administrator],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,38 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
using AutoMapper;
namespace cuqmbr.TravelGuide.Application.Companies.Queries.GetCompany;
public class GetCompanyQueryHandler :
IRequestHandler<GetCompanyQuery, CompanyDto>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public GetCompanyQueryHandler(
UnitOfWork unitOfWork,
IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
public async Task<CompanyDto> Handle(
GetCompanyQuery request,
CancellationToken cancellationToken)
{
var entity = await _unitOfWork.CompanyRepository.GetOneAsync(
e => e.Guid == request.Guid, cancellationToken);
_unitOfWork.Dispose();
if (entity == null)
{
throw new NotFoundException();
}
return _mapper.Map<CompanyDto>(entity);
}
}

View File

@ -0,0 +1,14 @@
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Companies.Queries.GetCompany;
public class GetCompanyQueryValidator : AbstractValidator<GetCompanyQuery>
{
public GetCompanyQueryValidator(IStringLocalizer localizer)
{
RuleFor(v => v.Guid)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
}
}

View File

@ -0,0 +1,12 @@
namespace cuqmbr.TravelGuide.Application.Companies.ViewModels;
public sealed class AddCompanyViewModel
{
public string Name { get; set; }
public string LegalAddress { get; set; }
public string ContactEmail { get; set; }
public string ContactPhoneNumber { get; set; }
}

View File

@ -0,0 +1,12 @@
namespace cuqmbr.TravelGuide.Application.Companies.ViewModels;
public sealed class UpdateCompanyViewModel
{
public string Name { get; set; }
public string LegalAddress { get; set; }
public string ContactEmail { get; set; }
public string ContactPhoneNumber { get; set; }
}

View File

@ -8,7 +8,7 @@ public class AddCountryCommandValidator : AbstractValidator<AddCountryCommand>
{
public AddCountryCommandValidator(
IStringLocalizer localizer,
CultureService cultureService)
SessionCultureService cultureService)
{
RuleFor(v => v.Name)
.NotEmpty()

View File

@ -8,7 +8,7 @@ public class UpdateCountryCommandValidator : AbstractValidator<UpdateCountryComm
{
public UpdateCountryCommandValidator(
IStringLocalizer localizer,
CultureService cultureService)
SessionCultureService cultureService)
{
RuleFor(v => v.Guid)
.NotEmpty()

View File

@ -9,7 +9,7 @@ public class GetCountriesPageQueryValidator :
{
public GetCountriesPageQueryValidator(
IStringLocalizer localizer,
CultureService cultureService)
SessionCultureService cultureService)
{
RuleFor(v => v.PageNumber)
.GreaterThanOrEqualTo(1)

View File

@ -0,0 +1,23 @@
using cuqmbr.TravelGuide.Domain.Enums;
using MediatR;
using cuqmbr.TravelGuide.Application.Employees.Models;
namespace cuqmbr.TravelGuide.Application.Employees.Commands.AddEmployee;
public record AddEmployeeCommand : IRequest<EmployeeDto>
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Patronymic { get; set; }
public Sex Sex { get; set; }
public DateOnly BirthDate { get; set; }
public Guid CompanyGuid { get; set; }
public ICollection<EmployeeDocumentModel> Documents { get; set; }
}

View File

@ -0,0 +1,31 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using cuqmbr.TravelGuide.Application.Common.Models;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Employees.Commands.AddEmployee;
public class AddEmployeeCommandAuthorizer :
AbstractRequestAuthorizer<AddEmployeeCommand>
{
private readonly SessionUserService _sessionUserService;
public AddEmployeeCommandAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(AddEmployeeCommand request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInRolesRequirement
{
RequiredRoles = [IdentityRole.Administrator],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,80 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence;
using cuqmbr.TravelGuide.Domain.Entities;
using AutoMapper;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Employees.Commands.AddEmployee;
public class AddEmployeeCommandHandler :
IRequestHandler<AddEmployeeCommand, EmployeeDto>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
private readonly IStringLocalizer _localizer;
public AddEmployeeCommandHandler(
UnitOfWork unitOfWork,
IMapper mapper,
IStringLocalizer localizer)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
_localizer = localizer;
}
public async Task<EmployeeDto> Handle(
AddEmployeeCommand request,
CancellationToken cancellationToken)
{
var parentEntity = await _unitOfWork.CompanyRepository.GetOneAsync(
e => e.Guid == request.CompanyGuid, cancellationToken);
if (parentEntity == null)
{
throw new NotFoundException(
$"Parent entity with Guid: {request.CompanyGuid} not found.");
}
var entity = await _unitOfWork.EmployeeRepository.GetOneAsync(
e =>
e.FirstName == request.FirstName &&
e.LastName == request.LastName &&
e.Patronymic == request.Patronymic &&
e.Sex == request.Sex &&
e.BirthDate == request.BirthDate &&
e.CompanyId == parentEntity.Id,
cancellationToken);
if (entity != null)
{
throw new DuplicateEntityException();
}
entity = new Employee()
{
FirstName = request.FirstName,
LastName = request.LastName,
Patronymic = request.Patronymic,
Sex = request.Sex,
BirthDate = request.BirthDate,
Documents = request.Documents.Select(
d => new EmployeeDocument()
{
DocumentType = d.DocumentType,
Information = d.Information
})
.ToArray(),
Company = parentEntity
};
entity = await _unitOfWork.EmployeeRepository.AddOneAsync(
entity, cancellationToken);
await _unitOfWork.SaveAsync(cancellationToken);
_unitOfWork.Dispose();
return _mapper.Map<EmployeeDto>(entity);
}
}

View File

@ -0,0 +1,83 @@
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using cuqmbr.TravelGuide.Domain.Enums;
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Employees.Commands.AddEmployee;
public class AddEmployeeCommandValidator : AbstractValidator<AddEmployeeCommand>
{
public AddEmployeeCommandValidator(
IStringLocalizer localizer,
SessionCultureService cultureService)
{
RuleFor(e => e.FirstName)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.MaximumLength(32)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
32));
RuleFor(e => e.LastName)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.MaximumLength(32)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
32));
RuleFor(e => e.Patronymic)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.MaximumLength(32)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
32));
RuleFor(e => e.Sex)
.Must((e, s) => Sex.Enumerations.ContainsValue(s))
.WithMessage(
String.Format(
localizer["FluentValidation.MustBeInEnum"],
String.Join(
", ",
Sex.Enumerations.Values.Select(e => e.Name))));
RuleFor(e => e.BirthDate)
.GreaterThanOrEqualTo(DateOnly.FromDateTime(DateTime.UtcNow.AddYears(-100)))
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.GreaterThanOrEqualTo"],
DateOnly.FromDateTime(DateTime.UtcNow.AddYears(-100))));
RuleForEach(e => e.Documents).ChildRules(d =>
{
d.RuleFor(d => d.DocumentType)
.Must(dt => DocumentType.Enumerations.ContainsValue(dt))
.WithMessage(
String.Format(
localizer["FluentValidation.MustBeInEnum"],
String.Join(
", ",
DocumentType.Enumerations.Values.Select(e => e.Name))));
d.RuleFor(d => d.Information)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.MaximumLength(256)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
256));
});
}
}

View File

@ -0,0 +1,8 @@
using MediatR;
namespace cuqmbr.TravelGuide.Application.Employees.Commands.DeleteEmployee;
public record DeleteEmployeeCommand : IRequest
{
public Guid Guid { get; set; }
}

View File

@ -0,0 +1,31 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using cuqmbr.TravelGuide.Application.Common.Models;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Employees.Commands.DeleteEmployee;
public class DeleteEmployeeCommandAuthorizer :
AbstractRequestAuthorizer<DeleteEmployeeCommand>
{
private readonly SessionUserService _sessionUserService;
public DeleteEmployeeCommandAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(DeleteEmployeeCommand request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInRolesRequirement
{
RequiredRoles = [IdentityRole.Administrator],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,37 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
namespace cuqmbr.TravelGuide.Application.Employees.Commands.DeleteEmployee;
public class DeleteEmployeeCommandHandler : IRequestHandler<DeleteEmployeeCommand>
{
private readonly UnitOfWork _unitOfWork;
public DeleteEmployeeCommandHandler(UnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public async Task Handle(
DeleteEmployeeCommand request,
CancellationToken cancellationToken)
{
var entity = await _unitOfWork.EmployeeRepository.GetOneAsync(
e => e.Guid == request.Guid, cancellationToken);
if (entity == null)
{
throw new NotFoundException();
}
// TODO: Check for Vehicles that using this employee in Enrollments
// Delete if there are no such Vehicles
await _unitOfWork.EmployeeRepository.DeleteOneAsync(
entity, cancellationToken);
await _unitOfWork.SaveAsync(cancellationToken);
_unitOfWork.Dispose();
}
}

View File

@ -0,0 +1,14 @@
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Employees.Commands.DeleteEmployee;
public class DeleteEmployeeCommandValidator : AbstractValidator<DeleteEmployeeCommand>
{
public DeleteEmployeeCommandValidator(IStringLocalizer localizer)
{
RuleFor(v => v.Guid)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
}
}

View File

@ -0,0 +1,25 @@
using MediatR;
using cuqmbr.TravelGuide.Domain.Enums;
using cuqmbr.TravelGuide.Application.Employees.Models;
namespace cuqmbr.TravelGuide.Application.Employees.Commands.UpdateEmployee;
public record UpdateEmployeeCommand : IRequest<EmployeeDto>
{
public Guid Guid { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Patronymic { get; set; }
public Sex Sex { get; set; }
public DateOnly BirthDate { get; set; }
public Guid CompanyGuid { get; set; }
public ICollection<EmployeeDocumentModel> Documents { get; set; }
}

View File

@ -0,0 +1,31 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using cuqmbr.TravelGuide.Application.Common.Models;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Employees.Commands.UpdateEmployee;
public class UpdateEmployeeCommandAuthorizer :
AbstractRequestAuthorizer<UpdateEmployeeCommand>
{
private readonly SessionUserService _sessionUserService;
public UpdateEmployeeCommandAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(UpdateEmployeeCommand request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInRolesRequirement
{
RequiredRoles = [IdentityRole.Administrator],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,108 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence;
using AutoMapper;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
using Microsoft.Extensions.Localization;
using cuqmbr.TravelGuide.Domain.Entities;
namespace cuqmbr.TravelGuide.Application.Employees.Commands.UpdateEmployee;
public class UpdateEmployeeCommandHandler :
IRequestHandler<UpdateEmployeeCommand, EmployeeDto>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public IStringLocalizer _localizer { get; set; }
public UpdateEmployeeCommandHandler(
UnitOfWork unitOfWork,
IMapper mapper,
IStringLocalizer localizer)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
_localizer = localizer;
}
public async Task<EmployeeDto> Handle(
UpdateEmployeeCommand request,
CancellationToken cancellationToken)
{
var parentEntity = await _unitOfWork.CompanyRepository.GetOneAsync(
e => e.Guid == request.CompanyGuid, cancellationToken);
if (parentEntity == null)
{
throw new NotFoundException(
$"Parent entity with Guid: {request.CompanyGuid} not found.");
}
var entity = await _unitOfWork.EmployeeRepository.GetOneAsync(
e =>
e.FirstName == request.FirstName &&
e.LastName == request.LastName &&
e.Patronymic == request.Patronymic &&
e.Sex == request.Sex &&
e.BirthDate == request.BirthDate &&
e.CompanyId == parentEntity.Id &&
e.Guid != request.Guid,
cancellationToken);
if (entity != null)
{
throw new DuplicateEntityException();
}
entity = await _unitOfWork.EmployeeRepository.GetOneAsync(
e => e.Guid == request.Guid, e => e.Documents, cancellationToken);
if (entity == null)
{
throw new NotFoundException();
}
entity.Guid = request.Guid;
entity.FirstName = request.FirstName;
entity.LastName = request.LastName;
entity.Patronymic = request.Patronymic;
entity.Sex = request.Sex;
entity.BirthDate = request.BirthDate;
entity.CompanyId = parentEntity.Id;
entity.Company = parentEntity;
var requestEmployeeDocuments = request.Documents.Select(
d => new EmployeeDocument()
{
DocumentType = d.DocumentType,
Information = d.Information
});
var commonEmployeeDocuments = entity.Documents.IntersectBy(
requestEmployeeDocuments.Select(
ed => (ed.DocumentType, ed.Information)),
ed => (ed.DocumentType, ed.Information));
var newEmployeeDocuments = requestEmployeeDocuments.ExceptBy(
entity.Documents.Select(ed => (ed.DocumentType, ed.Information)),
ed => (ed.DocumentType, ed.Information));
var combinedEmployeeDocuments = commonEmployeeDocuments.UnionBy(
newEmployeeDocuments, ed => (ed.DocumentType, ed.Information));
entity.Documents = combinedEmployeeDocuments.ToList();
entity = await _unitOfWork.EmployeeRepository.UpdateOneAsync(
entity, cancellationToken);
await _unitOfWork.SaveAsync(cancellationToken);
_unitOfWork.Dispose();
return _mapper.Map<EmployeeDto>(entity);
}
}

View File

@ -0,0 +1,87 @@
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using cuqmbr.TravelGuide.Domain.Enums;
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Employees.Commands.UpdateEmployee;
public class UpdateEmployeeCommandValidator : AbstractValidator<UpdateEmployeeCommand>
{
public UpdateEmployeeCommandValidator(
IStringLocalizer localizer,
SessionCultureService cultureService)
{
RuleFor(v => v.Guid)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
RuleFor(e => e.FirstName)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.MaximumLength(32)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
32));
RuleFor(e => e.LastName)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.MaximumLength(32)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
32));
RuleFor(e => e.Patronymic)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.MaximumLength(32)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
32));
RuleFor(e => e.Sex)
.Must((e, s) => Sex.Enumerations.ContainsValue(s))
.WithMessage(
String.Format(
localizer["FluentValidation.MustBeInEnum"],
String.Join(
", ",
Sex.Enumerations.Values.Select(e => e.Name))));
RuleFor(e => e.BirthDate)
.GreaterThanOrEqualTo(DateOnly.FromDateTime(DateTime.UtcNow.AddYears(-100)))
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.GreaterThanOrEqualTo"],
DateOnly.FromDateTime(DateTime.UtcNow.AddYears(-100))));
RuleForEach(e => e.Documents).ChildRules(d =>
{
d.RuleFor(d => d.DocumentType)
.Must(dt => DocumentType.Enumerations.ContainsValue(dt))
.WithMessage(
String.Format(
localizer["FluentValidation.MustBeInEnum"],
String.Join(
", ",
DocumentType.Enumerations.Values.Select(e => e.Name))));
d.RuleFor(d => d.Information)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.MaximumLength(256)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
256));
});
}
}

View File

@ -0,0 +1,19 @@
using cuqmbr.TravelGuide.Application.Common.Mappings;
using cuqmbr.TravelGuide.Domain.Entities;
namespace cuqmbr.TravelGuide.Application.Employees;
public sealed class EmployeeDocumentDto : IMapFrom<EmployeeDocument>
{
public string DocumentType { get; set; }
public string Information { get; set; }
public void Mapping(MappingProfile profile)
{
profile.CreateMap<EmployeeDocument, EmployeeDocumentDto>()
.ForMember(
d => d.DocumentType,
opt => opt.MapFrom(s => s.DocumentType.Name));
}
}

View File

@ -0,0 +1,38 @@
using cuqmbr.TravelGuide.Application.Common.Mappings;
using cuqmbr.TravelGuide.Domain.Entities;
namespace cuqmbr.TravelGuide.Application.Employees;
public sealed class EmployeeDto : IMapFrom<Employee>
{
public Guid Uuid { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Patronymic { get; set; }
public string Sex { get; set; }
public DateOnly BirthDate { get; set; }
public Guid CompanyUuid { get; set; }
public ICollection<EmployeeDocumentDto> Documents { get; set; }
public void Mapping(MappingProfile profile)
{
profile.CreateMap<Employee, EmployeeDto>()
.ForMember(
d => d.Uuid,
opt => opt.MapFrom(s => s.Guid))
.ForMember(
d => d.Sex,
opt => opt.MapFrom(s => s.Sex.Name))
.ForMember(
d => d.CompanyUuid,
opt => opt.MapFrom(s => s.Company.Guid));
}
}

View File

@ -0,0 +1,10 @@
using cuqmbr.TravelGuide.Domain.Enums;
namespace cuqmbr.TravelGuide.Application.Employees.Models;
public sealed class EmployeeDocumentModel
{
public DocumentType DocumentType { get; set; }
public string Information { get; set; }
}

View File

@ -0,0 +1,8 @@
using MediatR;
namespace cuqmbr.TravelGuide.Application.Employees.Queries.GetEmployee;
public record GetEmployeeQuery : IRequest<EmployeeDto>
{
public Guid Guid { get; set; }
}

View File

@ -0,0 +1,31 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using cuqmbr.TravelGuide.Application.Common.Models;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Employees.Queries.GetEmployee;
public class GetEmployeeQueryAuthorizer :
AbstractRequestAuthorizer<GetEmployeeQuery>
{
private readonly SessionUserService _sessionUserService;
public GetEmployeeQueryAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(GetEmployeeQuery request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInRolesRequirement
{
RequiredRoles = [IdentityRole.Administrator],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,48 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
using AutoMapper;
namespace cuqmbr.TravelGuide.Application.Employees.Queries.GetEmployee;
public class GetEmployeeQueryHandler :
IRequestHandler<GetEmployeeQuery, EmployeeDto>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public GetEmployeeQueryHandler(
UnitOfWork unitOfWork,
IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
public async Task<EmployeeDto> Handle(
GetEmployeeQuery request,
CancellationToken cancellationToken)
{
var entity = await _unitOfWork.EmployeeRepository.GetOneAsync(
e => e.Guid == request.Guid, e => e.Documents,
cancellationToken);
if (entity == null)
{
throw new NotFoundException();
}
// Hydrate employees with companies
var company = await _unitOfWork.CompanyRepository.GetOneAsync(
e => e.Id == entity.CompanyId, cancellationToken);
entity.Company = company;
_unitOfWork.Dispose();
return _mapper.Map<EmployeeDto>(entity);
}
}

View File

@ -0,0 +1,14 @@
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Employees.Queries.GetEmployee;
public class GetEmployeeQueryValidator : AbstractValidator<GetEmployeeQuery>
{
public GetEmployeeQueryValidator(IStringLocalizer localizer)
{
RuleFor(v => v.Guid)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
}
}

View File

@ -0,0 +1,24 @@
using cuqmbr.TravelGuide.Application.Common.Models;
using cuqmbr.TravelGuide.Domain.Enums;
using MediatR;
namespace cuqmbr.TravelGuide.Application.Employees.Queries.GetEmployeesPage;
public record GetEmployeesPageQuery : IRequest<PaginatedList<EmployeeDto>>
{
public int PageNumber { get; set; } = 1;
public int PageSize { get; set; } = 10;
public string Search { get; set; } = String.Empty;
public string Sort { get; set; } = String.Empty;
public Sex? Sex { get; set; }
public DateOnly? BirthDateGreaterThanOrEqualTo { get; set; }
public DateOnly? BirthDateLessThanOrEqualTo { get; set; }
public Guid? CompanyGuid { get; set; }
}

View File

@ -0,0 +1,31 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using cuqmbr.TravelGuide.Application.Common.Models;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Employees.Queries.GetEmployeesPage;
public class GetEmployeesPageQueryAuthorizer :
AbstractRequestAuthorizer<GetEmployeesPageQuery>
{
private readonly SessionUserService _sessionUserService;
public GetEmployeesPageQueryAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(GetEmployeesPageQuery request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInRolesRequirement
{
RequiredRoles = [IdentityRole.Administrator],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,78 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence;
using AutoMapper;
using cuqmbr.TravelGuide.Application.Common.Models;
using cuqmbr.TravelGuide.Application.Common.Extensions;
namespace cuqmbr.TravelGuide.Application.Employees.Queries.GetEmployeesPage;
public class GetEmployeesPageQueryHandler :
IRequestHandler<GetEmployeesPageQuery, PaginatedList<EmployeeDto>>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public GetEmployeesPageQueryHandler(
UnitOfWork unitOfWork,
IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
public async Task<PaginatedList<EmployeeDto>> Handle(
GetEmployeesPageQuery request,
CancellationToken cancellationToken)
{
var paginatedList = await _unitOfWork.EmployeeRepository.GetPageAsync(
e =>
(e.FirstName.ToLower().Contains(request.Search.ToLower()) ||
e.LastName.ToLower().Contains(request.Search.ToLower()) ||
e.Patronymic.ToLower().Contains(request.Search.ToLower()) ||
e.Documents
.Select(d => d.Information.ToLower())
.Contains(request.Search.ToLower())) &&
(request.CompanyGuid != null
? e.Company.Guid == request.CompanyGuid
: true) &&
(request.Sex != null
? e.Sex == request.Sex
: true) &&
(request.BirthDateLessThanOrEqualTo != null
? e.BirthDate <= request.BirthDateLessThanOrEqualTo
: true) &&
(request.BirthDateGreaterThanOrEqualTo != null
? e.BirthDate >= request.BirthDateGreaterThanOrEqualTo
: true),
e => e.Documents,
request.PageNumber, request.PageSize,
cancellationToken);
// Hydrate employees with companies
var companies = await _unitOfWork.CompanyRepository.GetPageAsync(
e => paginatedList.Items.Select(e => e.CompanyId).Contains(e.Id),
1, paginatedList.Items.Count, cancellationToken);
foreach (var employee in paginatedList.Items)
{
employee.Company =
companies.Items.First(c => c.Id == employee.CompanyId);
}
var mappedItems = _mapper
.ProjectTo<EmployeeDto>(paginatedList.Items.AsQueryable());
mappedItems = QueryableExtension<EmployeeDto>
.ApplySort(mappedItems, request.Sort);
_unitOfWork.Dispose();
return new PaginatedList<EmployeeDto>(
mappedItems.ToList(),
paginatedList.TotalCount, request.PageNumber,
request.PageSize);
}
}

View File

@ -0,0 +1,43 @@
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Employees.Queries.GetEmployeesPage;
public class GetEmployeesPageQueryValidator : AbstractValidator<GetEmployeesPageQuery>
{
public GetEmployeesPageQueryValidator(
IStringLocalizer localizer,
SessionCultureService cultureService)
{
RuleFor(v => v.PageNumber)
.GreaterThanOrEqualTo(1)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.GreaterThanOrEqualTo"],
1));
RuleFor(v => v.PageSize)
.GreaterThanOrEqualTo(1)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.GreaterThanOrEqualTo"],
1))
.LessThanOrEqualTo(50)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.LessThanOrEqualTo"],
50));
RuleFor(v => v.Search)
.MaximumLength(64)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
64));
}
}

View File

@ -0,0 +1,19 @@
namespace cuqmbr.TravelGuide.Application.Employees.ViewModels;
public sealed class AddEmployeeViewModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Patronymic { get; set; }
public string Sex { get; set; }
public DateOnly BirthDate { get; set; }
public Guid CompanyUuid { get; set; }
public ICollection<EmployeeDocumentViewModel> Documents { get; set; }
}

View File

@ -0,0 +1,8 @@
namespace cuqmbr.TravelGuide.Application.Employees.ViewModels;
public sealed class EmployeeDocumentViewModel
{
public string DocumentType { get; set; }
public string Information { get; set; }
}

View File

@ -0,0 +1,12 @@
namespace cuqmbr.TravelGuide.Application.Employees.ViewModels;
public sealed class GetEmployeesPageFilterViewModel
{
public string? Sex { get; set; }
public DateOnly? BirthDateGreaterThanOrEqualTo { get; set; }
public DateOnly? BirthDateLessThanOrEqualTo { get; set; }
public Guid? CompanyUuid { get; set; }
}

Some files were not shown because too many files have changed in this diff Show More