feat: add complex VehicleEnrollment addition and retrieval

This commit is contained in:
cuqmbr 2022-12-03 15:10:03 +02:00
parent 1ba0867390
commit 168b1fdec3
16 changed files with 390 additions and 34 deletions

View File

@ -46,10 +46,6 @@ public class MapperInitializer : Profile
CreateMap<Route, CreateRouteWithAddressesDto>().ReverseMap();
CreateMap<Route, RouteWithAddressesDto>().ReverseMap();
CreateMap<Ticket, TicketDto>().ReverseMap();
CreateMap<Ticket, CreateTicketDto>().ReverseMap();
CreateMap<Ticket, UpdateTicketDto>().ReverseMap();
@ -69,10 +65,17 @@ public class MapperInitializer : Profile
CreateMap<VehicleEnrollment, VehicleEnrollmentDto>().ReverseMap();
CreateMap<VehicleEnrollment, CreateVehicleEnrollmentDto>().ReverseMap();
CreateMap<VehicleEnrollment, UpdateVehicleEnrollmentDto>().ReverseMap();
CreateMap<VehicleEnrollment, VehicleEnrollmentWithDetailsDto>().ReverseMap();
CreateMap<VehicleEnrollment, CreateVehicleEnrollmentWithDetailsDto>().ReverseMap();
CreateMap<User, UserDto>().ReverseMap();
CreateMap<User, CreateUserDto>().ReverseMap();
CreateMap<User, UpdateUserDto>().ReverseMap();
CreateMap<RouteAddressDetails, RouteAddressDetailsDto>().ReverseMap();
CreateMap<RouteAddressDetails, CreateRouteAddressDetailsDto>().ReverseMap();
CreateMap<RouteAddressDetails, UpdateRouteAddressDetailsDto>().ReverseMap();
CreateMap<RouteAddressDetails, RouteAddressDetailsInVehicleEnrollmentDto>().ReverseMap();
CreateMap<RouteAddressDetails, CreateRouteAddressDetailsInVehicleEnrollmentDto>().ReverseMap();
}
}

View File

@ -30,6 +30,19 @@ public class VehicleEnrollmentController : ControllerBase
return CreatedAtAction(nameof(GetEnrollment), new {id = result.enrollment.Id}, result.enrollment);
}
[HttpPost("withDetails")]
public async Task<IActionResult> AddEnrollmentWithDetails(CreateVehicleEnrollmentWithDetailsDto enrollment)
{
var result = await _vehicleEnrollmentManagementService.AddEnrollmentWithDetails(enrollment);
if (!result.isSucceed)
{
return result.actionResult;
}
return CreatedAtAction(nameof(GetEnrollment), new {id = result.enrollment.Id}, result.enrollment);
}
[HttpGet]
public async Task<IActionResult> GetEnrollments([FromQuery] VehicleEnrollmentParameters parameters)
@ -46,6 +59,21 @@ public class VehicleEnrollmentController : ControllerBase
return Ok(result.enrollments);
}
[HttpGet("withDetails")]
public async Task<IActionResult> GetEnrollments([FromQuery] VehicleEnrollmentWithDetailsParameters parameters)
{
var result = await _vehicleEnrollmentManagementService.GetEnrollmentsWithDetails(parameters);
if (!result.isSucceed)
{
return result.actionResult;
}
Response.Headers.Add("X-Pagination", JsonConvert.SerializeObject(result.pagingMetadata));
return Ok(result.enrollments);
}
[HttpGet("{id}")]
public async Task<IActionResult> GetEnrollment(int id, [FromQuery] string? fields)
{
@ -58,6 +86,19 @@ public class VehicleEnrollmentController : ControllerBase
return Ok(result.enrollment);
}
[HttpGet("withDetails/{id}")]
public async Task<IActionResult> GetEnrollmentWithDetails(int id, [FromQuery] string? fields)
{
var result = await _vehicleEnrollmentManagementService.GetEnrollmentWithDetails(id, fields);
if (!result.isSucceed)
{
return result.actionResult;
}
return Ok(result.enrollment);
}
[HttpPut("{id}")]
public async Task<IActionResult> UpdateVehicle(int id, UpdateVehicleEnrollmentDto enrollment)

