Add AnalyzeEarlySlotRegistrationsAsync method and update Program menu

This commit is contained in:
2026-02-03 07:41:37 +01:00
parent 0b1e735a9f
commit d177a25c3d
3 changed files with 144 additions and 0 deletions

View File

@@ -159,6 +159,105 @@ namespace FsToolbox.Cli
#endregion
#region Public Method AnalyzeEarlySlotRegistrationsAsync
/// <summary>
/// Identifies store log entries where a user registered earlier than the automatic slot creation interval.
/// </summary>
/// <param name="httpClient">The HTTP client used to perform the requests.</param>
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<int> { 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<StoreLogIntervalEntry>();
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
/// <summary>

View File

@@ -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;

View File

@@ -82,6 +82,47 @@ namespace FsToolbox.Cli.Tasks
#endregion
#region Public Method GetStoreInformationAsync
/// <summary>
/// Retrieves store information including the calendar interval.
/// </summary>
/// <param name="httpClient">The HTTP client used to send the request.</param>
/// <param name="storeId">The store identifier to query.</param>
/// <returns>The store information, or null when the call fails.</returns>
public static async Task<StoreInformation?> 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<StoreInformation>(responseBody, opts);
}
catch (JsonException ex)
{
Logger.Error(ex, "Failed to parse store information response.");
return null;
}
}
#endregion
#region Public Method GetStoreLogAsync
/// <summary>