add companies management

This commit is contained in:
cuqmbr 2025-05-15 19:18:52 +03:00
parent 74dc7ceff3
commit f4611f029f
Signed by: cuqmbr
GPG Key ID: 0AA446880C766199
81 changed files with 2103 additions and 70 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,24 @@
using FluentValidation;
namespace cuqmbr.TravelGuide.Application.Common.FluentValidation;
public static class CustomValidators
{
// According to RFC 5321.
public static IRuleBuilderOptions<T, string> IsEmail<T>(
this IRuleBuilder<T, string> ruleBuilder)
{
return
ruleBuilder
.Matches(@"^[\w\.-]{1,64}@[\w\.-]{1,251}\.\w{2,4}$");
}
// According to ITU-T E.164, no spaces.
public static IRuleBuilderOptions<T, string> IsPhoneNumber<T>(
this IRuleBuilder<T, string> ruleBuilder)
{
return
ruleBuilder
.Matches(@"^\+[0-9]{7,15}$");
}
}

View File

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

View File

@ -26,6 +26,8 @@ public interface UnitOfWork : IDisposable
RouteAddressRepository RouteAddressRepository { get; }
CompanyRepository CompanyRepository { get; }
int Save();
Task<int> SaveAsync(CancellationToken cancellationToken);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,9 @@
"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}.",
"MustBeInEnum": "Must be one of the following: {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."
},
"Validation": {
"DistinctOrder": "Must have distinct order values.",

View File

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

View File

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

View File

@ -33,5 +33,9 @@ public class AddTrainCommandValidator : AbstractValidator<AddTrainCommand>
RuleFor(v => v.Capacity)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
RuleFor(v => v.CompanyGuid)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
}
}

View File

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

View File

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

View File

@ -37,5 +37,9 @@ public class UpdateTrainCommandValidator : AbstractValidator<UpdateTrainCommand>
RuleFor(v => v.Capacity)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
RuleFor(v => v.CompanyGuid)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
}
}

View File

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

View File

@ -13,9 +13,7 @@ public record GetTrainsPageQuery : IRequest<PaginatedList<TrainDto>>
public string Sort { get; set; } = String.Empty;
public string? Number { get; set; }
public string? Model { get; set; }
public Guid? CompanyGuid { get; set; }
public short? CapacityGreaterOrEqualThan { get; set; }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,15 @@
namespace cuqmbr.TravelGuide.Domain.Entities;
public sealed class Company : EntityBase
{
public string Name { get; set; }
public string LegalAddress { get; set; }
public string ContactEmail { get; set; }
public string ContactPhoneNumber { get; set; }
public ICollection<Vehicle> Vehicles { get; set; }
}

View File

@ -7,5 +7,10 @@ public abstract class Vehicle : EntityBase
public VehicleType VehicleType { get; set; }
public long CompanyId { get; set; }
public Company Company { get; set; }
public ICollection<VehicleEnrollment> Enrollments { get; set; }
}

View File

@ -2,7 +2,6 @@ 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.Aircrafts;
using cuqmbr.TravelGuide.Application.Aircrafts.Commands.AddAircraft;
using cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraftsPage;
@ -51,7 +50,8 @@ public class AircraftsController : ControllerBase
{
Number = viewModel.Number,
Model = viewModel.Model,
Capacity = viewModel.Capacity
Capacity = viewModel.Capacity,
CompanyGuid = viewModel.CompanyUuid
},
cancellationToken));
}
@ -87,10 +87,11 @@ public class AircraftsController : ControllerBase
PageSize = pageQuery.PageSize,
Search = searchQuery.Search,
Sort = sortQuery.Sort,
CapacityGreaterOrEqualThan =
filterQuery.CapacityGreaterOrEqualThan,
CapacityLessOrEqualThan =
filterQuery.CapacityLessOrEqualThan
CompanyGuid = filterQuery.CompanyUuid,
CapacityGreaterThanOrEqualTo =
filterQuery.CapacityGreaterThanOrEqualTo,
CapacityLessThanOrEqualTo =
filterQuery.CapacityLessThanOrEqualTo
},
cancellationToken);
}
@ -158,7 +159,8 @@ public class AircraftsController : ControllerBase
Guid = uuid,
Number = viewModel.Number,
Model = viewModel.Model,
Capacity = viewModel.Capacity
Capacity = viewModel.Capacity,
CompanyGuid = viewModel.CompanyUuid
},
cancellationToken);
}

