Restructure project with namespace updates and renamed files for better organization. Migrate project name from FsTool to FsToolbox.Cli.

This commit is contained in:
Andre Beging
2025-12-11 19:04:18 +01:00
parent 9f82cf491c
commit 46229a6dc7
14 changed files with 319 additions and 219 deletions

View File

@@ -5,6 +5,8 @@
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<RootNamespace>FsToolbox.Cli</RootNamespace>
<AssemblyName>FsToolbox.Cli</AssemblyName>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@@ -1,12 +1,18 @@
namespace FsTool using FsToolbox.Cli.Tasks;
namespace FsToolbox.Cli
{ {
public partial class CustomTasks public partial class CustomTasks
{ {
#region Record AldiMember
/// <summary> /// <summary>
/// Aggregates store and member information for ALDI membership analysis. /// Aggregates store and member information for ALDI membership analysis.
/// </summary> /// </summary>
/// <param name="Store">The store participating in the membership relation.</param> /// <param name="Store">The store participating in the membership relation.</param>
/// <param name="Member">The member associated with the store.</param> /// <param name="Member">The member associated with the store.</param>
private record AldiMember(FsTool.Tasks.RegionTasks.Store Store, FsTool.Tasks.StoreTasks.Member Member); private record AldiMember(RegionTasks.Store Store, StoreTasks.Member Member);
#endregion
} }
} }

View File

