Search
Mock
Step 1: Define your Search responses (Mocks)
Define the responses for your operation, it is mandatory to insert a supplier response into every mock or at least, into the mocks of the operation you will be developing.
We will be using the TwoAdultTwoDays mock through all the steps of the development.
File location: "test\MockServer\Tests\Search\Two_Adults_Two_Days.txt"
Step 2: Define the Models of your response (Request and Response models)
These models are crucial because they specify the structure of the objects contained within supplier responses. They'll also play a vital role in serializing and deserializing requests and responses during development.
Example of a SearchRequest model:
namespace ConnectorsIntegration.Search.Models.Request;
public class SearchRequest
{
public string CheckIn { get; set; }
public string CheckOut { get; set; }
public string HotelCode { get; set; }
public List<SupplierOccupancy> Occupancy { get; set; }
}
public class SupplierOccupancy
{
public int Adults { get; set; }
public int Children { get; set; }
public int Infants { get; set; }
public IEnumerable<int> ChildrenAges { get; set; }
}
File location: "ConnectorsIntegration\Search\Models\Request\SearchRequest.cs"
Example of a SearchResponse model:
namespace ConnectorsIntegration.Search.Models.Response;
public class SearchResponse
{
public List<SupplierOption> Options { get; set; }
public string HotelCode { get; set; }
public string BoardName { get; set; }
public string HotelName { get; set; }
}
public class SupplierOption
{
public string Status { get; set; }
public SupplierPrice SupplierPrice { get; set; }
public string SupplierPaymentType { get; set; }
public List<SupplierRoom> Rooms { get; set; }
public string BoardCode { get; set; }
public List<SupplierCancelPolicy> SupplierCancelPolicies { get; set; }
public List<SupplierSurcharge> SupplierSurcharges { get; internal set; }
}
public class SupplierPrice
{
public string Currency { get; set; }
public double Net { get; set; }
public double MinimumSellingPrice { get; set; }
}
public class SupplierRoom
{
public uint OccupancyId { get; set; }
public string RoomCode { get; set; }
public SupplierPrice SupplierPrice { get; set; }
public string RoomDescription { get; set; }
}
public class SupplierCancelPolicy
{
public double PenaltyAmount { get; set; }
public string PenaltyType { get; set; }
public string PenaltyCurrency { get; set; }
public string PenaltyDeadline { get; set; }
}
public class SupplierSurcharge
{
public string Type { get; set; }
public double Net { get; set; }
public string Currency { get; set; }
}
File location: "ConnectorsIntegration\Search\Models\Response\SearchResponse.cs"
Develop
Step 1: Register the serializers and operations
To specify which serializer and operations the developer will be using (based on the Seller's API) we can specify it in our "Extensions":
File location: "ConnectorsIntegration\Search\SearchExtensions.cs"
If the Seller works with JSON format, we can specify the integration to work with JSON with the following:
internal static class SearchExtensions
{
public static void AddSearchServices(this IServiceCollection services,
IConfiguration configuration)
{
//A JsonSerializer service is added along with the request and response model
services.AddJsonSerializer<SearchRequest, SearchResponse>(ConfigureJSONOptions);
//The operation is added, indicating what models should be used during the development of the operation
services.AddSearchOperation<SearchOperation, SearchRequest, SearchResponse, AccessModel>(TgxPlatform.Name,
configuration);
//Register the IPriceParser service to use afterwards in the SearchOperation
services.TryAddSingleton<IPriceParser, PriceParser>();
}
private static void ConfigureJSONOptions(JsonSerializerOptions options) { }
}
An example of an interface and implementation of the PriceParser service would be:
Interface
using Connectors.Core.Domain;
namespace ConnectorsIntegration.Search.Interfaces;
public interface IPriceParser
{
Price ParseSupplierPrice(Currency currency, double net, double minimumSellingPrice);
}
Parser
using Connectors.Core.Domain;
using ConnectorsIntegration.Search.Interfaces;
namespace ConnectorsIntegration.Search.Parsers;
public class PriceParser : IPriceParser
{
public Price ParseSupplierPrice(Currency currency, double net, double minimumSellingPrice)
{
var price = Price.BuildNetPrice(currency, net, minimumSellingPrice);
return price;
}
}
For details about others serializers, check Extensions
For more details about operations, check PreOperations and Operations
Step 2: Define the AccessModel
The AccessModel is a class that will allow the integration to establish some information before launching the operations.
File location: "ConnectorsIntegration\AccessModel.cs"
If the integration needs Generic and Search URLs, along with an Apikey, an AccessModel would look like this:
public class AccessModel : IBindAccessModel
{
public string User { get; private set; }
public string Password { get; private set; }
public string GenericUrl { get; private set; }
public string SearchUrl { get; private set; }
public string ApiKey { get; private set; }
public void Bind(string supplier, Access access)
{
User = access.User;
Password = access.Password;
GenericUrl = access.Urls.Generic;
SearchUrl = access.Urls.Search;
//Parameter that if is not present, will throw an exception
ApiKey = access.Parameters.GetRequiredOrException("ApiKey");
}
}
Step 3: SearchOperation validators
There are two previous validations that serve as a filter so the buildrequest and the parseresponse are as safe as possible. They can be found in the SearchOperation.cs class:
internal partial class SearchOperation : ISearchOperation<SearchRequest, SearchResponse, AccessModel>
{
// We will be using two services as an example in this search operation:
// This service is added internally and can be used to have such utilities as to get the timezone from the metadata.
private readonly IConnectorsUtilities _connectorsUtilities;
//This service is added in the extensions (see first step) and we will be using this as a helper to parse de supplier price
private readonly IPriceParser _priceParser;
public SearchOperation(IConnectorsUtilities connectorsUtilities, IPriceParser priceParser)
{
_connectorsUtilities = connectorsUtilities;
_priceParser = priceParser;
}
}
File location: "ConnectorsIntegration\Search\Operations\SearchOperation.cs"
TryValidateModelRequest
This step validates the incoming request from the client. While most validation is defined in the metadata, this step is used for specific edge cases that cannot be generalized.
Example Use Case: In a Search operation, validating that hotel codes are numeric because supplier do not allow non-numeric hotels. This type of validation would not be covered by metadata.
public bool TryValidateModelRequest(
SearchConnectorRequest connectorsRequest,
SearchParameters<CntAccessModel> connectorParameters,
out IEnumerable<AdviseMessage> adviseMessages)
{
//AdviseMessages are used to specify errors, such as checking if the hotel code is numeric and if not, add an AdviseMessage
adviseMessages = default;
return true; // Validation passes if no issues are found.
}
TryValidateSupplierResponses
Once the supplier's response is received, this step validates it for errors or anomalies. Suppliers may return incomplete or erroneous data, so this step ensures only valid responses are processed further.
Details:
- Check for supplier-specific error fields.
- Ensure required fields (e.g., hotel list) are present.
- Example Use Case: A supplier might return a response with an error code or an empty hotel list. This step would detect and handle such cases.
public bool TryValidateSupplierResponses(
SearchParameters<CntAccessModel> connectorParameters,
IEnumerable<SupplierResponseWrapper<SearchResponse>> supplierResponses,
out IEnumerable<AdviseMessage> adviseMessages)
{
var supplierResponseWrappers = supplierResponses as SupplierResponseWrapper<SearchResponse>[] ?? supplierResponses.ToArray();
var success = ResponseValidator.TryValidateSupplierResponses(supplierResponseWrappers, out adviseMessages);
if (!success) return false;
if (supplierResponseWrappers.ElementAt(0).Response.HotelSearch?.Hotel is null)
{
adviseMessages =
[
AdviseMessage.BuildSupplierNoResults() // Indicates no results from the supplier.
];
return false;
}
return true; // Validation passes if no issues are found.
}
Step 4: Build the Seller's request
This class will contain a "BuildRequests" method that will have the following arguments:
- Object of the requests from the models previously created (SearchRequest)
- The request that the Buyer sends (connectorsRequest)
- Parameters (connectorParameters) which will have some helpers:
File location: "ConnectorsIntegration\Search\Operations\SearchOperation.BuildRequest.cs"
Example of Build Request:
using Connectors.Core.Application.Connection;
using Connectors.Pull.Hotel.Application.Metadata;
using Connectors.Pull.Hotel.Application.Operations.Search;
using Connectors.Pull.Hotel.Domain.Contracts.Common;
using ConnectorsIntegration.Search.Models.Request;
namespace ConnectorsIntegration.Search.Operations;
internal partial class SearchOperation
{
public IEnumerable<SupplierRequestWrapper<SearchRequest>> BuildRequests(
SearchConnectorRequest connectorsRequest,
SearchParameters<AccessModel> connectorParameters)
{
//Refers to the checkIn of the booking
string checkIn = connectorsRequest.SearchRq.SearchCriteria.CheckIn;
//Refers to the checkOut of the booking
string checkOut = connectorsRequest.SearchRq.SearchCriteria.CheckOut;
//Refers to the hotelCode of the booking. If the seller allows requests with multiple hotels, the Accomodations should be iterated
string hotelCode = connectorsRequest.SearchRq.SearchCriteria.Destinations.Accommodations.First().Code;
//Refers to the occupancy of the booking. If the seller allows requests with multiple occupancies, the Occupancies should be iterated
Occupancy firstOccupancy = connectorsRequest.SearchRq.SearchCriteria.Occupancies.First();
OccupancyInfoDetailed occupancyInfoDetailed = _connectorsUtilities.MetadataConnectorsService.GetDetailedOccupancyInfo(firstOccupancy);
SearchRequest searchRequest = BuildSearchRequest(checkIn, checkOut, hotelCode, occupancyInfoDetailed);
//Generic URL we prepared back in the AccessModel, which will be passed by the buyer
string searchUrl = connectorParameters.ParametersModel.UrlSearch;
SupplierRequestWrapper<SearchRequest> supplierRequest = new(
searchRequest,
new Uri(searchUrl),
HttpMethod.Post);
return
[
supplierRequest
];
}
private static SearchRequest BuildSearchRequest(string checkIn, string checkOut, string hotelCode, OccupancyInfoDetailed occupancyInfoDetailed)
{
//The request towards the seller system
return new SearchRequest()
{
CheckIn = checkIn,
CheckOut = checkOut,
HotelCode = hotelCode,
Occupancy = new List<SupplierOccupancy>()
{
new() {
Adults = occupancyInfoDetailed.NumberOfAdults,
Children = occupancyInfoDetailed.NumberOfChildren,
Infants = occupancyInfoDetailed.NumberOfInfants,
ChildrenAges = occupancyInfoDetailed.ChildrenAges
}
}
};
}
}
Step 5: Parse the Seller's response
Once the request has been sent, we will have to control and parse the response returned by the Seller.
We will be implementing the "ParseResponse" step inside SearchOperation:
File location: "ConnectorsIntegration\Search\Operations\SearchOperation.ParseResponse.cs"
Example of Parse Response:
using Connectors.Core.Application.Connection;
using Connectors.Core.Application.Iso;
using Connectors.Core.Domain;
using Connectors.Pull.Hotel.Application.Operations.Search;
using Connectors.Pull.Hotel.Domain.Contracts.Common;
using Connectors.Pull.Hotel.Domain.Contracts.Search.Response;
using ConnectorsIntegration.Search.Models.Response;
namespace ConnectorsIntegration.Search.Operations;
internal partial class SearchOperation
{
public SearchConnectorResponse ParseResponses(
SearchConnectorRequest connectorsRequest,
SearchParameters<AccessModel> connectorParameters,
IEnumerable<SupplierResponseWrapper<SearchResponse>> supplierResponses,
CancellationToken cancellationToken)
{
//We can safely do First() in case the seller only has one response because we check errors previously
var supplierResponse = supplierResponses.First().Response;
return new SearchConnectorResponse(ParseSupplierResponse(connectorsRequest, supplierResponse));
}
private SearchRs ParseSupplierResponse(SearchConnectorRequest connectorsRequest, SearchResponse supplierResponse)
{
foreach (SupplierOption supplierOption in supplierResponse.Options)
{
if(supplierOption?.SupplierPrice?.Currency == null)
{
continue;
}
Currency supplierCurrency = CurrencyIso4217Mapper.Map(supplierOption.SupplierPrice.Currency);
// PriceParser used from the dependency injection
Price price = _priceParser.ParseSupplierPrice(supplierCurrency, supplierOption.SupplierPrice.Net, supplierOption.SupplierPrice.Net);
Status status = MapSellerToTgxStatus(supplierOption.Status);
PaymentType paymentType = MapSellerToTgxPaymentType(supplierOption.SupplierPaymentType);
List<Room> roomList = ParseSupplierRooms(supplierOption.Rooms);
var checkIn = connectorsRequest.SearchRq.SearchCriteria.CheckInAsDateTime;
var option = new Option(
status,
price,
[paymentType],
roomList);
// Additional elements can be added to the option, such as important information like surcharges or cancel policies
option.CancelPolicy = ParseSupplierCancelPolicies(checkIn, supplierOption.SupplierCancelPolicies);
option.Surcharges = ParseSupplierSurcharges(supplierOption.SupplierSurcharges);
_connectorsUtilities.OptionsGenerator.TryAddHotelOption(
supplierResponse.HotelCode,
supplierOption.BoardCode,
option,
supplierResponse.HotelName,
supplierResponse.BoardName);
}
return new SearchRs(_connectorsUtilities.OptionsGenerator.Combine());
}
private List<Surcharge> ParseSupplierSurcharges(List<SupplierSurcharge> surcharges)
{
List<Surcharge> surchargeList = [];
foreach (SupplierSurcharge supplierSurcharge in surcharges)
{
Currency supplierCurrency = CurrencyIso4217Mapper.Map(supplierSurcharge.Currency);
// PriceParser used from the dependency injection
Price price = _priceParser.ParseSupplierPrice(supplierCurrency, supplierSurcharge.Net, supplierSurcharge.Net);
surchargeList.Add(new Surcharge(
ChargeType.Included, // -> If it is included or excluded will totally depend on the seller
true,
"Seller surcharge",
price,
"SurchargeCode"));
}
return surchargeList;
}
private OptionCancelPolicy ParseSupplierCancelPolicies(DateTime checkIn, List<SupplierCancelPolicy> supplierCancelPolicies)
{
List<CancelPenalty> cancelPenalties = [];
foreach (SupplierCancelPolicy supplierCancelPolicy in supplierCancelPolicies)
{
Currency supplierCurrency = CurrencyIso4217Mapper.Map(supplierCancelPolicy.PenaltyCurrency);
PenaltyType penaltyType = MapSellerToTgxPenaltyType(supplierCancelPolicy.PenaltyType);
var penalty = _connectorsUtilities.CancelPenaltyManager.CancelPenaltyFromDateWithTimeZone(
checkIn,
penaltyType,
supplierCurrency,
supplierCancelPolicy.PenaltyAmount,
supplierCancelPolicy.PenaltyDeadline,
"yyyy-MM-ddTHH:mm:ss.fffffffzzz"
);
cancelPenalties.Add(penalty);
}
var refundable = cancelPenalties.Any();
return new OptionCancelPolicy(refundable, cancelPenalties);
}
private static PenaltyType MapSellerToTgxPenaltyType(string penaltyType) => penaltyType switch
{
"Percent" => PenaltyType.Percentage,
"Nights" => PenaltyType.Nights,
_ => PenaltyType.Amount
};
private List<Room> ParseSupplierRooms(List<SupplierRoom> rooms)
{
List<Room> roomList = [];
foreach (SupplierRoom supplierRoom in rooms)
{
Currency supplierCurrency = CurrencyIso4217Mapper.Map(supplierRoom.SupplierPrice.Currency);
// PriceParser used from the dependency injection
Price price = _priceParser.ParseSupplierPrice(supplierCurrency, supplierRoom.SupplierPrice.Net, supplierRoom.SupplierPrice.MinimumSellingPrice);
RoomPrice roomPrice = new(price);
roomList.Add(new Room(
supplierRoom.OccupancyId,
supplierRoom.RoomCode,
supplierRoom.RoomDescription,
roomPrice
));
}
return roomList;
}
private static PaymentType MapSellerToTgxPaymentType(string supplierPaymentType) => supplierPaymentType switch
{
"MerchantPay" => PaymentType.MerchantPay,
"CardBookingPay" => PaymentType.CardBookingPay,
_ => PaymentType.MerchantPay
};
private static Status MapSellerToTgxStatus(string status) => status switch
{
"Available" => Status.OK,
"OnRequest" => Status.RQ,
_ => Status.Unknown
};
}
For more details about helpers, check the - Price helpers and Policies helpers
For more details about the combinatory, check Recommended Helpers
Test
Option 1: Integration Tests
Use the integration tests provided by Travelgate to validate your implementation:
- Add the necessary use cases to the MockServer for each operation.
- Execute the associated tests for the implemented operation.
Option 2: FormTest Tool (Shopping)
Use the FormTest application to test each operation manually:
- Configure the tool to use your supplier's API settings.
- Test specific scenarios not covered by predefined use cases.
- View FormTest Documentation.
Code Review
Step 1: Create Pull Request
- Commit your Search changes and push them to a new branch called "SearchDevelopment" into the original repository.
- Separate the Pull Request into minimum these 4 commits:
- Mock responses
- Request and Response models
- BuildRequest
- ParseResponse
Step 2: Wait for Travelgate review
- This step involves waiting for the Travelgate team to review and approve the submitted pull request, for more details, check Code Review Details