View File

@ -2,7 +2,6 @@ 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.Buses;
using cuqmbr.TravelGuide.Application.Buses.Commands.AddBus;
using cuqmbr.TravelGuide.Application.Buses.Queries.GetBusesPage;
@ -51,7 +50,8 @@ public class BusesController : ControllerBase
{
Number = viewModel.Number,
Model = viewModel.Model,
Capacity = viewModel.Capacity
Capacity = viewModel.Capacity,
CompanyGuid = viewModel.CompanyUuid
},
cancellationToken));
}
@ -87,10 +87,11 @@ public class BusesController : ControllerBase
PageSize = pageQuery.PageSize,
Search = searchQuery.Search,
Sort = sortQuery.Sort,
CapacityGreaterOrEqualThan =
filterQuery.CapacityGreaterOrEqualThan,
CapacityLessOrEqualThan =
filterQuery.CapacityLessOrEqualThan
CompanyGuid = filterQuery.CompanyUuid,
CapacityGreaterThanOrEqualTo =
filterQuery.CapacityGreaterThanOrEqualTo,
CapacityLessThanOrEqualTo =
filterQuery.CapacityLessThanOrEqualTo
},
cancellationToken);
}
@ -158,7 +159,8 @@ public class BusesController : ControllerBase
Guid = uuid,
Number = viewModel.Number,
Model = viewModel.Model,
Capacity = viewModel.Capacity
Capacity = viewModel.Capacity,
CompanyGuid = viewModel.CompanyUuid
},
cancellationToken);
}

View File

@ -0,0 +1,190 @@
using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.Annotations;
using cuqmbr.TravelGuide.Application.Common.Models;
using cuqmbr.TravelGuide.Application.Common.ViewModels;
using cuqmbr.TravelGuide.Application.Companies;
using cuqmbr.TravelGuide.Application.Companies.Commands.AddCompany;
using cuqmbr.TravelGuide.Application.Companies.Queries.GetCompaniesPage;
using cuqmbr.TravelGuide.Application.Companies.Queries.GetCompany;
using cuqmbr.TravelGuide.Application.Companies.Commands.UpdateCompany;
using cuqmbr.TravelGuide.Application.Companies.Commands.DeleteCompany;
using cuqmbr.TravelGuide.Application.Companies.ViewModels;
namespace cuqmbr.TravelGuide.HttpApi.Controllers;
[Route("companies")]
public class CompaniesController : ControllerBase
{
[HttpPost]
[SwaggerOperation("Add a company")]
[SwaggerResponse(
StatusCodes.Status201Created, "Object successfuly created",
typeof(CompanyDto))]
[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, "Parent object not found",
typeof(ProblemDetails))]
[SwaggerResponse(
StatusCodes.Status500InternalServerError, "Internal server error",
typeof(ProblemDetails))]
public async Task<ActionResult<CompanyDto>> Add(
[FromBody] AddCompanyViewModel viewModel,
CancellationToken cancellationToken)
{
return StatusCode(
StatusCodes.Status201Created,
await Mediator.Send(
new AddCompanyCommand()
{
Name = viewModel.Name,
LegalAddress = viewModel.LegalAddress,
ContactEmail = viewModel.ContactEmail,
ContactPhoneNumber = viewModel.ContactPhoneNumber,
},
cancellationToken));
}
[HttpGet]
[SwaggerOperation("Get a list of all companies")]
[SwaggerResponse(
StatusCodes.Status200OK, "Request successful",
typeof(PaginatedList<CompanyDto>))]
[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<PaginatedList<CompanyDto>> GetPage(
[FromQuery] PageQuery pageQuery, [FromQuery] SearchQuery searchQuery,
[FromQuery] SortQuery sortQuery,
CancellationToken cancellationToken)
{
return await Mediator.Send(
new GetCompaniesPageQuery()
{
PageNumber = pageQuery.PageNumber,
PageSize = pageQuery.PageSize,
Search = searchQuery.Search,
Sort = sortQuery.Sort
},
cancellationToken);
}
[HttpGet("{uuid:guid}")]
[SwaggerOperation("Get a company by uuid")]
[SwaggerResponse(
StatusCodes.Status200OK, "Request successful", typeof(CompanyDto))]
[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(CompanyDto))]
[SwaggerResponse(
StatusCodes.Status500InternalServerError, "Internal server error",
typeof(ProblemDetails))]
public async Task<CompanyDto> Get(
[FromRoute] Guid uuid,
CancellationToken cancellationToken)
{
return await Mediator.Send(new GetCompanyQuery() { Guid = uuid },
cancellationToken);
}
[HttpPut("{uuid:guid}")]
[SwaggerOperation("Update a company")]
[SwaggerResponse(
StatusCodes.Status200OK, "Request successful", typeof(CompanyDto))]
[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(CompanyDto))]
[SwaggerResponse(
StatusCodes.Status404NotFound, "Parent object not found",
typeof(ProblemDetails))]
[SwaggerResponse(
StatusCodes.Status500InternalServerError, "Internal server error",
typeof(ProblemDetails))]
public async Task<CompanyDto> Update(
[FromRoute] Guid uuid,
[FromBody] UpdateCompanyViewModel viewModel,
CancellationToken cancellationToken)
{
return await Mediator.Send(
new UpdateCompanyCommand()
{
Guid = uuid,
Name = viewModel.Name,
LegalAddress = viewModel.LegalAddress,
ContactEmail = viewModel.ContactEmail,
ContactPhoneNumber = viewModel.ContactPhoneNumber,
},
cancellationToken);
}
[HttpDelete("{uuid:guid}")]
[SwaggerOperation("Delete a company")]
[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<IActionResult> Delete(
[FromRoute] Guid uuid,
CancellationToken cancellationToken)
{
await Mediator.Send(
new DeleteCompanyCommand() { Guid = uuid },
cancellationToken);
return StatusCode(StatusCodes.Status204NoContent);
}
}

