diff --git a/README.md b/README.md index 393c16d..78bed4e 100644 --- a/README.md +++ b/README.md @@ -27,3 +27,12 @@ TransitPulse is a transport reliability analytics platform that helps commuters ## Development This project follows GitHub Actions CI and automated testing. + +## API Testing + +This project includes a Postman collection for testing all endpoints. + +Import: + +- TransitPulse.postman_collection.json +- TransitPulse.postman_environment.json diff --git a/src/TransitPulse.API/Controllers/DashboardController.cs b/src/TransitPulse.API/Controllers/DashboardController.cs new file mode 100644 index 0000000..3afa765 --- /dev/null +++ b/src/TransitPulse.API/Controllers/DashboardController.cs @@ -0,0 +1,26 @@ +using MediatR; +using Microsoft.AspNetCore.Mvc; +using TransitPulse.Application.Features.Dashboard.GetTopRoutes; + +namespace TransitPulse.API.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class DashboardController : ControllerBase +{ + private readonly IMediator _mediator; + + public DashboardController(IMediator mediator) + { + _mediator = mediator; + } + + [HttpGet("top-routes")] + public async Task>> GetTopRoutes( + CancellationToken cancellationToken) + { + var result = await _mediator.Send(new GetTopRoutesQuery(), cancellationToken); + + return Ok(result); + } +} \ No newline at end of file diff --git a/src/TransitPulse.API/Controllers/ReliabilityController.cs b/src/TransitPulse.API/Controllers/ReliabilityController.cs index 5afc0ee..8b2582d 100644 --- a/src/TransitPulse.API/Controllers/ReliabilityController.cs +++ b/src/TransitPulse.API/Controllers/ReliabilityController.cs @@ -1,3 +1,4 @@ +using MediatR; using Microsoft.AspNetCore.Mvc; using TransitPulse.API.Contracts.Requests; using TransitPulse.API.Contracts.Responses; @@ -11,17 +12,12 @@ namespace TransitPulse.API.Controllers; [Route("api/[controller]")] public class ReliabilityController : ControllerBase { - private readonly GenerateReliabilitySnapshotHandler _generateHandler; - private readonly GetReliabilitySnapshotsHandler _getSnapshotsHandler; - private readonly GetReliabilitySnapshotHandler _getSnapshotHandler; - public ReliabilityController(GenerateReliabilitySnapshotHandler handler, - GetReliabilitySnapshotsHandler getSnapshotsHandler, - GetReliabilitySnapshotHandler getSnapshotHandler) + private readonly IMediator _mediator; + + public ReliabilityController(IMediator mediator) { - _generateHandler = handler; - _getSnapshotsHandler = getSnapshotsHandler; - _getSnapshotHandler = getSnapshotHandler; + _mediator = mediator; } [HttpPost("snapshots")] @@ -33,7 +29,7 @@ public async Task GenerateSnapshot(GenerateReliabilitySnapshotReq request.PeriodStart, request.PeriodEnd); - var result = await _generateHandler.HandleAsync(command, cancellationToken); + var result = await _mediator.Send(command, cancellationToken); var response = new GenerateReliabilitySnapshotResponse( @@ -53,7 +49,7 @@ public async Task GenerateSnapshot(GenerateReliabilitySnapshotReq [HttpGet("routes/{routeId}/snapshots")] public async Task GetSnapshots(Guid routeId, CancellationToken cancellationToken) { - var snapshots = await _getSnapshotsHandler.HandleAsync(routeId, cancellationToken); + var snapshots = await _mediator.Send(new GetReliabilitySnapshotsQuery(routeId), cancellationToken); var response = snapshots.Select(snapshot => new GetReliabilitySnapshotResponse( @@ -73,7 +69,8 @@ public async Task GetSnapshots(Guid routeId, CancellationToken ca public async Task Getsnapshot(Guid snapshotId, CancellationToken cancellationToken) { - var snapshot = await _getSnapshotHandler.HandleAsync(snapshotId, cancellationToken); + var snapshot = await _mediator.Send(new GetReliabilitySnapshotQuery(snapshotId), + cancellationToken); var response = new GetReliabilitySnapshotResponse( snapshot.SnapshotId, diff --git a/src/TransitPulse.API/Program.cs b/src/TransitPulse.API/Program.cs index 1e3e2a9..a6375d8 100644 --- a/src/TransitPulse.API/Program.cs +++ b/src/TransitPulse.API/Program.cs @@ -1,6 +1,7 @@ using TransitPulse.Application; using TransitPulse.Infrastructure; using TransitPulse.API.Middleware; +using TransitPulse.Infrastructure.Persistence; var builder = WebApplication.CreateBuilder(args); @@ -13,8 +14,13 @@ // Infrastructure layer builder.Services.AddInfrastructureServices(builder.Configuration); //Registers database, repositories, and infrastructure services. + + + var app = builder.Build(); + + app.UseMiddleware(); if (app.Environment.IsDevelopment()) @@ -23,6 +29,15 @@ } app.UseHttpsRedirection(); +app.UseAuthorization(); + +// 👇 Run the database seeder here +// +using (var scope = app.Services.CreateScope()) +{ + var seeder = scope.ServiceProvider.GetRequiredService(); + await seeder.SeedAsync(); +} app.MapControllers(); diff --git a/src/TransitPulse.Application/ApplicationAssemblyMarker.cs b/src/TransitPulse.Application/ApplicationAssemblyMarker.cs new file mode 100644 index 0000000..49a3027 --- /dev/null +++ b/src/TransitPulse.Application/ApplicationAssemblyMarker.cs @@ -0,0 +1,5 @@ +namespace TransitPulse.Application; + +public sealed class ApplicationAssemblyMarker +{ +} \ No newline at end of file diff --git a/src/TransitPulse.Application/DependencyInjection.cs b/src/TransitPulse.Application/DependencyInjection.cs index 3c43632..fb6201a 100644 --- a/src/TransitPulse.Application/DependencyInjection.cs +++ b/src/TransitPulse.Application/DependencyInjection.cs @@ -1,7 +1,7 @@ +using FluentValidation; +using MediatR; using Microsoft.Extensions.DependencyInjection; using TransitPulse.Application.Features.Reliability.GenerateReliabilitySnapshot; -using TransitPulse.Application.Features.Reliability.GetReliabilitySnapshot; -using TransitPulse.Application.Features.Reliability.GetReliabilitySnapshots; namespace TransitPulse.Application; @@ -10,13 +10,14 @@ public static class DependencyInjection public static IServiceCollection AddApplicationServices( this IServiceCollection services) { - services.AddScoped(); + services.AddMediatR(cfg => + { + cfg.RegisterServicesFromAssembly( + typeof(ApplicationAssemblyMarker).Assembly); + }); - services.AddScoped(); - - services.AddScoped(); - - services.AddScoped(); + services.AddValidatorsFromAssembly( + typeof(ApplicationAssemblyMarker).Assembly); return services; } diff --git a/src/TransitPulse.Application/Features/Dashboard/GetTopRoutes/GetTopRoutesHandler.cs b/src/TransitPulse.Application/Features/Dashboard/GetTopRoutes/GetTopRoutesHandler.cs new file mode 100644 index 0000000..c009193 --- /dev/null +++ b/src/TransitPulse.Application/Features/Dashboard/GetTopRoutes/GetTopRoutesHandler.cs @@ -0,0 +1,22 @@ +using MediatR; +using TransitPulse.Application.Interfaces; + +namespace TransitPulse.Application.Features.Dashboard.GetTopRoutes; + +public class GetTopRoutesHandler + : IRequestHandler> +{ + private readonly IReliabilitySnapshotRepository _repository; + + public GetTopRoutesHandler(IReliabilitySnapshotRepository repository) + { + _repository = repository; + } + + public async Task> Handle( + GetTopRoutesQuery request, + CancellationToken cancellationToken) + { + return await _repository.GetTopRoutesAsync(cancellationToken); + } +} \ No newline at end of file diff --git a/src/TransitPulse.Application/Features/Dashboard/GetTopRoutes/GetTopRoutesQuery.cs b/src/TransitPulse.Application/Features/Dashboard/GetTopRoutes/GetTopRoutesQuery.cs new file mode 100644 index 0000000..f9edd39 --- /dev/null +++ b/src/TransitPulse.Application/Features/Dashboard/GetTopRoutes/GetTopRoutesQuery.cs @@ -0,0 +1,5 @@ +using MediatR; + +namespace TransitPulse.Application.Features.Dashboard.GetTopRoutes; + +public record GetTopRoutesQuery : IRequest>; \ No newline at end of file diff --git a/src/TransitPulse.Application/Features/Dashboard/GetTopRoutes/TopRouteDto.cs b/src/TransitPulse.Application/Features/Dashboard/GetTopRoutes/TopRouteDto.cs new file mode 100644 index 0000000..b1bc2a6 --- /dev/null +++ b/src/TransitPulse.Application/Features/Dashboard/GetTopRoutes/TopRouteDto.cs @@ -0,0 +1,9 @@ +namespace TransitPulse.Application.Features.Dashboard.GetTopRoutes; + +public record TopRouteDto( + Guid RouteId, + string RouteCode, + string RouteName, + string TransportType, + double AverageScore +); \ No newline at end of file diff --git a/src/TransitPulse.Application/Features/Reliability/GetReliabilitySnapshot/GetReliabilitySnapshotResult.cs b/src/TransitPulse.Application/Features/Reliability/Common/GetReliabilitySnapshotDto.cs similarity index 54% rename from src/TransitPulse.Application/Features/Reliability/GetReliabilitySnapshot/GetReliabilitySnapshotResult.cs rename to src/TransitPulse.Application/Features/Reliability/Common/GetReliabilitySnapshotDto.cs index 9dfa6ff..c2b96c0 100644 --- a/src/TransitPulse.Application/Features/Reliability/GetReliabilitySnapshot/GetReliabilitySnapshotResult.cs +++ b/src/TransitPulse.Application/Features/Reliability/Common/GetReliabilitySnapshotDto.cs @@ -1,6 +1,6 @@ -namespace TransitPulse.Application.Features.Reliability.GetReliabilitySnapshot; +namespace TransitPulse.Application.Features.Reliability.Common; -public record GetReliabilitySnapshotResult( +public record GetReliabilitySnapshotDto( Guid SnapshotId, double Score, double AverageDelay, diff --git a/src/TransitPulse.Application/Features/Reliability/Common/GetReliabilitySnapshotsDto.cs b/src/TransitPulse.Application/Features/Reliability/Common/GetReliabilitySnapshotsDto.cs new file mode 100644 index 0000000..e69de29 diff --git a/src/TransitPulse.Application/Common/ReliabilityMetrics.cs b/src/TransitPulse.Application/Features/Reliability/Common/ReliabilityMetrics.cs similarity index 100% rename from src/TransitPulse.Application/Common/ReliabilityMetrics.cs rename to src/TransitPulse.Application/Features/Reliability/Common/ReliabilityMetrics.cs diff --git a/src/TransitPulse.Application/Features/Reliability/GenerateReliabilitySnapshot/GenerateReliabilitySnapshotCommand.cs b/src/TransitPulse.Application/Features/Reliability/GenerateReliabilitySnapshot/GenerateReliabilitySnapshotCommand.cs index 759a297..c976a02 100644 --- a/src/TransitPulse.Application/Features/Reliability/GenerateReliabilitySnapshot/GenerateReliabilitySnapshotCommand.cs +++ b/src/TransitPulse.Application/Features/Reliability/GenerateReliabilitySnapshot/GenerateReliabilitySnapshotCommand.cs @@ -1,6 +1,9 @@ +using MediatR; + namespace TransitPulse.Application.Features.Reliability.GenerateReliabilitySnapshot; public record GenerateReliabilitySnapshotCommand( Guid RouteId, DateTime PeriodStart, - DateTime PeriodEnd); + DateTime PeriodEnd) + : IRequest; \ No newline at end of file diff --git a/src/TransitPulse.Application/Features/Reliability/GenerateReliabilitySnapshot/GenerateReliabilitySnapshotHandler.cs b/src/TransitPulse.Application/Features/Reliability/GenerateReliabilitySnapshot/GenerateReliabilitySnapshotHandler.cs index 6481775..60cad26 100644 --- a/src/TransitPulse.Application/Features/Reliability/GenerateReliabilitySnapshot/GenerateReliabilitySnapshotHandler.cs +++ b/src/TransitPulse.Application/Features/Reliability/GenerateReliabilitySnapshot/GenerateReliabilitySnapshotHandler.cs @@ -1,27 +1,29 @@ using FluentValidation; +using MediatR; +using Microsoft.Extensions.Logging; using TransitPulse.Application.Exceptions; using TransitPulse.Application.Interfaces; using TransitPulse.Domain.Entities; -using Microsoft.Extensions.Logging; namespace TransitPulse.Application.Features.Reliability.GenerateReliabilitySnapshot; public class GenerateReliabilitySnapshotHandler + : IRequestHandler< + GenerateReliabilitySnapshotCommand, + GenerateReliabilitySnapshotResult> { - private readonly IRouteEventRepository _routeEventRepository; private readonly IReliabilityCalculator _reliabilityCalculator; private readonly IReliabilitySnapshotRepository _snapshotRepository; - private readonly GenerateReliabilitySnapshotValidator _validator; + private readonly IValidator _validator; private readonly ILogger _logger; public GenerateReliabilitySnapshotHandler( IRouteEventRepository routeEventRepository, IReliabilityCalculator reliabilityCalculator, IReliabilitySnapshotRepository snapshotRepository, - GenerateReliabilitySnapshotValidator validator, - ILogger logger - ) + IValidator validator, + ILogger logger) { _routeEventRepository = routeEventRepository; _reliabilityCalculator = reliabilityCalculator; @@ -30,22 +32,32 @@ ILogger logger _logger = logger; } - public async Task HandleAsync( - GenerateReliabilitySnapshotCommand command, - CancellationToken cancellationToken) + public async Task Handle( + GenerateReliabilitySnapshotCommand command, + CancellationToken cancellationToken) { _logger.LogInformation( - "Starting reliability snapshot generation for route {RouteId}.", command.RouteId); + "Starting reliability snapshot generation for route {RouteId}.", + command.RouteId); + + _validator.ValidateAndThrow(command); - var routeEvents = - await _routeEventRepository - .GetByRouteAndPeriodAsync( - command.RouteId, - command.PeriodStart, - command.PeriodEnd, - cancellationToken); + // Normalize request timestamps to UTC. + var periodStart = DateTime.SpecifyKind( + command.PeriodStart, + DateTimeKind.Utc); + + var periodEnd = DateTime.SpecifyKind( + command.PeriodEnd, + DateTimeKind.Utc); + + var routeEvents = await _routeEventRepository.GetByRouteAndPeriodAsync( + command.RouteId, + periodStart, + periodEnd, + cancellationToken); if (!routeEvents.Any()) { @@ -58,21 +70,18 @@ await _routeEventRepository throw new NotFoundException( $"No route events found for route {command.RouteId} between {command.PeriodStart:d} and {command.PeriodEnd:d}."); } - //Single Responsibility Principle in IReliabilityCalculator calculate - var metrics = - _reliabilityCalculator.Calculate( - routeEvents); - var snapshot = - new ReliabilitySnapshot( - command.RouteId, - metrics.Score, - metrics.AverageDelay, - metrics.CancellationRate, - metrics.OnTimePercentage, - command.PeriodStart, - command.PeriodEnd, - DateTime.UtcNow); + var metrics = _reliabilityCalculator.Calculate(routeEvents); + + var snapshot = new ReliabilitySnapshot( + command.RouteId, + metrics.Score, + metrics.AverageDelay, + metrics.CancellationRate, + metrics.OnTimePercentage, + periodStart, + periodEnd, + DateTime.UtcNow); await _snapshotRepository.AddAsync( snapshot, @@ -89,4 +98,4 @@ await _snapshotRepository.AddAsync( metrics.AverageDelay, metrics.CancellationRate); } -} +} \ No newline at end of file diff --git a/src/TransitPulse.Application/DTOs/ReliabilityDto.cs b/src/TransitPulse.Application/Features/Reliability/GetReliability/ReliabilityDto.cs similarity index 82% rename from src/TransitPulse.Application/DTOs/ReliabilityDto.cs rename to src/TransitPulse.Application/Features/Reliability/GetReliability/ReliabilityDto.cs index a718437..5e494b5 100644 --- a/src/TransitPulse.Application/DTOs/ReliabilityDto.cs +++ b/src/TransitPulse.Application/Features/Reliability/GetReliability/ReliabilityDto.cs @@ -1,4 +1,4 @@ -namespace TransitPulse.Application.DTOs; +namespace TransitPulse.Application.Features.Reliability.GetReliability; /// /// Reliability information returned to the client. @@ -6,9 +6,7 @@ namespace TransitPulse.Application.DTOs; public record ReliabilityDto( Guid RouteId, string RouteName, - double Score, - double AverageDelay, double CancellationRate, double OnTimePercentage, diff --git a/src/TransitPulse.Application/Features/Reliability/GetReliabilitySnapshot/GetReliabilitySnapshotHandler.cs b/src/TransitPulse.Application/Features/Reliability/GetReliabilitySnapshot/GetReliabilitySnapshotHandler.cs index 5e1a792..91a6d08 100644 --- a/src/TransitPulse.Application/Features/Reliability/GetReliabilitySnapshot/GetReliabilitySnapshotHandler.cs +++ b/src/TransitPulse.Application/Features/Reliability/GetReliabilitySnapshot/GetReliabilitySnapshotHandler.cs @@ -1,9 +1,14 @@ +using MediatR; using TransitPulse.Application.Exceptions; +using TransitPulse.Application.Features.Reliability.Common; using TransitPulse.Application.Interfaces; namespace TransitPulse.Application.Features.Reliability.GetReliabilitySnapshot; public class GetReliabilitySnapshotHandler + : IRequestHandler< + GetReliabilitySnapshotQuery, + GetReliabilitySnapshotDto> { private readonly IReliabilitySnapshotRepository _snapshotRepository; @@ -13,21 +18,21 @@ public GetReliabilitySnapshotHandler( _snapshotRepository = snapshotRepository; } - public async Task HandleAsync( - Guid snapshotId, + public async Task Handle( + GetReliabilitySnapshotQuery query, CancellationToken cancellationToken) { var snapshot = await _snapshotRepository.GetByIdAsync( - snapshotId, + query.SnapshotId, cancellationToken); if (snapshot is null) { throw new NotFoundException( - $"Snapshot {snapshotId} was not found."); + $"Snapshot {query.SnapshotId} was not found."); } - return new GetReliabilitySnapshotResult( + return new GetReliabilitySnapshotDto( snapshot.Id, snapshot.Score, snapshot.AverageDelay, diff --git a/src/TransitPulse.Application/Features/Reliability/GetReliabilitySnapshot/GetReliabilitySnapshotQuery.cs b/src/TransitPulse.Application/Features/Reliability/GetReliabilitySnapshot/GetReliabilitySnapshotQuery.cs new file mode 100644 index 0000000..ebee601 --- /dev/null +++ b/src/TransitPulse.Application/Features/Reliability/GetReliabilitySnapshot/GetReliabilitySnapshotQuery.cs @@ -0,0 +1,7 @@ +using MediatR; +using TransitPulse.Application.Features.Reliability.Common; + +namespace TransitPulse.Application.Features.Reliability.GetReliabilitySnapshot; + +public record GetReliabilitySnapshotQuery(Guid SnapshotId) + : IRequest; \ No newline at end of file diff --git a/src/TransitPulse.Application/Features/Reliability/GetReliabilitySnapshots/GetReliabilitySnapshotsHandler.cs b/src/TransitPulse.Application/Features/Reliability/GetReliabilitySnapshots/GetReliabilitySnapshotsHandler.cs index 3aa3a0b..85a42a7 100644 --- a/src/TransitPulse.Application/Features/Reliability/GetReliabilitySnapshots/GetReliabilitySnapshotsHandler.cs +++ b/src/TransitPulse.Application/Features/Reliability/GetReliabilitySnapshots/GetReliabilitySnapshotsHandler.cs @@ -1,12 +1,16 @@ +using MediatR; using TransitPulse.Application.Exceptions; +using TransitPulse.Application.Features.Reliability.Common; using TransitPulse.Application.Interfaces; namespace TransitPulse.Application.Features.Reliability.GetReliabilitySnapshots; public class GetReliabilitySnapshotsHandler + : IRequestHandler< + GetReliabilitySnapshotsQuery, + List> { - private readonly IReliabilitySnapshotRepository - _snapshotRepository; + private readonly IReliabilitySnapshotRepository _snapshotRepository; public GetReliabilitySnapshotsHandler( IReliabilitySnapshotRepository snapshotRepository) @@ -14,23 +18,22 @@ public GetReliabilitySnapshotsHandler( _snapshotRepository = snapshotRepository; } - public async Task> HandleAsync(Guid routeId, CancellationToken cancellationToken) + public async Task> Handle( + GetReliabilitySnapshotsQuery query, + CancellationToken cancellationToken) { - var snapshots = await _snapshotRepository.GetByRouteAsync(routeId, cancellationToken); + var snapshots = await _snapshotRepository.GetByRouteAsync( + query.RouteId, + cancellationToken); - if (!snapshots.Any()) - { - throw new NotFoundException( - $"No snapshots found for route {routeId}"); - } - return snapshots.Select( - snapshot => new GetReliabilitySnapshotsResult( + return snapshots.Select(snapshot => + new GetReliabilitySnapshotDto( snapshot.Id, snapshot.Score, snapshot.AverageDelay, snapshot.CancellationRate, snapshot.OnTimePercentage, - snapshot.CalculatedAt)).ToList(); - + snapshot.CalculatedAt)) + .ToList(); } } diff --git a/src/TransitPulse.Application/Features/Reliability/GetReliabilitySnapshots/GetReliabilitySnapshotsQuery.cs b/src/TransitPulse.Application/Features/Reliability/GetReliabilitySnapshots/GetReliabilitySnapshotsQuery.cs new file mode 100644 index 0000000..1477fe7 --- /dev/null +++ b/src/TransitPulse.Application/Features/Reliability/GetReliabilitySnapshots/GetReliabilitySnapshotsQuery.cs @@ -0,0 +1,7 @@ +using MediatR; +using TransitPulse.Application.Features.Reliability.Common; + +namespace TransitPulse.Application.Features.Reliability.GetReliabilitySnapshots; + +public record GetReliabilitySnapshotsQuery(Guid RouteId) + : IRequest>; \ No newline at end of file diff --git a/src/TransitPulse.Application/Interfaces/IReliabilityRepository.cs b/src/TransitPulse.Application/Interfaces/IReliabilityRepository.cs index 3230e40..74071b0 100644 --- a/src/TransitPulse.Application/Interfaces/IReliabilityRepository.cs +++ b/src/TransitPulse.Application/Interfaces/IReliabilityRepository.cs @@ -1,12 +1,12 @@ -using TransitPulse.Application.DTOs; +using TransitPulse.Application.Features.Reliability.GetReliability; namespace TransitPulse.Application.Interfaces; - public interface IReliabilityRepository - { - Task GetByRouteIdAsync( - Guid routeId, - CancellationToken cancellationToken); +public interface IReliabilityRepository +{ + Task GetByRouteIdAsync( + Guid routeId, + CancellationToken cancellationToken); - } +} diff --git a/src/TransitPulse.Application/Interfaces/IReliabilitySnapshotRepository.cs b/src/TransitPulse.Application/Interfaces/IReliabilitySnapshotRepository.cs index f3e933f..a235bc0 100644 --- a/src/TransitPulse.Application/Interfaces/IReliabilitySnapshotRepository.cs +++ b/src/TransitPulse.Application/Interfaces/IReliabilitySnapshotRepository.cs @@ -1,3 +1,4 @@ +using TransitPulse.Application.Features.Dashboard.GetTopRoutes; using TransitPulse.Domain.Entities; namespace TransitPulse.Application.Interfaces; @@ -7,8 +8,8 @@ public interface IReliabilitySnapshotRepository Task AddAsync(ReliabilitySnapshot snapshot, CancellationToken cancellationToken); Task> GetByRouteAsync(Guid routeId, CancellationToken cancellationToken); - Task GetByIdAsync( - Guid snapshotId, - CancellationToken cancellationToken); + Task GetByIdAsync(Guid snapshotId, CancellationToken cancellationToken); + + Task> GetTopRoutesAsync(CancellationToken cancellationToken); } diff --git a/src/TransitPulse.Infrastructure/Services/ReliabilityCalculator.cs b/src/TransitPulse.Application/Services/ReliabilityCalculator.cs similarity index 96% rename from src/TransitPulse.Infrastructure/Services/ReliabilityCalculator.cs rename to src/TransitPulse.Application/Services/ReliabilityCalculator.cs index 723e5d1..39fca0f 100644 --- a/src/TransitPulse.Infrastructure/Services/ReliabilityCalculator.cs +++ b/src/TransitPulse.Application/Services/ReliabilityCalculator.cs @@ -2,7 +2,7 @@ using TransitPulse.Application.Features.Reliability.GenerateReliabilitySnapshot; using TransitPulse.Domain.Entities; -namespace TransitPulse.Infrastructure.Services; +namespace TransitPulse.Application.Services; public class ReliabilityCalculator : IReliabilityCalculator { diff --git a/src/TransitPulse.Application/TransitPulse.Application.csproj b/src/TransitPulse.Application/TransitPulse.Application.csproj index 390c585..018afa7 100644 --- a/src/TransitPulse.Application/TransitPulse.Application.csproj +++ b/src/TransitPulse.Application/TransitPulse.Application.csproj @@ -12,6 +12,8 @@ + + diff --git a/src/TransitPulse.Domain/Entities/ReliabilitySnapshot.cs b/src/TransitPulse.Domain/Entities/ReliabilitySnapshot.cs index 173cb2e..1b05237 100644 --- a/src/TransitPulse.Domain/Entities/ReliabilitySnapshot.cs +++ b/src/TransitPulse.Domain/Entities/ReliabilitySnapshot.cs @@ -14,31 +14,46 @@ public class ReliabilitySnapshot public Guid RouteId { get; private set; } - // Overall reliability score (0-100). + /// + /// Overall reliability score (0-100). + /// public double Score { get; private set; } - // Average delay in minutes. + public Route Route { get; private set; } = null!; + + /// + /// Average delay in minutes. + /// public double AverageDelay { get; private set; } - // Percentage of cancelled trips. + /// + /// Percentage of cancelled trips. + /// public double CancellationRate { get; private set; } - // Percentage of trips arriving on time. + /// + /// Percentage of trips arriving on time. + /// public double OnTimePercentage { get; private set; } - // Period used for the calculation. + /// + /// Start of the reporting period (UTC). + /// public DateTime PeriodStart { get; private set; } + /// + /// End of the reporting period (UTC). + /// public DateTime PeriodEnd { get; private set; } - // Timestamp when the snapshot was generated. + /// + /// Timestamp when the snapshot was generated (UTC). + /// public DateTime CalculatedAt { get; private set; } private ReliabilitySnapshot() { - - // Used by EF Core when materializing entities - // from the database. + // Required by EF Core. } public ReliabilitySnapshot( @@ -51,26 +66,30 @@ public ReliabilitySnapshot( DateTime periodEnd, DateTime calculatedAt) { - if (score < 0 || score > 100) - throw new ArgumentException( - "Score must be between 0 and 100.", - nameof(score)); + throw new ArgumentOutOfRangeException( + nameof(score), + "Score must be between 0 and 100."); if (averageDelay < 0) - throw new ArgumentException( - "Average delay cannot be negative.", - nameof(averageDelay)); + throw new ArgumentOutOfRangeException( + nameof(averageDelay), + "Average delay cannot be negative."); if (cancellationRate < 0 || cancellationRate > 100) - throw new ArgumentException( - "Cancellation rate must be between 0 and 100.", - nameof(cancellationRate)); + throw new ArgumentOutOfRangeException( + nameof(cancellationRate), + "Cancellation rate must be between 0 and 100."); if (onTimePercentage < 0 || onTimePercentage > 100) - throw new ArgumentException( - "On-time percentage must be between 0 and 100.", - nameof(onTimePercentage)); + throw new ArgumentOutOfRangeException( + nameof(onTimePercentage), + "On-time percentage must be between 0 and 100."); + + // Normalize all timestamps to UTC. + periodStart = EnsureUtc(periodStart); + periodEnd = EnsureUtc(periodEnd); + calculatedAt = EnsureUtc(calculatedAt); if (periodEnd < periodStart) throw new ArgumentException( @@ -86,4 +105,17 @@ public ReliabilitySnapshot( PeriodEnd = periodEnd; CalculatedAt = calculatedAt; } + + private static DateTime EnsureUtc(DateTime value) + { + return value.Kind switch + { + DateTimeKind.Utc => value, + DateTimeKind.Local => value.ToUniversalTime(), + DateTimeKind.Unspecified => DateTime.SpecifyKind( + value, + DateTimeKind.Utc), + _ => value + }; + } } \ No newline at end of file diff --git a/src/TransitPulse.Domain/Entities/Route.cs b/src/TransitPulse.Domain/Entities/Route.cs index 8f3e791..cb57c7e 100644 --- a/src/TransitPulse.Domain/Entities/Route.cs +++ b/src/TransitPulse.Domain/Entities/Route.cs @@ -13,11 +13,22 @@ public class Route public string Name { get; private set; } public TransportType TransportType { get; private set; } + public bool IsActive { get; private set; } + + public ICollection ReliabilitySnapshots + { get; private set; } + = new List(); + + private Route() + { + // Required by EF Core + } public Route( string routeCode, string name, - TransportType transportType) + TransportType transportType, + bool isActive = true) { if (string.IsNullOrWhiteSpace(routeCode)) throw new ArgumentException( @@ -29,8 +40,10 @@ public Route( "Route name is required.", nameof(name)); + Id = Guid.NewGuid(); RouteCode = routeCode; Name = name; TransportType = transportType; + IsActive = isActive; } } \ No newline at end of file diff --git a/src/TransitPulse.Infrastructure/DependencyInjection.cs b/src/TransitPulse.Infrastructure/DependencyInjection.cs index deca58a..341f2d0 100644 --- a/src/TransitPulse.Infrastructure/DependencyInjection.cs +++ b/src/TransitPulse.Infrastructure/DependencyInjection.cs @@ -3,9 +3,8 @@ using Microsoft.Extensions.DependencyInjection; using TransitPulse.Application.Features.Reliability.GenerateReliabilitySnapshot; using TransitPulse.Application.Interfaces; +using TransitPulse.Application.Services; using TransitPulse.Infrastructure.Persistence; -using TransitPulse.Infrastructure.Repositories; -using TransitPulse.Infrastructure.Services; namespace TransitPulse.Infrastructure; @@ -24,6 +23,8 @@ public static IServiceCollection AddInfrastructureServices( services.AddSingleton(); + services.AddScoped(); + return services; } } \ No newline at end of file diff --git a/src/TransitPulse.Infrastructure/Migrations/20260619105201_InitialCreate.Designer.cs b/src/TransitPulse.Infrastructure/Migrations/20260619105201_InitialCreate.Designer.cs deleted file mode 100644 index 0f8d7ea..0000000 --- a/src/TransitPulse.Infrastructure/Migrations/20260619105201_InitialCreate.Designer.cs +++ /dev/null @@ -1,65 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using TransitPulse.Infrastructure.Persistence; - -#nullable disable - -namespace TransitPulse.Infrastructure.Migrations -{ - [DbContext(typeof(TransitPulseDbContext))] - [Migration("20260619105201_InitialCreate")] - partial class InitialCreate - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "10.0.9") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("TransitPulse.Domain.Entities.ReliabilitySnapshot", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AverageDelay") - .HasColumnType("double precision"); - - b.Property("CalculatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CancellationRate") - .HasColumnType("double precision"); - - b.Property("OnTimePercentage") - .HasColumnType("double precision"); - - b.Property("PeriodEnd") - .HasColumnType("timestamp with time zone"); - - b.Property("PeriodStart") - .HasColumnType("timestamp with time zone"); - - b.Property("RouteId") - .HasColumnType("uuid"); - - b.Property("Score") - .HasColumnType("double precision"); - - b.HasKey("Id"); - - b.ToTable("reliability_snapshots", (string)null); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/TransitPulse.Infrastructure/Migrations/20260619105201_InitialCreate.cs b/src/TransitPulse.Infrastructure/Migrations/20260619105201_InitialCreate.cs deleted file mode 100644 index 73abd02..0000000 --- a/src/TransitPulse.Infrastructure/Migrations/20260619105201_InitialCreate.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace TransitPulse.Infrastructure.Migrations -{ - /// - public partial class InitialCreate : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "reliability_snapshots", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - RouteId = table.Column(type: "uuid", nullable: false), - Score = table.Column(type: "double precision", nullable: false), - AverageDelay = table.Column(type: "double precision", nullable: false), - CancellationRate = table.Column(type: "double precision", nullable: false), - OnTimePercentage = table.Column(type: "double precision", nullable: false), - PeriodStart = table.Column(type: "timestamp with time zone", nullable: false), - PeriodEnd = table.Column(type: "timestamp with time zone", nullable: false), - CalculatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_reliability_snapshots", x => x.Id); - }); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "reliability_snapshots"); - } - } -} diff --git a/src/TransitPulse.Infrastructure/Migrations/20260620004910_AddRouteEvents.cs b/src/TransitPulse.Infrastructure/Migrations/20260620004910_AddRouteEvents.cs deleted file mode 100644 index 3d29e34..0000000 --- a/src/TransitPulse.Infrastructure/Migrations/20260620004910_AddRouteEvents.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace TransitPulse.Infrastructure.Migrations -{ - /// - public partial class AddRouteEvents : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "route_events", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - RouteId = table.Column(type: "uuid", nullable: false), - StopId = table.Column(type: "uuid", nullable: false), - ScheduledTime = table.Column(type: "timestamp with time zone", nullable: false), - ActualTime = table.Column(type: "timestamp with time zone", nullable: false), - IsCancelled = table.Column(type: "boolean", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_route_events", x => x.Id); - }); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "route_events"); - } - } -} diff --git a/src/TransitPulse.Infrastructure/Migrations/20260620004910_AddRouteEvents.Designer.cs b/src/TransitPulse.Infrastructure/Migrations/20260717225443_InitialCreate.Designer.cs similarity index 66% rename from src/TransitPulse.Infrastructure/Migrations/20260620004910_AddRouteEvents.Designer.cs rename to src/TransitPulse.Infrastructure/Migrations/20260717225443_InitialCreate.Designer.cs index 885741e..6eca786 100644 --- a/src/TransitPulse.Infrastructure/Migrations/20260620004910_AddRouteEvents.Designer.cs +++ b/src/TransitPulse.Infrastructure/Migrations/20260717225443_InitialCreate.Designer.cs @@ -12,8 +12,8 @@ namespace TransitPulse.Infrastructure.Migrations { [DbContext(typeof(TransitPulseDbContext))] - [Migration("20260620004910_AddRouteEvents")] - partial class AddRouteEvents + [Migration("20260717225443_InitialCreate")] + partial class InitialCreate { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -57,9 +57,36 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("RouteId"); + b.ToTable("reliability_snapshots", (string)null); }); + modelBuilder.Entity("TransitPulse.Domain.Entities.Route", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("RouteCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("TransportType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Routes"); + }); + modelBuilder.Entity("TransitPulse.Domain.Entities.RouteEvent", b => { b.Property("Id") @@ -85,6 +112,22 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("route_events", (string)null); }); + + modelBuilder.Entity("TransitPulse.Domain.Entities.ReliabilitySnapshot", b => + { + b.HasOne("TransitPulse.Domain.Entities.Route", "Route") + .WithMany("ReliabilitySnapshots") + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Route"); + }); + + modelBuilder.Entity("TransitPulse.Domain.Entities.Route", b => + { + b.Navigation("ReliabilitySnapshots"); + }); #pragma warning restore 612, 618 } } diff --git a/src/TransitPulse.Infrastructure/Migrations/20260717225443_InitialCreate.cs b/src/TransitPulse.Infrastructure/Migrations/20260717225443_InitialCreate.cs new file mode 100644 index 0000000..2998706 --- /dev/null +++ b/src/TransitPulse.Infrastructure/Migrations/20260717225443_InitialCreate.cs @@ -0,0 +1,89 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TransitPulse.Infrastructure.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "route_events", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + RouteId = table.Column(type: "uuid", nullable: false), + StopId = table.Column(type: "uuid", nullable: false), + ScheduledTime = table.Column(type: "timestamp with time zone", nullable: false), + ActualTime = table.Column(type: "timestamp with time zone", nullable: false), + IsCancelled = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_route_events", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Routes", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + RouteCode = table.Column(type: "text", nullable: false), + Name = table.Column(type: "text", nullable: false), + TransportType = table.Column(type: "integer", nullable: false), + IsActive = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Routes", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "reliability_snapshots", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + RouteId = table.Column(type: "uuid", nullable: false), + Score = table.Column(type: "double precision", nullable: false), + AverageDelay = table.Column(type: "double precision", nullable: false), + CancellationRate = table.Column(type: "double precision", nullable: false), + OnTimePercentage = table.Column(type: "double precision", nullable: false), + PeriodStart = table.Column(type: "timestamp with time zone", nullable: false), + PeriodEnd = table.Column(type: "timestamp with time zone", nullable: false), + CalculatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_reliability_snapshots", x => x.Id); + table.ForeignKey( + name: "FK_reliability_snapshots_Routes_RouteId", + column: x => x.RouteId, + principalTable: "Routes", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_reliability_snapshots_RouteId", + table: "reliability_snapshots", + column: "RouteId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "reliability_snapshots"); + + migrationBuilder.DropTable( + name: "route_events"); + + migrationBuilder.DropTable( + name: "Routes"); + } + } +} diff --git a/src/TransitPulse.Infrastructure/Migrations/TransitPulseDbContextModelSnapshot.cs b/src/TransitPulse.Infrastructure/Migrations/TransitPulseDbContextModelSnapshot.cs index 69ad19c..5d3c149 100644 --- a/src/TransitPulse.Infrastructure/Migrations/TransitPulseDbContextModelSnapshot.cs +++ b/src/TransitPulse.Infrastructure/Migrations/TransitPulseDbContextModelSnapshot.cs @@ -54,9 +54,36 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("RouteId"); + b.ToTable("reliability_snapshots", (string)null); }); + modelBuilder.Entity("TransitPulse.Domain.Entities.Route", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("RouteCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("TransportType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Routes"); + }); + modelBuilder.Entity("TransitPulse.Domain.Entities.RouteEvent", b => { b.Property("Id") @@ -82,6 +109,22 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("route_events", (string)null); }); + + modelBuilder.Entity("TransitPulse.Domain.Entities.ReliabilitySnapshot", b => + { + b.HasOne("TransitPulse.Domain.Entities.Route", "Route") + .WithMany("ReliabilitySnapshots") + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Route"); + }); + + modelBuilder.Entity("TransitPulse.Domain.Entities.Route", b => + { + b.Navigation("ReliabilitySnapshots"); + }); #pragma warning restore 612, 618 } } diff --git a/src/TransitPulse.Infrastructure/Persistence/Configurations/RouteConfiguration.cs b/src/TransitPulse.Infrastructure/Persistence/Configurations/RouteConfiguration.cs new file mode 100644 index 0000000..e69de29 diff --git a/src/TransitPulse.Infrastructure/Persistence/DbSeeder.cs b/src/TransitPulse.Infrastructure/Persistence/DbSeeder.cs new file mode 100644 index 0000000..ccef3c6 --- /dev/null +++ b/src/TransitPulse.Infrastructure/Persistence/DbSeeder.cs @@ -0,0 +1,74 @@ +using Microsoft.EntityFrameworkCore; +using TransitPulse.Domain.Entities; +using TransitPulse.Domain.Enums; + +namespace TransitPulse.Infrastructure.Persistence; + +public class DbSeeder +{ + private readonly TransitPulseDbContext _context; + + public DbSeeder(TransitPulseDbContext context) + { + _context = context; + } + + public async Task SeedAsync() + { + // Don't seed twice + if (await _context.Routes.AnyAsync()) + { + return; + } + + // ------------------------- + // Seed Routes + // ------------------------- + + var routes = new List + { + new("B31", "Airport Express", TransportType.Bus), + new("M1", "Metro Line 1", TransportType.Metro), + new("T5", "Tram Line 5", TransportType.Tram), + new("TR1", "Regional Train", TransportType.Train), + new("F1", "Harbor Ferry", TransportType.Ferry) + }; + + _context.Routes.AddRange(routes); + await _context.SaveChangesAsync(); + + // ------------------------- + // Seed Route Events + // ------------------------- + + var random = new Random(); + var events = new List(); + + foreach (var route in routes) + { + for (int i = 0; i < 20; i++) + { + var scheduled = DateTime.UtcNow + .AddDays(-random.Next(1, 30)) + .AddHours(random.Next(0, 24)) + .AddMinutes(random.Next(0, 60)); + + bool cancelled = random.Next(100) < 5; // 5% cancellation + + DateTime actual = cancelled + ? scheduled + : scheduled.AddMinutes(random.Next(0, 15)); + + events.Add(new RouteEvent( + route.Id, + Guid.NewGuid(), // Temporary StopId + scheduled, + actual, + cancelled)); + } + } + + _context.RouteEvents.AddRange(events); + await _context.SaveChangesAsync(); + } +} \ No newline at end of file diff --git a/src/TransitPulse.Infrastructure/Persistence/TransitPulseDbContext.cs b/src/TransitPulse.Infrastructure/Persistence/TransitPulseDbContext.cs index 4f81038..0a1b8bc 100644 --- a/src/TransitPulse.Infrastructure/Persistence/TransitPulseDbContext.cs +++ b/src/TransitPulse.Infrastructure/Persistence/TransitPulseDbContext.cs @@ -10,6 +10,7 @@ public TransitPulseDbContext( { } + public DbSet Routes => Set(); public DbSet ReliabilitySnapshots => Set(); diff --git a/src/TransitPulse.Infrastructure/Repositories/InMemoryReliabilitySnapshotRepository.cs b/src/TransitPulse.Infrastructure/Repositories/InMemoryReliabilitySnapshotRepository.cs index 6e5bae8..2ee57f5 100644 --- a/src/TransitPulse.Infrastructure/Repositories/InMemoryReliabilitySnapshotRepository.cs +++ b/src/TransitPulse.Infrastructure/Repositories/InMemoryReliabilitySnapshotRepository.cs @@ -1,3 +1,4 @@ +using TransitPulse.Application.Features.Dashboard.GetTopRoutes; using TransitPulse.Application.Interfaces; using TransitPulse.Domain.Entities; @@ -36,4 +37,10 @@ public Task> GetByRouteAsync( return Task.FromResult(snapshot); } + + public Task> GetTopRoutesAsync( + CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } } \ No newline at end of file diff --git a/src/TransitPulse.Infrastructure/Repositories/ReliabilitySnapshotRepository.cs b/src/TransitPulse.Infrastructure/Repositories/ReliabilitySnapshotRepository.cs index 446a3a5..c975a00 100644 --- a/src/TransitPulse.Infrastructure/Repositories/ReliabilitySnapshotRepository.cs +++ b/src/TransitPulse.Infrastructure/Repositories/ReliabilitySnapshotRepository.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore; +using TransitPulse.Application.Features.Dashboard.GetTopRoutes; using TransitPulse.Application.Interfaces; using TransitPulse.Domain.Entities; using TransitPulse.Infrastructure.Persistence; @@ -37,4 +38,41 @@ public async Task> GetByRouteAsync(Guid route snapshot => snapshot.Id == snapshotId, cancellationToken); } + + public async Task> GetTopRoutesAsync( + CancellationToken cancellationToken) + { + var thirtyDaysAgo = DateTime.UtcNow.AddDays(-30); + + var routes = await _context.ReliabilitySnapshots + .Where(snapshot => snapshot.CalculatedAt >= thirtyDaysAgo) + .Where(snapshot => snapshot.Route.IsActive) + .GroupBy(snapshot => new + { + snapshot.Route.Id, + snapshot.Route.RouteCode, + snapshot.Route.Name, + snapshot.Route.TransportType + }) + .Select(group => new + { + group.Key.Id, + group.Key.RouteCode, + group.Key.Name, + group.Key.TransportType, + AverageScore = group.Average(snapshot => snapshot.Score) + }) + .OrderByDescending(route => route.AverageScore) + .Take(5) + .ToListAsync(cancellationToken); + + return routes.Select(route => new TopRouteDto( + route.Id, + route.RouteCode, + route.Name, + route.TransportType.ToString(), + route.AverageScore + )).ToList(); + } + } \ No newline at end of file diff --git a/tests/TransitPulse.Application.Tests/Reliability/GenerateReliabilitySnapshotHandlerTests.cs b/tests/TransitPulse.Application.Tests/Reliability/GenerateReliabilitySnapshotHandlerTests.cs index 8b960a4..98eef8e 100644 --- a/tests/TransitPulse.Application.Tests/Reliability/GenerateReliabilitySnapshotHandlerTests.cs +++ b/tests/TransitPulse.Application.Tests/Reliability/GenerateReliabilitySnapshotHandlerTests.cs @@ -11,7 +11,7 @@ public class GenerateReliabilitySnapshotHandlerTests { [Fact] - public async Task HandleAsync_WithNoRouteEvents_ShouldThrowInvalidOperationException() + public async Task Handle_WithNoRouteEvents_ShouldThrowInvalidOperationException() { // Arrange @@ -55,14 +55,14 @@ public async Task HandleAsync_WithNoRouteEvents_ShouldThrowInvalidOperationExcep // Act - Func action = () => handler.HandleAsync(command, CancellationToken.None); + Func action = () => handler.Handle(command, CancellationToken.None); // Assert await action.Should().ThrowAsync(); } [Fact] - public async Task HandleAsync_WithValidRouteEvents_ShouldSaveSnapshotAndReturnResult() + public async Task Handle_WithValidRouteEvents_ShouldSaveSnapshotAndReturnResult() { // Arrange @@ -137,7 +137,7 @@ public async Task HandleAsync_WithValidRouteEvents_ShouldSaveSnapshotAndReturnRe // Act var result = - await handler.HandleAsync( + await handler.Handle( command, CancellationToken.None); diff --git a/tests/TransitPulse.Application.Tests/Reliability/GetReliabilitySnapshot/GetReliabilitySnapshotHandlerTests.cs b/tests/TransitPulse.Application.Tests/Reliability/GetReliabilitySnapshot/GetReliabilitySnapshotHandlerTests.cs index 7130f0f..f73f606 100644 --- a/tests/TransitPulse.Application.Tests/Reliability/GetReliabilitySnapshot/GetReliabilitySnapshotHandlerTests.cs +++ b/tests/TransitPulse.Application.Tests/Reliability/GetReliabilitySnapshot/GetReliabilitySnapshotHandlerTests.cs @@ -14,7 +14,7 @@ public class GetReliabilitySnapshotHandlerTests { [Fact] - public async Task HandleAsync_WithSnapshotNotFound_ShouldThrowNotFoundException() + public async Task Handle_WithSnapshotNotFound_ShouldThrowNotFoundException() { // Arrange @@ -30,9 +30,11 @@ public async Task HandleAsync_WithSnapshotNotFound_ShouldThrowNotFoundException( // Act - Func action = () => handler.HandleAsync( - Guid.NewGuid(), - CancellationToken.None); + var query = new GetReliabilitySnapshotQuery(Guid.NewGuid()); + + Func action = () => handler.Handle( + query, + CancellationToken.None); // Assert @@ -40,7 +42,7 @@ public async Task HandleAsync_WithSnapshotNotFound_ShouldThrowNotFoundException( } [Fact] - public async Task HandleAsync_WithSnapshot_ShouldReturnMappedResult() + public async Task Handle_WithSnapshot_ShouldReturnMappedResult() { // Arrange @@ -69,7 +71,11 @@ public async Task HandleAsync_WithSnapshot_ShouldReturnMappedResult() // Act - var result = await handler.HandleAsync(snapshot.Id, CancellationToken.None); + var query = new GetReliabilitySnapshotQuery(snapshot.Id); + + var result = await handler.Handle( + query, + CancellationToken.None); // Assert diff --git a/tests/TransitPulse.Application.Tests/Reliability/GetReliabilitySnapshotsHandlerTests.cs b/tests/TransitPulse.Application.Tests/Reliability/GetReliabilitySnapshotsHandlerTests.cs index 64c48a1..0dd577a 100644 --- a/tests/TransitPulse.Application.Tests/Reliability/GetReliabilitySnapshotsHandlerTests.cs +++ b/tests/TransitPulse.Application.Tests/Reliability/GetReliabilitySnapshotsHandlerTests.cs @@ -1,8 +1,5 @@ using FluentAssertions; -using FluentValidation; using Moq; -using TransitPulse.Application.Exceptions; -using TransitPulse.Application.Features.Reliability.GenerateReliabilitySnapshot; using TransitPulse.Application.Features.Reliability.GetReliabilitySnapshots; using TransitPulse.Application.Interfaces; using TransitPulse.Domain.Entities; @@ -13,10 +10,9 @@ public class GetReliabilitySnapshotsHandlerTests { [Fact] - public async Task HandleAsync_WithNoSnapshots_ShouldThrowNotFoundException() + public async Task Handle_WithNoSnapshots_ShouldReturnEmptyList() { // Arrange - var snapshotRepository = new Mock(); @@ -31,21 +27,19 @@ public async Task HandleAsync_WithNoSnapshots_ShouldThrowNotFoundException() new GetReliabilitySnapshotsHandler( snapshotRepository.Object); - // Act + var query = new GetReliabilitySnapshotsQuery(Guid.NewGuid()); - Func action = - () => handler.HandleAsync( - Guid.NewGuid(), - CancellationToken.None); + // Act + var result = await handler.Handle( + query, + CancellationToken.None); // Assert - - await action.Should() - .ThrowAsync(); + result.Should().BeEmpty(); } [Fact] - public async Task HandleAsync_WithSnapshots_ShouldReturnMappedResults() + public async Task Handle_WithSnapshots_ShouldReturnMappedResults() { // Arrange @@ -81,9 +75,11 @@ public async Task HandleAsync_WithSnapshots_ShouldReturnMappedResults() // Act + var query = new GetReliabilitySnapshotsQuery(routeId); + var result = - await handler.HandleAsync( - routeId, + await handler.Handle( + query, CancellationToken.None); // Assert diff --git a/tests/TransitPulse.Application.Tests/Reliability/ReliabiltyCalculatorTests.cs b/tests/TransitPulse.Application.Tests/Reliability/ReliabiltyCalculatorTests.cs index ed6411b..d4c7330 100644 --- a/tests/TransitPulse.Application.Tests/Reliability/ReliabiltyCalculatorTests.cs +++ b/tests/TransitPulse.Application.Tests/Reliability/ReliabiltyCalculatorTests.cs @@ -1,6 +1,7 @@ using FluentAssertions; +using TransitPulse.Application.Services; using TransitPulse.Domain.Entities; -using TransitPulse.Infrastructure.Services; + namespace TransitPulse.Application.Tests.Reliability;