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+ }
0 commit comments