From 5982fa7285f09b6e77ee568cd93fd1eedbc7f000 Mon Sep 17 00:00:00 2001 From: cuqmbr Date: Fri, 16 May 2025 15:22:44 +0300 Subject: [PATCH] add employee management --- .../Repositories/EmployeeRepository.cs | 6 + .../Interfaces/Persistence/UnitOfWork.cs | 2 + .../AddEmployee/AddEmployeeCommand.cs | 23 + .../AddEmployeeCommandAuthorizer.cs | 31 + .../AddEmployee/AddEmployeeCommandHandler.cs | 80 ++ .../AddEmployeeCommandValidator.cs | 83 ++ .../DeleteEmployee/DeleteEmployeeCommand.cs | 8 + .../DeleteEmployeeCommandAuthorizer.cs | 31 + .../DeleteEmployeeCommandHandler.cs | 37 + .../DeleteEmployeeCommandValidator.cs | 14 + .../UpdateEmployee/UpdateEmployeeCommand.cs | 25 + .../UpdateEmployeeCommandAuthorizer.cs | 31 + .../UpdateEmployeeCommandHandler.cs | 108 +++ .../UpdateEmployeeCommandValidator.cs | 87 ++ .../Employees/EmployeeDocumentDto.cs | 19 + src/Application/Employees/EmployeeDto.cs | 38 + .../Employees/Models/EmployeeDocumentModel.cs | 10 + .../Queries/GetEmployee/GetEmployeeQuery.cs | 8 + .../GetEmployee/GetEmployeeQueryAuthorizer.cs | 31 + .../GetEmployee/GetEmployeeQueryHandler.cs | 48 + .../GetEmployee/GetEmployeeQueryValidator.cs | 14 + .../GetEmployeesPage/GetEmployeesPageQuery.cs | 24 + .../GetEmployeesPageQueryAuthorizer.cs | 31 + .../GetEmployeesPageQueryHandler.cs | 78 ++ .../GetEmployeesPageQueryValidator.cs | 43 + .../ViewModels/AddEmployeeViewModel.cs | 19 + .../ViewModels/EmployeeDocumentViewModel.cs | 8 + .../GetEmployeesPageFilterViewModel.cs | 12 + .../ViewModels/UpdateEmployeeViewModel.cs | 19 + .../Resources/Localization/en-US.json | 4 +- src/Domain/Entities/Company.cs | 2 + src/Domain/Entities/Employee.cs | 23 + src/Domain/Entities/EmployeeDocument.cs | 15 + src/Domain/Entities/VehicleEnrollment.cs | 3 + src/Domain/Enums/DocumentType.cs | 16 + src/Domain/Enums/Sex.cs | 22 + .../Controllers/EmployeesController.cs | 212 +++++ src/Persistence/InMemory/InMemoryDbContext.cs | 6 - .../InMemory/InMemoryUnitOfWork.cs | 3 + .../InMemoryEmployeeRepository.cs | 11 + .../Configurations/EmployeeConfiguration.cs | 81 ++ .../EmployeeDocumentConfiguration.cs | 63 ++ ..._Employee_and_EmployeeDocument.Designer.cs | 841 ++++++++++++++++++ ...64353_Add_Employee_and_EmployeeDocument.cs | 108 +++ .../PostgreSqlDbContextModelSnapshot.cs | 135 +++ .../PostgreSql/PostgreSqlDbContext.cs | 10 + .../PostgreSql/PostgreSqlUnitOfWork.cs | 3 + .../PostgreSqlEmployeeRepository.cs | 11 + .../TypeConverters/DocumentTypeConverter.cs | 13 + .../TypeConverters/SexConverter.cs | 13 + 50 files changed, 2555 insertions(+), 8 deletions(-) create mode 100644 src/Application/Common/Interfaces/Persistence/Repositories/EmployeeRepository.cs create mode 100644 src/Application/Employees/Commands/AddEmployee/AddEmployeeCommand.cs create mode 100644 src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandAuthorizer.cs create mode 100644 src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandHandler.cs create mode 100644 src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandValidator.cs create mode 100644 src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommand.cs create mode 100644 src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommandAuthorizer.cs create mode 100644 src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommandHandler.cs create mode 100644 src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommandValidator.cs create mode 100644 src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommand.cs create mode 100644 src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandAuthorizer.cs create mode 100644 src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandHandler.cs create mode 100644 src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandValidator.cs create mode 100644 src/Application/Employees/EmployeeDocumentDto.cs create mode 100644 src/Application/Employees/EmployeeDto.cs create mode 100644 src/Application/Employees/Models/EmployeeDocumentModel.cs create mode 100644 src/Application/Employees/Queries/GetEmployee/GetEmployeeQuery.cs create mode 100644 src/Application/Employees/Queries/GetEmployee/GetEmployeeQueryAuthorizer.cs create mode 100644 src/Application/Employees/Queries/GetEmployee/GetEmployeeQueryHandler.cs create mode 100644 src/Application/Employees/Queries/GetEmployee/GetEmployeeQueryValidator.cs create mode 100644 src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQuery.cs create mode 100644 src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryAuthorizer.cs create mode 100644 src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryHandler.cs create mode 100644 src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryValidator.cs create mode 100644 src/Application/Employees/ViewModels/AddEmployeeViewModel.cs create mode 100644 src/Application/Employees/ViewModels/EmployeeDocumentViewModel.cs create mode 100644 src/Application/Employees/ViewModels/GetEmployeesPageFilterViewModel.cs create mode 100644 src/Application/Employees/ViewModels/UpdateEmployeeViewModel.cs create mode 100644 src/Domain/Entities/Employee.cs create mode 100644 src/Domain/Entities/EmployeeDocument.cs create mode 100644 src/Domain/Enums/DocumentType.cs create mode 100644 src/Domain/Enums/Sex.cs create mode 100644 src/HttpApi/Controllers/EmployeesController.cs create mode 100644 src/Persistence/InMemory/Repositories/InMemoryEmployeeRepository.cs create mode 100644 src/Persistence/PostgreSql/Configurations/EmployeeConfiguration.cs create mode 100644 src/Persistence/PostgreSql/Configurations/EmployeeDocumentConfiguration.cs create mode 100644 src/Persistence/PostgreSql/Migrations/20250515164353_Add_Employee_and_EmployeeDocument.Designer.cs create mode 100644 src/Persistence/PostgreSql/Migrations/20250515164353_Add_Employee_and_EmployeeDocument.cs create mode 100644 src/Persistence/PostgreSql/Repositories/PostgreSqlEmployeeRepository.cs create mode 100644 src/Persistence/TypeConverters/DocumentTypeConverter.cs create mode 100644 src/Persistence/TypeConverters/SexConverter.cs diff --git a/src/Application/Common/Interfaces/Persistence/Repositories/EmployeeRepository.cs b/src/Application/Common/Interfaces/Persistence/Repositories/EmployeeRepository.cs new file mode 100644 index 0000000..012cf93 --- /dev/null +++ b/src/Application/Common/Interfaces/Persistence/Repositories/EmployeeRepository.cs @@ -0,0 +1,6 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common.Interfaces + .Persistence.Repositories; + +public interface EmployeeRepository : BaseRepository { } diff --git a/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs b/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs index 6db81b1..7c0b461 100644 --- a/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs +++ b/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs @@ -28,6 +28,8 @@ public interface UnitOfWork : IDisposable CompanyRepository CompanyRepository { get; } + EmployeeRepository EmployeeRepository { get; } + int Save(); Task SaveAsync(CancellationToken cancellationToken); diff --git a/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommand.cs b/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommand.cs new file mode 100644 index 0000000..4fe21d7 --- /dev/null +++ b/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommand.cs @@ -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 +{ + 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 Documents { get; set; } +} diff --git a/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandAuthorizer.cs b/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandAuthorizer.cs new file mode 100644 index 0000000..8e2f0d1 --- /dev/null +++ b/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandAuthorizer.cs @@ -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 +{ + 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 + }); + } +} diff --git a/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandHandler.cs b/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandHandler.cs new file mode 100644 index 0000000..1d528b6 --- /dev/null +++ b/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandHandler.cs @@ -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 +{ + 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 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(entity); + } +} diff --git a/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandValidator.cs b/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandValidator.cs new file mode 100644 index 0000000..149bc95 --- /dev/null +++ b/src/Application/Employees/Commands/AddEmployee/AddEmployeeCommandValidator.cs @@ -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 +{ + 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)); + }); + } +} diff --git a/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommand.cs b/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommand.cs new file mode 100644 index 0000000..4a1d1be --- /dev/null +++ b/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommand.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Employees.Commands.DeleteEmployee; + +public record DeleteEmployeeCommand : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommandAuthorizer.cs b/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommandAuthorizer.cs new file mode 100644 index 0000000..e2f0478 --- /dev/null +++ b/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommandAuthorizer.cs @@ -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 +{ + 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 + }); + } +} diff --git a/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommandHandler.cs b/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommandHandler.cs new file mode 100644 index 0000000..2e7c8c3 --- /dev/null +++ b/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommandHandler.cs @@ -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 +{ + 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(); + } +} diff --git a/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommandValidator.cs b/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommandValidator.cs new file mode 100644 index 0000000..9509930 --- /dev/null +++ b/src/Application/Employees/Commands/DeleteEmployee/DeleteEmployeeCommandValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Employees.Commands.DeleteEmployee; + +public class DeleteEmployeeCommandValidator : AbstractValidator +{ + public DeleteEmployeeCommandValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommand.cs b/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommand.cs new file mode 100644 index 0000000..883a6a4 --- /dev/null +++ b/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommand.cs @@ -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 +{ + 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 Documents { get; set; } +} diff --git a/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandAuthorizer.cs b/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandAuthorizer.cs new file mode 100644 index 0000000..e3ff38e --- /dev/null +++ b/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandAuthorizer.cs @@ -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 +{ + 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 + }); + } +} diff --git a/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandHandler.cs b/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandHandler.cs new file mode 100644 index 0000000..80d3d26 --- /dev/null +++ b/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandHandler.cs @@ -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 +{ + 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 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(entity); + } +} diff --git a/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandValidator.cs b/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandValidator.cs new file mode 100644 index 0000000..e6cf45e --- /dev/null +++ b/src/Application/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandValidator.cs @@ -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 +{ + 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)); + }); + } +} diff --git a/src/Application/Employees/EmployeeDocumentDto.cs b/src/Application/Employees/EmployeeDocumentDto.cs new file mode 100644 index 0000000..8865f14 --- /dev/null +++ b/src/Application/Employees/EmployeeDocumentDto.cs @@ -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 +{ + public string DocumentType { get; set; } + + public string Information { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.DocumentType, + opt => opt.MapFrom(s => s.DocumentType.Name)); + } +} diff --git a/src/Application/Employees/EmployeeDto.cs b/src/Application/Employees/EmployeeDto.cs new file mode 100644 index 0000000..fec500f --- /dev/null +++ b/src/Application/Employees/EmployeeDto.cs @@ -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 +{ + 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 Documents { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .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)); + } +} diff --git a/src/Application/Employees/Models/EmployeeDocumentModel.cs b/src/Application/Employees/Models/EmployeeDocumentModel.cs new file mode 100644 index 0000000..2992dfa --- /dev/null +++ b/src/Application/Employees/Models/EmployeeDocumentModel.cs @@ -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; } +} diff --git a/src/Application/Employees/Queries/GetEmployee/GetEmployeeQuery.cs b/src/Application/Employees/Queries/GetEmployee/GetEmployeeQuery.cs new file mode 100644 index 0000000..67a4024 --- /dev/null +++ b/src/Application/Employees/Queries/GetEmployee/GetEmployeeQuery.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Employees.Queries.GetEmployee; + +public record GetEmployeeQuery : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/Employees/Queries/GetEmployee/GetEmployeeQueryAuthorizer.cs b/src/Application/Employees/Queries/GetEmployee/GetEmployeeQueryAuthorizer.cs new file mode 100644 index 0000000..6011990 --- /dev/null +++ b/src/Application/Employees/Queries/GetEmployee/GetEmployeeQueryAuthorizer.cs @@ -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 +{ + 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 + }); + } +} diff --git a/src/Application/Employees/Queries/GetEmployee/GetEmployeeQueryHandler.cs b/src/Application/Employees/Queries/GetEmployee/GetEmployeeQueryHandler.cs new file mode 100644 index 0000000..2e3edcb --- /dev/null +++ b/src/Application/Employees/Queries/GetEmployee/GetEmployeeQueryHandler.cs @@ -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 +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetEmployeeQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task 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(entity); + } +} diff --git a/src/Application/Employees/Queries/GetEmployee/GetEmployeeQueryValidator.cs b/src/Application/Employees/Queries/GetEmployee/GetEmployeeQueryValidator.cs new file mode 100644 index 0000000..da6a0ef --- /dev/null +++ b/src/Application/Employees/Queries/GetEmployee/GetEmployeeQueryValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Employees.Queries.GetEmployee; + +public class GetEmployeeQueryValidator : AbstractValidator +{ + public GetEmployeeQueryValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQuery.cs b/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQuery.cs new file mode 100644 index 0000000..209fcc2 --- /dev/null +++ b/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQuery.cs @@ -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> +{ + 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; } +} diff --git a/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryAuthorizer.cs b/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryAuthorizer.cs new file mode 100644 index 0000000..775eeb3 --- /dev/null +++ b/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryAuthorizer.cs @@ -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 +{ + 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 + }); + } +} diff --git a/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryHandler.cs b/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryHandler.cs new file mode 100644 index 0000000..21a5a81 --- /dev/null +++ b/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryHandler.cs @@ -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> +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetEmployeesPageQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task> 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(paginatedList.Items.AsQueryable()); + + mappedItems = QueryableExtension + .ApplySort(mappedItems, request.Sort); + + _unitOfWork.Dispose(); + + return new PaginatedList( + mappedItems.ToList(), + paginatedList.TotalCount, request.PageNumber, + request.PageSize); + } +} diff --git a/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryValidator.cs b/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryValidator.cs new file mode 100644 index 0000000..c85edf2 --- /dev/null +++ b/src/Application/Employees/Queries/GetEmployeesPage/GetEmployeesPageQueryValidator.cs @@ -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 +{ + 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)); + } +} diff --git a/src/Application/Employees/ViewModels/AddEmployeeViewModel.cs b/src/Application/Employees/ViewModels/AddEmployeeViewModel.cs new file mode 100644 index 0000000..97ba828 --- /dev/null +++ b/src/Application/Employees/ViewModels/AddEmployeeViewModel.cs @@ -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 Documents { get; set; } +} diff --git a/src/Application/Employees/ViewModels/EmployeeDocumentViewModel.cs b/src/Application/Employees/ViewModels/EmployeeDocumentViewModel.cs new file mode 100644 index 0000000..bb1155e --- /dev/null +++ b/src/Application/Employees/ViewModels/EmployeeDocumentViewModel.cs @@ -0,0 +1,8 @@ +namespace cuqmbr.TravelGuide.Application.Employees.ViewModels; + +public sealed class EmployeeDocumentViewModel +{ + public string DocumentType { get; set; } + + public string Information { get; set; } +} diff --git a/src/Application/Employees/ViewModels/GetEmployeesPageFilterViewModel.cs b/src/Application/Employees/ViewModels/GetEmployeesPageFilterViewModel.cs new file mode 100644 index 0000000..8a6e59d --- /dev/null +++ b/src/Application/Employees/ViewModels/GetEmployeesPageFilterViewModel.cs @@ -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; } +} diff --git a/src/Application/Employees/ViewModels/UpdateEmployeeViewModel.cs b/src/Application/Employees/ViewModels/UpdateEmployeeViewModel.cs new file mode 100644 index 0000000..0d2f4ed --- /dev/null +++ b/src/Application/Employees/ViewModels/UpdateEmployeeViewModel.cs @@ -0,0 +1,19 @@ +namespace cuqmbr.TravelGuide.Application.Employees.ViewModels; + +public sealed class UpdateEmployeeViewModel +{ + 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 Documents { get; set; } +} diff --git a/src/Application/Resources/Localization/en-US.json b/src/Application/Resources/Localization/en-US.json index 8d104df..65c8189 100644 --- a/src/Application/Resources/Localization/en-US.json +++ b/src/Application/Resources/Localization/en-US.json @@ -2,8 +2,8 @@ "FluentValidation": { "MaximumLength": "Must less than {0:G} characters.", "NotEmpty": "Must not be empty.", - "GreaterThanOrEqualTo": "Must be greater than or equal to {0:G}.", - "LessThanOrEqualTo": "Must be less than or equal to {0:G}.", + "GreaterThanOrEqualTo": "Must be greater than or equal to {0}.", + "LessThanOrEqualTo": "Must be less than or equal to {0}.", "MustBeInEnum": "Must be one of the following: {0}.", "IsEmail": "Must be a valid email address according to RFC 5321.", "IsPhoneNumber": "Must be a valid phone number according to ITU-T E.164 with no separator characters." diff --git a/src/Domain/Entities/Company.cs b/src/Domain/Entities/Company.cs index a5322fc..cab7f8e 100644 --- a/src/Domain/Entities/Company.cs +++ b/src/Domain/Entities/Company.cs @@ -11,5 +11,7 @@ public sealed class Company : EntityBase public string ContactPhoneNumber { get; set; } + public ICollection Employees { get; set; } + public ICollection Vehicles { get; set; } } diff --git a/src/Domain/Entities/Employee.cs b/src/Domain/Entities/Employee.cs new file mode 100644 index 0000000..ec518ce --- /dev/null +++ b/src/Domain/Entities/Employee.cs @@ -0,0 +1,23 @@ +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Domain.Entities; + +public sealed class Employee : EntityBase +{ + 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 long CompanyId { get; set; } + + public Company Company { get; set; } + + public ICollection Documents { get; set; } +} diff --git a/src/Domain/Entities/EmployeeDocument.cs b/src/Domain/Entities/EmployeeDocument.cs new file mode 100644 index 0000000..861c33c --- /dev/null +++ b/src/Domain/Entities/EmployeeDocument.cs @@ -0,0 +1,15 @@ +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Domain.Entities; + +public sealed class EmployeeDocument : EntityBase +{ + public DocumentType DocumentType { get; set; } + + public string Information { get; set; } + + + public long EmployeeId { get; set; } + + public Employee Employee { get; set; } +} diff --git a/src/Domain/Entities/VehicleEnrollment.cs b/src/Domain/Entities/VehicleEnrollment.cs index ac92112..31ae842 100644 --- a/src/Domain/Entities/VehicleEnrollment.cs +++ b/src/Domain/Entities/VehicleEnrollment.cs @@ -20,4 +20,7 @@ public class VehicleEnrollment : EntityBase public ICollection RouteAddressDetails { get; set; } + + + public ICollection Tickets { get; set; } } diff --git a/src/Domain/Enums/DocumentType.cs b/src/Domain/Enums/DocumentType.cs new file mode 100644 index 0000000..bde01d7 --- /dev/null +++ b/src/Domain/Enums/DocumentType.cs @@ -0,0 +1,16 @@ +namespace cuqmbr.TravelGuide.Domain.Enums; + +// Do not forget to update the schema of your database when changing +// this class (if you use it with a database) + +public abstract class DocumentType : Enumeration +{ + public static readonly DocumentType Passport = new PassportDocumentType(); + + protected DocumentType(int value, string name) : base(value, name) { } + + private sealed class PassportDocumentType : DocumentType + { + public PassportDocumentType() : base(0, "passport") { } + } +} diff --git a/src/Domain/Enums/Sex.cs b/src/Domain/Enums/Sex.cs new file mode 100644 index 0000000..f1a8460 --- /dev/null +++ b/src/Domain/Enums/Sex.cs @@ -0,0 +1,22 @@ +namespace cuqmbr.TravelGuide.Domain.Enums; + +// Do not forget to update the schema of your database when changing +// this class (if you use it with a database) + +public abstract class Sex : Enumeration +{ + public static readonly Sex Male = new MaleSex(); + public static readonly Sex Female = new FemaleSex(); + + protected Sex(int value, string name) : base(value, name) { } + + private sealed class MaleSex : Sex + { + public MaleSex() : base(Int32.MaxValue, "male") { } + } + + private sealed class FemaleSex : Sex + { + public FemaleSex() : base(Int32.MinValue, "female") { } + } +} diff --git a/src/HttpApi/Controllers/EmployeesController.cs b/src/HttpApi/Controllers/EmployeesController.cs new file mode 100644 index 0000000..4df1d5b --- /dev/null +++ b/src/HttpApi/Controllers/EmployeesController.cs @@ -0,0 +1,212 @@ +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.ViewModels; +using cuqmbr.TravelGuide.Domain.Enums; +using cuqmbr.TravelGuide.Application.Employees; +using cuqmbr.TravelGuide.Application.Employees.Commands.AddEmployee; +using cuqmbr.TravelGuide.Application.Employees.Queries.GetEmployeesPage; +using cuqmbr.TravelGuide.Application.Employees.Queries.GetEmployee; +using cuqmbr.TravelGuide.Application.Employees.Commands.UpdateEmployee; +using cuqmbr.TravelGuide.Application.Employees.Commands.DeleteEmployee; +using cuqmbr.TravelGuide.Application.Employees.ViewModels; +using cuqmbr.TravelGuide.Application.Employees.Models; + +namespace cuqmbr.TravelGuide.HttpApi.Controllers; + +[Route("employees")] +public class EmployeesController : ControllerBase +{ + [HttpPost] + [SwaggerOperation("Add an employee")] + [SwaggerResponse( + StatusCodes.Status201Created, "Object successfuly created", + typeof(EmployeeDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "One or more addresses was not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> Add( + [FromBody] AddEmployeeViewModel viewModel, + CancellationToken cancellationToken) + { + return StatusCode( + StatusCodes.Status201Created, + await Mediator.Send( + new AddEmployeeCommand() + { + FirstName = viewModel.FirstName, + LastName = viewModel.LastName, + Patronymic = viewModel.Patronymic, + Sex = Sex.FromName(viewModel.Sex), + BirthDate = viewModel.BirthDate, + Documents = viewModel.Documents.Select( + e => new EmployeeDocumentModel() + { + DocumentType = DocumentType.FromName(e.DocumentType), + Information = e.Information + + }).ToArray(), + CompanyGuid = viewModel.CompanyUuid + }, + cancellationToken)); + } + + [HttpGet] + [SwaggerOperation("Get a list of all employees")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", + typeof(PaginatedList))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> GetPage( + [FromQuery] PageQuery pageQuery, [FromQuery] SearchQuery searchQuery, + [FromQuery] SortQuery sortQuery, + [FromQuery] GetEmployeesPageFilterViewModel filterQuery, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new GetEmployeesPageQuery() + { + PageNumber = pageQuery.PageNumber, + PageSize = pageQuery.PageSize, + Search = searchQuery.Search, + Sort = sortQuery.Sort, + Sex = Sex.FromName(filterQuery.Sex), + BirthDateLessThanOrEqualTo = + filterQuery.BirthDateLessThanOrEqualTo, + BirthDateGreaterThanOrEqualTo = + filterQuery.BirthDateGreaterThanOrEqualTo, + CompanyGuid = filterQuery.CompanyUuid + }, + cancellationToken); + } + + [HttpGet("{uuid:guid}")] + [SwaggerOperation("Get an employee by uuid")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", typeof(EmployeeDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", typeof(EmployeeDto))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Get( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + return await Mediator.Send(new GetEmployeeQuery() { Guid = uuid }, + cancellationToken); + } + + [HttpPut("{uuid:guid}")] + [SwaggerOperation("Update an employee")] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Object already exists", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", typeof(EmployeeDto))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "One or more addresses was not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Update( + [FromRoute] Guid uuid, + [FromBody] UpdateEmployeeViewModel viewModel, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new UpdateEmployeeCommand() + { + Guid = uuid, + FirstName = viewModel.FirstName, + LastName = viewModel.LastName, + Patronymic = viewModel.Patronymic, + Sex = Sex.FromName(viewModel.Sex), + BirthDate = viewModel.BirthDate, + Documents = viewModel.Documents.Select( + e => new EmployeeDocumentModel() + { + DocumentType = DocumentType.FromName(e.DocumentType), + Information = e.Information + + }).ToArray(), + CompanyGuid = viewModel.CompanyUuid + }, + cancellationToken); + } + + [HttpDelete("{uuid:guid}")] + [SwaggerOperation("Delete an employee")] + [SwaggerResponse(StatusCodes.Status204NoContent, "Request successful")] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Delete( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + await Mediator.Send( + new DeleteEmployeeCommand() { Guid = uuid }, + cancellationToken); + return StatusCode(StatusCodes.Status204NoContent); + } +} diff --git a/src/Persistence/InMemory/InMemoryDbContext.cs b/src/Persistence/InMemory/InMemoryDbContext.cs index 9b2f919..e27b3ba 100644 --- a/src/Persistence/InMemory/InMemoryDbContext.cs +++ b/src/Persistence/InMemory/InMemoryDbContext.cs @@ -1,5 +1,4 @@ using cuqmbr.TravelGuide.Domain.Enums; -using cuqmbr.TravelGuide.Domain.Entities; using Microsoft.EntityFrameworkCore; using cuqmbr.TravelGuide.Persistence.TypeConverters; @@ -10,11 +9,6 @@ public class InMemoryDbContext : DbContext public InMemoryDbContext(DbContextOptions options) : base(options) { } - public DbSet Countries { get => Set(); } - public DbSet Regions { get => Set(); } - public DbSet Cities { get => Set(); } - public DbSet
Addresses { get => Set
(); } - protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); diff --git a/src/Persistence/InMemory/InMemoryUnitOfWork.cs b/src/Persistence/InMemory/InMemoryUnitOfWork.cs index 0773c64..f7f1d15 100644 --- a/src/Persistence/InMemory/InMemoryUnitOfWork.cs +++ b/src/Persistence/InMemory/InMemoryUnitOfWork.cs @@ -27,6 +27,7 @@ public sealed class InMemoryUnitOfWork : UnitOfWork RouteAddressRepository = new InMemoryRouteAddressRepository(_dbContext); CompanyRepository = new InMemoryCompanyRepository(_dbContext); + EmployeeRepository = new InMemoryEmployeeRepository(_dbContext); } public CountryRepository CountryRepository { get; init; } @@ -53,6 +54,8 @@ public sealed class InMemoryUnitOfWork : UnitOfWork public CompanyRepository CompanyRepository { get; init; } + public EmployeeRepository EmployeeRepository { get; init; } + public int Save() { return _dbContext.SaveChanges(); diff --git a/src/Persistence/InMemory/Repositories/InMemoryEmployeeRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryEmployeeRepository.cs new file mode 100644 index 0000000..40e9b63 --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryEmployeeRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; + +public sealed class InMemoryEmployeeRepository : + InMemoryBaseRepository, EmployeeRepository +{ + public InMemoryEmployeeRepository(InMemoryDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/PostgreSql/Configurations/EmployeeConfiguration.cs b/src/Persistence/PostgreSql/Configurations/EmployeeConfiguration.cs new file mode 100644 index 0000000..fd4ccb5 --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/EmployeeConfiguration.cs @@ -0,0 +1,81 @@ +using cuqmbr.TravelGuide.Domain.Entities; +using cuqmbr.TravelGuide.Domain.Enums; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations; + +public class EmployeeConfiguration : BaseConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + builder + .Property(e => e.Sex) + .HasColumnName("sex") + .IsRequired(true); + + builder + .ToTable( + "employees", + e => e.HasCheckConstraint( + "ck_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(e => e.Sex) + .Metadata.GetColumnName()}", + $"{builder.Property(e => e.Sex) + .Metadata.GetColumnName()} IN ('{String + .Join("', '", Sex.Enumerations + .Values.Select(v => v.Name))}')")); + + base.Configure(builder); + + + builder + .Property(e => e.FirstName) + .HasColumnName("first_name") + .HasColumnType("varchar(32)") + .IsRequired(true); + + builder + .Property(e => e.LastName) + .HasColumnName("last_name") + .HasColumnType("varchar(32)") + .IsRequired(true); + + builder + .Property(e => e.Patronymic) + .HasColumnName("patronymic") + .HasColumnType("varchar(32)") + .IsRequired(true); + + builder + .Property(e => e.BirthDate) + .HasColumnName("birth_date") + .HasColumnType("date") + .IsRequired(true); + + + builder + .Property(e => e.CompanyId) + .HasColumnName("company_id") + .HasColumnType("bigint") + .IsRequired(true); + + builder + .HasOne(e => e.Company) + .WithMany(c => c.Employees) + .HasForeignKey(e => e.CompanyId) + .HasConstraintName( + "fk_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(e => e.CompanyId).Metadata.GetColumnName()}") + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasIndex(e => e.CompanyId) + .HasDatabaseName( + "ix_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(e => e.CompanyId).Metadata.GetColumnName()}"); + } +} diff --git a/src/Persistence/PostgreSql/Configurations/EmployeeDocumentConfiguration.cs b/src/Persistence/PostgreSql/Configurations/EmployeeDocumentConfiguration.cs new file mode 100644 index 0000000..1878cc6 --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/EmployeeDocumentConfiguration.cs @@ -0,0 +1,63 @@ +using cuqmbr.TravelGuide.Domain.Entities; +using cuqmbr.TravelGuide.Domain.Enums; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations; + +public class EmployeeDocumentConfiguration : BaseConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + builder + .Property(ed => ed.DocumentType) + .HasColumnName("document_type") + .IsRequired(true); + + builder + .ToTable( + "employee_documents", + ed => ed.HasCheckConstraint( + "ck_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(ed => ed.DocumentType) + .Metadata.GetColumnName()}", + $"{builder.Property(ed => ed.DocumentType) + .Metadata.GetColumnName()} IN ('{String + .Join("', '", DocumentType.Enumerations + .Values.Select(v => v.Name))}')")); + + base.Configure(builder); + + + builder + .Property(ed => ed.Information) + .HasColumnName("information") + .HasColumnType("varchar(256)") + .IsRequired(true); + + + builder + .Property(ed => ed.EmployeeId) + .HasColumnName("employee_id") + .HasColumnType("bigint") + .IsRequired(true); + + builder + .HasOne(ed => ed.Employee) + .WithMany(ed => ed.Documents) + .HasForeignKey(ed => ed.EmployeeId) + .HasConstraintName( + "fk_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(ed => ed.EmployeeId).Metadata.GetColumnName()}") + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasIndex(ed => ed.EmployeeId) + .HasDatabaseName( + "ix_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(ed => ed.EmployeeId).Metadata.GetColumnName()}"); + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250515164353_Add_Employee_and_EmployeeDocument.Designer.cs b/src/Persistence/PostgreSql/Migrations/20250515164353_Add_Employee_and_EmployeeDocument.Designer.cs new file mode 100644 index 0000000..04dca68 --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250515164353_Add_Employee_and_EmployeeDocument.Designer.cs @@ -0,0 +1,841 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using cuqmbr.TravelGuide.Persistence.PostgreSql; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + [DbContext(typeof(PostgreSqlDbContext))] + [Migration("20250515164353_Add_Employee_and_EmployeeDocument")] + partial class Add_Employee_and_EmployeeDocument + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("application") + .HasAnnotation("ProductVersion", "9.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.HasSequence("addresses_id_sequence"); + + modelBuilder.HasSequence("cities_id_sequence"); + + modelBuilder.HasSequence("companies_id_sequence"); + + modelBuilder.HasSequence("countries_id_sequence"); + + modelBuilder.HasSequence("employee_documents_id_sequence"); + + modelBuilder.HasSequence("employees_id_sequence"); + + modelBuilder.HasSequence("regions_id_sequence"); + + modelBuilder.HasSequence("route_address_details_id_sequence"); + + modelBuilder.HasSequence("route_addresses_id_sequence"); + + modelBuilder.HasSequence("routes_id_sequence"); + + modelBuilder.HasSequence("vehicle_enrollments_id_sequence"); + + modelBuilder.HasSequence("vehicles_id_sequence"); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.addresses_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "addresses_id_sequence"); + + b.Property("CityId") + .HasColumnType("bigint") + .HasColumnName("city_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Latitude") + .HasColumnType("double precision"); + + b.Property("Longitude") + .HasColumnType("double precision"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(128)") + .HasColumnName("name"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_addresses"); + + b.HasAlternateKey("Guid") + .HasName("altk_addresses_uuid"); + + b.HasIndex("CityId") + .HasDatabaseName("ix_addresses_city_id"); + + b.ToTable("addresses", "application", t => + { + t.HasCheckConstraint("ck_addresses_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.cities_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "cities_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.Property("RegionId") + .HasColumnType("bigint") + .HasColumnName("region_id"); + + b.HasKey("Id") + .HasName("pk_cities"); + + b.HasAlternateKey("Guid") + .HasName("altk_cities_uuid"); + + b.HasIndex("RegionId") + .HasDatabaseName("ix_cities_region_id"); + + b.ToTable("cities", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Company", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.companies_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "companies_id_sequence"); + + b.Property("ContactEmail") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("contact_email"); + + b.Property("ContactPhoneNumber") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("contact_phone_number"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("LegalAddress") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("legal_address"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_companies"); + + b.HasAlternateKey("Guid") + .HasName("altk_companies_uuid"); + + b.ToTable("companies", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.countries_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "countries_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_countries"); + + b.HasAlternateKey("Guid") + .HasName("altk_countries_uuid"); + + b.ToTable("countries", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.employees_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "employees_id_sequence"); + + b.Property("BirthDate") + .HasColumnType("date") + .HasColumnName("birth_date"); + + b.Property("CompanyId") + .HasColumnType("bigint") + .HasColumnName("company_id"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("first_name"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("last_name"); + + b.Property("Patronymic") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("patronymic"); + + b.Property("Sex") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("sex"); + + b.HasKey("Id") + .HasName("pk_employees"); + + b.HasAlternateKey("Guid") + .HasName("altk_employees_uuid"); + + b.HasIndex("CompanyId") + .HasDatabaseName("ix_employees_company_id"); + + b.ToTable("employees", "application", t => + { + t.HasCheckConstraint("ck_employees_sex", "sex IN ('male', 'female')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.EmployeeDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.employee_documents_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "employee_documents_id_sequence"); + + b.Property("DocumentType") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("document_type"); + + b.Property("EmployeeId") + .HasColumnType("bigint") + .HasColumnName("employee_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Information") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("information"); + + b.HasKey("Id") + .HasName("pk_employee_documents"); + + b.HasAlternateKey("Guid") + .HasName("altk_employee_documents_uuid"); + + b.HasIndex("EmployeeId") + .HasDatabaseName("ix_employee_documents_employee_id"); + + b.ToTable("employee_documents", "application", t => + { + t.HasCheckConstraint("ck_employee_documents_document_type", "document_type IN ('passport')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.regions_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "regions_id_sequence"); + + b.Property("CountryId") + .HasColumnType("bigint") + .HasColumnName("country_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_regions"); + + b.HasAlternateKey("Guid") + .HasName("altk_regions_uuid"); + + b.HasIndex("CountryId") + .HasDatabaseName("ix_regions_country_id"); + + b.ToTable("regions", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.routes_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "routes_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_routes"); + + b.HasAlternateKey("Guid") + .HasName("altk_routes_uuid"); + + b.ToTable("routes", "application", t => + { + t.HasCheckConstraint("ck_routes_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.route_addresses_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "route_addresses_id_sequence"); + + b.Property("AddressId") + .HasColumnType("bigint") + .HasColumnName("address_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Order") + .HasColumnType("smallint") + .HasColumnName("order"); + + b.Property("RouteId") + .HasColumnType("bigint") + .HasColumnName("route_id"); + + b.HasKey("Id") + .HasName("pk_route_addresses"); + + b.HasAlternateKey("Guid") + .HasName("altk_route_addresses_uuid"); + + b.HasAlternateKey("AddressId", "RouteId", "Order") + .HasName("altk_route_addresses_address_id_route_id_order"); + + b.HasIndex("AddressId") + .HasDatabaseName("ix_route_addresses_address_id"); + + b.HasIndex("RouteId") + .HasDatabaseName("ix_route_addresses_route_id"); + + b.ToTable("route_addresses", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddressDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.route_address_details_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "route_address_details_id_sequence"); + + b.Property("CostToNextAddress") + .HasColumnType("numeric(24,12)") + .HasColumnName("cost_to_next_address"); + + b.Property("CurrentAddressStopTime") + .HasColumnType("interval") + .HasColumnName("current_address_stop_time"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RouteAddressId") + .HasColumnType("bigint") + .HasColumnName("route_address_id"); + + b.Property("TimeToNextAddress") + .HasColumnType("interval") + .HasColumnName("time_to_next_address"); + + b.Property("VehicleEnrollmentId") + .HasColumnType("bigint") + .HasColumnName("vehicle_enrollment_id"); + + b.HasKey("Id") + .HasName("pk_route_address_details"); + + b.HasAlternateKey("Guid") + .HasName("altk_route_address_details_uuid"); + + b.HasIndex("RouteAddressId") + .HasDatabaseName("ix_route_address_details_route_address_id"); + + b.HasIndex("VehicleEnrollmentId") + .HasDatabaseName("ix_route_address_details_vehicle_enrollment_id"); + + b.ToTable("route_address_details", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.vehicles_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "vehicles_id_sequence"); + + b.Property("CompanyId") + .HasColumnType("bigint") + .HasColumnName("company_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("varchar(16)") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_vehicles"); + + b.HasAlternateKey("Guid") + .HasName("altk_vehicles_uuid"); + + b.HasIndex("CompanyId") + .HasDatabaseName("ix_vehicles_company_id"); + + b.ToTable("vehicles", "application", t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator("VehicleType"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.vehicle_enrollments_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "vehicle_enrollments_id_sequence"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("varchar(8)") + .HasColumnName("currency"); + + b.Property("DepartureTime") + .HasColumnType("timestamptz") + .HasColumnName("departure_time"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("RouteId") + .HasColumnType("bigint") + .HasColumnName("route_id"); + + b.Property("VehicleId") + .HasColumnType("bigint") + .HasColumnName("vehicle_id"); + + b.HasKey("Id") + .HasName("pk_vehicle_enrollments"); + + b.HasAlternateKey("Guid") + .HasName("altk_vehicle_enrollments_uuid"); + + b.HasIndex("RouteId") + .HasDatabaseName("ix_vehicle_enrollments_route_id"); + + b.HasIndex("VehicleId") + .HasDatabaseName("ix_vehicle_enrollments_vehicle_id"); + + b.ToTable("vehicle_enrollments", "application", t => + { + t.HasCheckConstraint("ck_vehicle_enrollments_currency", "currency IN ('DEFAULT', 'USD', 'EUR', 'UAH')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Aircraft", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("aircraft"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Bus", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("bus"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Train", b => + { + b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle"); + + b.Property("Capacity") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("smallint") + .HasColumnName("capacity"); + + b.Property("Model") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(64)") + .HasColumnName("model"); + + b.Property("Number") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("varchar(32)") + .HasColumnName("number"); + + b.ToTable(t => + { + t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')"); + }); + + b.HasDiscriminator().HasValue("train"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.City", "City") + .WithMany("Addresses") + .HasForeignKey("CityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_addresses_city_id"); + + b.Navigation("City"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Region", "Region") + .WithMany("Cities") + .HasForeignKey("RegionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_cities_region_id"); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Company", "Company") + .WithMany("Employees") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employees_company_id"); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.EmployeeDocument", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Employee", "Employee") + .WithMany("Documents") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employee_documents_employee_id"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Country", "Country") + .WithMany("Regions") + .HasForeignKey("CountryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_regions_country_id"); + + b.Navigation("Country"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Address", "Address") + .WithMany("AddressRoutes") + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_addresses_address_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Route", "Route") + .WithMany("RouteAddresses") + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_addresses_route_id"); + + b.Navigation("Address"); + + b.Navigation("Route"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddressDetail", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", "RouteAddress") + .WithMany("Details") + .HasForeignKey("RouteAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_address_details_route_address_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", "VehicleEnrollment") + .WithMany("RouteAddressDetails") + .HasForeignKey("VehicleEnrollmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_address_details_vehicle_enrollment_id"); + + b.Navigation("RouteAddress"); + + b.Navigation("VehicleEnrollment"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Company", "Company") + .WithMany("Vehicles") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicles_company_id"); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Route", "Route") + .WithMany("VehicleEnrollments") + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollments_route_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Vehicle", "Vehicle") + .WithMany("Enrollments") + .HasForeignKey("VehicleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_vehicle_enrollments_vehicle_id"); + + b.Navigation("Route"); + + b.Navigation("Vehicle"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.Navigation("AddressRoutes"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.Navigation("Addresses"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Company", b => + { + b.Navigation("Employees"); + + b.Navigation("Vehicles"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Navigation("Regions"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.Navigation("Cities"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => + { + b.Navigation("RouteAddresses"); + + b.Navigation("VehicleEnrollments"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.Navigation("Details"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b => + { + b.Navigation("Enrollments"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b => + { + b.Navigation("RouteAddressDetails"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250515164353_Add_Employee_and_EmployeeDocument.cs b/src/Persistence/PostgreSql/Migrations/20250515164353_Add_Employee_and_EmployeeDocument.cs new file mode 100644 index 0000000..1f8fc90 --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250515164353_Add_Employee_and_EmployeeDocument.cs @@ -0,0 +1,108 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + /// + public partial class Add_Employee_and_EmployeeDocument : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateSequence( + name: "employee_documents_id_sequence", + schema: "application"); + + migrationBuilder.CreateSequence( + name: "employees_id_sequence", + schema: "application"); + + migrationBuilder.CreateTable( + name: "employees", + schema: "application", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false, defaultValueSql: "nextval('application.employees_id_sequence')"), + first_name = table.Column(type: "varchar(32)", nullable: false), + last_name = table.Column(type: "varchar(32)", nullable: false), + patronymic = table.Column(type: "varchar(32)", nullable: false), + sex = table.Column(type: "varchar(32)", nullable: false), + birth_date = table.Column(type: "date", nullable: false), + company_id = table.Column(type: "bigint", nullable: false), + uuid = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_employees", x => x.id); + table.UniqueConstraint("altk_employees_uuid", x => x.uuid); + table.CheckConstraint("ck_employees_sex", "sex IN ('male', 'female')"); + table.ForeignKey( + name: "fk_employees_company_id", + column: x => x.company_id, + principalSchema: "application", + principalTable: "companies", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "employee_documents", + schema: "application", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false, defaultValueSql: "nextval('application.employee_documents_id_sequence')"), + document_type = table.Column(type: "varchar(64)", nullable: false), + information = table.Column(type: "varchar(256)", nullable: false), + employee_id = table.Column(type: "bigint", nullable: false), + uuid = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_employee_documents", x => x.id); + table.UniqueConstraint("altk_employee_documents_uuid", x => x.uuid); + table.CheckConstraint("ck_employee_documents_document_type", "document_type IN ('passport')"); + table.ForeignKey( + name: "fk_employee_documents_employee_id", + column: x => x.employee_id, + principalSchema: "application", + principalTable: "employees", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_employee_documents_employee_id", + schema: "application", + table: "employee_documents", + column: "employee_id"); + + migrationBuilder.CreateIndex( + name: "ix_employees_company_id", + schema: "application", + table: "employees", + column: "company_id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "employee_documents", + schema: "application"); + + migrationBuilder.DropTable( + name: "employees", + schema: "application"); + + migrationBuilder.DropSequence( + name: "employee_documents_id_sequence", + schema: "application"); + + migrationBuilder.DropSequence( + name: "employees_id_sequence", + schema: "application"); + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs b/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs index 2d7ebeb..c2eb06f 100644 --- a/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs +++ b/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs @@ -31,6 +31,10 @@ namespace Persistence.PostgreSql.Migrations modelBuilder.HasSequence("countries_id_sequence"); + modelBuilder.HasSequence("employee_documents_id_sequence"); + + modelBuilder.HasSequence("employees_id_sequence"); + modelBuilder.HasSequence("regions_id_sequence"); modelBuilder.HasSequence("route_address_details_id_sequence"); @@ -198,6 +202,106 @@ namespace Persistence.PostgreSql.Migrations b.ToTable("countries", "application"); }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.employees_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "employees_id_sequence"); + + b.Property("BirthDate") + .HasColumnType("date") + .HasColumnName("birth_date"); + + b.Property("CompanyId") + .HasColumnType("bigint") + .HasColumnName("company_id"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("first_name"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("last_name"); + + b.Property("Patronymic") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("patronymic"); + + b.Property("Sex") + .IsRequired() + .HasColumnType("varchar(32)") + .HasColumnName("sex"); + + b.HasKey("Id") + .HasName("pk_employees"); + + b.HasAlternateKey("Guid") + .HasName("altk_employees_uuid"); + + b.HasIndex("CompanyId") + .HasDatabaseName("ix_employees_company_id"); + + b.ToTable("employees", "application", t => + { + t.HasCheckConstraint("ck_employees_sex", "sex IN ('male', 'female')"); + }); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.EmployeeDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.employee_documents_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "employee_documents_id_sequence"); + + b.Property("DocumentType") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("document_type"); + + b.Property("EmployeeId") + .HasColumnType("bigint") + .HasColumnName("employee_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Information") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("information"); + + b.HasKey("Id") + .HasName("pk_employee_documents"); + + b.HasAlternateKey("Guid") + .HasName("altk_employee_documents_uuid"); + + b.HasIndex("EmployeeId") + .HasDatabaseName("ix_employee_documents_employee_id"); + + b.ToTable("employee_documents", "application", t => + { + t.HasCheckConstraint("ck_employee_documents_document_type", "document_type IN ('passport')"); + }); + }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => { b.Property("Id") @@ -564,6 +668,30 @@ namespace Persistence.PostgreSql.Migrations b.Navigation("Region"); }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Company", "Company") + .WithMany("Employees") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employees_company_id"); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.EmployeeDocument", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Employee", "Employee") + .WithMany("Documents") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employee_documents_employee_id"); + + b.Navigation("Employee"); + }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => { b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Country", "Country") @@ -663,6 +791,8 @@ namespace Persistence.PostgreSql.Migrations modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Company", b => { + b.Navigation("Employees"); + b.Navigation("Vehicles"); }); @@ -671,6 +801,11 @@ namespace Persistence.PostgreSql.Migrations b.Navigation("Regions"); }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b => + { + b.Navigation("Documents"); + }); + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => { b.Navigation("Cities"); diff --git a/src/Persistence/PostgreSql/PostgreSqlDbContext.cs b/src/Persistence/PostgreSql/PostgreSqlDbContext.cs index c9e667e..11e5d63 100644 --- a/src/Persistence/PostgreSql/PostgreSqlDbContext.cs +++ b/src/Persistence/PostgreSql/PostgreSqlDbContext.cs @@ -44,6 +44,16 @@ public class PostgreSqlDbContext : DbContext .HaveColumnType("varchar(8)") .HaveConversion(); + builder + .Properties() + .HaveColumnType("varchar(64)") + .HaveConversion(); + + builder + .Properties() + .HaveColumnType("varchar(32)") + .HaveConversion(); + builder .Properties() .HaveConversion(); diff --git a/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs b/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs index 11eab91..46ff3a9 100644 --- a/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs +++ b/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs @@ -27,6 +27,7 @@ public sealed class PostgreSqlUnitOfWork : UnitOfWork RouteAddressRepository = new PostgreSqlRouteAddressRepository(_dbContext); CompanyRepository = new PostgreSqlCompanyRepository(_dbContext); + EmployeeRepository = new PostgreSqlEmployeeRepository(_dbContext); } public CountryRepository CountryRepository { get; init; } @@ -53,6 +54,8 @@ public sealed class PostgreSqlUnitOfWork : UnitOfWork public CompanyRepository CompanyRepository { get; init; } + public EmployeeRepository EmployeeRepository { get; init; } + public int Save() { return _dbContext.SaveChanges(); diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlEmployeeRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlEmployeeRepository.cs new file mode 100644 index 0000000..03319c3 --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlEmployeeRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; + +public sealed class PostgreSqlEmployeeRepository : + PostgreSqlBaseRepository, EmployeeRepository +{ + public PostgreSqlEmployeeRepository(PostgreSqlDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/TypeConverters/DocumentTypeConverter.cs b/src/Persistence/TypeConverters/DocumentTypeConverter.cs new file mode 100644 index 0000000..83e2632 --- /dev/null +++ b/src/Persistence/TypeConverters/DocumentTypeConverter.cs @@ -0,0 +1,13 @@ +using cuqmbr.TravelGuide.Domain.Enums; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace cuqmbr.TravelGuide.Persistence.TypeConverters; + +public class DocumentTypeConverter : ValueConverter +{ + public DocumentTypeConverter() + : base( + v => v.Name, + v => DocumentType.FromName(v)) + { } +} diff --git a/src/Persistence/TypeConverters/SexConverter.cs b/src/Persistence/TypeConverters/SexConverter.cs new file mode 100644 index 0000000..f3d2fd5 --- /dev/null +++ b/src/Persistence/TypeConverters/SexConverter.cs @@ -0,0 +1,13 @@ +using cuqmbr.TravelGuide.Domain.Enums; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace cuqmbr.TravelGuide.Persistence.TypeConverters; + +public class SexConverter : ValueConverter +{ + public SexConverter() + : base( + v => v.Name, + v => Sex.FromName(v)) + { } +}