@@ -1,12 +1,73 @@
using System.Text; using System.Text;
using FsTool.Helpers; using FsToolbox.Cli.Helpers;
using FsTool.Tasks; using FsToolbox.Cli.Tasks;
namespace FsTool namespace FsToolbox.Cli
{ {
public partial class CustomTasks public partial class CustomTasks
{ {
#region Public Method GetUnconfirmedPickupsLindenbergAsync #region Public Method CheckAldiMembershipsAsync
/// <summary>
/// Analyzes ALDI store memberships in the Siegen region and writes users with multiple memberships to a report file.
/// </summary>
/// <param name="httpClient">The HTTP client used to perform the requests.</param>
public static async Task CheckAldiMembershipsAsync(HttpClient httpClient)
{
await AuthHelper.EnsureAuthenticationAsync(httpClient);
// Implementation for checking Aldi memberships would go here.
var stores = await RegionTasks.GetStoresInRegionAsync(httpClient, 139);
var activeAldis = stores.Where(x => x.CooperationStatus == RegionTasks.CooperationStatus.Cooperating && x.Name.Contains("ALDI", StringComparison.CurrentCultureIgnoreCase)).ToList();
Console.WriteLine("Found " + activeAldis.Count + " active ALDI stores in region Siegen.");
var aldiMembers = new List<AldiMember>();
// Collect members from each active ALDI store
foreach (var activeAldi in activeAldis)
{
Thread.Sleep(200);
Console.WriteLine("Checking members for store: " + activeAldi.Name);
var members = await StoreTasks.GetStoreMembersAsync(httpClient, activeAldi.Id);
var activeMembers = members.Where(x => x is { Verified: StoreTasks.VerifiedStatus.Verified, Team_Active: StoreTasks.TeamActiveStatus.Active }).ToList();
activeMembers.ForEach(x => aldiMembers.Add(new(activeAldi, x)));
}
// Group memberships
var grouped = aldiMembers.GroupBy(x => x.Member.Id);
// Find groups with more than two memberships
var multipleMemberships = grouped.Where(g => g.Count() > 2).OrderByDescending(g => g.Count()).ToList();
Console.WriteLine($"Users with more than two ALDI store memberships: {multipleMemberships.Count}");
Console.WriteLine($"Saving data to file.");
var sb = new StringBuilder();
sb.AppendLine("ALDI Store Memberships Report");
sb.AppendLine("Generated: " + DateTime.Now.ToString("dd.MM.yyyy HH:mm"));
sb.AppendLine("============================");
sb.AppendLine();
foreach (var group in multipleMemberships)
{
sb.AppendLine($"{group.First().Member.Name} ({group.Key}) - {group.Count()} ALDIs");
foreach (var membership in group) sb.AppendLine($" - {membership.Store.Name}");
sb.AppendLine();
}
// Write to file with timestamp
var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
var filename = $"AldiMemberships_{timestamp}.txt";
await File.WriteAllTextAsync(filename, sb.ToString());
}
#endregion
#region Public Method ConfirmUnconfirmedPickupsLindenbergAsync
/// <summary> /// <summary>
/// Confirms all unconfirmed pickup slots for the configured Lindenberg store after user confirmation. /// Confirms all unconfirmed pickup slots for the configured Lindenberg store after user confirmation.
@@ -57,66 +118,5 @@ namespace FsTool
} }
#endregion #endregion
/// <summary>
/// Analyzes ALDI store memberships in the Siegen region and writes users with multiple memberships to a report file.
/// </summary>
/// <param name="httpClient">The HTTP client used to perform the requests.</param>
public static async Task CheckAldiMembershipsAsync(HttpClient httpClient)
{
await AuthHelper.EnsureAuthenticationAsync(httpClient);
// Implementation for checking Aldi memberships would go here.
var stores = await RegionTasks.GetStoresInRegionAsync(httpClient, 139);
var activeAldis = stores.Where(x => x.CooperationStatus == RegionTasks.CooperationStatus.Cooperating && x.Name.Contains("ALDI", StringComparison.CurrentCultureIgnoreCase)).ToList();
Console.WriteLine("Found " + activeAldis.Count + " active ALDI stores in region Siegen.");
var aldiMembers = new List<AldiMember>();
// Collect members from each active ALDI store
foreach (var activeAldi in activeAldis)
{
Thread.Sleep(200);
Console.WriteLine("Checking members for store: " + activeAldi.Name);
var members = await StoreTasks.GetStoreMembersAsync(httpClient, activeAldi.Id);
var activeMembers = members.Where(x => x is { Verified: StoreTasks.VerifiedStatus.Verified, Team_Active: StoreTasks.TeamActiveStatus.Active }).ToList();
activeMembers.ForEach(x => aldiMembers.Add(new(activeAldi, x)));
}
// Group memberships
var grouped = aldiMembers.GroupBy(x => x.Member.Id);
// Find groups with more than two memberships
var multipleMemberships = grouped.Where(g => g.Count() > 2).OrderByDescending(g => g.Count()).ToList();
Console.WriteLine($"Users with more than two ALDI store memberships: {multipleMemberships.Count}");
Console.WriteLine($"Saving data to file.");
var sb = new StringBuilder();
sb.AppendLine("ALDI Store Memberships Report");
sb.AppendLine("Generated: " + DateTime.Now.ToString("dd.MM.yyyy HH:mm"));
sb.AppendLine("============================");
sb.AppendLine();
foreach (var group in multipleMemberships)
{
sb.AppendLine($"{group.First().Member.Name} ({group.Key}) - {group.Count()} ALDIs");
foreach (var membership in group)
{
sb.AppendLine($" - {membership.Store.Name}");
}
sb.AppendLine();
}
// Write to file with timestamp
var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
var filename = $"AldiMemberships_{timestamp}.txt";
await File.WriteAllTextAsync(filename, sb.ToString());
}
} }
} }

View File

