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
7 changes: 7 additions & 0 deletions Areas/User/Controllers/TripImportController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ namespace Wayfarer.Areas.User.Controllers;
[Area("User"), Authorize, Route("User/Trip/[action]")]
public class TripImportController : BaseController
{
private const string GenericRouteReminder =
"Imported KML routes do not contain reliable transport information. Select a transport mode for each route where needed to enable automatic duration estimates.";
private readonly ITripImportService _svc;

public TripImportController(
Expand Down Expand Up @@ -37,6 +39,11 @@ public async Task<IActionResult> Import(
{
var result = await _svc.ImportWayfarerKmlAsync(stream, userId, mode, HttpContext.RequestAborted);
var redirectUrl = $"/User/Trip/Edit/{result.TripId:D}";
if (result.IsGenericWithRoutes)
{
TempData["AlertType"] = "info";
TempData["AlertMessage"] = GenericRouteReminder;
}
return Json(new
{
status = "success",
Expand Down
3 changes: 2 additions & 1 deletion Parsers/GoogleMyMapsKmlParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,8 @@ private static Trip BuildTrip(
Id = Guid.NewGuid(),
TripId = tripId,
UserId = userId,
Mode = route.Owner.Element(Kml + "name")?.Value ?? "drive",
Mode = string.Empty,
TransportProfileId = null,
RouteGeometry = new LineString(route.Budget.Coordinates.ToArray()) { SRID = 4326 },
EstimatedDistanceKm = null,
EstimatedDuration = null,
Expand Down
5 changes: 4 additions & 1 deletion Services/ITripImportService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ public sealed record TripImportNotice(
int? AdditionalRouteCount = null);

/// <summary>Bounded successful import result returned without source geometry.</summary>
public sealed record TripImportResult(Guid TripId, IReadOnlyList<TripImportNotice> Notices)
public sealed record TripImportResult(
Guid TripId,
IReadOnlyList<TripImportNotice> Notices,
bool IsGenericWithRoutes = false)
{
/// <summary>Supports existing internal consumers that require only the imported identity.</summary>
public static implicit operator Guid(TripImportResult result) => result.TripId;
Expand Down
3 changes: 1 addition & 2 deletions Services/TripImportService.Generic.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,14 @@ private async Task<TripImportResult> ImportGenericAsync(
var reconciledTags = await _tagReconciler.ReconcileAsync(importedTagTokens, cancellationToken);
foreach (var tag in reconciledTags) target.Tags.Add(tag);
AddShadowRegion(target, userId);
await ResolveImportedProfilesAsync(target.Segments, cancellationToken);
_dbContext.Trips.Add(target);
await _dbContext.SaveChangesAsync(cancellationToken);
_dbContext.ChangeTracker.Clear();
await SegmentMeasurementWriterReconciler.ReconcileTripAsync(
_dbContext, target.Id, allowUnavailableAutomatic: true, cancellationToken);
if (transaction is not null) await transaction.CommitAsync(cancellationToken);
_dbContext.ChangeTracker.Clear();
return new(target.Id, parsed.Notices);
return new(target.Id, parsed.Notices, target.Segments.Count > 0);
}
catch
{
Expand Down
12 changes: 0 additions & 12 deletions Services/TripImportService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -166,18 +166,6 @@ private async Task ValidateCompatibilityMeasurementsAsync(
}
}

/// <summary>Links known import modes before reconciliation while leaving unknown modes to the database compatibility trigger.</summary>
private async Task ResolveImportedProfilesAsync(IEnumerable<Segment> segments, CancellationToken cancellationToken)
{
var profiles = await _dbContext.Set<TransportProfile>().AsNoTracking().ToDictionaryAsync(
profile => profile.Key, cancellationToken);
foreach (var segment in segments)
{
var normalized = TransportProfile.NormalizeKey(segment.Mode);
segment.TransportProfileId = profiles.TryGetValue(normalized, out var profile) ? profile.Id : null;
}
}

/* ---------- helpers ------------------------------------------------- */
static Trip CreateNewShell(Trip parsed, string userId)
{
Expand Down
30 changes: 30 additions & 0 deletions tests/Wayfarer.Tests/Controllers/TripImportControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Text;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Wayfarer.Areas.User.Controllers;
Expand All @@ -18,6 +19,9 @@ namespace Wayfarer.Tests.Controllers;
/// </summary>
public class TripImportControllerTests : TestBase
{
private const string GenericRouteReminder =
"Imported KML routes do not contain reliable transport information. Select a transport mode for each route where needed to enable automatic duration estimates.";

[Fact]
public async Task Import_ReturnsBadRequest_WhenFileMissing()
{
Expand Down Expand Up @@ -56,6 +60,32 @@ public async Task Import_ReturnsBoundedCanonicalSuccessJson(TripImportMode mode)
Assert.Single(Property<IReadOnlyList<TripImportNotice>>(json.Value, "notices"));
}

/// <summary>Successful generic route imports install one informational editor reminder.</summary>
[Fact]
public async Task Import_GenericRoute_InstallsOneTimeInformationAndReturnsEditorRedirect()
{
var db = CreateDbContext();
var service = new TripImportService(db, NullLogger<TripImportService>.Instance);
var controller = BuildController(service);
ConfigureControllerWithUser(controller, "u1");
controller.TempData = new TempDataDictionary(controller.HttpContext, Mock.Of<ITempDataProvider>());
var file = CreateFormFile("""
<kml xmlns="http://www.opengis.net/kml/2.2"><Document><name>Generic</name>
<Placemark><name>Ella to Kandy by TRAIN</name><LineString><coordinates>80,7 81,7</coordinates></LineString></Placemark>
</Document></kml>
""");

var result = await controller.Import(file, TripImportMode.CreateNew);

var json = Assert.IsType<JsonResult>(result);
Assert.Equal("success", Property<string>(json.Value, "status"));
var tripId = Property<Guid>(json.Value, "tripId");
Assert.Equal($"/User/Trip/Edit/{tripId:D}", Property<string>(json.Value, "redirectUrl"));
Assert.Equal("info", controller.TempData["AlertType"]);
Assert.Equal(GenericRouteReminder, controller.TempData["AlertMessage"]);
Assert.Equal(2, controller.TempData.Count);
}

[Fact]
public async Task Import_ReturnsDuplicateJson_WhenDuplicateDetected()
{
Expand Down
4 changes: 2 additions & 2 deletions tests/Wayfarer.Tests/Parsers/GoogleMyMapsKmlParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ public void Parse_PlacemarkWithLineString_CreatesSegment()
// Assert
Assert.Single(trip.Segments);
var segment = trip.Segments.First();
Assert.Equal("drive", segment.Mode);
Assert.Equal(string.Empty, segment.Mode);
Assert.Equal(trip.Id, segment.TripId);
Assert.Equal("user1", segment.UserId);
Assert.IsType<LineString>(segment.RouteGeometry);
Expand Down Expand Up @@ -221,7 +221,7 @@ public void Parse_SegmentOutsideFolder_AddsToTripSegments()

// Assert
Assert.Single(trip.Segments);
Assert.Equal("route", trip.Segments.First().Mode);
Assert.Equal(string.Empty, trip.Segments.First().Mode);
}

[Fact]
Expand Down
12 changes: 7 additions & 5 deletions tests/Wayfarer.Tests/Services/TripImportPostgresTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,23 +51,25 @@ public async Task WayfarerV1RouteImport_ResolvesProfileAndReconcilesMeasurements
Assert.NotNull(segment.EstimatedDuration);
}

/// <summary>Generic route KML defaults Automatic and derives distance and duration through the known catalog profile.</summary>
/// <summary>Generic route titles remain descriptive text and never select a transport profile.</summary>
[PostgresFact]
public async Task GenericKmlRouteImport_DefaultsAutomaticAndCalculatesKnownMode()
public async Task GenericKmlRouteImport_LeavesTransportUnassigned()
{
fixture.RequireAvailable();
var user = await fixture.CreateUserAsync();
await using var context = fixture.CreateContext();
var service = new TripImportService(context, NullLogger<TripImportService>.Instance, CreateReconciler(context));

var tripId = await service.ImportWayfarerKmlAsync(ToStream(CreateGenericRouteKml("walk")), user.Id, TripImportMode.CreateNew);
var tripId = await service.ImportWayfarerKmlAsync(
ToStream(CreateGenericRouteKml("Ella to Kandy by TRAIN")), user.Id, TripImportMode.CreateNew);
fixture.RegisterTrip(tripId);

var segment = await context.Segments.AsNoTracking().SingleAsync(item => item.TripId == tripId);
Assert.Equal(string.Empty, segment.Mode);
Assert.Null(segment.TransportProfileId);
Assert.Equal(EstimatedDurationSource.Automatic, segment.EstimatedDurationSource);
Assert.NotNull(segment.TransportProfileId);
Assert.NotNull(segment.EstimatedDistanceKm);
Assert.NotNull(segment.EstimatedDuration);
Assert.Null(segment.EstimatedDuration);
}

/// <summary>Proves generic rollback clears failed state and a retry persists only final budgeted geometry and measurements.</summary>
Expand Down
Loading