Add store log analysis feature and related data structures
- Implement AnalyzeSlotUnregistrationsAsync method to retrieve and report store log data for cooperating stores in region 139. - Introduce StoreLogProfileEntry record to aggregate log information. - Add StoreLog endpoint for API access. - Create GetStoreLogAsync and GetStoreLogEntriesAsync methods for fetching and deserializing store log entries. - Update Program.cs to include the new analysis task option.
This commit is contained in:
@@ -14,5 +14,17 @@ namespace FsToolbox.Cli
|
|||||||
private record AldiMember(RegionTasks.Store Store, StoreTasks.Member Member);
|
private record AldiMember(RegionTasks.Store Store, StoreTasks.Member Member);
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region Record StoreLogProfileEntry
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Aggregates store log information by profile and store.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Profile">The foodsaver profile that performed the action.</param>
|
||||||
|
/// <param name="Store">The store for which the log entry applies.</param>
|
||||||
|
/// <param name="DateReference">The log entry date reference.</param>
|
||||||
|
private record StoreLogProfileEntry(StoreTasks.FoodsaverProfile Profile, RegionTasks.Store Store, DateTime? DateReference, DateTime? PerformedAt);
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,6 +70,95 @@ namespace FsToolbox.Cli
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region Public Method AnalyzeSlotUnregistrationsAsync
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the store log for the first cooperating store in region 135 for the last month.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="httpClient">The HTTP client used to perform the requests.</param>
|
||||||
|
public static async Task AnalyzeSlotUnregistrationsAsync(HttpClient httpClient)
|
||||||
|
{
|
||||||
|
await AuthHelper.EnsureAuthenticationAsync(httpClient);
|
||||||
|
|
||||||
|
var stores = await RegionTasks.GetStoresInRegionAsync(httpClient, 139);
|
||||||
|
var cooperatingStores = stores.Where(x => x.CooperationStatus == RegionTasks.CooperationStatus.Cooperating).ToList();
|
||||||
|
|
||||||
|
if (cooperatingStores.Count == 0)
|
||||||
|
{
|
||||||
|
Logger.Info("No cooperating stores found in region 139.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fromDate = DateTime.Today.AddMonths(-1).ToString("yyyy-MM-dd");
|
||||||
|
var toDate = DateTime.Today.ToString("yyyy-MM-dd");
|
||||||
|
const string storeLogActionIds = "12";
|
||||||
|
|
||||||
|
var collected = new List<StoreLogProfileEntry>();
|
||||||
|
|
||||||
|
foreach (var store in cooperatingStores)
|
||||||
|
{
|
||||||
|
Logger.Info("Fetching store log for {StoreId} {StoreName} ({From} to {To})", store.Id, store.Name, fromDate, toDate);
|
||||||
|
|
||||||
|
var logEntries = await StoreTasks.GetStoreLogEntriesAsync(httpClient, store.Id, fromDate, toDate, storeLogActionIds);
|
||||||
|
|
||||||
|
foreach (var entry in logEntries)
|
||||||
|
{
|
||||||
|
if (entry.ActingFoodsaver == null) continue;
|
||||||
|
collected.Add(new StoreLogProfileEntry(entry.ActingFoodsaver, store, entry.DateReference, entry.PerformedAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.Info("Collected {Count} log entries for {StoreName}.", collected.Count, store.Name);
|
||||||
|
|
||||||
|
Thread.Sleep(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group entries by profile and sort by count
|
||||||
|
var grouped = collected
|
||||||
|
.GroupBy(x => x.Profile.Id)
|
||||||
|
.Select(g => new
|
||||||
|
{
|
||||||
|
Profile = g.First().Profile,
|
||||||
|
Count = g.Count()
|
||||||
|
})
|
||||||
|
.OrderByDescending(x => x.Count)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// Create txt file report
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine("Store Log Unregistration Report");
|
||||||
|
sb.AppendLine("Generated: " + DateTime.Now.ToString("dd.MM.yyyy HH:mm"));
|
||||||
|
sb.AppendLine($"Timeframe: {fromDate} to {toDate}");
|
||||||
|
sb.AppendLine($"Total analyzed stores: {cooperatingStores.Count} (cooperating)");
|
||||||
|
sb.AppendLine($"Total unregistrations: {collected.Count}");
|
||||||
|
sb.AppendLine("Region: Siegen (139)");
|
||||||
|
sb.AppendLine("============================");
|
||||||
|
sb.AppendLine();
|
||||||
|
|
||||||
|
foreach (var entry in grouped)
|
||||||
|
{
|
||||||
|
sb.AppendLine($"{entry.Profile.Name} ({entry.Profile.Id}) - {entry.Count} unregistrations");
|
||||||
|
sb.AppendLine("----------------------------");
|
||||||
|
|
||||||
|
// List the stores and dates
|
||||||
|
var userEntries = collected
|
||||||
|
.Where(x => x.Profile.Id == entry.Profile.Id)
|
||||||
|
.OrderBy(x => x.DateReference)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var userEntry in userEntries)
|
||||||
|
sb.AppendLine($" - {userEntry.Store.Name} on {userEntry.DateReference} (Performed at: {userEntry.PerformedAt})");
|
||||||
|
|
||||||
|
sb.AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write to file with timestamp
|
||||||
|
var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
|
||||||
|
var filename = $"SlotUnregistrations_{timestamp}.txt";
|
||||||
|
await File.WriteAllTextAsync(filename, sb.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region Public Method ConfirmUnconfirmedPickupsLindenbergAsync
|
#region Public Method ConfirmUnconfirmedPickupsLindenbergAsync
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ namespace FsToolbox.Cli
|
|||||||
public static string UserCurrent => $"{ApiBase}/api/user/current";
|
public static string UserCurrent => $"{ApiBase}/api/user/current";
|
||||||
|
|
||||||
public static string UserLogin => $"{ApiBase}/api/user/login";
|
public static string UserLogin => $"{ApiBase}/api/user/login";
|
||||||
|
public static string StoreLog => $"{ApiBase}/api/stores/{{0}}/log/{{1}}/{{2}}/{{3}}";
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ using var httpClient = new HttpClient();
|
|||||||
logger.Info("Choose a task to execute:");
|
logger.Info("Choose a task to execute:");
|
||||||
logger.Info("1. Check Aldi Memberships");
|
logger.Info("1. Check Aldi Memberships");
|
||||||
logger.Info("2. Confirm all Unconfirmed Pickups for Lindenberg");
|
logger.Info("2. Confirm all Unconfirmed Pickups for Lindenberg");
|
||||||
|
logger.Info("3. Analyze slot unregistrations for region Siegen");
|
||||||
logger.Info("Enter the number of the task to execute (or any other key to exit): ");
|
logger.Info("Enter the number of the task to execute (or any other key to exit): ");
|
||||||
var choice = Console.ReadLine();
|
var choice = Console.ReadLine();
|
||||||
|
|
||||||
@@ -29,6 +30,9 @@ switch (choice)
|
|||||||
case "2":
|
case "2":
|
||||||
await CustomTasks.ConfirmUnconfirmedPickupsLindenbergAsync(httpClient);
|
await CustomTasks.ConfirmUnconfirmedPickupsLindenbergAsync(httpClient);
|
||||||
break;
|
break;
|
||||||
|
case "3":
|
||||||
|
await CustomTasks.AnalyzeSlotUnregistrationsAsync(httpClient);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
logger.Info("Exiting...");
|
logger.Info("Exiting...");
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace FsToolbox.Cli.Tasks
|
namespace FsToolbox.Cli.Tasks
|
||||||
{
|
{
|
||||||
public static partial class StoreTasks
|
public static partial class StoreTasks
|
||||||
@@ -15,6 +17,35 @@ namespace FsToolbox.Cli.Tasks
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region Record FoodsaverProfile
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Minimal foodsaver profile information for store logs.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Id">The foodsaver identifier.</param>
|
||||||
|
/// <param name="Name">The foodsaver display name.</param>
|
||||||
|
/// <param name="Avatar">The foodsaver avatar URL (relative).</param>
|
||||||
|
/// <param name="IsSleeping">Indicates whether the foodsaver is sleeping.</param>
|
||||||
|
public record FoodsaverProfile(int Id, string Name, string? Avatar, bool IsSleeping);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Record StoreLogEntry
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a store log entry with foodsaver and action information.
|
||||||
|
/// </summary>
|
||||||
|
public record StoreLogEntry(
|
||||||
|
[property: JsonPropertyName("performed_at")] DateTime PerformedAt,
|
||||||
|
[property: JsonPropertyName("action_id")] int ActionId,
|
||||||
|
[property: JsonPropertyName("date_reference")] DateTime? DateReference,
|
||||||
|
[property: JsonPropertyName("content")] string? Content,
|
||||||
|
[property: JsonPropertyName("reason")] string? Reason,
|
||||||
|
[property: JsonPropertyName("acting_foodsaver")] FoodsaverProfile? ActingFoodsaver,
|
||||||
|
[property: JsonPropertyName("affected_foodsaver")] FoodsaverProfile? AffectedFoodsaver);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region Record Pickup
|
#region Record Pickup
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -82,6 +82,68 @@ namespace FsToolbox.Cli.Tasks
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region Public Method GetStoreLogAsync
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the store log for a specified store and date range.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="httpClient">The HTTP client used to send the request.</param>
|
||||||
|
/// <param name="storeId">The store identifier to query.</param>
|
||||||
|
/// <param name="fromDate">Start date (formatted for the API).</param>
|
||||||
|
/// <param name="toDate">End date (formatted for the API).</param>
|
||||||
|
/// <param name="storeLogActionIds">Comma-separated action IDs.</param>
|
||||||
|
/// <returns>The raw response body from the API.</returns>
|
||||||
|
public static async Task<string> GetStoreLogAsync(HttpClient httpClient, int storeId, string fromDate, string toDate, string storeLogActionIds)
|
||||||
|
{
|
||||||
|
await AuthHelper.EnsureAuthenticationAsync(httpClient);
|
||||||
|
|
||||||
|
var uri = string.Format(Endpoints.StoreLog, storeId, fromDate, toDate, storeLogActionIds);
|
||||||
|
var response = await httpClient.GetAsync(uri);
|
||||||
|
var responseBody = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
// handle unsuccessful response
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
Logger.Error("Store log retrieval failed ({Status} {Reason}): {Body}", (int)response.StatusCode, response.ReasonPhrase, responseBody);
|
||||||
|
|
||||||
|
return responseBody;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Public Method GetStoreLogEntriesAsync
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves and deserializes store log entries for a specified store and date range.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="httpClient">The HTTP client used to send the request.</param>
|
||||||
|
/// <param name="storeId">The store identifier to query.</param>
|
||||||
|
/// <param name="fromDate">Start date (formatted for the API).</param>
|
||||||
|
/// <param name="toDate">End date (formatted for the API).</param>
|
||||||
|
/// <param name="storeLogActionIds">Comma-separated action IDs.</param>
|
||||||
|
/// <returns>A list of store log entries, or an empty list when parsing fails.</returns>
|
||||||
|
public static async Task<List<StoreLogEntry>> GetStoreLogEntriesAsync(HttpClient httpClient, int storeId, string fromDate, string toDate, string storeLogActionIds)
|
||||||
|
{
|
||||||
|
var responseBody = await GetStoreLogAsync(httpClient, storeId, fromDate, toDate, storeLogActionIds);
|
||||||
|
|
||||||
|
var opts = new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true,
|
||||||
|
PropertyNamingPolicy = null
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return JsonSerializer.Deserialize<List<StoreLogEntry>>(responseBody, opts) ?? [];
|
||||||
|
}
|
||||||
|
catch (JsonException ex)
|
||||||
|
{
|
||||||
|
Logger.Error(ex, "Failed to parse store log response.");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region Public Method PatchPickupAsync
|
#region Public Method PatchPickupAsync
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
Reference in New Issue
Block a user