@@ -1,21 +1,29 @@
using FsTool.Helpers; using FsToolbox.Cli.Helpers;
namespace FsTool namespace FsToolbox.Cli
{ {
public static class Endpoints public static class Endpoints
{ {
private static string ApiBase => SettingsProvider.Current.Api.BaseUrl.TrimEnd('/'); #region Public Properties
public static string UserLogin => $"{ApiBase}/api/user/login"; public static string RegionStores => $"{ApiBase}/api/region/{{0}}/stores";
public static string UserCurrent => $"{ApiBase}/api/user/current";
public static string StorePickups => $"{ApiBase}/api/stores/{{0}}/pickups";
public static string StoreMembers => $"{ApiBase}/api/stores/{{0}}/member"; public static string StoreMembers => $"{ApiBase}/api/stores/{{0}}/member";
public static string StorePickups => $"{ApiBase}/api/stores/{{0}}/pickups";
public static string StorePickupsSlot => $"{ApiBase}/api/stores/{{0}}/pickups/{{1}}/{{2}}"; public static string StorePickupsSlot => $"{ApiBase}/api/stores/{{0}}/pickups/{{1}}/{{2}}";
public static string RegionStores => $"{ApiBase}/api/region/{{0}}/stores"; public static string UserCurrent => $"{ApiBase}/api/user/current";
public static string UserLogin => $"{ApiBase}/api/user/login";
#endregion
#region Private Properties
private static string ApiBase => SettingsProvider.Current.Api.BaseUrl.TrimEnd('/');
#endregion
} }
} }

View File

@@ -2,7 +2,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17 # Visual Studio Version 17
VisualStudioVersion = 17.5.2.0 VisualStudioVersion = 17.5.2.0
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FsTool", "FsTool.csproj", "{D8D12A22-90E0-56DB-D8E0-629AC330F42B}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cli", "Cli.csproj", "{D8D12A22-90E0-56DB-D8E0-629AC330F42B}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution

View File

@@ -1,56 +1,20 @@
using System;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Nodes; using System.Text.Json.Nodes;
using FsTool.Tasks; using FsToolbox.Cli.Tasks;
namespace FsTool.Helpers; namespace FsToolbox.Cli.Helpers
public static class AuthHelper
{ {
private static async Task<string?> LoadCsrfTokenAsync() /// <summary>
/// Provides helper methods to manage authentication and cached CSRF tokens.
/// </summary>
public static class AuthHelper
{ {
if (!File.Exists("csrf_token.json")) return null; #region Public Method EnsureAuthenticationAsync
var json = await File.ReadAllTextAsync("csrf_token.json");
var tokenInfo = JsonSerializer.Deserialize<JsonObject>(json);
if (tokenInfo == null) return null;
var token = tokenInfo["token"]?.GetValue<string>();
var expiresAtString = tokenInfo["expiresAt"]?.GetValue<string>();
if (string.IsNullOrWhiteSpace(token) || string.IsNullOrWhiteSpace(expiresAtString)) return null;
if (DateTime.TryParse(expiresAtString, out var expiresAt))
{
if (DateTime.UtcNow < expiresAt.ToUniversalTime())
return token;
}
return null;
}
internal static async Task StoreCsrfTokenAsync(string? csrfToken, DateTime expiration)
{
if (string.IsNullOrWhiteSpace(csrfToken)) return;
var tokenInfo = new
{
token = csrfToken,
expiresAt = expiration
};
var json = JsonSerializer.Serialize(tokenInfo, new JsonSerializerOptions
{
WriteIndented = true
});
await File.WriteAllTextAsync("csrf_token.json", json);
}
/// <summary>
/// Ensures the HTTP client carries a valid CSRF token, logging in when needed.
/// </summary>
/// <param name="httpClient">The HTTP client whose headers should be updated.</param>
public static async Task EnsureAuthenticationAsync(HttpClient httpClient) public static async Task EnsureAuthenticationAsync(HttpClient httpClient)
{ {
// Check if header already contains a CSRF token // Check if header already contains a CSRF token
@@ -78,4 +42,59 @@ public static class AuthHelper
httpClient.DefaultRequestHeaders.Add("X-CSRF-Token", csrfToken); httpClient.DefaultRequestHeaders.Add("X-CSRF-Token", csrfToken);
} }
} }
#endregion
#region Public Method StoreCsrfTokenAsync
/// <summary>
/// Persists the CSRF token to the local cache file until it expires.
/// </summary>
/// <param name="csrfToken">The token to store; ignored when null or whitespace.</param>
/// <param name="expiration">The UTC expiration timestamp received from the server.</param>
public static async Task StoreCsrfTokenAsync(string? csrfToken, DateTime expiration)
{
if (string.IsNullOrWhiteSpace(csrfToken)) return;
var tokenInfo = new
{
token = csrfToken,
expiresAt = expiration
};
var json = JsonSerializer.Serialize(tokenInfo, new JsonSerializerOptions
{
WriteIndented = true
});
await File.WriteAllTextAsync("csrf_token.json", json);
}
#endregion
#region Private Method LoadCsrfTokenAsync
private static async Task<string?> LoadCsrfTokenAsync()
{
if (!File.Exists("csrf_token.json")) return null;
var json = await File.ReadAllTextAsync("csrf_token.json");
var tokenInfo = JsonSerializer.Deserialize<JsonObject>(json);
if (tokenInfo == null) return null;
var token = tokenInfo["token"]?.GetValue<string>();
var expiresAtString = tokenInfo["expiresAt"]?.GetValue<string>();
if (string.IsNullOrWhiteSpace(token) || string.IsNullOrWhiteSpace(expiresAtString)) return null;
if (DateTime.TryParse(expiresAtString, out var expiresAt))
if (DateTime.UtcNow < expiresAt.ToUniversalTime())
return token;
return null;
}
#endregion
}
} }

