Skip to content

Commit 5466093

Browse files
committed
get huettenholiday availability
1 parent 1fd4145 commit 5466093

File tree

4 files changed

+304
-6
lines changed

4 files changed

+304
-6
lines changed

FetchDataFunctions/Functions/HuettenHoliday/HuettenHolidayGetHutFromProvider.cs renamed to FetchDataFunctions/Functions/HuettenHoliday/HuettenHolidayGetHutsFromProvider.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,14 @@
1414

1515
namespace FetchDataFunctions.Functions.HuettenHoliday;
1616

17-
public class HuettenHolidayGetHutFromProvider
17+
public class HuettenHolidayGetHutsFromProvider
1818
{
19-
private readonly ILogger<HuettenHolidayGetHutFromProvider> _logger;
19+
private readonly ILogger<HuettenHolidayGetHutsFromProvider> _logger;
2020
private readonly IHttpClientFactory _clientFactory;
2121

2222
private const int HutIdOffset = 10000; // Offset to avoid conflicts with other hut IDs
2323

24-
public HuettenHolidayGetHutFromProvider(ILogger<HuettenHolidayGetHutFromProvider> logger, IHttpClientFactory clientFactory)
24+
public HuettenHolidayGetHutsFromProvider(ILogger<HuettenHolidayGetHutsFromProvider> logger, IHttpClientFactory clientFactory)
2525
{
2626
_logger = logger;
2727
_clientFactory = clientFactory;
@@ -35,7 +35,7 @@ public async Task<IActionResult> HuettenHolidayUpdateHutHttpTriggered([HttpTrigg
3535
var hutIdsList = hutId.Split(',').Select(i => int.Parse(i) + HutIdOffset).ToList();
3636

3737
// Get all huts from the provider, then filter by hutId
38-
var allHuts = await HuettenHolidayGetHutsFromProvider(null);
38+
var allHuts = await HuettenHolidayUpdateHutsActivityTrigger(null);
3939
var huts = allHuts?.Where(h => hutIdsList.Contains(h.Id)).ToList();
4040
if (huts == null)
4141
{
@@ -45,8 +45,8 @@ public async Task<IActionResult> HuettenHolidayUpdateHutHttpTriggered([HttpTrigg
4545
return new OkObjectResult(huts);
4646
}
4747

48-
[Function(nameof(HuettenHolidayGetHutFromProvider))]
49-
public async Task<IEnumerable<Hut>?> HuettenHolidayGetHutsFromProvider([ActivityTrigger] string? input)
48+
[Function(nameof(HuettenHolidayUpdateHutsActivityTrigger))]
49+
public async Task<IEnumerable<Hut>?> HuettenHolidayUpdateHutsActivityTrigger([ActivityTrigger] string? input)
5050
{
5151
try
5252
{
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Data;
4+
using System.Linq;
5+
using System.Net.Http;
6+
using System.Net.Http.Json;
7+
using System.Threading;
8+
using System.Threading.Tasks;
9+
using FetchDataFunctions.Models;
10+
using FetchDataFunctions.Models.HuettenHoliday;
11+
using Microsoft.AspNetCore.Http;
12+
using Microsoft.AspNetCore.Mvc;
13+
using Microsoft.Azure.Functions.Worker;
14+
using Microsoft.Azure.Functions.Worker.Extensions.Sql;
15+
using Microsoft.DurableTask;
16+
using Microsoft.DurableTask.Client;
17+
using Microsoft.EntityFrameworkCore;
18+
using Microsoft.Extensions.Logging;
19+
20+
namespace FetchDataFunctions.Functions.HuettenHoliday;
21+
22+
public class HuettenHolidayUpdateAvailabilityFromProvider
23+
{
24+
private readonly ILogger<HuettenHolidayUpdateAvailabilityFromProvider> _logger;
25+
private readonly IHttpClientFactory _clientFactory;
26+
27+
private const int HutIdOffset = 10000; // Offset to avoid conflicts with other hut IDs
28+
29+
public HuettenHolidayUpdateAvailabilityFromProvider(ILogger<HuettenHolidayUpdateAvailabilityFromProvider> logger, IHttpClientFactory clientFactory)
30+
{
31+
_logger = logger;
32+
_clientFactory = clientFactory;
33+
}
34+
35+
[Function(nameof(HuettenHolidayUpdateAvailabilityHttpTriggered))]
36+
public async Task<IActionResult> HuettenHolidayUpdateAvailabilityHttpTriggered([HttpTrigger(AuthorizationLevel.Function, "get")] HttpRequest req, string hutId)
37+
{
38+
_logger.LogInformation("HuettenHolidayUpdateAvailabilityHttpTriggered called with hutIds: {HutId}", hutId);
39+
40+
var hutIdsList = hutId.Split(',').Select(i => int.Parse(i) + HutIdOffset).ToList();
41+
42+
var availabilities = new List<Availability>();
43+
foreach (var hutIdInt in hutIdsList)
44+
{
45+
var availability = await HuettenHolidayUpdateAvailabilityActivityTrigger(hutIdInt);
46+
if (availability != null)
47+
{
48+
availabilities.AddRange(availability);
49+
}
50+
}
51+
52+
// Get all huts from the provider, then filter by hutId
53+
if (availabilities.Count == 0)
54+
{
55+
return new NotFoundObjectResult("No availability found.");
56+
}
57+
58+
return new OkObjectResult(availabilities);
59+
}
60+
61+
[Function(nameof(HuettenHolidayUpdateAvailabilityTimerTriggered))]
62+
public async Task HuettenHolidayUpdateAvailabilityTimerTriggered(
63+
[TimerTrigger("0 0 13,22 * * *")] TimerInfo myTimer,
64+
[SqlInput("SELECT Id FROM [dbo].[Huts] WHERE Enabled = 1 and Source = 'HuettenHoliday'",
65+
"DatabaseConnectionString",
66+
CommandType.Text, "")]
67+
IEnumerable<Hut> huts,
68+
[DurableClient] DurableTaskClient starter)
69+
{
70+
if (Environment.GetEnvironmentVariable("AZURE_FUNCTIONS_ENVIRONMENT") == "Development")
71+
{
72+
return;
73+
}
74+
75+
_logger.LogInformation($"{nameof(HuettenHolidayUpdateAvailabilityTimerTriggered)} function executed at: {DateTime.Now}");
76+
77+
string instanceId =
78+
await starter.ScheduleNewOrchestrationInstanceAsync(nameof(HuettenHolidayUpdateAvailabilityOrchestrator), huts.Select(h => h.Id).ToList());
79+
_logger.LogInformation($"{nameof(HuettenHolidayUpdateAvailabilityOrchestrator)} started. Instance ID={instanceId}");
80+
}
81+
82+
83+
[Function(nameof(HuettenHolidayUpdateAvailabilityOrchestrator))]
84+
public async Task HuettenHolidayUpdateAvailabilityOrchestrator([OrchestrationTrigger] TaskOrchestrationContext context)
85+
{
86+
var orchestratorLogger = context.CreateReplaySafeLogger<UpdateAvailabilityFunctions>();
87+
88+
var hutIds = context.GetInput<List<int>>();
89+
90+
orchestratorLogger.LogInformation($"Starting HuettenHoliday orchestrator with {hutIds!.Count} hut IDs");
91+
92+
var tasks = new List<Task>();
93+
94+
// Fan-out
95+
foreach (var hutId in hutIds)
96+
{
97+
orchestratorLogger.LogInformation("Starting UpdateHutAvailability Activity Function for hutId={hutId}", hutId);
98+
tasks.Add(context.CallActivityAsync(nameof(HuettenHolidayUpdateAvailabilityActivityTrigger), hutId));
99+
100+
// In order not to run into rate limiting, we process in batches of 10 and then wait for 1 minute
101+
if (tasks.Count >= 10)
102+
{
103+
orchestratorLogger.LogInformation("Delaying next batch for 1 minute, last hutId={hutid}", hutId);
104+
await context.CreateTimer(context.CurrentUtcDateTime.AddMinutes(1), CancellationToken.None);
105+
106+
orchestratorLogger.LogInformation("Waiting for batch to finishing UpdateHutAvailability Activity Functions");
107+
// Fan-in (wait for all tasks to be completed)
108+
await Task.WhenAll(tasks);
109+
orchestratorLogger.LogInformation("Finished batch");
110+
111+
tasks.Clear();
112+
}
113+
}
114+
115+
orchestratorLogger.LogInformation("All UpdateHutAvailability Activity Functions scheduled. Waiting for finishing last batch");
116+
117+
// Fan-in (wait for all tasks to be completed)
118+
await Task.WhenAll(tasks);
119+
120+
// Call stored proc to update reporting table
121+
//await context.CallActivityAsync(nameof(UpdateAvailabilityReporting), new object()); // using new object instead of null to satisfy analyzer warning
122+
123+
orchestratorLogger.LogInformation($"HuettenHoliday Update availability orchestrator finished");
124+
}
125+
126+
[Function(nameof(HuettenHolidayUpdateAvailabilityActivityTrigger))]
127+
public async Task<IEnumerable<Availability>?> HuettenHolidayUpdateAvailabilityActivityTrigger([ActivityTrigger] int hutId)
128+
{
129+
try
130+
{
131+
var cabinId = hutId > HutIdOffset ? hutId - HutIdOffset : hutId;
132+
133+
var dbContext = Helpers.GetDbContext();
134+
// get hut from database to check if it exists
135+
var hut = await dbContext.Huts.Include(h => h.Availability)
136+
.ThenInclude(a => a.BedCategory)
137+
.AsNoTracking()
138+
.SingleOrDefaultAsync(h => h.Id == hutId);
139+
140+
if (hut == null)
141+
{
142+
_logger.LogWarning("Hut with id {HutId} not found in database", hutId);
143+
return null;
144+
}
145+
146+
var httpClient = _clientFactory.CreateClient("HttpClient");
147+
148+
149+
// First, we need to make one GET call to huts booking page: https://www.huetten-holiday.com/huts/xxx
150+
// This returns two set-cookies that we need to use in the POST request:
151+
// XSRF-TOKEN
152+
// huettenholiday_session
153+
// Also XSRF-TOKEN from the cookie needs to be set in the header of the POST request
154+
155+
_logger.LogInformation("Fetching initial cookies for hutId {HutId}", hutId);
156+
var initialResponse = await httpClient.GetAsync(hut.Link);
157+
if (!initialResponse.IsSuccessStatusCode)
158+
{
159+
_logger.LogError("Failed to fetch initial cookies from HuettenHoliday. Status code: {StatusCode}", initialResponse.StatusCode);
160+
return null;
161+
}
162+
163+
// Extract cookies from the initial response
164+
var xsrfToken = initialResponse.Headers.GetValues("Set-Cookie")
165+
.FirstOrDefault(c => c.StartsWith("XSRF-TOKEN="))?.Split(';').FirstOrDefault()?.Replace("XSRF-TOKEN=", "");
166+
var sessionCookie = initialResponse.Headers.GetValues("Set-Cookie")
167+
.FirstOrDefault(c => c.StartsWith("huettenholiday_session="))?.Split(';').FirstOrDefault()?.Replace("huettenholiday_session=", "");
168+
169+
if (xsrfToken == null || sessionCookie == null)
170+
{
171+
_logger.LogError("Failed to extract cookies from initial response for hutId {HutId}", hutId);
172+
return null;
173+
}
174+
175+
var availabilities = new List<Availability>();
176+
const int monthsToFetch = 6; // Number of months to fetch availability for
177+
178+
for (var month = 0; month < monthsToFetch; month++)
179+
{
180+
var content = new GetMonthAvailabilityPayload
181+
{
182+
cabinId = cabinId,
183+
selectedMonth = new SelectedMonth
184+
{
185+
monthNumber = DateTime.UtcNow.AddMonths(month).Month,
186+
year = DateTime.UtcNow.AddMonths(month).Year
187+
}
188+
};
189+
_logger.LogInformation("Fetching availability from HuettenHoliday for hutId {hutId} for month {Month}-{Year}", hutId, content.selectedMonth.monthNumber, content.selectedMonth.year);
190+
191+
const string getMonthAvailabilityUrl = "https://www.huetten-holiday.com/cabins/get-month-availability";
192+
var requestMessage = new HttpRequestMessage(HttpMethod.Post, getMonthAvailabilityUrl)
193+
{
194+
Content = JsonContent.Create(content)
195+
};
196+
requestMessage.Headers.Add("X-XSRF-TOKEN", xsrfToken.Replace("%3D", "=")); // Replace URL encoded equals sign with actual equals sign
197+
requestMessage.Headers.Add("Cookie", $"XSRF-TOKEN={xsrfToken}; huettenholiday_session={sessionCookie}");
198+
199+
var response = await httpClient.SendAsync(requestMessage);
200+
201+
if (!response.IsSuccessStatusCode)
202+
{
203+
_logger.LogError("Failed to fetch availability from HuettenHoliday. Status code: {StatusCode}", response.StatusCode);
204+
break;
205+
}
206+
207+
var responseData = (await response.Content.ReadFromJsonAsync<IEnumerable<AvailabilityResult>>())?.ToList();
208+
if (responseData == null || responseData.Count == 0)
209+
{
210+
_logger.LogInformation("No availability data found for hutId {HutId}", hutId);
211+
continue;
212+
}
213+
214+
foreach (var dateResult in responseData)
215+
{
216+
var existingAvailabilities = await dbContext.Availability.Where(a =>
217+
a.Hutid == hutId && a.Date == dateResult.date &&
218+
a.BedCategoryId != BedCategory.HutClosedBedcatoryId).ToListAsync();
219+
220+
var existingAvailability = existingAvailabilities.FirstOrDefault();
221+
222+
// This is quite crude so far. Not sure, I fully understand the structure of the response yet. But it seems to be:
223+
// Sum of booked_places gives the number of free beds
224+
var totalFreeBeds = dateResult.rooms.Select(r => r.booked_places).Sum();
225+
var totalBeds = dateResult.totalPlaces;
226+
if (existingAvailability == null)
227+
{
228+
_logger.LogInformation("Creating new availability for hutId {HutId} on date {Date}", hutId, dateResult.date);
229+
var availability = new Availability
230+
{
231+
Hutid = hutId,
232+
Date = dateResult.date,
233+
FreeRoom = totalFreeBeds,
234+
TotalRoom = totalBeds,
235+
LastUpdated = DateTime.UtcNow,
236+
BedCategoryId = 2, // Hardcoded to "Zimmer" for now
237+
TenantBedCategoryId = 2,
238+
};
239+
dbContext.Availability.Add(availability);
240+
availabilities.Add(availability);
241+
}
242+
else
243+
{
244+
_logger.LogInformation("Updating existing availability for hutId {HutId} on date {Date}", hutId, dateResult.date);
245+
existingAvailability.FreeRoom = totalFreeBeds;
246+
existingAvailability.TotalRoom = totalBeds;
247+
existingAvailability.LastUpdated = DateTime.UtcNow;
248+
249+
dbContext.Availability.Update(existingAvailability);
250+
availabilities.Add(existingAvailability);
251+
}
252+
}
253+
254+
await dbContext.SaveChangesAsync();
255+
}
256+
257+
_logger.LogInformation("Fetched {Count} availability records for hutId {HutId}", availabilities.Count, hutId);
258+
return availabilities;
259+
}
260+
catch (Exception e)
261+
{
262+
_logger.LogError(e, "Error while fetching huts from HuettenHoliday");
263+
return null;
264+
}
265+
}
266+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using System;
2+
3+
namespace FetchDataFunctions.Models.HuettenHoliday;
4+
5+
public class AvailabilityResult
6+
{
7+
public DateTime date { get; set; }
8+
public AvailabilityRooms[] rooms { get; set; }
9+
public int totalPlaces { get; set; }
10+
}
11+
12+
public class AvailabilityRooms
13+
{
14+
public int room_id { get; set; }
15+
public int places { get; set; }
16+
public int paid_places { get; set; }
17+
public int booked_places { get; set; }
18+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
namespace FetchDataFunctions.Models.HuettenHoliday;
2+
3+
public class GetMonthAvailabilityPayload
4+
{
5+
public int cabinId { get; set; }
6+
public SelectedMonth selectedMonth { get; set; }
7+
public bool multipleCalendar { get; set; } = false;
8+
}
9+
10+
public class SelectedMonth
11+
{
12+
public int monthNumber { get; set; }
13+
public int year { get; set; }
14+
}

0 commit comments

Comments
 (0)