View File

@ -88,9 +88,9 @@ public class TrainsController : ControllerBase
Search = searchQuery.Search,
Sort = sortQuery.Sort,
CapacityGreaterOrEqualThan =
filterQuery.CapacityGreaterOrEqualThan,
filterQuery.CapacityGreaterThanOrEqualTo,
CapacityLessOrEqualThan =
filterQuery.CapacityLessOrEqualThan
filterQuery.CapacityLessThanOrEqualTo
},
cancellationToken);
}

View File

@ -26,6 +26,7 @@ public sealed class InMemoryUnitOfWork : UnitOfWork
new InMemoryVehicleEnrollmentRepository(_dbContext);
RouteAddressRepository =
new InMemoryRouteAddressRepository(_dbContext);
CompanyRepository = new InMemoryCompanyRepository(_dbContext);
}
public CountryRepository CountryRepository { get; init; }
@ -50,6 +51,8 @@ public sealed class InMemoryUnitOfWork : UnitOfWork
public RouteAddressRepository RouteAddressRepository { get; init; }
public CompanyRepository CompanyRepository { get; init; }
public int Save()
{
return _dbContext.SaveChanges();

View File

@ -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 InMemoryCompanyRepository :
InMemoryBaseRepository<Company>, CompanyRepository
{
public InMemoryCompanyRepository(InMemoryDbContext dbContext)
: base(dbContext) { }
}

View File

@ -0,0 +1,42 @@
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 CompanyConfiguration : BaseConfiguration<Company>
{
public override void Configure(EntityTypeBuilder<Company> builder)
{
builder
.ToTable("companies");
base.Configure(builder);
builder
.Property(c => c.Name)
.HasColumnName("name")
.HasColumnType("varchar(64)")
.IsRequired(true);
builder
.Property(c => c.LegalAddress)
.HasColumnName("legal_address")
.HasColumnType("varchar(256)")
.IsRequired(true);
builder
.Property(c => c.ContactEmail)
.HasColumnName("contact_email")
.HasColumnType("varchar(256)")
.IsRequired(true);
builder
.Property(c => c.ContactPhoneNumber)
.HasColumnName("contact_phone_number")
.HasColumnType("varchar(64)")
.IsRequired(true);
}
}

View File

@ -37,5 +37,29 @@ public class VehicleConfiguration : BaseConfiguration<Vehicle>
.HasValue<Train>(VehicleType.Train);
base.Configure(builder);
builder
.Property(v => v.CompanyId)
.HasColumnName("company_id")
.HasColumnType("bigint")
.IsRequired(true);
builder
.HasOne(v => v.Company)
.WithMany(c => c.Vehicles)
.HasForeignKey(v => v.CompanyId)
.HasConstraintName(
"fk_" +
$"{builder.Metadata.GetTableName()}_" +
$"{builder.Property(v => v.CompanyId).Metadata.GetColumnName()}")
.OnDelete(DeleteBehavior.Cascade);
builder
.HasIndex(v => v.CompanyId)
.HasDatabaseName(
"ix_" +
$"{builder.Metadata.GetTableName()}_" +
$"{builder.Property(v => v.CompanyId).Metadata.GetColumnName()}");
}
}

