Example #1
0
        private void ValidateAndSanitazeHotelsSearchRequest(HotelsSearchUserRequest hotelsSearchRequest)
        {
            if (String.IsNullOrEmpty(hotelsSearchRequest.CityCode))
            {
                throw new ArgumentException("City code must be provided.");
            }
            if (hotelsSearchRequest.CityCode.Length != 3)
            {
                throw new ArgumentException("City code must have three letters.");
            }
            if (hotelsSearchRequest.CheckInDate.Date < DateTime.Now.Date)
            {
                throw new ArgumentException("Check-in date can not be in past.");
            }
            if (hotelsSearchRequest.CheckOutDate <= hotelsSearchRequest.CheckInDate.AddDays(1))
            {
                throw new ArgumentException("Check-out date must be at least one day after check-in date.");
            }
            if (hotelsSearchRequest.PageSize < 1 || hotelsSearchRequest.PageOffset < 0)
            {
                throw new ArgumentException("Invalid page size or page offset values.");
            }
            if (hotelsSearchRequest.PageSize > 100)
            {
                throw new ArgumentException("Maximum page size is 100.");
            }

            hotelsSearchRequest.CheckInDate  = hotelsSearchRequest.CheckInDate.Date;
            hotelsSearchRequest.CheckOutDate = hotelsSearchRequest.CheckOutDate.Date;
        }
Example #2
0
        public async Task <ActionResult <HotelsSearchResponse> > Search(string cityCode, DateTime checkInDate, DateTime checkOutDate, int pageSize, int pageOffset, CancellationToken cancellationToken)
        {
            try
            {
                HotelsSearchUserRequest hotelsSearchRequest = new HotelsSearchUserRequest(cityCode, checkInDate, checkOutDate, pageSize, pageOffset);
                ValidateAndSanitazeHotelsSearchRequest(hotelsSearchRequest);

                HotelsSearchResponse response;

                bool isCacheHit = _cache.TryGetValue(hotelsSearchRequest.ToCacheKey(), out response);

                if (!isCacheHit)
                {
                    _logger.LogInformation($"No cache hit. CityCode: {hotelsSearchRequest.CityCode}, " +
                                           $"CheckIn: {hotelsSearchRequest.CheckInDate}, CheckOut: { hotelsSearchRequest.CheckOutDate}, pageSize: {hotelsSearchRequest.PageSize}, pageOffset: {hotelsSearchRequest.PageOffset}");

                    response = await _hotelsSearchService.SearchHotels(hotelsSearchRequest, cancellationToken);

                    var cacheEntryOptions = new MemoryCacheEntryOptions()
                                            .SetSize(1)
                                            .SetSlidingExpiration(TimeSpan.FromMinutes(2))
                                            // Remove from cache after this time, regardless of sliding expiration
                                            .SetAbsoluteExpiration(TimeSpan.FromMinutes(10));

                    _cache.Set(hotelsSearchRequest.ToCacheKey(), response, cacheEntryOptions);
                }
                else
                {
                    _logger.LogInformation($"Cache hit for request. CityCode: {hotelsSearchRequest.CityCode}, " +
                                           $"CheckIn: {hotelsSearchRequest.CheckInDate}, CheckOut: { hotelsSearchRequest.CheckOutDate}, pageSize: {hotelsSearchRequest.PageSize}, pageOffset: {hotelsSearchRequest.PageOffset}");
                }


                return(Ok(response));
            }
            catch (ArgumentException argEx)
            {
                _logger.LogWarning(argEx, argEx.Message);
                return(StatusCode((int)HttpStatusCode.BadRequest, new { message = argEx.Message }));
            }
            catch (HttpRequestException reqEx)
            {
                _logger.LogError(reqEx, "Cannot retrieve Amadeus Hotels information from Amadeus API.");
                return(StatusCode((int)HttpStatusCode.BadGateway, new { message = "Cannot retrieve Amadeus Hotels information from Amadeus API. Reason: " + reqEx.Message }));
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Internal error.");
                return(StatusCode((int)HttpStatusCode.InternalServerError, new { message = "Internal error." }));
            }
        }
