Skip to main content

Book

caution

If a second Quote is necessary before the Book operation, consult the Repeat Operations section before developing the Book operation.

Mock

Step 1: Define your Book 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\Book\Two_Adults_Two_Days.txt"

Mock Example

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.

Response example

Example of a BookRequest model:

namespace ConnectorsIntegration.Book.Models.Request;

public class BookRequest
{
public string CheckIn { get; set; }
public string CheckOut { get; set; }
public string HotelCode { get; set; }
public List<SupplierOccupancy> Occupancy { get; set; }
public string RateId { get; set; }
public string ClientReference { get; set; }
}

public class SupplierOccupancy
{
public int Adults { get; set; }
public int Children { get; set; }
public IEnumerable<int> Infants { get; set; }
}

File location: "ConnectorsIntegration\Book\Models\Response\BookRequest.cs"

Example of a BookResponse model:

namespace ConnectorsIntegration.Book.Models.Response;

public class BookResponse
{
public SupplierOption Option { get; set; }
}

public class SupplierOption
{
public DateTime Checkin {get; set;}
public string Status { get; set; }
public SupplierPrice SupplierPrice { get; set; }
public string SupplierPaymentType { get; set; }
public string ClientReference { get; set; }
public string SupplierReference { get; set; }
public List<SupplierRoom> Rooms { get; set; }
public List<SupplierCancelPolicy> SupplierCancelPolicies { 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 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; }
}

File location: "ConnectorsIntegration\Book\Models\Response\BookResponse.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\Book\BookExtensions.cs"

If the Seller works with JSON format, we can specify the integration to work with JSON with the following:

internal static class BookExtensions
{
public static void AddBookServices(this IServiceCollection services,
IConfiguration configuration)
{
//A JsonSerializer service is added along with the request and response model
services.AddJsonSerializer<BookRequest, BookResponse>(ConfigureJSONOptions);
//The operation is added, indicating what models should be used during the development of the operation
services.AddBookOperation<BookOperation, BookRequest, BookResponse, AccessModel>(TgxPlatform.Name,
configuration);
}
private static void ConfigureJSONOptions(JsonSerializerOptions options) { }
}

For details about others serializers, check Extensions

For more details about operations, check PreOperations and Operations

Step 2: BookOperation 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 BookOperation.cs class:

internal partial class BookOperation : IBookOperation<BookRequest, BookResponse, AccessModel>
{
// Implement any services needed just like in SearchOperation.cs
}

File location: "ConnectorsIntegration\Book\Operations\BookOperation.cs"