View File

@@ -1,7 +1,7 @@
using System.Text; using System.Text;
using System.Text.Json.Nodes; using System.Text.Json.Nodes;
namespace FsTool.Helpers namespace FsToolbox.Cli.Helpers
{ {
public static class Extensions public static class Extensions
{ {

View File

@@ -1,12 +1,14 @@
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
namespace FsTool.Helpers namespace FsToolbox.Cli.Helpers
{ {
/// <summary> /// <summary>
/// Represents the root configuration settings for the application, including API and credentials. /// Represents the root configuration settings for the application, including API and credentials.
/// </summary> /// </summary>
public class AppSettings public class AppSettings
{ {
#region Public Properties
/// <summary> /// <summary>
/// Gets the API-related settings. /// Gets the API-related settings.
/// </summary> /// </summary>
@@ -16,6 +18,8 @@ namespace FsTool.Helpers
/// Gets the credentials settings for authentication. /// Gets the credentials settings for authentication.
/// </summary> /// </summary>
public CredentialsSettings Credentials { get; init; } = new(); public CredentialsSettings Credentials { get; init; } = new();
#endregion
} }
/// <summary> /// <summary>
@@ -23,10 +27,14 @@ namespace FsTool.Helpers
/// </summary> /// </summary>
public class ApiSettings public class ApiSettings
{ {
#region Public Properties
/// <summary> /// <summary>
/// Gets the base URL for the API, without the /api path segment. /// Gets the base URL for the API, without the /api path segment.
/// </summary> /// </summary>
public string BaseUrl { get; init; } = "https://beta.foodsharing.de"; public string BaseUrl { get; init; } = "https://beta.foodsharing.de";
#endregion
} }
/// <summary> /// <summary>
@@ -34,6 +42,8 @@ namespace FsTool.Helpers
/// </summary> /// </summary>
public class CredentialsSettings public class CredentialsSettings
{ {
#region Public Properties
/// <summary> /// <summary>
/// Gets the email address used for login. /// Gets the email address used for login.
/// </summary> /// </summary>
@@ -48,6 +58,8 @@ namespace FsTool.Helpers
/// Gets a value indicating whether two-factor authentication is enabled. /// Gets a value indicating whether two-factor authentication is enabled.
/// </summary> /// </summary>
public bool TwoFactorEnabled { get; init; } public bool TwoFactorEnabled { get; init; }
#endregion
} }
/// <summary> /// <summary>
@@ -55,23 +67,35 @@ namespace FsTool.Helpers
/// </summary> /// </summary>
public static class SettingsProvider public static class SettingsProvider
{ {
#region Constants
private static AppSettings? _current; private static AppSettings? _current;
#endregion
#region Public Properties
/// <summary> /// <summary>
/// Gets the current application settings instance. /// Gets the current application settings instance.
/// </summary> /// </summary>
public static AppSettings Current => _current ??= Load(); public static AppSettings Current => _current ??= Load();
#endregion
#region Private Method Load
private static AppSettings Load() private static AppSettings Load()
{ {
var configuration = new ConfigurationBuilder() var configuration = new ConfigurationBuilder()
.SetBasePath(AppContext.BaseDirectory) .SetBasePath(AppContext.BaseDirectory)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) .AddJsonFile("appsettings.json", false, true)
.Build(); .Build();
var settings = new AppSettings(); var settings = new AppSettings();
configuration.Bind(settings); configuration.Bind(settings);
return settings; return settings;
} }
#endregion
} }
} }

View File

@@ -3,7 +3,7 @@ using System.Net.Http.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using FsTool; using FsToolbox.Cli;
// See https://aka.ms/new-console-template for more information // See https://aka.ms/new-console-template for more information

View File

@@ -1,7 +1,9 @@
namespace FsTool.Tasks namespace FsToolbox.Cli.Tasks
{ {
public partial class RegionTasks public partial class RegionTasks
{ {
#region Record Store
/// <summary> /// <summary>
/// Basic region store information including cooperation status. /// Basic region store information including cooperation status.
/// </summary> /// </summary>
@@ -10,6 +12,10 @@ namespace FsTool.Tasks
/// <param name="CooperationStatus">The cooperation status of the store.</param> /// <param name="CooperationStatus">The cooperation status of the store.</param>
public record Store(int Id, string Name, CooperationStatus CooperationStatus); public record Store(int Id, string Name, CooperationStatus CooperationStatus);
#endregion
#region Enum CooperationStatus
/// <summary> /// <summary>
/// Describes the cooperation state between the store and the organization. /// Describes the cooperation state between the store and the organization.
/// </summary> /// </summary>
@@ -21,7 +27,9 @@ namespace FsTool.Tasks
DoNotWant = 4, DoNotWant = 4,
Cooperating = 5, Cooperating = 5,
DonatingToTafel = 6, DonatingToTafel = 6,
NoExisting = 7, NoExisting = 7
} }
#endregion
} }
} }

View File

@@ -1,12 +1,13 @@
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Nodes; using System.Text.Json.Nodes;
using FsTool; using FsToolbox.Cli.Helpers;
using FsTool.Helpers;
namespace FsTool.Tasks namespace FsToolbox.Cli.Tasks
{ {
public partial class RegionTasks public partial class RegionTasks
{ {
#region Public Method GetStoresInRegionAsync
/// <summary> /// <summary>
/// Retrieves all stores within the specified region, ensuring the request is authenticated. /// Retrieves all stores within the specified region, ensuring the request is authenticated.
/// </summary> /// </summary>
@@ -43,5 +44,7 @@ namespace FsTool.Tasks
return root["stores"].Deserialize<List<Store>>(opts) ?? []; return root["stores"].Deserialize<List<Store>>(opts) ?? [];
} }
#endregion
} }
} }