View File

@ -16,8 +16,6 @@ public class VehicleEnrollment
public int RouteId { get; set; }
public Route Route { get; set; } = null!;
[ForeignKey("RouteAddressDetailsId")]
public int RouteAddressDetailsId { get; set; }
public virtual IList<RouteAddressDetails> RouteAddressDetails { get; set; } = null!;
public DateTime DepartureDateTimeUtc { get; set; }

View File

@ -23,6 +23,7 @@ builder.Services.AddControllers().AddNewtonsoftJson(options => {
options.SerializerSettings.Formatting = Formatting.Indented;
options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Error;
options.SerializerSettings.DateFormatHandling = DateFormatHandling.IsoDateFormat;
});
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
@ -116,6 +117,7 @@ builder.Services.AddScoped<IDataShaper<ReviewDto>, DataShaper<ReviewDto>>();
builder.Services.AddScoped<IDataShaper<CompanyDto>, DataShaper<CompanyDto>>();
builder.Services.AddScoped<IDataShaper<VehicleDto>, DataShaper<VehicleDto>>();
builder.Services.AddScoped<IDataShaper<VehicleEnrollmentDto>, DataShaper<VehicleEnrollmentDto>>();
builder.Services.AddScoped<IDataShaper<VehicleEnrollmentWithDetailsDto>, DataShaper<VehicleEnrollmentWithDetailsDto>>();
builder.Services.AddScoped<IDataShaper<RouteDto>, DataShaper<RouteDto>>();
builder.Services.AddScoped<IDataShaper<RouteWithAddressesDto>, DataShaper<RouteWithAddressesDto>>();
builder.Services.AddScoped<IDataShaper<RouteAddressDto>, DataShaper<RouteAddressDto>>();

View File

@ -36,6 +36,10 @@ public class AddressManagementService : IAddressManagementService
await _dbContext.Addresses.AddAsync(address);
await _dbContext.SaveChangesAsync();
address = await _dbContext.Addresses.Include(a => a.City)
.ThenInclude(c => c.State).ThenInclude(s => s.Country)
.FirstAsync(a => a.Id == address.Id);
return (true, null, _mapper.Map<AddressDto>(address));
}

View File

@ -36,6 +36,9 @@ public class CityManagementService : ICityManagementService
await _dbContext.Cities.AddAsync(city);
await _dbContext.SaveChangesAsync();
city = await _dbContext.Cities.Include(c => c.State)
.ThenInclude(s => s.Country).FirstAsync(c => c.Id == city.Id);
return (true, null, _mapper.Map<CityDto>(city));
}

View File

@ -9,9 +9,13 @@ namespace Server.Services;
public interface IVehicleEnrollmentManagementService
{
Task<(bool isSucceed, IActionResult? actionResult, VehicleEnrollmentDto enrollment)> AddEnrollment(CreateVehicleEnrollmentDto createEnrollmentDto);
Task<(bool isSucceed, IActionResult? actionResult, VehicleEnrollmentWithDetailsDto enrollment)> AddEnrollmentWithDetails(CreateVehicleEnrollmentWithDetailsDto createEnrollmentDto);
Task<(bool isSucceed, IActionResult? actionResult, IEnumerable<ExpandoObject> enrollments,
PagingMetadata<ExpandoObject> pagingMetadata)> GetEnrollments(VehicleEnrollmentParameters parameters);
PagingMetadata<ExpandoObject> pagingMetadata)> GetEnrollments(VehicleEnrollmentParameters parameters);
Task<(bool isSucceed, IActionResult? actionResult, IEnumerable<ExpandoObject> enrollments,
PagingMetadata<ExpandoObject> pagingMetadata)> GetEnrollmentsWithDetails(VehicleEnrollmentWithDetailsParameters parameters);
Task<(bool isSucceed, IActionResult? actionResult, ExpandoObject enrollment)> GetEnrollment(int id, string? fields);
Task<(bool isSucceed, IActionResult? actionResult, ExpandoObject enrollment)> GetEnrollmentWithDetails(int id, string? fields);
Task<(bool isSucceed, IActionResult? actionResult, VehicleEnrollmentDto enrollment)> UpdateEnrollment(UpdateVehicleEnrollmentDto updateEnrollmentDto);
Task<(bool isSucceed, IActionResult? actionResult)> DeleteEnrollment(int id);
Task<bool> IsEnrollmentExists(int id);