View File

@ -0,0 +1,706 @@
// <auto-generated />
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("20250515101417_Add_Companies")]
partial class Add_Companies
{
/// <inheritdoc />
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("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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id")
.HasDefaultValueSql("nextval('application.addresses_id_sequence')");
NpgsqlPropertyBuilderExtensions.UseSequence(b.Property<long>("Id"), "addresses_id_sequence");
b.Property<long>("CityId")
.HasColumnType("bigint")
.HasColumnName("city_id");
b.Property<Guid>("Guid")
.HasColumnType("uuid")
.HasColumnName("uuid");
b.Property<double>("Latitude")
.HasColumnType("double precision");
b.Property<double>("Longitude")
.HasColumnType("double precision");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("varchar(128)")
.HasColumnName("name");
b.Property<string>("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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id")
.HasDefaultValueSql("nextval('application.cities_id_sequence')");
NpgsqlPropertyBuilderExtensions.UseSequence(b.Property<long>("Id"), "cities_id_sequence");
b.Property<Guid>("Guid")
.HasColumnType("uuid")
.HasColumnName("uuid");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("varchar(64)")
.HasColumnName("name");
b.Property<long>("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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id")
.HasDefaultValueSql("nextval('application.companies_id_sequence')");
NpgsqlPropertyBuilderExtensions.UseSequence(b.Property<long>("Id"), "companies_id_sequence");
b.Property<string>("ContactEmail")
.IsRequired()
.HasColumnType("varchar(256)")
.HasColumnName("contact_email");
b.Property<string>("ContactPhoneNumber")
.IsRequired()
.HasColumnType("varchar(64)")
.HasColumnName("contact_phone_number");
b.Property<Guid>("Guid")
.HasColumnType("uuid")
.HasColumnName("uuid");
b.Property<string>("LegalAddress")
.IsRequired()
.HasColumnType("varchar(256)")
.HasColumnName("legal_address");
b.Property<string>("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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id")
.HasDefaultValueSql("nextval('application.countries_id_sequence')");
NpgsqlPropertyBuilderExtensions.UseSequence(b.Property<long>("Id"), "countries_id_sequence");
b.Property<Guid>("Guid")
.HasColumnType("uuid")
.HasColumnName("uuid");
b.Property<string>("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.Region", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id")
.HasDefaultValueSql("nextval('application.regions_id_sequence')");
NpgsqlPropertyBuilderExtensions.UseSequence(b.Property<long>("Id"), "regions_id_sequence");
b.Property<long>("CountryId")
.HasColumnType("bigint")
.HasColumnName("country_id");
b.Property<Guid>("Guid")
.HasColumnType("uuid")
.HasColumnName("uuid");
b.Property<string>("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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id")
.HasDefaultValueSql("nextval('application.routes_id_sequence')");
NpgsqlPropertyBuilderExtensions.UseSequence(b.Property<long>("Id"), "routes_id_sequence");
b.Property<Guid>("Guid")
.HasColumnType("uuid")
.HasColumnName("uuid");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("varchar(64)")
.HasColumnName("name");
b.Property<string>("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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id")
.HasDefaultValueSql("nextval('application.route_addresses_id_sequence')");
NpgsqlPropertyBuilderExtensions.UseSequence(b.Property<long>("Id"), "route_addresses_id_sequence");
b.Property<long>("AddressId")
.HasColumnType("bigint")
.HasColumnName("address_id");
b.Property<Guid>("Guid")
.HasColumnType("uuid")
.HasColumnName("uuid");
b.Property<short>("Order")
.HasColumnType("smallint")
.HasColumnName("order");
b.Property<long>("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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id")
.HasDefaultValueSql("nextval('application.route_address_details_id_sequence')");
NpgsqlPropertyBuilderExtensions.UseSequence(b.Property<long>("Id"), "route_address_details_id_sequence");
b.Property<decimal>("CostToNextAddress")
.HasColumnType("numeric(24,12)")
.HasColumnName("cost_to_next_address");
b.Property<TimeSpan>("CurrentAddressStopTime")
.HasColumnType("interval")
.HasColumnName("current_address_stop_time");
b.Property<Guid>("Guid")
.HasColumnType("uuid")
.HasColumnName("uuid");
b.Property<long>("RouteAddressId")
.HasColumnType("bigint")
.HasColumnName("route_address_id");
b.Property<TimeSpan>("TimeToNextAddress")
.HasColumnType("interval")
.HasColumnName("time_to_next_address");
b.Property<long>("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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id")
.HasDefaultValueSql("nextval('application.vehicles_id_sequence')");
NpgsqlPropertyBuilderExtensions.UseSequence(b.Property<long>("Id"), "vehicles_id_sequence");
b.Property<long>("CompanyId")
.HasColumnType("bigint")
.HasColumnName("company_id");
b.Property<Guid>("Guid")
.HasColumnType("uuid")
.HasColumnName("uuid");
b.Property<string>("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<string>("VehicleType");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.VehicleEnrollment", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id")
.HasDefaultValueSql("nextval('application.vehicle_enrollments_id_sequence')");
NpgsqlPropertyBuilderExtensions.UseSequence(b.Property<long>("Id"), "vehicle_enrollments_id_sequence");
b.Property<string>("Currency")
.IsRequired()
.HasColumnType("varchar(8)")
.HasColumnName("currency");
b.Property<DateTimeOffset>("DepartureTime")
.HasColumnType("timestamptz")
.HasColumnName("departure_time");
b.Property<Guid>("Guid")
.HasColumnType("uuid")
.HasColumnName("uuid");
b.Property<long>("RouteId")
.HasColumnType("bigint")
.HasColumnName("route_id");
b.Property<long>("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<short>("Capacity")
.ValueGeneratedOnUpdateSometimes()
.HasColumnType("smallint")
.HasColumnName("capacity");
b.Property<string>("Model")
.IsRequired()
.ValueGeneratedOnUpdateSometimes()
.HasColumnType("varchar(64)")
.HasColumnName("model");
b.Property<string>("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<short>("Capacity")
.ValueGeneratedOnUpdateSometimes()
.HasColumnType("smallint")
.HasColumnName("capacity");
b.Property<string>("Model")
.IsRequired()
.ValueGeneratedOnUpdateSometimes()
.HasColumnType("varchar(64)")
.HasColumnName("model");
b.Property<string>("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<short>("Capacity")
.ValueGeneratedOnUpdateSometimes()
.HasColumnType("smallint")
.HasColumnName("capacity");
b.Property<string>("Model")
.IsRequired()
.ValueGeneratedOnUpdateSometimes()
.HasColumnType("varchar(64)")
.HasColumnName("model");
b.Property<string>("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.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("Vehicles");
});
modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b =>
{
b.Navigation("Regions");
});
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
}
}
}

View File

@ -0,0 +1,110 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Persistence.PostgreSql.Migrations
{
/// <inheritdoc />
public partial class Add_Companies : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropCheckConstraint(
name: "ck_vehicle_enrollments_currency",
schema: "application",
table: "vehicle_enrollments");
migrationBuilder.CreateSequence(
name: "companies_id_sequence",
schema: "application");
migrationBuilder.AddColumn<long>(
name: "company_id",
schema: "application",
table: "vehicles",
type: "bigint",
nullable: false,
defaultValue: 0L);
migrationBuilder.CreateTable(
name: "companies",
schema: "application",
columns: table => new
{
id = table.Column<long>(type: "bigint", nullable: false, defaultValueSql: "nextval('application.companies_id_sequence')"),
name = table.Column<string>(type: "varchar(64)", nullable: false),
legal_address = table.Column<string>(type: "varchar(256)", nullable: false),
contact_email = table.Column<string>(type: "varchar(256)", nullable: false),
contact_phone_number = table.Column<string>(type: "varchar(64)", nullable: false),
uuid = table.Column<Guid>(type: "uuid", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_companies", x => x.id);
table.UniqueConstraint("altk_companies_uuid", x => x.uuid);
});
migrationBuilder.CreateIndex(
name: "ix_vehicles_company_id",
schema: "application",
table: "vehicles",
column: "company_id");
migrationBuilder.AddCheckConstraint(
name: "ck_vehicle_enrollments_currency",
schema: "application",
table: "vehicle_enrollments",
sql: "currency IN ('DEFAULT', 'USD', 'EUR', 'UAH')");
migrationBuilder.AddForeignKey(
name: "fk_vehicles_company_id",
schema: "application",
table: "vehicles",
column: "company_id",
principalSchema: "application",
principalTable: "companies",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "fk_vehicles_company_id",
schema: "application",
table: "vehicles");
migrationBuilder.DropTable(
name: "companies",
schema: "application");
migrationBuilder.DropIndex(
name: "ix_vehicles_company_id",
schema: "application",
table: "vehicles");
migrationBuilder.DropCheckConstraint(
name: "ck_vehicle_enrollments_currency",
schema: "application",
table: "vehicle_enrollments");
migrationBuilder.DropColumn(
name: "company_id",
schema: "application",
table: "vehicles");
migrationBuilder.DropSequence(
name: "companies_id_sequence",
schema: "application");
migrationBuilder.AddCheckConstraint(
name: "ck_vehicle_enrollments_currency",
schema: "application",
table: "vehicle_enrollments",
sql: "currency IN ('USD', 'EUR', 'UAH')");
}
}
}