View File

@@ -1,27 +1,8 @@
namespace FsTool.Tasks namespace FsToolbox.Cli.Tasks
{ {
public static partial class StoreTasks public static partial class StoreTasks
{ {
/// <summary> #region Record Member
/// Represents a pickup date and the occupied slots for that date.
/// </summary>
/// <param name="Date">The pickup date string.</param>
/// <param name="OccupiedSlots">The slots already assigned for the date.</param>
public record Pickup(string Date, List<Slot> OccupiedSlots);
/// <summary>
/// Describes a booked slot and the profile occupying it.
/// </summary>
/// <param name="IsConfirmed">Indicates whether the slot is confirmed.</param>
/// <param name="Profile">The profile assigned to the slot.</param>
public record Slot(bool IsConfirmed, Profile Profile);
/// <summary>
/// Minimal profile information for a store member.
/// </summary>
/// <param name="Id">The Foodsharing profile identifier.</param>
/// <param name="Name">The profile display name.</param>
public record Profile(int Id, string Name);
/// <summary> /// <summary>
/// Detailed store member information including verification and team status flags. /// Detailed store member information including verification and team status flags.
@@ -32,22 +13,65 @@ namespace FsTool.Tasks
/// <param name="Verified">The verification state of the member.</param> /// <param name="Verified">The verification state of the member.</param>
public record Member(int Id, string Name, TeamActiveStatus Team_Active, VerifiedStatus Verified); public record Member(int Id, string Name, TeamActiveStatus Team_Active, VerifiedStatus Verified);
#endregion
#region Record Pickup
/// <summary>
/// Represents a pickup date and the occupied slots for that date.
/// </summary>
/// <param name="Date">The pickup date string.</param>
/// <param name="OccupiedSlots">The slots already assigned for the date.</param>
public record Pickup(string Date, List<Slot> OccupiedSlots);
#endregion
#region Record Profile
/// <summary>
/// Minimal profile information for a store member.
/// </summary>
/// <param name="Id">The Foodsharing profile identifier.</param>
/// <param name="Name">The profile display name.</param>
public record Profile(int Id, string Name);
#endregion
#region Record Slot
/// <summary>
/// Describes a booked slot and the profile occupying it.
/// </summary>
/// <param name="IsConfirmed">Indicates whether the slot is confirmed.</param>
/// <param name="Profile">The profile assigned to the slot.</param>
public record Slot(bool IsConfirmed, Profile Profile);
#endregion
#region Enum TeamActiveStatus
/// <summary> /// <summary>
/// Indicates whether a team member is active or only available as a jumper. /// Indicates whether a team member is active or only available as a jumper.
/// </summary> /// </summary>
public enum TeamActiveStatus public enum TeamActiveStatus
{ {
Jumper = 2, Jumper = 2,
Active = 1, Active = 1
} }
#endregion
#region Enum VerifiedStatus
/// <summary> /// <summary>
/// Specifies whether a member has completed verification. /// Specifies whether a member has completed verification.
/// </summary> /// </summary>
public enum VerifiedStatus public enum VerifiedStatus
{ {
Unverified = 0, Unverified = 0,
Verified = 1, Verified = 1
} }
#endregion
} }
} }