Example #3
0
        /// <summary>
        /// Fetch from Amadeus API, maximum nubmer of items Amadeus API returns in one request is 100 (we always query for this maximum number), so if more items are needed it fetches recursively.
        /// Method returns data from Amadues Search Hotels Api including all preceding data and data for requested page (+ surplus up to 100 from current request).
        /// </summary>
        /// <param name="hotelsSearchRequest"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public async Task <HotelsSearchAmaduesFetchModel> FetchAmadeusHotels(HotelsSearchUserRequest hotelsSearchRequest, CancellationToken cancellationToken)
        {
            HotelsSearchAmaduesFetchModel amadeusFetchModel = new HotelsSearchAmaduesFetchModel();

            amadeusFetchModel.Items = new List <AmadeusApiHotelsSearchResponseItem>();

            string tokenString = await _amadeusTokenService.getAmadeusToken(cancellationToken);

            httpClient.DefaultRequestHeaders.Add("Accept", MediaTypeNames.Application.Json);
            httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokenString);

            // request to Amadeus Api is made in a way that Amadeus Api returns maximum possible number of items it can (it seems that limit is 100 items per request)
            var requestHotelsModel = new AmadeusApiHotelsSearchRequest(hotelsSearchRequest.CityCode, hotelsSearchRequest.CheckInDate, hotelsSearchRequest.CheckOutDate);

            // flag - if our user requsts certain page, all preceeding data should be fetched so they can be stored in db
            int minimumItemsToReturn = hotelsSearchRequest.PageSize * (hotelsSearchRequest.PageOffset + 1);
            int currentItemsReturnedCount;

            var urlParams = await requestHotelsModel.ToUrlParamsString();

            HttpResponseMessage response = await httpClient.GetAsync("/v2/shopping/hotel-offers" + "?" + urlParams, cancellationToken);

            if (response.StatusCode == System.Net.HttpStatusCode.BadRequest)
            {
                var errors = await ProccessError(response);

                var firstError = errors.Errors[0];
                throw new HttpRequestException(firstError.Code + " - " + firstError.Title);
            }

            response.EnsureSuccessStatusCode();

            var currentHotelsResponse = await ProccessResponse(response);

            _logger.LogInformation("Successful in first request from Amadeus API");

            amadeusFetchModel.Items.AddRange(currentHotelsResponse.Data);

            currentItemsReturnedCount = amadeusFetchModel.Items.Count;

            int    iterationCount = 1;
            bool   hasMoreItems   = currentHotelsResponse.Meta != null && currentHotelsResponse.Meta.Links != null && !String.IsNullOrEmpty(currentHotelsResponse.Meta.Links.Next);
            string nextItemsLink  = hasMoreItems ? currentHotelsResponse.Meta.Links.Next : null;

            while (currentItemsReturnedCount < minimumItemsToReturn && hasMoreItems)
            {
                nextItemsLink = null;
                hasMoreItems  = currentHotelsResponse.Meta != null && currentHotelsResponse.Meta.Links != null && !String.IsNullOrEmpty(currentHotelsResponse.Meta.Links.Next);
                if (hasMoreItems)
                {
                    //nextAmadeusHotelsResponse = await FetchNextAmadeusHotels(currentHotelsResponse.Meta.Links.Next, cancellationToken);
                    nextItemsLink = currentHotelsResponse.Meta.Links.Next;

                    currentHotelsResponse = await FetchNextAmadeusHotels(currentHotelsResponse.Meta.Links.Next, cancellationToken);

                    _logger.LogInformation("Iteration count for getting next items: " + iterationCount);
                    iterationCount++;

                    amadeusFetchModel.Items.AddRange(currentHotelsResponse.Data);
                }

                currentItemsReturnedCount = amadeusFetchModel.Items.Count;
            }

            _logger.LogInformation("Successful in getting data from Amadeus API. Returned Search Hotels items: " + currentItemsReturnedCount);
            amadeusFetchModel.nextItemsUrl = nextItemsLink;
            return(amadeusFetchModel);
        }
        /// <summary>
        /// Main method for hotels search data, combines logic for getting data from database or fetching it from Amadues Api service or combination of the two
        /// </summary>
        /// <param name="hotelsSearchRequest"></param>
        /// <param name="cancellationToken"></param>
        /// <returns>HotelsSearchAmaduesFetchModel - requested items and nextItemsUrl with url from Amadues Api to get next items</returns>
        public async Task <HotelsSearchResponse> SearchHotels(HotelsSearchUserRequest hotelsSearchRequest, CancellationToken cancellationToken)
        {
            var response = new HotelsSearchResponse(hotelsSearchRequest);

            var tuple = await _searchRequestRepository.GetTupleWithItemsCountAsync(hotelsSearchRequest.CityCode,
                                                                                   hotelsSearchRequest.CheckInDate, hotelsSearchRequest.CheckOutDate, true);

            SearchRequest searchRequestInDb  = tuple.Item1;
            int           currentlyItemsInDb = tuple.Item2;

            int minimumItemsNeededInDb = hotelsSearchRequest.PageSize * (hotelsSearchRequest.PageOffset + 1);

            bool commonAmadeusNextLinkError = false;

            // there is some data in database, try to get it from db
            if (searchRequestInDb != null)
            {
                // if there is enough items in database, return them
                if (currentlyItemsInDb >= minimumItemsNeededInDb)
                {
                    var requstedItemsFromDb = await _searchRequestHotelRepository.GetForCurrentPageIncludedAsync(searchRequestInDb.SearchRequestId, hotelsSearchRequest.PageSize, hotelsSearchRequest.PageOffset);

                    response.Items       = _mapper.Map <List <HotelSearchItemResponse> >(requstedItemsFromDb);
                    response.HasNextPage = currentlyItemsInDb > minimumItemsNeededInDb || !String.IsNullOrEmpty(searchRequestInDb.NextItemsLink);

                    _logger.LogInformation($"All items returned from database. There was no fetching data from API. CityCode: {hotelsSearchRequest.CityCode}, " +
                                           $"CheckIn: {hotelsSearchRequest.CheckInDate}, CheckOut: { hotelsSearchRequest.CheckOutDate}, pageSize: {hotelsSearchRequest.PageSize}, pageOffset: {hotelsSearchRequest.PageOffset}");
                    return(response);
                }
                // no enough items in database, try to fetch next items from stored NextItemsLink (Amadeus api does not provide classical pagination, returns only nextItemsLink)
                // if there is no NextItemsLink, that is it, page will be empty
                else if (!String.IsNullOrEmpty(searchRequestInDb.NextItemsLink))
                {
                    // get NextItemsLink from db, if that is not enogh
                    int moreItemsToFetch = minimumItemsNeededInDb - currentlyItemsInDb;

                    try
                    {
                        var amaduesFetchModelFromNextLink = await _amadeusApiServiceProvider.FetchNextAmadeusHotelsRecursively(searchRequestInDb.NextItemsLink, moreItemsToFetch, cancellationToken);

                        // NextItemsLink that is stored to db is fetched from Amadeus Api Response
                        searchRequestInDb.NextItemsLink = amaduesFetchModelFromNextLink.nextItemsUrl;

                        _searchRequestRepository.Update(searchRequestInDb);
                        await _unitOfWork.CompleteAsync();

                        await this.SaveFetchedItems(amaduesFetchModelFromNextLink, searchRequestInDb.SearchRequestId, cancellationToken);

                        var requstedItemsFromDb = await _searchRequestHotelRepository.GetForCurrentPageIncludedAsync(searchRequestInDb.SearchRequestId, hotelsSearchRequest.PageSize, hotelsSearchRequest.PageOffset);

                        response.Items       = _mapper.Map <List <HotelSearchItemResponse> >(requstedItemsFromDb);
                        response.HasNextPage = amaduesFetchModelFromNextLink.Items.Count > minimumItemsNeededInDb || !String.IsNullOrEmpty(searchRequestInDb.NextItemsLink);

                        _logger.LogInformation($"Some items were in database, but other items where needed to fetch from API. CityCode: {hotelsSearchRequest.CityCode}, " +
                                               $"CheckIn: {hotelsSearchRequest.CheckInDate}, CheckOut: { hotelsSearchRequest.CheckOutDate}, pageSize: {hotelsSearchRequest.PageSize}, pageOffset: {hotelsSearchRequest.PageOffset}");
                        return(response);
                    }
                    // Problem with amadeus api is that NextItemsLink is valid for undefined amount of time, after that returns an error.
                    // Customer support was contacted and confirmed they have problem with pagination (next items link)
                    catch (HttpRequestException ex)
                    {
                        _logger.LogWarning(ex, "Expected Error from Amadeus API they have some problems with pagination for getting next items. Link expires after some undefined time." +
                                           "Even Customer Support was contacted. They confirmed they have some problems regarding pagination.");
                        commonAmadeusNextLinkError = true;
                    }
                }
            }
            // no data in database, we need to fetch it from Api and store in db - searchRequestInDb == null
            // Amadeus Api problem with pagination and next link, fetch from beggining - commonAmadeusNextLinkError
            if (searchRequestInDb == null || commonAmadeusNextLinkError)
            {
                var amaduesFetchModel = await _amadeusApiServiceProvider.FetchAmadeusHotels(hotelsSearchRequest, cancellationToken);

                var searchRequest = _mapper.Map <SearchRequest>(hotelsSearchRequest);
                // NextItemsLink that is stored to db is fetched from Amadeus Api Response
                searchRequest.NextItemsLink = amaduesFetchModel.nextItemsUrl;

                await _searchRequestRepository.AddAsync(searchRequest);

                await _unitOfWork.CompleteAsync();

                await this.SaveFetchedItems(amaduesFetchModel, searchRequest.SearchRequestId, cancellationToken);

                var requstedItemsFromDb = await _searchRequestHotelRepository.GetForCurrentPageIncludedAsync(searchRequest.SearchRequestId, hotelsSearchRequest.PageSize, hotelsSearchRequest.PageOffset);

                response.Items       = _mapper.Map <List <HotelSearchItemResponse> >(requstedItemsFromDb);
                response.HasNextPage = amaduesFetchModel.Items.Count > minimumItemsNeededInDb || !String.IsNullOrEmpty(searchRequest.NextItemsLink);

                _logger.LogInformation($"All items fetched from API from first to last requested. CityCode: {hotelsSearchRequest.CityCode}, " +
                                       $"CheckIn: {hotelsSearchRequest.CheckInDate}, CheckOut: { hotelsSearchRequest.CheckOutDate}, pageSize: {hotelsSearchRequest.PageSize}, pageOffset: {hotelsSearchRequest.PageOffset}");
                return(response);
            }

            return(response);
        }