Skip to content

Commit bccb271

Browse files
committed
feat: add migros products provider
1 parent ab46306 commit bccb271

File tree

3 files changed

+136
-0
lines changed

3 files changed

+136
-0
lines changed
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
using System.Collections.Immutable;
2+
using System.Net.Http.Headers;
3+
using System.Text.Json;
4+
using GrocyScanner.Core.Models;
5+
using Microsoft.Extensions.Logging;
6+
7+
namespace GrocyScanner.Core.Providers;
8+
9+
public class MigrosProductProvider : IProductProvider
10+
{
11+
private readonly IHttpClientFactory _httpClientFactory;
12+
private readonly ILogger<MigrosProductProvider> _logger;
13+
14+
private string? _authorizationHeader;
15+
16+
public MigrosProductProvider(IHttpClientFactory httpClientFactory, ILogger<MigrosProductProvider> logger)
17+
{
18+
_httpClientFactory = httpClientFactory;
19+
_logger = logger;
20+
}
21+
22+
public async Task<Product?> GetProductByGtin(string gtin)
23+
{
24+
IReadOnlyList<int> productIds = await GetProductIds(gtin);
25+
26+
if (productIds.Count != 1)
27+
{
28+
_logger.LogError("Unexpected count for gtin {Gtin}: {Count}", gtin, productIds.Count);
29+
return null;
30+
}
31+
32+
return await GetProductByIdsAsync(gtin, productIds);
33+
}
34+
35+
private async Task<Product?> GetProductByIdsAsync(string gtin, IEnumerable<int> productIds)
36+
{
37+
using HttpClient httpClient = _httpClientFactory.CreateClient();
38+
39+
HttpRequestMessage httpRequestMessage =
40+
new(HttpMethod.Post, "https://www.migros.ch/product-display/public/v4/product-cards");
41+
string requestJson =
42+
@$"{{""offerFilter"":{{""ongoingOfferDate"":""{DateTime.Now:yyyy-MM-dd}T00:00:00"",""region"":""national"",""storeType"":""OFFLINE""}},""productFilter"":{{""uids"":[{string.Join(",", productIds)}]}}}}";
43+
httpRequestMessage.Content = new StringContent(
44+
requestJson,
45+
MediaTypeHeaderValue.Parse("application/json"));
46+
httpRequestMessage.Headers.Add("leshopch", await GetAndCreateAuthorizationHeader());
47+
HttpResponseMessage httpResponseMessage = await httpClient.SendAsync(httpRequestMessage);
48+
string content = await httpResponseMessage.Content.ReadAsStringAsync();
49+
try
50+
{
51+
httpResponseMessage.EnsureSuccessStatusCode();
52+
using JsonDocument jsonDocument = JsonDocument.Parse(content);
53+
54+
if (jsonDocument.RootElement.GetArrayLength() != 1)
55+
{
56+
_logger.LogError("Migros returned more than one product for the product ids {ProductId}", productIds);
57+
return null;
58+
}
59+
60+
JsonElement.ArrayEnumerator arrayEnumerator = jsonDocument.RootElement.EnumerateArray();
61+
arrayEnumerator.MoveNext();
62+
63+
return new Product
64+
{
65+
Name = arrayEnumerator.Current.GetProperty("name").GetString()!,
66+
Gtin = gtin,
67+
Categories = ImmutableList<string>.Empty,
68+
ImageUrl = GetImageFromJsonDocument(arrayEnumerator.Current)
69+
};
70+
}
71+
catch (HttpRequestException httpRequestException)
72+
{
73+
_logger.LogError(httpRequestException,
74+
"Failed to request product details for {ProductIds} at Migros: {Content}", productIds, content);
75+
return null;
76+
}
77+
}
78+
79+
private async ValueTask<string> GetAndCreateAuthorizationHeader()
80+
{
81+
if (!string.IsNullOrEmpty(_authorizationHeader))
82+
{
83+
return _authorizationHeader;
84+
}
85+
86+
using HttpClient httpClient = _httpClientFactory.CreateClient();
87+
using HttpRequestMessage httpRequestMessage = new(HttpMethod.Get, "https://www.migros.ch/authentication/public/v1/api/guest?authorizationNotRequired=true");
88+
HttpResponseMessage httpResponseMessage = await httpClient.SendAsync(httpRequestMessage);
89+
httpResponseMessage.EnsureSuccessStatusCode();
90+
var values = httpResponseMessage.Headers.GetValues("leshopch");
91+
string authorizationToken = values.First();
92+
_authorizationHeader = authorizationToken;
93+
return authorizationToken;
94+
}
95+
96+
private static string? GetImageFromJsonDocument(JsonElement jsonElement)
97+
{
98+
JsonElement imagesProperty = jsonElement.GetProperty("images");
99+
return imagesProperty
100+
.EnumerateArray()
101+
.Select(element =>
102+
element.GetProperty("url").GetString())
103+
.FirstOrDefault(url => !string.IsNullOrEmpty(url));
104+
}
105+
106+
private async Task<IReadOnlyList<int>> GetProductIds(string gtin)
107+
{
108+
using HttpClient httpClient = _httpClientFactory.CreateClient();
109+
110+
HttpRequestMessage httpRequestMessage =
111+
new(HttpMethod.Post, "https://www.migros.ch/onesearch-oc-seaapi/public/v5/search");
112+
httpRequestMessage.Content = new StringContent(
113+
@$"{{""algorithm"":""DEFAULT"",""filters"":{{}},""language"":""en"",""productIds"":[],""query"":{gtin},""regionId"":""national"",""sortFields"":[],""sortOrder"":""asc""}}",
114+
MediaTypeHeaderValue.Parse("application/json"));
115+
httpRequestMessage.Headers.Add("leshopch", await GetAndCreateAuthorizationHeader());
116+
HttpResponseMessage httpResponseMessage = await httpClient.SendAsync(httpRequestMessage);
117+
string content = await httpResponseMessage.Content.ReadAsStringAsync();
118+
try
119+
{
120+
httpResponseMessage.EnsureSuccessStatusCode();
121+
using JsonDocument jsonDocument = JsonDocument.Parse(content);
122+
JsonElement productIdsElement = jsonDocument.RootElement.GetProperty("productIds");
123+
return productIdsElement.EnumerateArray().Select(jsonElement => jsonElement.GetInt32()).ToList();
124+
}
125+
catch (HttpRequestException httpRequestException)
126+
{
127+
_logger.LogError(httpRequestException, "Failed to request {Gtin} at Migros: {Content}", gtin, content);
128+
return ImmutableList<int>.Empty;
129+
}
130+
}
131+
132+
public string Name => "Migros";
133+
public string IconUri => "/migros-logo.png";
134+
public string Country => "Switzerland";
135+
}

GrocyScanner.Service/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
builder.Services.AddSingleton<IGtinValidator, GtinValidator>();
2222
builder.Services.AddSingleton<IProductProvider, OpenFoodFactsProductProvider>();
2323
builder.Services.AddSingleton<IProductProvider, CoopProductProvider>();
24+
builder.Services.AddSingleton<IProductProvider, MigrosProductProvider>();
2425
builder.Services.AddSingleton<IGrocyClient, GrocyClient>();
2526
builder.Services.AddSingleton<IGrocyQuantityUnit, GrocyQuantityUnitsMasterData>();
2627
builder.Services.AddSingleton<IGrocyLocations, GrocyLocationMasterData>();
2.29 KB
Loading

0 commit comments

Comments
 (0)