View File

@ -49,21 +49,15 @@ public class RouteManagementService : IRouteManagementService
{
var route = _mapper.Map<Route>(createRouteWithAddressesDto);
foreach (var routeAddress in route.RouteAddresses)
{
var dbAddress = await _dbContext.Addresses
.FirstOrDefaultAsync(a => a.Id == routeAddress.AddressId);
if (dbAddress == null)
{
return (false, new BadRequestObjectResult($"Address with Id {routeAddress.AddressId} doesn't exist"), null!);
}
routeAddress.Address = dbAddress;
}
await _dbContext.Routes.AddAsync(route);
await _dbContext.SaveChangesAsync();
route = await _dbContext.Routes
.Include(r => r.RouteAddresses).ThenInclude(ra => ra.Address)
.ThenInclude(a => a.City).ThenInclude(c => c.State)
.ThenInclude(s => s.Country).FirstAsync(r => r.Id == route.Id);
return (true, null, _mapper.Map<RouteWithAddressesDto>(route));
}
@ -248,7 +242,7 @@ public class RouteManagementService : IRouteManagementService
}
var routeDto = _mapper.Map<RouteWithAddressesDto>(dbRoute);
var shapedData = _routeDataShaper.ShapeData(routeDto, fields);
var shapedData = _routeWithAddressesDataShaper.ShapeData(routeDto, fields);
return (true, null, shapedData);
}

View File

@ -36,6 +36,9 @@ public class StateManagementService : IStateManagementService
await _dbContext.States.AddAsync(state);
await _dbContext.SaveChangesAsync();
state = await _dbContext.States.Include(s => s.Country)
.FirstAsync(s => s.Id == state.Id);
return (true, null, _mapper.Map<StateDto>(state));
}

View File

