diff --git a/Services/CalendarEventBuilder.cs b/Services/CalendarEventBuilder.cs new file mode 100644 index 0000000..692c44f --- /dev/null +++ b/Services/CalendarEventBuilder.cs @@ -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 ExecuteWithRetryAsync(Func> 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(); + } +} diff --git a/ShiftScheduler.Services.Tests/CalendarEventBuilderTests.cs b/ShiftScheduler.Services.Tests/CalendarEventBuilderTests.cs new file mode 100644 index 0000000..62852c5 --- /dev/null +++ b/ShiftScheduler.Services.Tests/CalendarEventBuilderTests.cs @@ -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)"); + } +}