Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
26 changes: 26 additions & 0 deletions src/TransitPulse.API/Controllers/DashboardController.cs
Original file line number Diff line number Diff line change
@@ -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<ActionResult<List<TopRouteDto>>> GetTopRoutes(
CancellationToken cancellationToken)
{
var result = await _mediator.Send(new GetTopRoutesQuery(), cancellationToken);

return Ok(result);
}
}
21 changes: 9 additions & 12 deletions src/TransitPulse.API/Controllers/ReliabilityController.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using MediatR;
using Microsoft.AspNetCore.Mvc;
using TransitPulse.API.Contracts.Requests;
using TransitPulse.API.Contracts.Responses;
Expand All @@ -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")]
Expand All @@ -33,7 +29,7 @@ public async Task<IActionResult> GenerateSnapshot(GenerateReliabilitySnapshotReq
request.PeriodStart,
request.PeriodEnd);

var result = await _generateHandler.HandleAsync(command, cancellationToken);
var result = await _mediator.Send(command, cancellationToken);

var response = new GenerateReliabilitySnapshotResponse(

Expand All @@ -53,7 +49,7 @@ public async Task<IActionResult> GenerateSnapshot(GenerateReliabilitySnapshotReq
[HttpGet("routes/{routeId}/snapshots")]
public async Task<IActionResult> 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(
Expand All @@ -73,7 +69,8 @@ public async Task<IActionResult> GetSnapshots(Guid routeId, CancellationToken ca
public async Task<IActionResult> 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,
Expand Down
15 changes: 15 additions & 0 deletions src/TransitPulse.API/Program.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using TransitPulse.Application;
using TransitPulse.Infrastructure;
using TransitPulse.API.Middleware;
using TransitPulse.Infrastructure.Persistence;

var builder = WebApplication.CreateBuilder(args);

Expand All @@ -13,8 +14,13 @@

// Infrastructure layer
builder.Services.AddInfrastructureServices(builder.Configuration); //Registers database, repositories, and infrastructure services.



var app = builder.Build();



app.UseMiddleware<ExceptionHandlingMiddleware>();

if (app.Environment.IsDevelopment())
Expand All @@ -23,6 +29,15 @@
}

app.UseHttpsRedirection();
app.UseAuthorization();

// 👇 Run the database seeder here
//
using (var scope = app.Services.CreateScope())
{
var seeder = scope.ServiceProvider.GetRequiredService<DbSeeder>();
await seeder.SeedAsync();
}

app.MapControllers();

Expand Down
5 changes: 5 additions & 0 deletions src/TransitPulse.Application/ApplicationAssemblyMarker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
namespace TransitPulse.Application;

public sealed class ApplicationAssemblyMarker
{
}
17 changes: 9 additions & 8 deletions src/TransitPulse.Application/DependencyInjection.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -10,13 +10,14 @@ public static class DependencyInjection
public static IServiceCollection AddApplicationServices(
this IServiceCollection services)
{
services.AddScoped<GetReliabilitySnapshotsHandler>();
services.AddMediatR(cfg =>
{
cfg.RegisterServicesFromAssembly(
typeof(ApplicationAssemblyMarker).Assembly);
});

services.AddScoped<GetReliabilitySnapshotHandler>();

services.AddScoped<GenerateReliabilitySnapshotHandler>();

services.AddScoped<GenerateReliabilitySnapshotValidator>();
services.AddValidatorsFromAssembly(
typeof(ApplicationAssemblyMarker).Assembly);

return services;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using MediatR;
using TransitPulse.Application.Interfaces;

namespace TransitPulse.Application.Features.Dashboard.GetTopRoutes;

public class GetTopRoutesHandler
: IRequestHandler<GetTopRoutesQuery, List<TopRouteDto>>
{
private readonly IReliabilitySnapshotRepository _repository;

public GetTopRoutesHandler(IReliabilitySnapshotRepository repository)
{
_repository = repository;
}

public async Task<List<TopRouteDto>> Handle(
GetTopRoutesQuery request,
CancellationToken cancellationToken)
{
return await _repository.GetTopRoutesAsync(cancellationToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using MediatR;

namespace TransitPulse.Application.Features.Dashboard.GetTopRoutes;

public record GetTopRoutesQuery : IRequest<List<TopRouteDto>>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace TransitPulse.Application.Features.Dashboard.GetTopRoutes;

public record TopRouteDto(
Guid RouteId,
string RouteCode,
string RouteName,
string TransportType,
double AverageScore
);
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
using MediatR;

namespace TransitPulse.Application.Features.Reliability.GenerateReliabilitySnapshot;

public record GenerateReliabilitySnapshotCommand(
Guid RouteId,
DateTime PeriodStart,
DateTime PeriodEnd);
DateTime PeriodEnd)
: IRequest<GenerateReliabilitySnapshotResult>;
Original file line number Diff line number Diff line change
@@ -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<GenerateReliabilitySnapshotCommand> _validator;
private readonly ILogger<GenerateReliabilitySnapshotHandler> _logger;

public GenerateReliabilitySnapshotHandler(
IRouteEventRepository routeEventRepository,
IReliabilityCalculator reliabilityCalculator,
IReliabilitySnapshotRepository snapshotRepository,
GenerateReliabilitySnapshotValidator validator,
ILogger<GenerateReliabilitySnapshotHandler> logger
)
IValidator<GenerateReliabilitySnapshotCommand> validator,
ILogger<GenerateReliabilitySnapshotHandler> logger)
{
_routeEventRepository = routeEventRepository;
_reliabilityCalculator = reliabilityCalculator;
Expand All @@ -30,22 +32,32 @@ ILogger<GenerateReliabilitySnapshotHandler> logger
_logger = logger;
}

public async Task<GenerateReliabilitySnapshotResult> HandleAsync(
GenerateReliabilitySnapshotCommand command,
CancellationToken cancellationToken)
public async Task<GenerateReliabilitySnapshotResult> 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())
{
Expand All @@ -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,
Expand All @@ -89,4 +98,4 @@ await _snapshotRepository.AddAsync(
metrics.AverageDelay,
metrics.CancellationRate);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
namespace TransitPulse.Application.DTOs;
namespace TransitPulse.Application.Features.Reliability.GetReliability;

/// <summary>
/// Reliability information returned to the client.
/// </summary>
public record ReliabilityDto(
Guid RouteId,
string RouteName,

double Score,

double AverageDelay,
double CancellationRate,
double OnTimePercentage,
Expand Down
Loading
Loading