feat: add CalendarEventBuilder with shared event/transport logic

This commit is contained in:
Claudio Schaad 2026-04-10 22:46:20 +02:00
parent 839abe6cf4
commit 10ccd6fce0
2 changed files with 122 additions and 0 deletions

View file

@ -0,0 +1,53 @@
using ShiftScheduler.Shared;
namespace ShiftScheduler.Services;
public record CalendarEventData(string Summary, string Description);
public static class CalendarEventBuilder
{
public static CalendarEventData BuildEventContent(Shift shift, TransportConnection? transport)
{
var description = transport != null
? $"Transport: {FormatTransportInfo(transport)}"
: string.Empty;
return new CalendarEventData(shift.Name, description);
}
public static string FormatTransportInfo(TransportConnection transport)
{
var departure = transport.DepartureTime.ToString("HH:mm");
var arrival = transport.ArrivalTime.ToString("HH:mm");
return $"{transport.Platform}: {departure} → {arrival} ({transport.Duration.TotalMinutes} Minutes)";
}
public static async Task<T> ExecuteWithRetryAsync<T>(Func<Task<T>> operation, int maxRetries = 3)
{
var delay = TimeSpan.FromSeconds(1);
for (var attempt = 0; attempt <= maxRetries; attempt++)
{
try
{
return await operation();
}
catch (HttpRequestException ex) when (ex.Message.Contains("429") && attempt < maxRetries)
{
await Task.Delay(delay);
delay = TimeSpan.FromMilliseconds(delay.TotalMilliseconds * 2);
}
catch (Exception ex) when (ex.Message.Contains("Rate Limit") && attempt < maxRetries)
{
await Task.Delay(delay);
delay = TimeSpan.FromMilliseconds(delay.TotalMilliseconds * 2);
}
catch (Exception ex) when (ex.Message.Contains("Forbidden") && ex.Message.Contains("quota") && attempt < maxRetries)
{
await Task.Delay(delay);
delay = TimeSpan.FromMilliseconds(delay.TotalMilliseconds * 2);
}
}
return await operation();
}
}

View file

@ -0,0 +1,69 @@
using ShiftScheduler.Services;
using ShiftScheduler.Shared;
using Shouldly;
namespace ShiftScheduler.Services.Tests;
public class CalendarEventBuilderTests
{
[Fact]
public void BuildEventContent_WithTransport_IncludesTransportInDescription()
{
var shift = new Shift { Name = "Früh" };
var transport = new TransportConnection
{
Platform = "Gleis 3",
DepartureTime = DateTimeOffset.Parse("2026-04-01T05:30:00+02:00"),
ArrivalTime = DateTimeOffset.Parse("2026-04-01T05:55:00+02:00"),
Duration = TimeSpan.FromMinutes(25)
};
var result = CalendarEventBuilder.BuildEventContent(shift, transport);
result.Summary.ShouldBe("Früh");
result.Description.ShouldBe("Transport: Gleis 3: 05:30 → 05:55 (25 Minutes)");
}
[Fact]
public void BuildEventContent_WithoutTransport_EmptyDescription()
{
var shift = new Shift { Name = "Spät" };
var result = CalendarEventBuilder.BuildEventContent(shift, null);
result.Summary.ShouldBe("Spät");
result.Description.ShouldBeEmpty();
}
[Fact]
public void FormatTransportInfo_FormatsCorrectly()
{
var transport = new TransportConnection
{
Platform = "Gleis 3",
DepartureTime = DateTimeOffset.Parse("2026-04-01T05:30:00+02:00"),
ArrivalTime = DateTimeOffset.Parse("2026-04-01T05:55:00+02:00"),
Duration = TimeSpan.FromMinutes(25)
};
var result = CalendarEventBuilder.FormatTransportInfo(transport);
result.ShouldBe("Gleis 3: 05:30 → 05:55 (25 Minutes)");
}
[Fact]
public void FormatTransportInfo_WithNullPlatform_UsesEmptyString()
{
var transport = new TransportConnection
{
Platform = null,
DepartureTime = DateTimeOffset.Parse("2026-04-01T05:30:00+02:00"),
ArrivalTime = DateTimeOffset.Parse("2026-04-01T05:55:00+02:00"),
Duration = TimeSpan.FromMinutes(25)
};
var result = CalendarEventBuilder.FormatTransportInfo(transport);
result.ShouldBe(": 05:30 → 05:55 (25 Minutes)");
}
}