From d177a25c3d619feea900a04ba2f7459e8e917f55 Mon Sep 17 00:00:00 2001 From: "gitea@beging.de" Date: Tue, 3 Feb 2026 07:41:37 +0100 Subject: [PATCH] Add AnalyzeEarlySlotRegistrationsAsync method and update Program menu --- Cli/CustomTasks.cs | 99 +++++++++++++++++++++++++++++++++++++++++ Cli/Program.cs | 4 ++ Cli/Tasks/StoreTasks.cs | 41 +++++++++++++++++ 3 files changed, 144 insertions(+) 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 ///