diff --git a/Cli/CustomTasks.cs b/Cli/CustomTasks.cs
index ba12582..8de19e9 100644
--- a/Cli/CustomTasks.cs
+++ b/Cli/CustomTasks.cs
@@ -159,6 +159,105 @@ namespace FsToolbox.Cli
#endregion
+ #region Public Method AnalyzeEarlySlotRegistrationsAsync
+
+ ///
+ /// Identifies store log entries where a user registered earlier than the automatic slot creation interval.
+ ///
+ /// The HTTP client used to perform the requests.
+ public static async Task AnalyzeEarlySlotRegistrationsAsync(HttpClient httpClient)
+ {
+ await AuthHelper.EnsureAuthenticationAsync(httpClient);
+
+ const int regionId = 139;
+ var stores = await RegionTasks.GetStoresInRegionAsync(httpClient, regionId);
+ var cooperatingStores = stores.Where(x => x.CooperationStatus == RegionTasks.CooperationStatus.Cooperating).ToList();
+
+ if (cooperatingStores.Count == 0)
+ {
+ Logger.Info("No cooperating stores found in region {RegionId}.", regionId);
+ return;
+ }
+
+ var fromDate = DateTime.Today.AddMonths(-6).ToString("yyyy-MM-dd");
+ var toDate = DateTime.Today.ToString("yyyy-MM-dd");
+ const string storeLogActionIds = "11";
+
+ var ignoredStoreIds = new HashSet { 51224, 61913 };
+ var storesToProcess = cooperatingStores
+ .Where(x => !ignoredStoreIds.Contains(x.Id))
+ .ToList();
+ Logger.Info("Debug mode: processing first {Count} cooperating stores.", storesToProcess.Count);
+
+ var analyzed = new List();
+
+ foreach (var store in storesToProcess)
+ {
+ var info = await StoreTasks.GetStoreInformationAsync(httpClient, store.Id);
+ if (info == null)
+ {
+ Logger.Info("Store information not available for {StoreId}.", store.Id);
+ continue;
+ }
+
+ var calendarInterval = info.CalendarInterval;
+ if (calendarInterval <= 0)
+ {
+ Logger.Info("Skipping {StoreName} because calendar interval is {Interval}.", store.Name, calendarInterval);
+ continue;
+ }
+ Logger.Info("Calendar interval for {StoreName} is {Interval} seconds.", store.Name, calendarInterval);
+
+ var logEntries = await StoreTasks.GetStoreLogEntriesAsync(httpClient, store.Id, fromDate, toDate, storeLogActionIds);
+ Thread.Sleep(1000);
+
+ foreach (var entry in logEntries)
+ {
+ if (entry.DateReference == null) continue;
+
+ var secondsDifference = (entry.DateReference.Value - entry.PerformedAt).TotalSeconds;
+ if (secondsDifference <= calendarInterval) continue;
+
+ analyzed.Add(new StoreLogIntervalEntry(store, entry, secondsDifference, calendarInterval, true));
+ }
+ }
+
+ var exceeding = analyzed.Count;
+
+ var sb = new StringBuilder();
+ sb.AppendLine("Early Slot Registration Report");
+ sb.AppendLine("Generated: " + DateTime.Now.ToString("dd.MM.yyyy HH:mm"));
+ sb.AppendLine($"Region: {regionId}");
+ sb.AppendLine($"Stores analyzed: {storesToProcess.Count}");
+ sb.AppendLine($"Timeframe: {fromDate} to {toDate}");
+ sb.AppendLine($"Total log entries: {analyzed.Count}");
+ sb.AppendLine($"Exceeding interval: {exceeding}");
+ sb.AppendLine("============================");
+ sb.AppendLine();
+
+ foreach (var entry in analyzed.OrderByDescending(x => x.SecondsDifference))
+ {
+ var foodsaver = entry.Entry.ActingFoodsaver;
+ var foodsaverName = foodsaver?.Name ?? "(unknown)";
+ var foodsaverId = foodsaver?.Id.ToString() ?? "n/a";
+ var reference = entry.Entry.DateReference?.ToString("dd.MM.yyyy - HH:mm") ?? "n/a";
+ var performed = entry.Entry.PerformedAt.ToString("dd.MM.yyyy - HH:mm");
+ var intervalWeeks = entry.CalendarIntervalSeconds / (60.0 * 60 * 24 * 7);
+ var earlySeconds = entry.SecondsDifference - entry.CalendarIntervalSeconds;
+ var earlySpan = TimeSpan.FromSeconds(earlySeconds);
+
+ sb.AppendLine($"- {foodsaverName} ({foodsaverId}) | Store: {entry.Store.Name} ({entry.Store.Id}) | Performed: {performed} | Slot Date: {reference} | Interval: {intervalWeeks:F2} weeks | Early: {earlySpan.Days}d {earlySpan.Hours}h {earlySpan.Minutes}m");
+ }
+
+ var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
+ var filename = $"EarlySlotRegistrations_{timestamp}.txt";
+ await File.WriteAllTextAsync(filename, sb.ToString());
+
+ Logger.Info("Report saved: {FileName}", filename);
+ }
+
+ #endregion
+
#region Public Method ConfirmUnconfirmedPickupsLindenbergAsync
///
diff --git a/Cli/Program.cs b/Cli/Program.cs
index c5f32cd..71f42f2 100644
--- a/Cli/Program.cs
+++ b/Cli/Program.cs
@@ -19,6 +19,7 @@ logger.Info("Choose a task to execute:");
logger.Info("1. Check Aldi Memberships");
logger.Info("2. Confirm all Unconfirmed Pickups for Lindenberg");
logger.Info("3. Analyze slot unregistrations for region Siegen");
+logger.Info("4. Analyze early slot registrations (calendar interval)");
logger.Info("Enter the number of the task to execute (or any other key to exit): ");
var choice = Console.ReadLine();
@@ -33,6 +34,9 @@ switch (choice)
case "3":
await CustomTasks.AnalyzeSlotUnregistrationsAsync(httpClient);
break;
+ case "4":
+ await CustomTasks.AnalyzeEarlySlotRegistrationsAsync(httpClient);
+ break;
default:
logger.Info("Exiting...");
break;
diff --git a/Cli/Tasks/StoreTasks.cs b/Cli/Tasks/StoreTasks.cs
index 17d6c5f..2c976cc 100644
--- a/Cli/Tasks/StoreTasks.cs
+++ b/Cli/Tasks/StoreTasks.cs
@@ -82,6 +82,47 @@ namespace FsToolbox.Cli.Tasks
#endregion
+ #region Public Method GetStoreInformationAsync
+
+ ///
+ /// Retrieves store information including the calendar interval.
+ ///
+ /// The HTTP client used to send the request.
+ /// The store identifier to query.
+ /// The store information, or null when the call fails.
+ public static async Task GetStoreInformationAsync(HttpClient httpClient, int storeId)
+ {
+ await AuthHelper.EnsureAuthenticationAsync(httpClient);
+
+ var uri = string.Format(Endpoints.StoreInformation, storeId);
+ var response = await httpClient.GetAsync(uri);
+ var responseBody = await response.Content.ReadAsStringAsync();
+
+ if (!response.IsSuccessStatusCode)
+ {
+ Logger.Error("Store information retrieval failed ({Status} {Reason}): {Body}", (int)response.StatusCode, response.ReasonPhrase, responseBody);
+ return null;
+ }
+
+ var opts = new JsonSerializerOptions
+ {
+ PropertyNameCaseInsensitive = true,
+ PropertyNamingPolicy = null
+ };
+
+ try
+ {
+ return JsonSerializer.Deserialize(responseBody, opts);
+ }
+ catch (JsonException ex)
+ {
+ Logger.Error(ex, "Failed to parse store information response.");
+ return null;
+ }
+ }
+
+ #endregion
+
#region Public Method GetStoreLogAsync
///