BookOperation

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 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(
BookConnectorRequest connectorsRequest,
BookParameters<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(
BookParameters<CntAccessModel> connectorParameters,
IEnumerable<SupplierResponseWrapper<BookResponse>> supplierResponses,
out IEnumerable<AdviseMessage> adviseMessages)
{
var supplierResponseWrappers = supplierResponses as SupplierResponseWrapper<BookResponse>[] ?? supplierResponses.ToArray();

var success = ResponseValidator.TryValidateSupplierResponses(supplierResponseWrappers, out adviseMessages);

if (!success) return false;

if (supplierResponseWrappers.ElementAt(0).Response.HotelBook?.Hotel is null)
{
adviseMessages =
[
AdviseMessage.BuildSupplierNoResults() // Indicates no results from the supplier.
];

return false;
}

return true; // Validation passes if no issues are found.
}

Step 3: 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 (BookRequest)
  • The request that the Buyer sends (connectorsRequest)
  • Parameters (connectorParameters) which will have some helpers:

File location: "ConnectorsIntegration\Book\Operations\BookOperation.BuildRequest.cs"

Build Request Book

Example of Build Request:

using Connectors.Core.Application.Connection;
using Connectors.Pull.Hotel.Application.Metadata;
using Connectors.Pull.Hotel.Application.Operations.Book;
using Connectors.Pull.Hotel.Domain.Contracts;
using Connectors.Pull.Hotel.Domain.Contracts.Common;
using Connectors.Pull.Hotel.Domain.Contracts.Book.Request;
using ConnectorsIntegration.Book.Models.Request;
using static ConnectorsIntegration.Common.Constants.TgxPlatform;

namespace ConnectorsIntegration.Book.Operations;

internal partial class BookOperation
{
public IEnumerable<SupplierRequestWrapper<BookRequest>> BuildRequests(
BookConnectorRequest connectorsRequest,
BookParameters<AccessModel> connectorParameters)
{
BookCriteria bookCriteria = connectorsRequest.BookRq.BookCriteria;
//Refers to the checkIn of the booking
string checkIn = bookCriteria.CheckIn;
//Refers to the checkOut of the booking
string checkOut = bookCriteria.CheckOut;
string hotelCode = bookCriteria.Accommodation.Code;
//Refers to the occupancy of the booking. If the Seller allows requests with multiple occupancies, the Occupancies should be iterated
Occupancy firstOccupancy = bookCriteria.Occupancies.First();
OccupancyInfoDetailed occupancyInfoDetailed = _connectorsUtilities.MetadataConnectorsService.GetDetailedOccupancyInfo(firstOccupancy);

var rateIdParameter = bookCriteria.Parameters.GetParameter(SupplierParameters.RateId).Value;

var clientReference = connectorsRequest.BookRq.BookCriteria.ClientReference;

BookRequest bookRequest = BuildBookRequest(
checkIn,
checkOut,
hotelCode,
rateIdParameter,
clientReference,
occupancyInfoDetailed);

//Generic URL we prepared back in the AccessModel, which will be passed by the Buyer
string genericUrl = connectorParameters.ParametersModel.UrlGeneric;

SupplierRequestWrapper<BookRequest> supplierRequest = new(
bookRequest,
new Uri(genericUrl),
HttpMethod.Post);

return
[
supplierRequest
];
}

private static BookRequest BuildBookRequest(
string checkIn,
string checkOut,
string hotelCode,
string rateIdParameter,
string clientReference,
OccupancyInfoDetailed occupancyInfoDetailed)
{
//The request towards the Seller system

return new BookRequest()
{
CheckIn = checkIn,
CheckOut = checkOut,
HotelCode = hotelCode,
Occupancy = new List<SupplierOccupancy>()
{
new() {
Adults = occupancyInfoDetailed.NumberOfAdults,
Children = occupancyInfoDetailed.NumberOfChildren,
Infants = occupancyInfoDetailed.InfantAges
}
},
RateId = rateIdParameter
ClientReference = clientReference
};
}
}

Step 4: 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 BookOperation:

File location: "ConnectorsIntegration\Book\Operations\BookOperation.ParseResponse.cs"

Build Response Book

Example of Parse Response:

using Connectors.Core.Application.Connection;
using Connectors.Core.Application.Iso;
using Connectors.Core.Domain;
using Connectors.Pull.Hotel.Application.Operations.Book;
using Connectors.Pull.Hotel.Domain.Contracts.Common;
using Connectors.Pull.Hotel.Domain.Contracts.Book.Response;
using Connectors.Pull.Hotel.Domain.Contracts.Search.Response;
using ConnectorsIntegration.Book.Models.Response;

namespace ConnectorsIntegration.Book.Operations;

internal partial class BookOperation
{
public BookConnectorResponse ParseResponses(
BookConnectorRequest connectorsRequest,
BookParameters<AccessModel> connectorParameters,
IEnumerable<SupplierResponseWrapper<BookResponse>> supplierResponses,
CancellationToken cancellationToken)
{
var supplierResponse = supplierResponses.First().Response;
return new BookConnectorResponse(ParseSupplierResponse(connectorsRequest, supplierResponse));
}

private BookRs ParseSupplierResponse(BookConnectorRequest connectorsRequest, BookResponse supplierResponse)
{
if (supplierResponse.Option == null)
{
return BookRs.BuildErrorResponse(new[]
{
AdviseMessage.BuildSupplierError(new External("", "Option null from supplier response"))
});
}
var supplierOption = supplierResponse.Option;

BookStatus status = MapSellerToTgxStatus(supplierOption.Status);
Price price = ParseSupplierPrice(supplierOption.SupplierPrice);
PaymentType paymentType = MapSellerToTgxPaymentType(supplierOption.SupplierPaymentType);
List<Room> roomList = ParseSupplierRooms(supplierOption.Rooms);

var cancelPolicy = ParseSupplierCancelPolicies(supplierOption.CheckIn, supplierOption.SupplierCancelPolicies);

var clientReference = supplierOption.clientReference;
var supplierReference = supplierOption.SupplierReference;

BookDetails bookDetails = new BookDetails(status, new Reference(clientReference, supplierReference), price)
{
CancelPolicy = cancelPolicy,
Hotel = new HotelDetail()
{
Rooms = roomList,
}
};
return new BookRs(bookDetails);
}

private OptionCancelPolicy ParseSupplierCancelPolicies(DateTime checkIn, List<SupplierCancelPolicy> supplierCancelPolicies)
{
List<CancelPenalty> cancelPenalties = [];

foreach (SupplierCancelPolicy supplierCancelPolicy in supplierCancelPolicies)
{
//Helper to map a string currency
Currency supplierCurrency = CurrencyIso4217Mapper.Map(supplierCancelPolicy.PenaltyCurrency);
PenaltyType penaltyType = MapSellerToTgxPenaltyType(supplierCancelPolicy.PenaltyType);
//Helper to create a policy based in a Seller cancel penalty with deadline that has timezone
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 List<Room> ParseSupplierRooms(List<SupplierRoom> rooms)
{
List<Room> roomList = [];

foreach (SupplierRoom supplierRoom in rooms)
{
var price = ParseSupplierPrice(supplierRoom.SupplierPrice);

RoomPrice roomPrice = new(price);

roomList.Add(new Room(
supplierRoom.OccupancyId,
supplierRoom.RoomCode,
supplierRoom.RoomDescription,
roomPrice
));
}
return roomList;
}

private static PenaltyType MapSellerToTgxPenaltyType(string penaltyType) => penaltyType switch
{
"Percent" => PenaltyType.Percentage,
"Nights" => PenaltyType.Nights,
_ => PenaltyType.Amount
};

private static PaymentType MapSellerToTgxPaymentType(string supplierPaymentType) => supplierPaymentType switch
{
"MerchantPay" => PaymentType.MerchantPay,
"CardBookingPay" => PaymentType.CardBookingPay,
_ => PaymentType.MerchantPay
};

private static BookStatus MapSellerToTgxStatus(string status) => status switch
{
"Booked" => BookStatus.Ok,
"OnRequest" => BookStatus.OnRequest,
_ => BookStatus.Unknown
};

private static Price ParseSupplierPrice(SupplierPrice supplierPrice)
{
Currency supplierCurrency = CurrencyIso4217Mapper.Map(supplierPrice.Currency);
//Helper that builds a net price
var price = Price.BuildNetPrice(supplierCurrency, supplierPrice.Net, supplierPrice.MinimumSellingPrice);

return price;
}
}

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:

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 changes and push them to a new branch called "BookDevelopment" 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