View File

@ -27,6 +27,8 @@ namespace Persistence.PostgreSql.Migrations
modelBuilder.HasSequence("cities_id_sequence");
modelBuilder.HasSequence("companies_id_sequence");
modelBuilder.HasSequence("countries_id_sequence");
modelBuilder.HasSequence("regions_id_sequence");
@ -125,6 +127,49 @@ namespace Persistence.PostgreSql.Migrations
b.ToTable("cities", "application");
});
modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Company", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id")
.HasDefaultValueSql("nextval('application.companies_id_sequence')");
NpgsqlPropertyBuilderExtensions.UseSequence(b.Property<long>("Id"), "companies_id_sequence");
b.Property<string>("ContactEmail")
.IsRequired()
.HasColumnType("varchar(256)")
.HasColumnName("contact_email");
b.Property<string>("ContactPhoneNumber")
.IsRequired()
.HasColumnType("varchar(64)")
.HasColumnName("contact_phone_number");
b.Property<Guid>("Guid")
.HasColumnType("uuid")
.HasColumnName("uuid");
b.Property<string>("LegalAddress")
.IsRequired()
.HasColumnType("varchar(256)")
.HasColumnName("legal_address");
b.Property<string>("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<long>("Id")
@ -327,6 +372,10 @@ namespace Persistence.PostgreSql.Migrations
NpgsqlPropertyBuilderExtensions.UseSequence(b.Property<long>("Id"), "vehicles_id_sequence");
b.Property<long>("CompanyId")
.HasColumnType("bigint")
.HasColumnName("company_id");
b.Property<Guid>("Guid")
.HasColumnType("uuid")
.HasColumnName("uuid");
@ -342,6 +391,9 @@ namespace Persistence.PostgreSql.Migrations
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')");
@ -397,7 +449,7 @@ namespace Persistence.PostgreSql.Migrations
b.ToTable("vehicle_enrollments", "application", t =>
{
t.HasCheckConstraint("ck_vehicle_enrollments_currency", "currency IN ('USD', 'EUR', 'UAH')");
t.HasCheckConstraint("ck_vehicle_enrollments_currency", "currency IN ('DEFAULT', 'USD', 'EUR', 'UAH')");
});
});
@ -566,6 +618,18 @@ namespace Persistence.PostgreSql.Migrations
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")
@ -597,6 +661,11 @@ namespace Persistence.PostgreSql.Migrations
b.Navigation("Addresses");
});
modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Company", b =>
{
b.Navigation("Vehicles");
});
modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b =>
{
b.Navigation("Regions");

View File

@ -26,6 +26,7 @@ public sealed class PostgreSqlUnitOfWork : UnitOfWork
new PostgreSqlVehicleEnrollmentRepository(_dbContext);
RouteAddressRepository =
new PostgreSqlRouteAddressRepository(_dbContext);
CompanyRepository = new PostgreSqlCompanyRepository(_dbContext);
}
public CountryRepository CountryRepository { get; init; }
@ -50,6 +51,8 @@ public sealed class PostgreSqlUnitOfWork : UnitOfWork
public RouteAddressRepository RouteAddressRepository { get; init; }
public CompanyRepository CompanyRepository { get; init; }
public int Save()
{
return _dbContext.SaveChanges();

View File

@ -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 PostgreSqlCompanyRepository :
PostgreSqlBaseRepository<Company>, CompanyRepository
{
public PostgreSqlCompanyRepository(PostgreSqlDbContext dbContext)
: base(dbContext) { }
}