View File

@@ -1,13 +1,48 @@
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Nodes; using System.Text.Json.Nodes;
using FsTool; using FsToolbox.Cli.Helpers;
using FsTool.Helpers;
namespace FsTool.Tasks namespace FsToolbox.Cli.Tasks
{ {
public static partial class StoreTasks public static partial class StoreTasks
{ {
#region Public Method GetPickupsAsync
/// <summary>
/// Retrieves all pickups for the specified store with authentication handled automatically.
/// </summary>
/// <param name="httpClient">The HTTP client used to send the request.</param>
/// <param name="storeId">The store identifier to query.</param>
/// <returns>A list of pickups, or an empty list when no data is available.</returns>
public static async Task<List<Pickup>> GetPickupsAsync(HttpClient httpClient, int storeId)
{
await AuthHelper.EnsureAuthenticationAsync(httpClient);
var uri = string.Format(Endpoints.StorePickups, storeId);
var response = await httpClient.GetAsync(uri);
var responseBody = await response.Content.ReadAsStringAsync();
// handle unsuccessful response
if (!response.IsSuccessStatusCode) await Console.Error.WriteLineAsync($"Pickup retrieval failed ({(int)response.StatusCode} {response.ReasonPhrase}): {responseBody}");
var root = JsonNode.Parse(responseBody);
if (root == null) return [];
// Deserialize JsonNode to List<Pickup>
var opts = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = null // <── WICHTIG
};
return root["pickups"].Deserialize<List<Pickup>>(opts) ?? [];
}
#endregion
#region Public Method GetStoreMembersAsync
/// <summary> /// <summary>
/// Retrieves the members of the specified store, requiring authentication before the request. /// Retrieves the members of the specified store, requiring authentication before the request.
/// </summary> /// </summary>
@@ -40,41 +75,11 @@ namespace FsTool.Tasks
}; };
return root.Deserialize<List<Member>>(opts) ?? []; return root.Deserialize<List<Member>>(opts) ?? [];
} }
/// <summary> #endregion
/// Retrieves all pickups for the specified store with authentication handled automatically.
/// </summary>
/// <param name="httpClient">The HTTP client used to send the request.</param>
/// <param name="storeId">The store identifier to query.</param>
/// <returns>A list of pickups, or an empty list when no data is available.</returns>
public static async Task<List<Pickup>> GetPickupsAsync(HttpClient httpClient, int storeId)
{
await AuthHelper.EnsureAuthenticationAsync(httpClient);
var uri = string.Format(Endpoints.StorePickups, storeId); #region Public Method PatchPickupAsync
var response = await httpClient.GetAsync(uri);
var responseBody = await response.Content.ReadAsStringAsync();
// handle unsuccessful response
if (!response.IsSuccessStatusCode)
{
await Console.Error.WriteLineAsync($"Pickup retrieval failed ({(int)response.StatusCode} {response.ReasonPhrase}): {responseBody}");
}
var root = JsonNode.Parse(responseBody);
if (root == null) return [];
// Deserialize JsonNode to List<Pickup>
var opts = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = null // <── WICHTIG
};
return root["pickups"].Deserialize<List<Pickup>>(opts) ?? [];
}
/// <summary> /// <summary>
/// Marks a pickup slot as confirmed for the specified store, date, and user. /// Marks a pickup slot as confirmed for the specified store, date, and user.
@@ -102,5 +107,7 @@ namespace FsTool.Tasks
else else
Console.WriteLine($"Pickup patch succeeded {fsId} on {pickupDate}"); Console.WriteLine($"Pickup patch succeeded {fsId} on {pickupDate}");
} }
#endregion
} }
} }