@ -17,17 +17,20 @@ public class VehicleEnrollmentManagementService : IVehicleEnrollmentManagementSe
private readonly IMapper _mapper;
private readonly ISortHelper<ExpandoObject> _enrollmentSortHelper;
private readonly IDataShaper<VehicleEnrollmentDto> _enrollmentDataShaper;
private readonly IDataShaper<VehicleEnrollmentWithDetailsDto> _enrollmentWithDetailsDataShaper;
private readonly IPager<ExpandoObject> _pager;
public VehicleEnrollmentManagementService(ApplicationDbContext dbContext,
IMapper mapper, ISortHelper<ExpandoObject> enrollmentSortHelper,
IDataShaper<VehicleEnrollmentDto> enrollmentDataShaper, IPager<ExpandoObject> pager)
IDataShaper<VehicleEnrollmentDto> enrollmentDataShaper, IPager<ExpandoObject> pager,
IDataShaper<VehicleEnrollmentWithDetailsDto> enrollmentWithDetailsDataShaper)
{
_dbContext = dbContext;
_mapper = mapper;
_enrollmentSortHelper = enrollmentSortHelper;
_enrollmentDataShaper = enrollmentDataShaper;
_pager = pager;
_enrollmentWithDetailsDataShaper = enrollmentWithDetailsDataShaper;
}
public async Task<(bool isSucceed, IActionResult? actionResult, VehicleEnrollmentDto enrollment)> AddEnrollment(CreateVehicleEnrollmentDto createEnrollmentDto)
@ -39,6 +42,20 @@ public class VehicleEnrollmentManagementService : IVehicleEnrollmentManagementSe
return (true, null, _mapper.Map<VehicleEnrollmentDto>(enrollment));
}
public async Task<(bool isSucceed, IActionResult? actionResult, VehicleEnrollmentWithDetailsDto enrollment)> AddEnrollmentWithDetails(CreateVehicleEnrollmentWithDetailsDto createEnrollmentDto)
{
var enrollment = _mapper.Map<VehicleEnrollment>(createEnrollmentDto);
await _dbContext.VehicleEnrollments.AddAsync(enrollment);
await _dbContext.SaveChangesAsync();
enrollment = await _dbContext.VehicleEnrollments
.Include(ve => ve.RouteAddressDetails)
.FirstAsync(ve => ve.Id == enrollment.Id);
return (true, null, _mapper.Map<VehicleEnrollmentWithDetailsDto>(enrollment));
}
public async Task<(bool isSucceed, IActionResult? actionResult, IEnumerable<ExpandoObject> enrollments,
PagingMetadata<ExpandoObject> pagingMetadata)> GetEnrollments(VehicleEnrollmentParameters parameters)
@ -80,7 +97,7 @@ public class VehicleEnrollmentManagementService : IVehicleEnrollmentManagementSe
}
enrollment = enrollment.Where(e =>
e.CancelationComment.ToLower().Contains(search.ToLower()));
e.CancelationComment != null && e.CancelationComment.ToLower().Contains(search.ToLower()));
}
void FilterByEnrollmentVehicleId(ref IQueryable<VehicleEnrollment> enrollments,
@ -142,7 +159,175 @@ public class VehicleEnrollmentManagementService : IVehicleEnrollmentManagementSe
enrollments = enrollments.Where(e => e.IsCanceled == isCancelled);
}
}
public async Task<(bool isSucceed, IActionResult? actionResult, IEnumerable<ExpandoObject> enrollments,
PagingMetadata<ExpandoObject> pagingMetadata)> GetEnrollmentsWithDetails(VehicleEnrollmentWithDetailsParameters parameters)
{
var dbEnrollments = _dbContext.VehicleEnrollments
.Include(ve => ve.RouteAddressDetails)
.AsQueryable();
SearchByAllEnrollmentFields(ref dbEnrollments, parameters.Search);
FilterByEnrollmentVehicleId(ref dbEnrollments, parameters.VehicleId);
FilterByEnrollmentRouteId(ref dbEnrollments, parameters.RouteId);
FilterByEnrollmentDepartureDateTime(ref dbEnrollments,
parameters.FromDepartureDateTime, parameters.ToDepartureDateTime);
FilterByEnrollmentDelayedValue(ref dbEnrollments, parameters.IsDelayed);
FilterByEnrollmentCancelledValue(ref dbEnrollments, parameters.IsCanceled);
FilterByEnrollmentTotalDuration(ref dbEnrollments,
parameters.FromTotalTripDuration, parameters.ToTotalTripDuration);
FilterByEnrollmentTotalCost(ref dbEnrollments, parameters.FromCost,
parameters.ToCost);
var enrollmentDtos = _mapper.ProjectTo<VehicleEnrollmentWithDetailsDto>(dbEnrollments);
var shapedData = _enrollmentWithDetailsDataShaper.ShapeData(enrollmentDtos, parameters.Fields).AsQueryable();
try
{
shapedData = _enrollmentSortHelper.ApplySort(shapedData, parameters.Sort);
}
catch (Exception)
{
return (false, new BadRequestObjectResult("Invalid sorting string"), null!, null!);
}
var pagingMetadata = _pager.ApplyPaging(ref shapedData, parameters.PageNumber,
parameters.PageSize);
return (true, null, shapedData, pagingMetadata);
void SearchByAllEnrollmentFields(ref IQueryable<VehicleEnrollment> enrollment,
string? search)
{
if (!enrollment.Any() || String.IsNullOrWhiteSpace(search))
{
return;
}
enrollment = enrollment.Where(e =>
e.CancelationComment != null && e.CancelationComment.ToLower().Contains(search.ToLower()));
}
void FilterByEnrollmentVehicleId(ref IQueryable<VehicleEnrollment> enrollments,
int? vehicleId)
{
if (!enrollments.Any() || vehicleId == null)
{
return;
}
enrollments = enrollments.Where(e => e.VehicleId == vehicleId);
}
void FilterByEnrollmentRouteId(ref IQueryable<VehicleEnrollment> enrollments,
int? routeId)
{
if (!enrollments.Any() || routeId == null)
{
return;
}
enrollments = enrollments.Where(e => e.RouteId == routeId);
}
void FilterByEnrollmentDepartureDateTime(ref IQueryable<VehicleEnrollment> enrollments,
DateTime? fromDateTime, DateTime? toDateTime)
{
if (!enrollments.Any() || fromDateTime == null || toDateTime == null)
{
return;
}
enrollments = enrollments.Where(e =>
e.DepartureDateTimeUtc >= fromDateTime.Value.ToUniversalTime() &&
e.DepartureDateTimeUtc <= toDateTime.Value.ToUniversalTime());
}
void FilterByEnrollmentDelayedValue(ref IQueryable<VehicleEnrollment> enrollments,
bool? isDelayed)
{
if (!enrollments.Any() || !isDelayed.HasValue)
{
return;
}
enrollments = isDelayed.Value
? enrollments.Where(e => e.DelayTimeSpan != null)
: enrollments.Where(e => e.DelayTimeSpan == null);
}
void FilterByEnrollmentCancelledValue(ref IQueryable<VehicleEnrollment> enrollments,
bool? isCancelled)
{
if (!enrollments.Any() || !isCancelled.HasValue)
{
return;
}
enrollments = enrollments.Where(e => e.IsCanceled == isCancelled);
}
void FilterByEnrollmentTotalDuration(ref IQueryable<VehicleEnrollment> enrollments,
TimeSpan? fromDuration, TimeSpan? toDuration)
{
if (!enrollments.Any() )
{
return;
}
fromDuration ??= TimeSpan.Zero;
toDuration ??= TimeSpan.MaxValue;
List<int> filteredEnrollmentsIds = new List<int>();
foreach (var enrollment in enrollments)
{
TimeSpan duration = TimeSpan.Zero;
foreach (var details in enrollment.RouteAddressDetails)
{
duration += details.WaitTimeSpan + details.TimeSpanToNextCity;
}
if (duration >= fromDuration && duration <= toDuration)
{
filteredEnrollmentsIds.Add(enrollment.Id);
}
}
enrollments = enrollments.Where(e =>
filteredEnrollmentsIds.Any(id => id == e.Id));
}
void FilterByEnrollmentTotalCost(ref IQueryable<VehicleEnrollment> enrollments,
double? fromCost, double? toCost)
{
if (!enrollments.Any() )
{
return;
}
fromCost ??= 0;
toCost ??= Double.MaxValue;
List<int> filteredEnrollmentsIds = new List<int>();
foreach (var enrollment in enrollments)
{
double cost = 0;
foreach (var details in enrollment.RouteAddressDetails)
{
cost += details.CostToNextCity;
}
if (cost >= fromCost && cost <= toCost)
{
filteredEnrollmentsIds.Add(enrollment.Id);
}
}
enrollments = enrollments.Where(e =>
filteredEnrollmentsIds.Any(id => id == e.Id));
}
}
public async Task<(bool isSucceed, IActionResult? actionResult, ExpandoObject enrollment)> GetEnrollment(int id, string? fields)
{
if (!await IsEnrollmentExists(id))
@ -164,6 +349,28 @@ public class VehicleEnrollmentManagementService : IVehicleEnrollmentManagementSe
return (true, null, shapedData);
}
public async Task<(bool isSucceed, IActionResult? actionResult, ExpandoObject enrollment)> GetEnrollmentWithDetails(int id, string? fields)
{
if (!await IsEnrollmentExists(id))
{
return (false, new NotFoundResult(), null!);
}
var dbEnrollment = await _dbContext.VehicleEnrollments
.Include(ve => ve.RouteAddressDetails)
.FirstAsync(e => e.Id == id);
if (String.IsNullOrWhiteSpace(fields))
{
fields = VehicleEnrollmentWithDetailsParameters.DefaultFields;
}
var enrollmentDto = _mapper.Map<VehicleEnrollmentWithDetailsDto>(dbEnrollment);
var shapedData = _enrollmentWithDetailsDataShaper.ShapeData(enrollmentDto, fields);
return (true, null, shapedData);
}
public async Task<(bool isSucceed, IActionResult? actionResult, VehicleEnrollmentDto enrollment)> UpdateEnrollment(UpdateVehicleEnrollmentDto updateEnrollmentDto)
{
var enrollment = _mapper.Map<VehicleEnrollment>(updateEnrollmentDto);

View File

@ -40,18 +40,16 @@ public class InCityAddressDto
public class CreateAddressInRouteAddress
{
public int? Id { get; set; }
[StringLength(maximumLength: 250, ErrorMessage = "Address name is too long")]
public string? Name { get; set; } = null!;
public string Name { get; set; } = null!;
[Range(-90, 90, ErrorMessage = "Latitude must be in range(-90, 90)")]
public double? Latitude { get; set; }
public double Latitude { get; set; }
[Range(-180, 180, ErrorMessage = "Longitude must be in range(-180, 180)")]
public double? Longitude { get; set; }
public double Longitude { get; set; }
public int? CityId { get; set; }
public int CityId { get; set; }
}
public class AddressInRouteAddress : CreateAddressInRouteAddress

View File

@ -0,0 +1,52 @@
using System.ComponentModel.DataAnnotations;
namespace SharedModels.DataTransferObjects;
public class RouteAddressDetailsDto : CreateRouteAddressDetailsDto
{
public int Id { get; set; }
}
public class CreateRouteAddressDetailsDto
{
[Required]
public int VehicleEnrollmentId { get; set; }
[Required]
public int RouteAddressId { get; set; }
[Required]
public TimeSpan TimeSpanToNextCity { get; set; }
[Required]
public TimeSpan WaitTimeSpan { get; set; }
[Required]
public double CostToNextCity { get; set; }
}
public class UpdateRouteAddressDetailsDto : CreateRouteAddressDetailsDto
{
[Required]
public int Id { get; set; }
}
public class CreateRouteAddressDetailsInVehicleEnrollmentDto
{
[Required]
public int RouteAddressId { get; set; }
[Required]
public TimeSpan TimeSpanToNextCity { get; set; }
[Required]
public TimeSpan WaitTimeSpan { get; set; }
[Required]
public double CostToNextCity { get; set; }
}
public class RouteAddressDetailsInVehicleEnrollmentDto : CreateRouteAddressDetailsInVehicleEnrollmentDto
{
public int Id { get; set; }
}

View File

@ -30,14 +30,19 @@ public class CreateRouteAddressWithAddressDto
{
[Range(0, Int32.MaxValue)]
public int Order { get; set; }
[Required]
public CreateAddressInRouteAddress Address { get; set; } = null!;
public CreateAddressInRouteAddress? Address { get; set; }
public int? AddressId { get; set; }
}
public class RouteAddressWithAddressDto
{
public int Id { get; set; }
public int Order { get; set; }
public AddressInRouteAddress Address { get; set; } = null!;
public int AddressId { get; set; }
}

View File

@ -0,0 +1,6 @@
namespace SharedModels.DataTransferObjects;
public class TicketGroupDto
{
}

View File

@ -29,4 +29,16 @@ public class UpdateVehicleEnrollmentDto : CreateVehicleEnrollmentDto
public bool IsCanceled { get; set; } = false;
public string CancelationComment { get; set; } = null!;
}
public class CreateVehicleEnrollmentWithDetailsDto : CreateVehicleEnrollmentDto
{
public IList<CreateRouteAddressDetailsInVehicleEnrollmentDto> RouteAddressDetails { get; set; } = null!;
}
public class VehicleEnrollmentWithDetailsDto
{
public int Id { get; set; }
public IList<RouteAddressDetailsInVehicleEnrollmentDto> RouteAddressDetails { get; set; } = null!;
}

View File

@ -0,0 +1,24 @@
namespace SharedModels.QueryParameters.Objects;
public class VehicleEnrollmentWithDetailsParameters : ParametersBase
{
public const string DefaultFields = "id,vehicleId,routeId,departureDateTimeUtc," +
"delayTimeSpan,isCancelled,cancellationComment," +
"routeAddressDetails";
public VehicleEnrollmentWithDetailsParameters()
{
Fields = DefaultFields;
}
public int? VehicleId { get; set; }
public int? RouteId { get; set; }
public DateTime? FromDepartureDateTime { get; set; }
public DateTime? ToDepartureDateTime { get; set; }
public bool? IsDelayed { get; set; }
public bool? IsCanceled {get; set; }
public TimeSpan? FromTotalTripDuration { get; set; }
public TimeSpan? ToTotalTripDuration { get; set; }
public double? FromCost { get; set; }
public double? ToCost { get; set; }
}