View File

@@ -1,10 +1,9 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Net.Http.Json; using System.Net.Http.Json;
using FsTool; using FsToolbox.Cli.Helpers;
using FsTool.Helpers;
namespace FsTool.Tasks namespace FsToolbox.Cli.Tasks
{ {
public static class UserTasks public static class UserTasks
{ {
@@ -77,7 +76,7 @@ namespace FsTool.Tasks
var expiryEntry = cookies.FirstOrDefault(c => c.Trim().StartsWith("expires=")); var expiryEntry = cookies.FirstOrDefault(c => c.Trim().StartsWith("expires="));
var expireString = expiryEntry?.Split('=')[1]; var expireString = expiryEntry?.Split('=')[1];
if (DateTime.TryParse(expireString, out var expiration)) if (DateTime.TryParse(expireString, out var expiration))
await AuthHelper.StoreCsrfTokenAsync(csrfToken, expiration); await FsToolbox.Cli.Helpers.AuthHelper.StoreCsrfTokenAsync(csrfToken, expiration);
} }
return csrfToken; return csrfToken;
@@ -90,7 +89,7 @@ namespace FsTool.Tasks
/// <returns>A task that represents the asynchronous operation.</returns> /// <returns>A task that represents the asynchronous operation.</returns>
public static async Task GetCurrentUserAsync(HttpClient httpClient) public static async Task GetCurrentUserAsync(HttpClient httpClient)
{ {
await AuthHelper.EnsureAuthenticationAsync(httpClient); await FsToolbox.Cli.Helpers.AuthHelper.EnsureAuthenticationAsync(httpClient);
var response = await httpClient.GetAsync(Endpoints.UserCurrent); var response = await httpClient.GetAsync(Endpoints.UserCurrent);