GoGlobal Hotel Integration - Technical Implementation¶
1. Overview¶
This document provides comprehensive technical implementation details for the GoGlobal hotel integration plugin (tqgglbl) in TQPro. The plugin integrates with the Yanolja GoGlobal hotel distribution API (v3.16.8) to provide online hotel search, booking, and management capabilities.
Key Features¶
| Feature | Description |
|---|---|
| Hotel Availability Search | City/hotel-based search with multi-room, children, filters |
| Offer Validation | Price confirmation and cancellation policy retrieval |
| Booking Management | Create, check, amend, cancel hotel bookings |
| Voucher Generation | PDF voucher for confirmed bookings |
| Static Data Cache | Local PostgreSQL cache of countries, cities, hotels |
| Dual-Mode Routing | Online (GoGlobal) vs contracted (NTS) search modes |
| Pluggable Architecture | Supplier-agnostic interface for future provider swaps |
Integration Points¶
+-------------------+ +-------------------+ +-------------------+
| Frontend |---->| TQPro API |---->| GoGlobal API |
| (tqweb-adm, | | (tqapi) | | (SOAP/REST) |
| tqweb-b2b, | +-------------------+ +-------------------+
| tqweb/pub) | |
+-------------------+ v
+-------------------+
| Local Cache |
| (PostgreSQL |
| goglobal.*) |
+-------------------+
Related Documents¶
- Hotel Booking Use Cases - Use case specification
- Integration Plan - Implementation plan
- Hotel API Specification - Hotel API specification
2. Module Structure¶
2.1 Gradle Module¶
Module: tqgglbl
Build file: tqgglbl/build.gradle.kts (pattern: tqryb2b/build.gradle.kts)
Dependencies:
- tqapp - Business logic, entity facades, OnlineHotelSupplierI interface
- tqcommon - Core framework, entity base classes, config, transformers
- JUnit Jupiter - Testing
- Hibernate 6.5 - JPA/ORM for static data cache
- Gson - JSON parsing (availability response, Op 11)
- DOM (javax.xml.parsers) - XML parsing (all other operations)
- java.net.http.HttpClient - HTTP transport
Registration: settings.gradle.kts includes tqgglbl
2.2 Package Layout¶
tqgglbl/src/main/java/com/perun/tlinq/
+-- framework/
| +-- GoGlobalPlugin.java # Plugin lifecycle management
|
+-- client/goglobal/
+-- config/
| +-- GoGlobalPluginConfig.java # JAXB config object for goglobal-client.xml
|
+-- db/
| +-- GGCountryEntity.java # JPA: goglobal.country
| +-- GGCityEntity.java # JPA: goglobal.city
| +-- GGHotelEntity.java # JPA: goglobal.hotel
| +-- GGStarRatingEntity.java # JPA: goglobal.star_rating
| +-- GGRefreshLogEntity.java # JPA: goglobal.refresh_log
|
+-- entity/
| +-- GGEntity.java # Base native entity (extends AbstractEntity)
| +-- GGHotelOffer.java # Native entity for availability results
| +-- GGBooking.java # Native entity for booking data
| +-- GGHotelInfo.java # Native entity for hotel information
| +-- GGPriceBreakdown.java # Native entity for nightly pricing
|
+-- remote/
| +-- GoGlobalSoapClient.java # SOAP 1.2 transport layer
| +-- GoGlobalRequestBuilder.java # XML request builder per operation
| +-- GoGlobalResponseParser.java # JSON/XML response parser
| +-- dto/ # Response Data Transfer Objects
| +-- GGAvailabilityResponse.java
| +-- GGValuationResponse.java
| +-- GGBookingResponse.java
| +-- GGBookingStatusResponse.java
| +-- GGBookingDetailsResponse.java
| +-- GGCancellationResponse.java
| +-- GGHotelInfoResponse.java
| +-- GGPriceBreakdownResponse.java
|
+-- service/
| +-- GoGlobalServiceFactory.java # Service factory for entity framework
| +-- GoGlobalEntityService.java # Entity service implementation
| +-- SDRefreshRunner.java # Scheduled refresh runner (Runnable)
| +-- StaticDataRefresher.java # Static data refresh logic
| +-- hotel/
| +-- GoGlobalHotelService.java # Hotel-specific service methods
| +-- GoGlobalHotelFacade.java # Implements OnlineHotelSupplierI
|
+-- util/
+-- GoGlobalClientConfig.java # Config accessor for goglobal-client.xml
+-- GoGlobalDBSession.java # Hibernate session singleton
2.3 Dependency Graph¶
tqgglbl
+-- tqapp
| +-- tqcommon
+-- tqcommon (direct)
Class dependencies:
GoGlobalPlugin
+-- GoGlobalClientConfig
+-- GoGlobalDBSession
+-- GoGlobalServiceFactory
+-- GoGlobalHotelFacade
+-- SDRefreshRunner
+-- OnlineHotelSupplierRegistry (from tqapp)
GoGlobalHotelFacade (implements OnlineHotelSupplierI)
+-- GoGlobalHotelService
+-- GoGlobalSoapClient
+-- GoGlobalRequestBuilder
+-- GoGlobalResponseParser
+-- GoGlobalDBSession (for cache queries)
GoGlobalSoapClient
+-- GoGlobalClientConfig (credentials, endpoint)
+-- java.net.http.HttpClient
GoGlobalServiceFactory (extends AbstractServiceFactory)
+-- GoGlobalEntityService
+-- GoGlobalHotelService
3. Plugin Lifecycle¶
3.1 GoGlobalPlugin¶
Class: com.perun.tlinq.framework.GoGlobalPlugin extends AbstractPlugin
Initialization Sequence:
GoGlobalPlugin.initializePlugin()
|
+--> 1. Load config via GoGlobalClientConfig
| - Parse goglobal-client.xml
| - Extract API credentials, endpoint URL, DB settings
|
+--> 2. Initialize GoGlobalDBSession
| - Build Hibernate SessionFactory for goglobal schema
| - Register JPA entities: GGCountryEntity, GGCityEntity, GGHotelEntity, etc.
| - Test database connection
|
+--> 3. Check static data refresh schedule
| - Query goglobal.refresh_log for last successful refresh
| - If elapsed days >= goglobal.staticdata.refresh.days (default: 7)
| OR no previous refresh exists:
| Launch SDRefreshRunner in background thread
|
+--> 4. Initialize GoGlobalServiceFactory
| - Register services from goglobal-client.xml <Services>
| - Wire GoGlobalHotelService methods
|
+--> 5. Register as online hotel supplier
OnlineHotelSupplierRegistry.register(
"GoGlobalServiceFactory",
GoGlobalHotelFacade.getInstance()
)
Shutdown:
GoGlobalPlugin.shutdownPlugin()
|
+--> Cancel SDRefreshRunner if running
+--> Close GoGlobalDBSession
+--> Deregister from OnlineHotelSupplierRegistry
3.2 Plugin Registration (tourlinq-config.xml)¶
<!-- Under <Plugins> -->
<Plugin name="GoGlobalPlugin"
constructorClass="com.perun.tlinq.framework.GoGlobalPlugin">
<properties>
<property name="dbname" value="##tlinq.dbname"/>
<property name="dbpass" value="##tlinq.dbpass"/>
<property name="configFile" value="goglobal-client.xml"/>
</properties>
</Plugin>
<!-- Under <ServiceFactories> -->
<ServiceFactory name="GoGlobalServiceFactory"
code="GOGLOBAL" type="remote" enabled="true"
class="com.perun.tlinq.client.goglobal.service.GoGlobalServiceFactory">
<properties>
<property name="supplier.code" value="GOGLOBAL"/>
</properties>
</ServiceFactory>
<!-- Under <AppProperties> -->
<property name="hotel.online.supplier" value="GoGlobalServiceFactory"/>
4. Online Hotel Supplier Interface¶
4.1 OnlineHotelSupplierI Contract¶
Location: tqapp/src/main/java/com/perun/tlinq/entity/hotel/OnlineHotelSupplierI.java
This interface defines the contract that any online hotel supplier plugin must implement. It allows swapping GoGlobal for another supplier by implementing the interface and changing the config property.
public interface OnlineHotelSupplierI {
/** Supplier identifier (e.g., "goglobal"). */
String getSupplierName();
/**
* Search for hotel offers by city/hotel, dates, and room configuration.
* Maps to GoGlobal Op 11 (Availability Search).
*/
List<CHotelOffer> searchOffers(CHotelSearch search) throws TlinqClientException;
/**
* Validate an offer's current price and cancellation terms.
* Maps to GoGlobal Op 9 (Valuation).
*/
CHotelOffer valuateOffer(String hotelSearchCode) throws TlinqClientException;
/**
* Create a hotel booking.
* Maps to GoGlobal Op 2 (Insert Booking).
*/
Object createBooking(String hotelSearchCode, Object bookingData)
throws TlinqClientException;
/**
* Check booking status.
* Maps to GoGlobal Op 5 (Booking Status).
*/
Object getBookingStatus(String bookingCode) throws TlinqClientException;
/**
* Get full booking details.
* Maps to GoGlobal Op 4 (Booking Details).
*/
Object getBookingDetails(String bookingCode) throws TlinqClientException;
/**
* Cancel a booking.
* Maps to GoGlobal Op 3 (Cancel Booking).
*/
Object cancelBooking(String bookingCode) throws TlinqClientException;
/**
* Search hotels by name keyword from cached static data.
* Uses local goglobal.hotel table (ILIKE).
*/
List searchHotelsByName(String keyword, String countryCode, Integer max)
throws TlinqClientException;
/**
* Get hotels in a specific city from cached static data.
* Uses local goglobal.hotel table.
*/
List getHotelsByCity(String cityCode, Integer radius, String ratings)
throws TlinqClientException;
/**
* Get nightly price breakdown per room.
* Maps to GoGlobal Op 14 (Price Breakdown).
*/
Object getPriceBreakdown(String hotelSearchCode) throws TlinqClientException;
}
4.2 OnlineHotelSupplierRegistry¶
Location: tqapp/src/main/java/com/perun/tlinq/entity/hotel/OnlineHotelSupplierRegistry.java
Singleton registry that resolves the active supplier at runtime based on configuration.
public class OnlineHotelSupplierRegistry {
private static final Map<String, OnlineHotelSupplierI> suppliers =
new ConcurrentHashMap<>();
public static void register(String factoryName, OnlineHotelSupplierI supplier) {
suppliers.put(factoryName, supplier);
}
public static void unregister(String factoryName) {
suppliers.remove(factoryName);
}
public static OnlineHotelSupplierI getActiveSupplier() {
String active = ClientConfig.instance()
.getAppProperty("hotel.online.supplier");
return suppliers.get(active);
}
public static boolean hasActiveSupplier() {
return getActiveSupplier() != null;
}
}
4.3 Runtime Resolution Flow¶
HotelApi.searchHotelOffers(reqData)
|
+--> Extract searchMode (default: "online")
|
+--> if "online":
| OnlineHotelSupplierRegistry.getActiveSupplier()
| --> reads "hotel.online.supplier" = "GoGlobalServiceFactory"
| --> returns GoGlobalHotelFacade instance
| --> GoGlobalHotelFacade.searchOffers(search)
|
+--> if "contracted":
NTSHotelFacade.searchAllHotels(criteria)
5. SOAP Communication¶
5.1 GoGlobalSoapClient Architecture¶
Class: com.perun.tlinq.client.goglobal.remote.GoGlobalSoapClient
The GoGlobal API uses SOAP 1.2 with a specific envelope structure per API spec v3.16.8 section 5.1. The SOAP envelope uses the gg: namespace prefix for GoGlobal elements and carries two child elements: <gg:requestType> for the numeric operation code, and <gg:xmlRequest> for the CDATA-wrapped XML request body.
Envelope Structure:
<?xml version="1.0" encoding="utf-8"?>
<soap12:Envelope xmlns:soap12="http://www.w3.org/2003/05/soap-envelope"
xmlns:gg="http://www.goglobal.travel/">
<soap12:Body>
<gg:MakeRequest>
<gg:requestType>11</gg:requestType>
<gg:xmlRequest><![CDATA[
<Root>
<Header>
<Agency>AGENCY_ID</Agency>
<User>USERNAME</User>
<Password>PASSWORD</Password>
<Operation>HOTEL_SEARCH_REQUEST</Operation>
<OperationType>Request</OperationType>
</Header>
<Main Version="2.4" ResponseFormat="JSON">
<!-- Operation-specific content -->
</Main>
</Root>
]]></gg:xmlRequest>
</gg:MakeRequest>
</soap12:Body>
</soap12:Envelope>
Key Design Points:
- The <gg:requestType> element carries the numeric operation code (e.g., 11, 9, 2) -- not the XML body
- The <gg:xmlRequest> element carries the full <Root> XML request wrapped in <![CDATA[...]]> to prevent namespace conflicts
- The <Root> element contains <Header> (auth + operation name) and <Main> (request payload with version attributes)
- The <Header>/<Operation> field contains the spec-defined operation name (e.g., HOTEL_SEARCH_REQUEST), not the numeric code
- The <Main> tag carries a Version attribute for operations that require it, and ResponseFormat="JSON" for Op 11
- Response format is JSON for Op 11 (availability) only; all other operations return XML
- Error responses from GoGlobal may come as XML even for Op 11
HTTP Headers:
| Header | Value | Description |
|---|---|---|
Content-Type |
application/soap+xml; charset=utf-8 |
SOAP 1.2 content type |
API-Operation |
Operation code (e.g., 11, 9, 2) |
Numeric operation identifier |
API-AgencyID |
Agency ID string | Agency identification |
Transport:
- Uses java.net.http.HttpClient (Java 11+)
- Endpoint URL configurable via goglobal.apiServer property in goglobal-client.xml
- All communication over HTTPS
- Connection and read timeouts configured via GoGlobalClientConfig.getTimeout()
5.2 Client Implementation Pattern¶
The sendRequest method accepts an int operation code and the full inner XML request body (the complete <Root>...</Root> document built by GoGlobalRequestBuilder). It wraps this in a SOAP 1.2 envelope with the operation code in <gg:requestType> and the XML in <gg:xmlRequest> CDATA.
public class GoGlobalSoapClient {
private static final String SOAP_NS = "http://www.w3.org/2003/05/soap-envelope";
private static final String GG_NS = "http://www.goglobal.travel/";
private final HttpClient httpClient;
private final GoGlobalClientConfig config;
public GoGlobalSoapClient() {
this.config = GoGlobalClientConfig.instance();
this.httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofMillis(config.getTimeout()))
.build();
}
/**
* Send a SOAP request and return the raw response body.
* @param operationCode The GoGlobal operation number (e.g., 11, 9, 2)
* @param requestXml The complete inner XML request (<Root>...</Root>)
* @return Raw response string (JSON for Op 11, XML for others)
*/
public String sendRequest(int operationCode, String requestXml)
throws TlinqClientException {
String soapEnvelope = buildSoapEnvelope(operationCode, requestXml);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(config.getApiUrl()))
.timeout(Duration.ofMillis(config.getTimeout()))
.header("Content-Type", "application/soap+xml; charset=utf-8")
.header("API-Operation", String.valueOf(operationCode))
.header("API-AgencyID", config.getAgencyId())
.POST(HttpRequest.BodyPublishers.ofString(soapEnvelope))
.build();
HttpResponse<String> response = httpClient.send(request,
HttpResponse.BodyHandlers.ofString());
return extractResponseContent(response.body());
}
private String buildSoapEnvelope(int operationCode, String requestXml) {
return "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
"<soap12:Envelope xmlns:soap12=\"" + SOAP_NS +
"\" xmlns:gg=\"" + GG_NS + "\">" +
"<soap12:Body>" +
"<gg:MakeRequest>" +
"<gg:requestType>" + operationCode + "</gg:requestType>" +
"<gg:xmlRequest><![CDATA[" + requestXml + "]]></gg:xmlRequest>" +
"</gg:MakeRequest>" +
"</soap12:Body>" +
"</soap12:Envelope>";
}
}
Response Extraction: The response body is extracted from the <MakeRequestResult> element in the SOAP response. HTML entity encoding (<, >, etc.) is decoded automatically, yielding either raw JSON (Op 11) or raw XML (all other operations).
6. Request Building¶
6.1 GoGlobalRequestBuilder¶
Class: com.perun.tlinq.client.goglobal.remote.GoGlobalRequestBuilder
Provides one method per GoGlobal operation type. Each method builds the complete <Root><Header><Main>...</Main></Root> XML document per the GoGlobal API spec v3.16.8 section 5.5. The builder handles:
- Constructing the
<Header>with credentials (Agency,User,Password) fromGoGlobalClientConfig - Setting the spec-defined
<Operation>name (e.g.,HOTEL_SEARCH_REQUEST) - Setting
<OperationType>Request</OperationType> - Setting the
<Main>tag with the correctVersionattribute and any extra attributes (e.g.,ResponseFormat="JSON") - Populating operation-specific fields inside
<Main> - XML-escaping all user-supplied values to prevent injection
Operation Names and Versions:
Each operation uses its spec-mandated operation name and version attribute on <Main>:
| Op | Operation Name | Version | Extra Attributes |
|---|---|---|---|
| 11 | HOTEL_SEARCH_REQUEST |
2.4 | ResponseFormat="JSON" |
| 9 | BOOKING_VALUATION_REQUEST |
2.0 | -- |
| 2 | BOOKING_INSERT_REQUEST |
2.3 | -- |
| 5 | BOOKING_STATUS_REQUEST |
(none) | -- |
| 4 | BOOKING_SEARCH_REQUEST |
2.3 | -- |
| 3 | BOOKING_CANCEL_REQUEST |
(none) | -- |
| 6 | HOTEL_INFO_REQUEST |
2.2 | -- |
| 61 | HOTEL_INFO_REQUEST |
2.2 | -- |
| 8 | VOUCHER_DETAILS_REQUEST |
2.3 | -- |
| 10 | ADV_BOOKING_SEARCH_REQUEST |
1 | -- |
| 14 | PRICE_BREAKDOWN_REQUEST |
(none) | -- |
| 15 | BOOKING_INFO_FOR_AMENDMENT_REQUEST |
(none) | -- |
| 16 | BOOKING_AMENDMENT_REQUEST |
(none) | -- |
Methods:
| Method | Op | Spec Operation Name | Description |
|---|---|---|---|
buildAvailabilityRequest(cityCode, checkIn, checkOut, rooms, adults, children, nationality, currency) |
11 | HOTEL_SEARCH_REQUEST | Hotel availability search |
buildValuationRequest(String hotelSearchCode) |
9 | BOOKING_VALUATION_REQUEST | Offer validation |
buildBookingRequest(hotelSearchCode, clientRef, title, firstName, lastName, email, phone, arrivalTime) |
2 | BOOKING_INSERT_REQUEST | Create booking |
buildCancellationRequest(String goBookingCode) |
3 | BOOKING_CANCEL_REQUEST | Cancel booking |
buildBookingDetailsRequest(String goBookingCode) |
4 | BOOKING_SEARCH_REQUEST | Get booking details |
buildBookingStatusRequest(String goBookingCode) |
5 | BOOKING_STATUS_REQUEST | Check booking status |
buildHotelInfoRequest(String hotelId) |
6 | HOTEL_INFO_REQUEST | Get hotel info by ID |
buildHotelInfoBySearchCodeRequest(String hotelSearchCode) |
61 | HOTEL_INFO_REQUEST | Get hotel info by search code |
buildVoucherRequest(String goBookingCode) |
8 | VOUCHER_DETAILS_REQUEST | Generate voucher |
buildAdvancedBookingSearchRequest(dateFrom, dateTo, status, clientRef) |
10 | ADV_BOOKING_SEARCH_REQUEST | Search bookings |
buildPriceBreakdownRequest(String hotelSearchCode) |
14 | PRICE_BREAKDOWN_REQUEST | Nightly price breakdown |
buildAmendmentOptionsRequest(String goBookingCode) |
15 | BOOKING_INFO_FOR_AMENDMENT_REQUEST | Get amendment options |
buildAmendmentRequest(String goBookingCode, String hotelSearchCode) |
16 | BOOKING_AMENDMENT_REQUEST | Submit amendment |
6.2 Availability Request Example (Op 11)¶
The availability request uses ResponseFormat="JSON" on the <Main> tag per the spec, which states "Using the JSON response format is mandatory" for the availability operation. Room configuration uses the <Rooms><Room/> structure with Adults, RoomCount, and ChildCount attributes.
<Root>
<Header>
<Agency>AGENCY_ID</Agency>
<User>USERNAME</User>
<Password>PASSWORD</Password>
<Operation>HOTEL_SEARCH_REQUEST</Operation>
<OperationType>Request</OperationType>
</Header>
<Main Version="2.4" ResponseFormat="JSON">
<CityCode>1234</CityCode>
<ArrivalDate>2026-03-15</ArrivalDate>
<Nights>3</Nights>
<Rooms>
<Room Adults="2" RoomCount="1" ChildCount="0"/>
</Rooms>
<Nationality>GB</Nationality>
<Currency>USD</Currency>
</Main>
</Root>
6.3 Booking Request Example (Op 2)¶
<Root>
<Header>
<Agency>AGENCY_ID</Agency>
<User>USERNAME</User>
<Password>PASSWORD</Password>
<Operation>BOOKING_INSERT_REQUEST</Operation>
<OperationType>Request</OperationType>
</Header>
<Main Version="2.3">
<HotelSearchCode>ABC123XYZ</HotelSearchCode>
<ClientBookingCode>TQ-BK-2026-0001</ClientBookingCode>
<Leader>
<Title>MR.</Title>
<FirstName>John</FirstName>
<LastName>Smith</LastName>
</Leader>
<Email>john.smith@example.com</Email>
<Phone>+44123456789</Phone>
</Main>
</Root>
6.4 Valuation Request Example (Op 9)¶
<Root>
<Header>
<Agency>AGENCY_ID</Agency>
<User>USERNAME</User>
<Password>PASSWORD</Password>
<Operation>BOOKING_VALUATION_REQUEST</Operation>
<OperationType>Request</OperationType>
</Header>
<Main Version="2.0">
<HotelSearchCode>ABC123XYZ</HotelSearchCode>
</Main>
</Root>
6.5 Cancellation Request Example (Op 3)¶
Operations without a version attribute (such as Op 3, 5, 14, 15, 16) emit <Main> without any attributes.
<Root>
<Header>
<Agency>AGENCY_ID</Agency>
<User>USERNAME</User>
<Password>PASSWORD</Password>
<Operation>BOOKING_CANCEL_REQUEST</Operation>
<OperationType>Request</OperationType>
</Header>
<Main>
<GoBookingCode>GG-123456</GoBookingCode>
</Main>
</Root>
7. Response Parsing¶
7.1 GoGlobalResponseParser¶
Class: com.perun.tlinq.client.goglobal.remote.GoGlobalResponseParser
Handles two response formats:
- JSON for Operation 11 (availability search) -- parsed with Gson (com.google.gson.Gson)
- XML for all other operations -- parsed with DOM (javax.xml.parsers.DocumentBuilder)
The parser also handles the case where GoGlobal returns an XML error response for Op 11 instead of JSON. If the Op 11 response starts with <?xml or <Root, it is parsed as XML and checked for error elements. The XML parser includes XXE prevention (disabled DTDs, external entities, and external parameter entities) for security.
7.2 Availability Response (JSON - Op 11)¶
The availability response is returned as JSON when ResponseFormat="JSON" is set on the request <Main> tag. Per the GoGlobal spec v3.16.8, using the JSON response format is mandatory for the availability operation.
Important: Rooms is List<String> (room type names). The Rooms field in each offer is an array of room type name strings (e.g., ["DOUBLE OCEAN VIEW", "VILLA BEACH FRONT"]), not an integer or an array of objects. The list size indicates the number of rooms, and each entry is the room type description for that room.
{
"Hotels": [
{
"HotelName": "Grand Hotel Dubai",
"HotelCode": "56789",
"Category": "5",
"CityId": 1234,
"Offers": [
{
"HotelSearchCode": "HSC-2026031500001",
"CxlDeadline": "2026-03-13T23:59:59",
"NonRef": false,
"Rooms": ["DOUBLE OCEAN VIEW"],
"RoomBasis": "BB",
"RoomType": "Deluxe Double",
"BoardBasis": "Bed & Breakfast",
"AvailFlag": "Y",
"TotalPrice": 450.00,
"Currency": "USD",
"Category": "5"
}
]
}
]
}
DTO: GGAvailabilityResponse
The DTO uses uppercase field names matching the JSON keys directly (Gson maps by field name):
public class GGAvailabilityResponse {
private List<HotelResult> Hotels;
public static class HotelResult {
private String HotelCode;
private String HotelName;
private String Category;
private Integer CityId;
private List<Offer> Offers;
}
public static class Offer {
private String HotelSearchCode;
private Double TotalPrice;
private String Currency;
private String CxlDeadline;
private Boolean NonRef;
private List<String> Rooms; // Room type names, NOT objects
private String RoomBasis;
private String Category;
private String RoomType;
private String BoardBasis;
private String AvailFlag; // "Y" or "N"
}
}
Rooms field mapping in service/facade layers:
The GoGlobalHotelService and GoGlobalHotelFacade map the Rooms list as follows:
- rooms.size() determines the room count (mapped to GGHotelOffer.rooms as int and CHotelOffer.beds)
- rooms.get(0) provides the primary room type description (mapped to GGHotelOffer.roomType and CHotelOffer.roomType)
- String.join(", ", rooms) provides the full room description (mapped to CHotelOffer.roomDescription)
- If Rooms is null or empty, offer.getRoomType() is used as fallback
7.3 Valuation Response (XML - Op 9)¶
<Root>
<Header>
<Agency>AGENCY_ID</Agency>
<Operation>9</Operation>
<OperationType>Response</OperationType>
</Header>
<Main>
<HotelSearchCode>HSC-2026031500001</HotelSearchCode>
<ArrivalDate>2026-03-15</ArrivalDate>
<CancellationDeadline>2026-03-13T23:59:59</CancellationDeadline>
<Remarks>Check-in from 14:00. Late checkout subject to availability.</Remarks>
<Rates>
<Rate>
<RoomRate>150.00</RoomRate>
<Currency>USD</Currency>
</Rate>
</Rates>
<TotalTax>45.00</TotalTax>
<NewRate>450.00</NewRate>
<NonRef>false</NonRef>
<CancellationPolicies>
<Policy>
<Id>1</Id>
<Starting>2026-03-13T00:00:00</Starting>
<BasedOn>BOOKINGPRICE</BasedOn>
<Mode>PCT</Mode>
<Value>100</Value>
</Policy>
</CancellationPolicies>
</Main>
</Root>
7.4 Booking Response (XML - Op 2)¶
<Root>
<Header>
<Agency>AGENCY_ID</Agency>
<Operation>2</Operation>
<OperationType>Response</OperationType>
</Header>
<Main>
<GoBookingCode>GG-123456</GoBookingCode>
<GoReference>REF-789012</GoReference>
<BookingStatus>C</BookingStatus>
<TotalPrice>450.00</TotalPrice>
<Currency>USD</Currency>
<HotelId>56789</HotelId>
<HotelName>Grand Hotel Dubai</HotelName>
<HotelConfirmation>HC-GRAND-2026-001</HotelConfirmation>
<Rooms>
<Room Id="1" Adults="2" RoomBasis="BB">
<Pax Title="MR." FirstName="John" LastName="Smith"/>
<Pax Title="MRS." FirstName="Jane" LastName="Smith"/>
</Room>
</Rooms>
<Leader Title="MR." FirstName="John" LastName="Smith"/>
<PaymentTransactions/>
</Main>
</Root>
8. Entity Mapping¶
8.1 GGHotelOffer to CHotelOffer¶
The EntityTransformer uses field mappings defined in config/entities/hotel-entities.xml to convert between native GGHotelOffer and canonical CHotelOffer.
| CHotelOffer (canonical) | GGHotelOffer (native) | Mapping Type | Notes |
|---|---|---|---|
offerId |
hotelSearchCode |
DirectMapping | Primary key for offer operations |
hotelId |
hotelCode |
DirectMapping | GoGlobal hotel ID |
hotelName |
hotelName |
DirectMapping | |
cityCode |
cityId |
DirectMapping | GoGlobal city ID |
available |
available |
DirectMapping | Availability status |
checkInDate |
checkInDate |
DirectMapping | Arrival date |
checkOutDate |
checkOutDate |
DirectMapping | Calculated: arrival + nights |
roomType |
rooms |
DirectMapping | Room type description |
roomCategory |
category |
DirectMapping | Star rating code |
currencyCode |
currency |
DirectMapping | 3-letter currency code |
totalAmount |
totalPrice |
DirectMapping | Total stay price |
cancellationType |
cancellationType |
DirectMapping | "NON_REFUNDABLE" or "REFUNDABLE" (set by service from nonRef boolean) |
cancellationDeadline |
cxlDeadline |
DirectMapping | CXL deadline datetime |
boardType |
roomBasis |
DirectMapping | Meal plan code |
Entity Configuration (hotel-entities.xml):
<Factory name="GoGlobalServiceFactory"
nativeEntity="com.perun.tlinq.client.goglobal.entity.GGHotelOffer">
<ServiceList>
<Service name="searchAvailability" action="search"
returnClass="com.perun.tlinq.entity.hotel.CHotelOffer"/>
</ServiceList>
<FieldMappingList>
<FieldMapping targetField="offerId" sourceField="hotelSearchCode"
mapping="DirectMapping"/>
<FieldMapping targetField="hotelId" sourceField="hotelCode"
mapping="DirectMapping"/>
<FieldMapping targetField="hotelName" sourceField="hotelName"
mapping="DirectMapping"/>
<FieldMapping targetField="cityCode" sourceField="cityId"
mapping="DirectMapping"/>
<FieldMapping targetField="available" sourceField="available"
mapping="DirectMapping"/>
<FieldMapping targetField="checkInDate" sourceField="checkInDate"
mapping="DirectMapping"/>
<FieldMapping targetField="checkOutDate" sourceField="checkOutDate"
mapping="DirectMapping"/>
<FieldMapping targetField="roomType" sourceField="rooms"
mapping="DirectMapping"/>
<FieldMapping targetField="roomCategory" sourceField="category"
mapping="DirectMapping"/>
<FieldMapping targetField="currencyCode" sourceField="currency"
mapping="DirectMapping"/>
<FieldMapping targetField="totalAmount" sourceField="totalPrice"
mapping="DirectMapping"/>
<FieldMapping targetField="cancellationType" sourceField="cancellationType"
mapping="DirectMapping"/>
<FieldMapping targetField="cancellationDeadline" sourceField="cxlDeadline"
mapping="DirectMapping"/>
<FieldMapping targetField="boardType" sourceField="roomBasis"
mapping="DirectMapping"/>
</FieldMappingList>
</Factory>
8.2 GGBooking Fields¶
| Field | Type | Description | Source Operation |
|---|---|---|---|
goBookingCode |
String | Unique GoGlobal booking ID | Op 2 response |
goReference |
String | Supplier reference | Op 2 response |
bookingStatus |
String | Status code (C, RQ, RJ, X, XP, RX, VCH) | Op 2/5 response |
totalPrice |
BigDecimal | Total booking price | Op 2/4 response |
currency |
String | Currency code | Op 2/4 response |
hotelId |
String | GoGlobal hotel ID | Op 2/4 response |
hotelName |
String | Hotel name | Op 2/4 response |
hotelConfirmation |
String | Hotel's own confirmation number | Op 4 response |
arrivalDate |
Date | Check-in date | Op 2/4 response |
nights |
int | Number of nights | Op 2/4 response |
clientBookingCode |
String | Agency reference | Op 2 request |
commPercent |
BigDecimal | Commission percentage | Op 4 response |
commValue |
BigDecimal | Commission amount | Op 4 response |
cancellationDeadline |
Date | Free cancellation deadline | Op 4/9 response |
voucherUrl |
String | PDF voucher download URL | Op 8 response |
8.3 GGHotelInfo Fields¶
| Field | Type | Description | Source |
|---|---|---|---|
hotelId |
int | GoGlobal hotel ID | Op 6/61 or cache |
hotelName |
String | Hotel name | Op 6/61 or cache |
cityId |
int | City ID | Op 6/61 or cache |
countryId |
int | Country ID | Op 6/61 or cache |
starRating |
int | Star rating code (1-11) | Op 6/61 or cache |
address |
String | Hotel address | Op 6/61 or cache |
description |
String | Hotel description | Op 6/61 or cache |
thumbnail |
String | Thumbnail image URL | Op 6/61 or cache |
latitude |
Double | Latitude (Op 61 only) | Op 61 or cache |
longitude |
Double | Longitude (Op 61 only) | Op 61 or cache |
hotelFacilities |
List |
Hotel-level facilities | Op 6/61 |
roomFacilities |
List |
Room-level facilities | Op 6/61 |
8.4 CHotelSearch Entity and Search Persistence¶
The hotel search flow persists search criteria and results in a 3-table chain for pagination support via hotel/fetchOfferResults.
CHotelSearch (Canonical Entity)¶
Class: com.perun.tlinq.entity.hotel.CHotelSearch
The canonical search criteria entity carries all parameters needed for a hotel search, including the nationality field added for GoGlobal pricing:
| Field | Type | Description |
|---|---|---|
hotelSearchId |
Integer | Auto-generated PK |
searchId |
String | Links to CPersistentSearch |
sessionId |
String | User session ID |
searchBaseId |
Integer | FK to nts.searchbase |
cityCode |
String | GoGlobal city ID |
hotelId |
String | Specific hotel ID (optional) |
checkInDate |
String | Arrival date (YYYY-MM-DD) |
checkOutDate |
String | Departure date |
roomQuantity |
Integer | Number of rooms |
adults |
Integer | Number of adults |
children |
Integer | Number of children |
nationality |
String | Guest nationality (ISO-2, default "AE") |
currency |
String | Preferred currency |
starRating |
String | Star rating filter |
boardType |
String | Board basis filter |
HotelsearchEntity (NTS Native Entity)¶
Class: com.perun.tlinq.client.nts.entity.hotel.HotelsearchEntity
JPA entity mapped to nts.hotelsearch table. Uses sequence generator nts.hotelsearch_seq for ID. All fields annotated with @TlinqEntityField.
Entity Configuration (hotel-entities.xml)¶
<Entity name="HotelSearch"
class="com.perun.tlinq.entity.hotel.CHotelSearch"
idField="hotelSearchId"
defaultFactory="NTSServiceFactory">
<EntityFactoryList>
<Factory name="NTSServiceFactory"
nativeEntity="com.perun.tlinq.client.nts.entity.hotel.HotelsearchEntity">
<ServiceList>
<Service name="getHotelSearch" action="read"/>
<Service name="getHotelSearch" action="search"/>
<Service name="createHotelSearch" action="create"/>
<Service name="updateHotelSearch" action="update"/>
</ServiceList>
<FieldMappingList>
<!-- All fields use DirectMapping -->
<FieldMapping targetField="nationality" sourceField="nationality"
mapping="DirectMapping"/>
<!-- ... other field mappings ... -->
</FieldMappingList>
</Factory>
</EntityFactoryList>
</Entity>
3-Table Persistence Chain¶
The HotelSearchFacade.saveGroupedResults() method persists search results in three related tables.
Results are grouped by hotel — each CSearchResult row represents one hotel containing all its room offers as a JSON array.
1. CPersistentSearch → nts.searchbase
- searchId (UUID), searchType="HOTELSEARCH", searchStart, searchExpiry (+30min)
- resultCount = number of unique hotels (not individual offers)
2. CHotelSearch → nts.hotelsearch
- searchId (links to searchbase), sessionId, searchBaseId (FK)
- All search criteria: cityCode, checkIn/Out, rooms, adults, children, nationality
3. CSearchResult[] → nts.searchres (one row per HOTEL)
- searchId, searchBaseId (FK)
- sourceObject = JSON array of all CHotelOffer objects for that hotel (via Gson)
- groupKey = hotelId, groupDesc = hotelName, sortIndex (hotel sort order)
Flow:
HotelSearchFacade.getAndSaveSearchResults(CHotelSearch, resPerPage, sortMethod)
|
+--> executeBasicSearch(search, resPerPage)
| --> OnlineHotelSupplierRegistry.getActiveSupplier().searchOffers(search)
| --> Returns List<CHotelOffer> (flat list of all room offers)
|
+--> groupByHotel(offers)
| --> Map<String, List<CHotelOffer>> grouped by hotelId (LinkedHashMap)
|
+--> sortHotelGroups(groups, sortMethod)
| --> Sorts hotel groups by min price (ASC/DESC) or hotel name
|
+--> saveGroupedResults(search, sortedGroups)
|
+--> 1. Create CPersistentSearch (searchId=UUID, type="HOTELSEARCH")
| resultCount = sortedGroups.size() (hotel count)
| ef.write(persistentSearch) --> nts.searchbase
|
+--> 2. Set search.searchId, search.searchBaseId
| ef.write(search) --> nts.hotelsearch
|
+--> 3. For each hotel group (Map.Entry<hotelId, List<CHotelOffer>>):
Create CSearchResult(
sourceObject = gson.toJson(group.getValue()), // JSON array
groupKey = hotelId,
groupDesc = hotelName,
sortIndex = hotel position)
ef.write(sres) --> nts.searchres
Pagination: Page size is configurable via goglobal.hotel.search.pageSize in goglobal-client.xml (default: 20). Each page returns N hotels with all their room offers. The getSearchResults() method queries nts.searchres by sortIdx range, deserializes each sourceObject JSON array back into CHotelOffer[] via gson.fromJson(so, CHotelOffer[].class), and flattens all offers into the response.
Note: The stored JSON uses canonical field names (e.g., totalAmount, currencyCode, offerId), not GoGlobal native field names.
Nationality Flow Through the Stack¶
The nationality field flows through four layers from API to SOAP request:
1. HotelApi.doSearchHotelOffers()
- Extracts: nationality = ApiUtil.gmp(reqData, "nationality", String.class, false)
- Defaults: if null → "AE"
- Sets: chs.setNationality(nationality)
2. CHotelSearch (canonical entity)
- Field: protected String nationality
- Persisted to nts.hotelsearch via EntityFacade.write()
3. GoGlobalHotelService.searchAvailability()
- Reads: String nationality = (String) getNamedParam("nationality")
- Note: Field name is "nationality" in both canonical and native entities (no mapping transformation)
- Passes to: requestBuilder.buildAvailabilityRequest(..., nationality, currency)
4. GoGlobalRequestBuilder.buildAvailabilityRequest()
- Conditionally emits: <Nationality>AE</Nationality>
- Only included when nationality is non-null and non-empty
9. Static Data Cache¶
9.1 Database Schema (goglobal schema)¶
SQL: config/sql/goglobal-schema.sql
CREATE SCHEMA IF NOT EXISTS goglobal;
-- Countries
CREATE TABLE goglobal.country (
country_id INTEGER PRIMARY KEY,
country_name VARCHAR(200),
country_code VARCHAR(10),
last_updated TIMESTAMP DEFAULT NOW()
);
-- Cities
CREATE TABLE goglobal.city (
city_id INTEGER PRIMARY KEY,
city_name VARCHAR(200),
country_id INTEGER REFERENCES goglobal.country(country_id),
last_updated TIMESTAMP DEFAULT NOW()
);
-- Hotels (reference data)
CREATE TABLE goglobal.hotel (
hotel_id INTEGER PRIMARY KEY,
hotel_name VARCHAR(500),
city_id INTEGER REFERENCES goglobal.city(city_id),
star_rating INTEGER,
address VARCHAR(500),
latitude DOUBLE PRECISION,
longitude DOUBLE PRECISION,
thumbnail_url VARCHAR(500),
description TEXT,
last_updated TIMESTAMP DEFAULT NOW()
);
-- Star rating reference
CREATE TABLE goglobal.star_rating (
star_id INTEGER PRIMARY KEY,
star_name VARCHAR(50),
star_value NUMERIC(2,1)
);
-- Refresh audit log
CREATE TABLE goglobal.refresh_log (
id SERIAL PRIMARY KEY,
refresh_type VARCHAR(50),
refresh_date TIMESTAMP DEFAULT NOW(),
record_count INTEGER,
status VARCHAR(20),
error_message TEXT
);
-- Indexes for common queries
CREATE INDEX idx_hotel_name ON goglobal.hotel (hotel_name);
CREATE INDEX idx_hotel_city ON goglobal.hotel (city_id);
CREATE INDEX idx_city_country ON goglobal.city (country_id);
9.2 JPA Entities¶
Package: com.perun.tlinq.client.goglobal.db
All JPA entities use the goglobal schema qualifier. Pattern follows existing tqryb2b/db/ entities.
| Class | Table | Purpose |
|---|---|---|
GGCountryEntity |
goglobal.country |
Cached country data |
GGCityEntity |
goglobal.city |
Cached city data with country FK |
GGHotelEntity |
goglobal.hotel |
Cached hotel reference data |
GGStarRatingEntity |
goglobal.star_rating |
Star rating code reference |
GGRefreshLogEntity |
goglobal.refresh_log |
Refresh audit entries |
Example JPA Entity:
@Entity
@Table(name = "hotel", schema = "goglobal")
public class GGHotelEntity {
@Id
@Column(name = "hotel_id")
private Integer hotelId;
@Column(name = "hotel_name", length = 500)
private String hotelName;
@Column(name = "city_id")
private Integer cityId;
@Column(name = "star_rating")
private Integer starRating;
@Column(name = "address", length = 500)
private String address;
@Column(name = "latitude")
private Double latitude;
@Column(name = "longitude")
private Double longitude;
@Column(name = "thumbnail_url", length = 500)
private String thumbnailUrl;
@Column(name = "description", columnDefinition = "TEXT")
private String description;
@Column(name = "last_updated")
private Timestamp lastUpdated;
// Getters and setters
}
9.3 Refresh Mechanism¶
StaticDataRefresher handles the actual data fetching and database operations:
StaticDataRefresher.refreshAll()
|
+--> 1. Fetch countries from GoGlobal static data feed
| - Bulk insert/update into goglobal.country
|
+--> 2. Fetch cities from GoGlobal static data feed
| - Bulk insert/update into goglobal.city
| - Link to country via country_id FK
|
+--> 3. Fetch hotels from GoGlobal static data feed
| - Bulk insert/update into goglobal.hotel
| - Link to city via city_id FK
| - Include star rating, address, coordinates, thumbnail
|
+--> 4. Log refresh in goglobal.refresh_log
- refresh_type: "FULL"
- record_count: total records processed
- status: "COMPLETED" or "FAILED"
- error_message: if failed
SDRefreshRunner (Runnable) is scheduled by the plugin:
public class SDRefreshRunner implements Runnable {
private final StaticDataRefresher refresher;
private final int refreshIntervalDays;
@Override
public void run() {
// Check if refresh is needed
GGRefreshLogEntity lastRefresh = getLastSuccessfulRefresh();
if (lastRefresh == null || daysSince(lastRefresh.getRefreshDate()) >= refreshIntervalDays) {
try {
refresher.refreshAll();
} catch (Exception e) {
logRefreshFailure(e);
}
}
}
}
9.4 Cache Usage Patterns¶
Search hotels by name:
SELECT * FROM goglobal.hotel
WHERE hotel_name ILIKE '%keyword%'
AND city_id = ? -- optional city filter
ORDER BY hotel_name
LIMIT 50
Get hotels by city:
Enrich availability results:
Resolve city code:
9.5 API Endpoints for Static Data Queries¶
Two HotelApi endpoints query the goglobal schema directly via GoGlobalDBSession, bypassing the SOAP layer entirely. These support the B2C frontend hotel search form's country/city selection.
hotel/searchCountries — Country autocomplete (3+ char keyword):
// HotelApi.doSearchCountries()
try (var session = GoGlobalDBSession.getSession()) {
List<GGCountryEntity> countries = session.createQuery(
"FROM GGCountryEntity WHERE LOWER(countryName) LIKE :kw ORDER BY countryName",
GGCountryEntity.class)
.setParameter("kw", "%" + keyword.toLowerCase() + "%")
.setMaxResults(20)
.getResultList();
// Returns: [{countryId, countryName, countryCode}, ...]
}
hotel/getCities — All cities for a country:
// HotelApi.doGetCities()
try (var session = GoGlobalDBSession.getSession()) {
List<GGCityEntity> cities = session.createQuery(
"FROM GGCityEntity WHERE countryId = :cid ORDER BY cityName",
GGCityEntity.class)
.setParameter("cid", countryId)
.getResultList();
// Returns: [{cityId, cityName, cityCode}, ...]
}
Key design points:
- Both endpoints use Hibernate try-with-resources on GoGlobalDBSession.getSession() for automatic session cleanup
- Results are returned as List<Map<String, Object>> (not entity objects) to avoid exposing JPA internals
- searchCountries requires minimum 3 characters (returns empty list otherwise)
- The cityId returned by getCities is the GoGlobal city ID used as cityCode in hotel/searchOffers
- Both endpoints are registered as guest,agent,admin in api-roles.properties
10. Dual-Mode Routing¶
10.1 HotelApi searchMode Parameter¶
Modified file: tqapi/src/main/java/com/perun/tlinq/api/HotelApi.java
The searchMode parameter is added to relevant hotel API endpoints to route between online (GoGlobal) and contracted (NTS) backends.
// In HotelApi.searchHotelOffers()
String searchMode = ApiUtil.gmp(reqData, "searchMode", String.class, false);
if (searchMode == null) searchMode = "online"; // default
if ("contracted".equals(searchMode)) {
ar = doSearchContractedHotels(session, reqData);
} else {
ar = doSearchHotelOffers(session, reqData); // uses OnlineHotelSupplierRegistry
}
10.2 Endpoint Routing Table¶
| Endpoint | searchMode=online |
searchMode=contracted |
Default |
|---|---|---|---|
hotel/searchOffers |
OnlineHotelSupplierI.searchOffers() |
NTSHotelFacade.searchAllHotels() |
online |
hotel/fetchOfferResults |
Paginated from persistent search | Same (uses saved results) | online |
hotel/searchByName |
OnlineHotelSupplierI.searchHotelsByName() -> cache |
NTSServiceFactory hotel search |
online |
hotel/getByCity |
OnlineHotelSupplierI.getHotelsByCity() -> cache |
N/A | online |
hotel/listHotels |
N/A (always contracted) | NTS contracted hotels | contracted |
hotel/searchCountries |
Direct DB query (goglobal.country) |
N/A | N/A (no searchMode) |
hotel/getCities |
Direct DB query (goglobal.city) |
N/A | N/A (no searchMode) |
Note: hotel/searchCountries and hotel/getCities do not use the searchMode parameter. They always query the local goglobal schema directly via GoGlobalDBSession, bypassing both the supplier interface and the entity framework.
10.3 HotelSearchFacade Refactoring¶
Modified file: tqapp/src/main/java/com/perun/tlinq/entity/hotel/HotelSearchFacade.java
The facade is refactored to be supplier-agnostic:
Before (hardcoded Amadeus):
executeBasicSearch() --> ef.search("HotelOffer", scl) via AmadeusServiceFactory
After (pluggable):
executeBasicSearch() --> OnlineHotelSupplierRegistry.getActiveSupplier().searchOffers()
The sorting and pagination logic in HotelSearchFacade operates on the CHotelOffer canonical entities regardless of source. Results are grouped by hotel ID before persisting — each CSearchResult row stores one hotel's complete set of room offers as a JSON array. Pagination is per-hotel (not per-offer), so each page returns N hotels with all their room offers. Page size is configurable via goglobal.hotel.search.pageSize in goglobal-client.xml (default: 20).
10.4 TripMakerApi Integration¶
Modified file: tqapi/src/main/java/com/perun/tlinq/api/TripMakerApi.java
The searchAccommodations() method in TripMakerApi uses HotelSearchFacade, which now delegates to the active online supplier. No hardcoded Amadeus dependency remains.
11. Configuration¶
11.1 goglobal-client.xml¶
Location: config/goglobal-client.xml
<GoGlobalPluginConfig>
<PluginProperties>
<property name="dbname" value="local"/>
<property name="goglobal.agency" value="AGENCY_ID"/>
<property name="goglobal.user" value="USERNAME"/>
<property name="goglobal.password" value="PASSWORD"/>
<property name="goglobal.apiServer" value="https://api.goglobal.travel"/>
<property name="goglobal.apiVersion" value="2.4"/>
<property name="goglobal.currency" value="USD"/>
<property name="goglobal.responseFormat" value="JSON"/>
<property name="goglobal.hotel.search.pageSize" value="20"/>
<property name="goglobal.staticdata.refresh.days" value="7"/>
</PluginProperties>
<Databases>
<Database name="local"
url="jdbc:postgresql://localhost:5432/tlinq"
username="tlinq"
password="TlinqAdmin"/>
</Databases>
<Services>
<Service name="searchAvailability"
class="com.perun.tlinq.client.goglobal.service.hotel.GoGlobalHotelService"
method="searchAvailability"
entity="com.perun.tlinq.client.goglobal.entity.GGHotelOffer"
idField="hotelSearchCode"/>
<Service name="valuateBooking"
class="com.perun.tlinq.client.goglobal.service.hotel.GoGlobalHotelService"
method="valuateBooking"
entity="com.perun.tlinq.client.goglobal.entity.GGHotelOffer"
idField="hotelSearchCode"/>
<Service name="createBooking"
class="com.perun.tlinq.client.goglobal.service.hotel.GoGlobalHotelService"
method="createBooking"
entity="com.perun.tlinq.client.goglobal.entity.GGBooking"
idField="goBookingCode"/>
<Service name="getBookingStatus"
class="com.perun.tlinq.client.goglobal.service.hotel.GoGlobalHotelService"
method="getBookingStatus"
entity="com.perun.tlinq.client.goglobal.entity.GGBooking"
idField="goBookingCode"/>
<Service name="getBookingDetails"
class="com.perun.tlinq.client.goglobal.service.hotel.GoGlobalHotelService"
method="getBookingDetails"
entity="com.perun.tlinq.client.goglobal.entity.GGBooking"
idField="goBookingCode"/>
<Service name="cancelBooking"
class="com.perun.tlinq.client.goglobal.service.hotel.GoGlobalHotelService"
method="cancelBooking"
entity="com.perun.tlinq.client.goglobal.entity.GGBooking"
idField="goBookingCode"/>
<Service name="searchHotelsByName"
class="com.perun.tlinq.client.goglobal.service.hotel.GoGlobalHotelService"
method="searchHotelsByName"
entity="com.perun.tlinq.client.goglobal.entity.GGHotelInfo"
idField="hotelId"/>
<Service name="getHotelsByCity"
class="com.perun.tlinq.client.goglobal.service.hotel.GoGlobalHotelService"
method="getHotelsByCity"
entity="com.perun.tlinq.client.goglobal.entity.GGHotelInfo"
idField="hotelId"/>
<Service name="getHotelInfo"
class="com.perun.tlinq.client.goglobal.service.hotel.GoGlobalHotelService"
method="getHotelInfo"
entity="com.perun.tlinq.client.goglobal.entity.GGHotelInfo"
idField="hotelId"/>
<Service name="getPriceBreakdown"
class="com.perun.tlinq.client.goglobal.service.hotel.GoGlobalHotelService"
method="getPriceBreakdown"
entity="com.perun.tlinq.client.goglobal.entity.GGPriceBreakdown"
idField="hotelSearchCode"/>
</Services>
</GoGlobalPluginConfig>
11.2 tourlinq-config.xml Additions¶
Three additions to the main configuration:
- Plugin registration under
<Plugins>(see Section 3.2) - Service factory under
<ServiceFactories>(see Section 3.2) - App property under
<AppProperties>:
11.3 hotel-entities.xml Changes¶
Modified file: config/entities/hotel-entities.xml
- Disable Amadeus factories (set
enabled="false"): - HotelOffer entity -> AmadeusServiceFactory
- HotelByName entity -> AmadeusServiceFactory
-
HotelByCity entity -> AmadeusServiceFactory
-
Add GoGlobal factory to HotelOffer, HotelByName, HotelByCity entities (see Section 8.1 for field mappings)
11.4 api-roles.properties Updates¶
Modified file: config/api-roles.properties
Add hotel endpoints with guest access for B2C frontend:
hotel/searchOffers=guest,agent,admin
hotel/fetchOfferResults=guest,agent,admin
hotel/searchByName=guest,agent,admin
hotel/getByCity=guest,agent,admin
hotel/searchCountries=guest,agent,admin
hotel/getCities=guest,agent,admin
hotel/valuateOffer=guest,agent,admin
hotel/getPriceBreakdown=guest,agent,admin
hotel/getHotel=guest,agent,admin
hotel/createBooking=guest,agent,admin
Note: hotel/createBooking is set to guest access because the B2C post-payment flow runs under a guest session. This is safe because booking requires a valid HotelSearchCode that went through the payment gateway.
12. Error Handling¶
12.1 GoGlobal Error Codes¶
The GoGlobal API returns error codes in the response XML/JSON. These are mapped to TQPro error responses.
Complete Error Code Table:
| Code Range | Category | Codes |
|---|---|---|
| 100-109 | General/Auth | 100 (server error), 101 (auth failure), 102 (invalid version), 103 (invalid operation) |
| 200-219 | Search | 207 (no availability), 208 (invalid date), 209 (invalid city), 210 (>99 nights), 211 (extra bed/cot limit), 214 (room limit) |
| 140-149 | Validation | 143 (>8 pax/room) |
| 300-319 | Booking | 312 (offer expired), 314 (invalid pax), 315 (room mismatch) |
| 400-409 | Cancellation | 401 (booking not found), 402 (not found for agency) |
| 700-709 | Voucher/Amendment | 700 (not confirmed), 701 (agency not allowed), 702 (credit limit), 704 (not amendable) |
12.2 Error Mapping to TQPro¶
public class GoGlobalErrorMapper {
public static TlinqApiStatus mapError(int ggErrorCode, String ggMessage) {
return switch (ggErrorCode) {
case 207 -> new TlinqApiStatus("NORESULTS", "No hotel availability found");
case 208 -> new TlinqApiStatus("INVALID_PARAM", "Invalid date format: " + ggMessage);
case 209 -> new TlinqApiStatus("INVALID_PARAM", "City not found: " + ggMessage);
case 210 -> new TlinqApiStatus("INVALID_PARAM", "Nights exceed maximum (99)");
case 312 -> new TlinqApiStatus("EXPIRED", "Offer expired. Please search again.");
case 314 -> new TlinqApiStatus("INVALID_PARAM", "Invalid passenger data: " + ggMessage);
case 401, 402 -> new TlinqApiStatus("NOTFOUND", "Booking not found");
case 700 -> new TlinqApiStatus("INVALID_STATE", "Booking not confirmed for voucher");
case 704 -> new TlinqApiStatus("INVALID_STATE", "Booking cannot be amended");
default -> new TlinqApiStatus("ERR00001", "GoGlobal error " + ggErrorCode + ": " + ggMessage);
};
}
}
12.3 Retry and Timeout Strategy¶
| Scenario | Strategy |
|---|---|
| API connection timeout | 30-second connect timeout, 60-second read timeout |
| Availability search slow | Use MaximumWaitTime parameter (up to 20 seconds) |
| Cancellation no response | MUST retry -- cancellation is NOT queued |
| Offer expired (312) | Prompt re-search, do not retry with same code |
| Auth failure (101) | Log error, do not retry (config issue) |
| General server error (100) | Retry once after 5 seconds |
13. GoGlobal API Reference¶
13.1 Operation Summary¶
All operations use SOAP 1.2 POST with the <Root><Header><Main> envelope structure. The <Header>/<Operation> field carries the spec-defined operation name shown below.
| Op | Spec Operation Name | Version | Response | Description |
|---|---|---|---|---|
| 11 | HOTEL_SEARCH_REQUEST |
2.4 | JSON (ResponseFormat="JSON") | Search hotel offers |
| 9 | BOOKING_VALUATION_REQUEST |
2.0 | XML | Validate offer pricing |
| 2 | BOOKING_INSERT_REQUEST |
2.3 | XML | Create booking |
| 3 | BOOKING_CANCEL_REQUEST |
-- | XML | Cancel booking |
| 4 | BOOKING_SEARCH_REQUEST |
2.3 | XML | Get full booking info |
| 5 | BOOKING_STATUS_REQUEST |
-- | XML | Check booking status |
| 6 | HOTEL_INFO_REQUEST |
2.2 | XML | Get hotel details (by hotel ID) |
| 61 | HOTEL_INFO_REQUEST |
2.2 | XML | Get hotel details (by search code) |
| 8 | VOUCHER_DETAILS_REQUEST |
2.3 | XML | Generate PDF voucher |
| 10 | ADV_BOOKING_SEARCH_REQUEST |
1 | XML | Search bookings by criteria |
| 14 | PRICE_BREAKDOWN_REQUEST |
-- | XML | Nightly rates per room |
| 15 | BOOKING_INFO_FOR_AMENDMENT_REQUEST |
-- | XML | Get amendable IDs |
| 16 | BOOKING_AMENDMENT_REQUEST |
-- | XML | Submit amendments |
Note on response formats: Only Op 11 (Availability) supports JSON responses via ResponseFormat="JSON" on the <Main> tag. All other operations return XML responses exclusively. This is per the GoGlobal API spec v3.16.8 which states "Using the JSON response format is mandatory" for availability but does not offer JSON for other operations.
13.2 Key Identifiers¶
| Identifier | Source | Used By | Lifetime |
|---|---|---|---|
HotelSearchCode |
Op 11 (Search) | Op 6, 9, 14, 2 | ~20 minutes |
GoBookingCode |
Op 2 (Booking) | Op 3, 4, 5, 8, 15, 16 | Permanent |
GoReference |
Op 2 (Booking) | Display only | Permanent |
InfoHotelId |
Static data / Op 6 | Op 6 | Permanent |
ClientBookingCode |
Agent input | Op 2, 10 | Permanent |
RoomId |
Op 15 (Amendment) | Op 16 | Per-amendment session |
PersonId |
Op 15 (Amendment) | Op 16 | Per-amendment session |
14. Booking Status State Machine¶
14.1 State Diagram¶
stateDiagram-v2
[*] --> RQ : Op 2 (on-request hotel)
[*] --> C : Op 2 (instant confirm)
[*] --> RJ : Op 2 (instant reject)
RQ --> C : Hotel confirms (poll Op 5)
RQ --> RJ : Hotel rejects (poll Op 5)
C --> RX : Op 3 (Cancel)
C --> VRQ : Op 8 (Voucher request)
RX --> X : Cancellation processed (free)
RX --> XP : Cancellation processed (penalty)
RX --> C : Cancellation denied (rare)
VRQ --> VCH : Voucher generated
X --> [*]
XP --> [*]
RJ --> [*]
14.2 Transition Rules¶
| From | To | Trigger | Notes |
|---|---|---|---|
| (new) | RQ | Op 2 booking, on-request hotel | Poll Op 5 for resolution |
| (new) | C | Op 2 booking, instant confirm | Ready for voucher |
| (new) | RJ | Op 2 booking, instant reject | No charges |
| RQ | C | Hotel confirms (Op 5 poll) | Notification to agent |
| RQ | RJ | Hotel rejects (Op 5 poll) | Notification to agent |
| C | RX | Op 3 cancel request | Transitional state |
| RX | X | Cancellation processed, free | No charges |
| RX | XP | Cancellation processed, penalty | Penalty per CXL policy |
| RX | C | Cancellation denied | Rare edge case |
| C | VRQ | Op 8 voucher request | Transitional state |
| VRQ | VCH | Voucher generated | PDF URL available |
15. Data Flow Diagrams¶
15.1 Search-to-Book Flow¶
User Input (country → city selection via hotel/searchCountries + hotel/getCities)
User Input (city, dates, rooms, nationality)
|
v
+--[hotel/searchOffers (searchMode=online)]-->
|
v
HotelApi.doSearchHotelOffers()
+--> Extract nationality (default "AE")
+--> Create CHotelSearch with all search criteria
|
v
HotelSearchFacade.getAndSaveSearchResults(CHotelSearch, resPerPage, sortMethod)
|
v
OnlineHotelSupplierRegistry.getActiveSupplier()
|
v
GoGlobalHotelFacade.searchOffers(CHotelSearch)
|
v
GoGlobalRequestBuilder.buildAvailabilityRequest(city, dates, rooms, adults, children, nationality, currency)
|
v
GoGlobalSoapClient.sendRequest(11, xml)
|
v
GoGlobal API -- returns JSON (ResponseFormat="JSON")
|
v
GoGlobalResponseParser.parseAvailabilityResponse(json)
|
v
GGAvailabilityResponse.Hotels[].Offers[]
|
+--> Each Offer has HotelSearchCode (HSC)
|
v
EntityTransformer: GGHotelOffer -> CHotelOffer
|
v
Enrich with goglobal.hotel cache data
|
v
HotelSearchFacade.saveSearchResults()
+--> nts.searchbase (CPersistentSearch: searchId, expiry +30min)
+--> nts.hotelsearch (CHotelSearch: criteria + searchBaseId FK)
+--> nts.searchres[] (CSearchResult: one per offer, sourceObject=JSON)
|
v
Return CHotelOfferSet {searchId, offers[], totalRes} to frontend
|
| User selects an offer (HSC=ABC123)
|
v
+--[hotel/valuateOffer (HSC=ABC123)]--->
|
v
GoGlobalHotelFacade.valuateOffer("ABC123")
|
v
GoGlobalSoapClient.sendRequest(9, xml) -> XML response
|
v
GGValuationResponse: confirmed price, CXL deadline, remarks
|
| User provides guest details and confirms
|
v
+--[hotel/createBooking (HSC=ABC123, guests)]--->
|
v
GoGlobalHotelFacade.createBooking(hotelSearchCode, bookingData)
|
v
GoGlobalSoapClient.sendRequest(2, xml) -> XML response
|
v
GGBookingResponse: GoBookingCode=GG-123456, Status=C
|
v
Store booking in TQPro database
|
v
Return booking confirmation to frontend
15.2 Cancellation Flow¶
Agent provides GoBookingCode=GG-123456
|
v
+--[hotel/cancelBooking]--->
|
v
GoGlobalHotelFacade.cancelBooking("GG-123456")
|
v
GoGlobalSoapClient.sendRequest(3, xml)
|
+---> Valid response received?
| |
| YES | NO
| v v
| Parse status RETRY (CRITICAL!)
| | Cancellation NOT queued
| v
| Status = X (free) or XP (penalty) or RX (pending)
| |
| v
| Update TQPro booking record
| |
| v
| Return result to agent
15.3 Static Data Refresh Flow¶
GoGlobalPlugin.initializePlugin()
|
v
SDRefreshRunner.run() -- scheduled or on-demand
|
v
Check goglobal.refresh_log: days since last refresh >= threshold?
|
YES
|
v
StaticDataRefresher.refreshAll()
|
+--> Fetch countries --> UPSERT goglobal.country
|
+--> Fetch cities --> UPSERT goglobal.city (FK: country_id)
|
+--> Fetch hotels --> UPSERT goglobal.hotel (FK: city_id)
|
+--> INSERT goglobal.refresh_log (type=FULL, status=COMPLETED)
15.4 Dual-Mode Decision Flow¶
hotel/searchOffers request arrives
|
v
Extract searchMode parameter
|
+-- searchMode=null --> default "online"
|
+-- searchMode="online"
| |
| v
| OnlineHotelSupplierRegistry.getActiveSupplier()
| |
| v
| hotel.online.supplier = "GoGlobalServiceFactory"
| |
| v
| GoGlobalHotelFacade.searchOffers()
| |
| v
| GoGlobal Op 11 --> CHotelOffer[]
|
+-- searchMode="contracted"
|
v
NTSHotelFacade.searchAllHotels()
|
v
PostgreSQL nts.hotel + room_calendar --> CHotelSearchResult
16. Testing Strategy¶
16.1 Test Levels¶
Following the CLAUDE.md guidelines, tests are generated at three levels:
1. Database Integration Tests (JPA entities):
- CRUD operations on goglobal.country, goglobal.city, goglobal.hotel
- Verify JPA mappings and schema compatibility
- Test GoGlobalDBSession initialization
2. Facade Method Tests (canonical entities):
- Test GoGlobalHotelFacade methods with mocked GoGlobalSoapClient
- Verify GGHotelOffer -> CHotelOffer mapping via EntityTransformer
- Verify GGBooking field mapping
- Test dual-mode routing logic
3. E2E Integration Tests (API level):
- Test hotel/searchOffers with searchMode=online -> GoGlobal
- Test hotel/searchOffers with searchMode=contracted -> NTS
- Test booking lifecycle: search -> valuate -> book -> status -> cancel
- Test hotel/searchByName and hotel/getByCity against cache
- Test TripMaker searchAccommodations integration
16.2 Test Configuration¶
All test classes must include a @BeforeAll or @BeforeEach method to initialize the TLINQ_HOME environment:
@BeforeAll
static void setup() {
System.setProperty("TLINQ_HOME",
System.getProperty("user.dir") + "/config");
}
17. Files Summary¶
New Files (~35 files)¶
| Category | Files | Count |
|---|---|---|
| Module setup | tqgglbl/build.gradle.kts |
1 |
| Config | config/goglobal-client.xml, config/sql/goglobal-schema.sql |
2 |
| Plugin | GoGlobalPlugin.java |
1 |
| Configuration | GoGlobalPluginConfig.java, GoGlobalClientConfig.java |
2 |
| Database | GoGlobalDBSession.java, GGCountryEntity.java, GGCityEntity.java, GGHotelEntity.java, GGStarRatingEntity.java, GGRefreshLogEntity.java |
6 |
| Native entities | GGEntity.java, GGHotelOffer.java, GGBooking.java, GGHotelInfo.java, GGPriceBreakdown.java |
5 |
| Remote | GoGlobalSoapClient.java, GoGlobalRequestBuilder.java, GoGlobalResponseParser.java |
3 |
| DTOs | GGAvailabilityResponse.java, GGValuationResponse.java, GGBookingResponse.java, GGBookingStatusResponse.java, GGBookingDetailsResponse.java, GGCancellationResponse.java, GGHotelInfoResponse.java, GGPriceBreakdownResponse.java |
8 |
| Services | GoGlobalServiceFactory.java, GoGlobalEntityService.java, GoGlobalHotelService.java, GoGlobalHotelFacade.java, SDRefreshRunner.java, StaticDataRefresher.java |
6 |
| Interface (tqapp) | OnlineHotelSupplierI.java, OnlineHotelSupplierRegistry.java |
2 |
| Total | ~36 |
Modified Files¶
| File | Change |
|---|---|
settings.gradle.kts |
Add include("tqgglbl") |
config/tourlinq-config.xml |
Add plugin, service factory, app property |
config/entities/hotel-entities.xml |
Disable Amadeus factories, add GoGlobal factories |
config/api-roles.properties |
Add 4 missing hotel endpoints |
tqapi/.../HotelApi.java |
Add searchMode routing, use supplier registry |
tqapi/.../TripMakerApi.java |
Refactor searchAccommodations() to use supplier registry |
tqapp/.../HotelSearchFacade.java |
Refactor to use OnlineHotelSupplierI |
Disabled (Not Deleted)¶
| File/Component | Action |
|---|---|
Amadeus hotel entities (5 files in tqamds/) |
Retained, factory disabled in config |
Amadeus hotel services (2 files in tqamds/) |
Retained, not called |
Amadeus hotel tests (2 files in tqamds/) |
Retained, can be skipped |
18. Hotel Booking Journey Wiring¶
This section documents the end-to-end wiring of the four hotel booking journey steps through the GoGlobal online supplier. Each step maps a TQPro REST API endpoint through the entity conversion chain to a specific GoGlobal SOAP operation.
18.1 Booking Journey Overview¶
The hotel booking journey consists of four sequential steps that must be executed in order. Each step uses the HotelSearchCode identifier obtained from the availability search (or the preceding step) to maintain state on the GoGlobal side.
Step 1: Get Hotel Info POST /hotel/getHotel (searchMode="online") --> GoGlobal Op 6
Step 2: Valuate Offer POST /hotel/valuateOffer --> GoGlobal Op 9
Step 3: Get Price Breakdown POST /hotel/getPriceBreakdown --> GoGlobal Op 14
Step 4: Create Booking POST /hotel/createBooking --> GoGlobal Op 2
Prerequisite: The user must first perform an availability search via POST /hotel/searchOffers (GoGlobal Op 11) to obtain one or more HotelSearchCode values. These codes are valid for approximately 20 minutes.
18.2 Entity Conversion Chain¶
All four booking journey steps follow the same layered conversion chain:
REST API (HotelApi.java)
|
v
HotelSearchFacade / OnlineHotelSupplierRegistry
|
v
GoGlobalHotelFacade (implements OnlineHotelSupplierI)
|
v
GoGlobalHotelService
|
v
GoGlobalRequestBuilder --> GoGlobalSoapClient --> GoGlobal SOAP API
| |
v v
GoGlobalResponseParser <-- Raw XML/JSON Response <--------+
|
v
Native Entity (GGHotelInfo / GGHotelOffer / GGPriceBreakdown / GGBooking)
|
v
EntityTransformer (field mappings from hotel-entities.xml)
|
v
Canonical Entity (CHotelOffer / CHotelInfo / CHotelPriceBreakdown / CHotelBooking)
|
v
TlinqApiResponse --> JSON to frontend
18.3 Step 1: Get Hotel Info (Op 6)¶
Endpoint: POST /hotel/getHotel with searchMode="online"
Purpose: Retrieve detailed hotel information including description, facilities, images, and geo-coordinates for a specific hotel identified by its GoGlobal hotel ID or HotelSearchCode.
Entity Conversion Chain:
| Layer | Class | Method |
|---|---|---|
| REST API | HotelApi |
getHotel() with searchMode="online" |
| Supplier Registry | OnlineHotelSupplierRegistry |
getActiveSupplier() |
| Supplier Facade | GoGlobalHotelFacade |
getHotelInfo(hotelId) |
| Service | GoGlobalHotelService |
getHotelInfo(hotelId) |
| Request Builder | GoGlobalRequestBuilder |
buildHotelInfoRequest(hotelId) |
| SOAP Client | GoGlobalSoapClient |
sendRequest(6, xml) |
| Response Parser | GoGlobalResponseParser |
parseHotelInfoResponse(xml) |
| Native Entity | GGHotelInfo |
Populated from parsed XML |
| Canonical Entity | CHotelOffer (enriched) |
Mapped via EntityTransformer |
GoGlobal Request (Op 6):
<Root>
<Header>
<Agency>AGENCY_ID</Agency>
<User>USERNAME</User>
<Password>PASSWORD</Password>
<Operation>HOTEL_INFO_REQUEST</Operation>
<OperationType>Request</OperationType>
</Header>
<Main Version="2.2">
<InfoHotelId>56789</InfoHotelId>
</Main>
</Root>
GoGlobal Response (XML):
<Root>
<Header>
<Agency>AGENCY_ID</Agency>
<Operation>6</Operation>
<OperationType>Response</OperationType>
</Header>
<Main>
<HotelId>56789</HotelId>
<HotelName>Grand Hotel Dubai</HotelName>
<Address>Sheikh Zayed Road, Downtown Dubai</Address>
<StarRating>5</StarRating>
<Description>Luxury 5-star hotel with panoramic views...</Description>
<Thumbnail>https://images.goglobal.travel/hotels/56789/thumb.jpg</Thumbnail>
<HotelFacilities>
<Facility>Swimming Pool</Facility>
<Facility>Spa</Facility>
<Facility>Gym</Facility>
</HotelFacilities>
<RoomFacilities>
<Facility>Air Conditioning</Facility>
<Facility>WiFi</Facility>
<Facility>Mini Bar</Facility>
</RoomFacilities>
</Main>
</Root>
TQPro API Request:
TQPro API Response:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": {
"hotelId": "56789",
"hotelName": "Grand Hotel Dubai",
"description": "Luxury 5-star hotel with panoramic views...",
"address": "Sheikh Zayed Road, Downtown Dubai",
"starRating": 5,
"thumbnail": "https://images.goglobal.travel/hotels/56789/thumb.jpg",
"hotelFacilities": ["Swimming Pool", "Spa", "Gym"],
"roomFacilities": ["Air Conditioning", "WiFi", "Mini Bar"],
"latitude": 25.2048,
"longitude": 55.2708
}
}
Key Implementation Files:
- tqapi/src/main/java/com/perun/tlinq/api/HotelApi.java -- endpoint routing with searchMode
- tqgglbl/src/main/java/com/perun/tlinq/client/goglobal/service/hotel/GoGlobalHotelFacade.java -- supplier implementation
- tqgglbl/src/main/java/com/perun/tlinq/client/goglobal/remote/GoGlobalRequestBuilder.java -- builds Op 6 XML
- tqgglbl/src/main/java/com/perun/tlinq/client/goglobal/entity/GGHotelInfo.java -- native entity
18.4 Step 2: Valuate Offer (Op 9)¶
Endpoint: POST /hotel/valuateOffer
Purpose: Validate the current price, cancellation policies, and terms for a selected hotel offer before proceeding to booking. This step confirms whether the price has changed since the availability search and retrieves detailed cancellation rules.
Entity Conversion Chain:
| Layer | Class | Method |
|---|---|---|
| REST API | HotelApi |
valuateOffer() |
| Supplier Registry | OnlineHotelSupplierRegistry |
getActiveSupplier() |
| Supplier Facade | GoGlobalHotelFacade |
valuateOffer(hotelSearchCode) |
| Service | GoGlobalHotelService |
valuateBooking(hotelSearchCode) |
| Request Builder | GoGlobalRequestBuilder |
buildValuationRequest(hotelSearchCode) |
| SOAP Client | GoGlobalSoapClient |
sendRequest(9, xml) |
| Response Parser | GoGlobalResponseParser |
parseValuationResponse(xml) |
| DTO | GGValuationResponse |
Populated from parsed XML |
| Canonical Entity | CHotelOffer (updated pricing) |
Mapped with confirmed price and CXL terms |
GoGlobal Request (Op 9):
<Root>
<Header>
<Agency>AGENCY_ID</Agency>
<User>USERNAME</User>
<Password>PASSWORD</Password>
<Operation>BOOKING_VALUATION_REQUEST</Operation>
<OperationType>Request</OperationType>
</Header>
<Main Version="2.0">
<HotelSearchCode>HSC-2026031500001</HotelSearchCode>
</Main>
</Root>
GoGlobal Response (XML):
<Root>
<Header>
<Agency>AGENCY_ID</Agency>
<Operation>9</Operation>
<OperationType>Response</OperationType>
</Header>
<Main>
<HotelSearchCode>HSC-2026031500001</HotelSearchCode>
<ArrivalDate>2026-03-15</ArrivalDate>
<CancellationDeadline>2026-03-13T23:59:59</CancellationDeadline>
<Remarks>Check-in from 14:00. Late checkout subject to availability.</Remarks>
<Rates>
<Rate>
<RoomRate>150.00</RoomRate>
<Currency>USD</Currency>
</Rate>
</Rates>
<TotalTax>45.00</TotalTax>
<NewRate>450.00</NewRate>
<NonRef>false</NonRef>
<CancellationPolicies>
<Policy>
<Id>1</Id>
<Starting>2026-03-13T00:00:00</Starting>
<BasedOn>BOOKINGPRICE</BasedOn>
<Mode>PCT</Mode>
<Value>100</Value>
</Policy>
</CancellationPolicies>
</Main>
</Root>
TQPro API Request:
TQPro API Response:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": {
"offerId": "HSC-2026031500001",
"checkInDate": "2026-03-15",
"totalAmount": 450.00,
"currencyCode": "USD",
"cancellationDeadline": "2026-03-13T23:59:59",
"cancellationType": "REFUNDABLE",
"remarks": "Check-in from 14:00. Late checkout subject to availability.",
"totalTax": 45.00,
"cancellationPolicies": [
{
"id": 1,
"starting": "2026-03-13T00:00:00",
"basedOn": "BOOKINGPRICE",
"mode": "PCT",
"value": 100
}
]
}
}
Key Implementation Files:
- tqapi/src/main/java/com/perun/tlinq/api/HotelApi.java -- valuateOffer() endpoint
- tqgglbl/src/main/java/com/perun/tlinq/client/goglobal/service/hotel/GoGlobalHotelFacade.java -- valuateOffer()
- tqgglbl/src/main/java/com/perun/tlinq/client/goglobal/remote/GoGlobalRequestBuilder.java -- buildValuationRequest()
- tqgglbl/src/main/java/com/perun/tlinq/client/goglobal/remote/dto/GGValuationResponse.java -- response DTO
- config/goglobal-client.xml -- service valuateBooking definition
18.5 Step 3: Get Price Breakdown (Op 14)¶
Endpoint: POST /hotel/getPriceBreakdown
Purpose: Retrieve the nightly price breakdown per room for a selected offer. This provides granular pricing detail that helps the agent or customer understand how the total price is calculated across the stay period.
Entity Conversion Chain:
| Layer | Class | Method |
|---|---|---|
| REST API | HotelApi |
getPriceBreakdown() |
| Supplier Registry | OnlineHotelSupplierRegistry |
getActiveSupplier() |
| Supplier Facade | GoGlobalHotelFacade |
getPriceBreakdown(hotelSearchCode) |
| Service | GoGlobalHotelService |
getPriceBreakdown(hotelSearchCode) |
| Request Builder | GoGlobalRequestBuilder |
buildPriceBreakdownRequest(hotelSearchCode) |
| SOAP Client | GoGlobalSoapClient |
sendRequest(14, xml) |
| Response Parser | GoGlobalResponseParser |
parsePriceBreakdownResponse(xml) |
| DTO | GGPriceBreakdownResponse |
Populated from parsed XML |
| Native Entity | GGPriceBreakdown |
Mapped from DTO |
| Canonical Entity | Return as structured response | Nightly rates per room |
GoGlobal Request (Op 14):
<Root>
<Header>
<Agency>AGENCY_ID</Agency>
<User>USERNAME</User>
<Password>PASSWORD</Password>
<Operation>PRICE_BREAKDOWN_REQUEST</Operation>
<OperationType>Request</OperationType>
</Header>
<Main>
<HotelSearchCode>HSC-2026031500001</HotelSearchCode>
</Main>
</Root>
GoGlobal Response (XML):
<Root>
<Header>
<Agency>AGENCY_ID</Agency>
<Operation>14</Operation>
<OperationType>Response</OperationType>
</Header>
<Main>
<Rooms>
<Room Id="1">
<Nights>
<Night Date="2026-03-15" Price="150.00" Currency="USD"/>
<Night Date="2026-03-16" Price="150.00" Currency="USD"/>
<Night Date="2026-03-17" Price="150.00" Currency="USD"/>
</Nights>
</Room>
</Rooms>
</Main>
</Root>
TQPro API Request:
TQPro API Response:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": {
"hotelSearchCode": "HSC-2026031500001",
"rooms": [
{
"roomId": 1,
"nights": [
{ "date": "2026-03-15", "price": 150.00, "currency": "USD" },
{ "date": "2026-03-16", "price": 150.00, "currency": "USD" },
{ "date": "2026-03-17", "price": 150.00, "currency": "USD" }
]
}
]
}
}
Key Implementation Files:
- tqapi/src/main/java/com/perun/tlinq/api/HotelApi.java -- getPriceBreakdown() endpoint
- tqgglbl/src/main/java/com/perun/tlinq/client/goglobal/service/hotel/GoGlobalHotelFacade.java -- getPriceBreakdown()
- tqgglbl/src/main/java/com/perun/tlinq/client/goglobal/remote/GoGlobalRequestBuilder.java -- buildPriceBreakdownRequest()
- tqgglbl/src/main/java/com/perun/tlinq/client/goglobal/remote/dto/GGPriceBreakdownResponse.java -- response DTO
- tqgglbl/src/main/java/com/perun/tlinq/client/goglobal/entity/GGPriceBreakdown.java -- native entity
- config/goglobal-client.xml -- service getPriceBreakdown definition
18.6 Step 4: Create Booking (Op 2)¶
Endpoint: POST /hotel/createBooking
Purpose: Create a hotel booking for a validated offer. This step submits guest details and generates a booking confirmation with a GoGlobal booking code.
Entity Conversion Chain:
| Layer | Class | Method |
|---|---|---|
| REST API | HotelApi |
createBooking() |
| Supplier Registry | OnlineHotelSupplierRegistry |
getActiveSupplier() |
| Supplier Facade | GoGlobalHotelFacade |
createBooking(hotelSearchCode, bookingData) |
| Service | GoGlobalHotelService |
createBooking(hotelSearchCode, bookingData) |
| Request Builder | GoGlobalRequestBuilder |
buildBookingRequest(hotelSearchCode, clientRef, ...) |
| SOAP Client | GoGlobalSoapClient |
sendRequest(2, xml) |
| Response Parser | GoGlobalResponseParser |
parseBookingResponse(xml) |
| DTO | GGBookingResponse |
Populated from parsed XML |
| Native Entity | GGBooking |
Mapped from DTO |
| Canonical Entity | Booking confirmation object | Returned to frontend |
GoGlobal Request (Op 2):
<Root>
<Header>
<Agency>AGENCY_ID</Agency>
<User>USERNAME</User>
<Password>PASSWORD</Password>
<Operation>BOOKING_INSERT_REQUEST</Operation>
<OperationType>Request</OperationType>
</Header>
<Main Version="2.3">
<HotelSearchCode>HSC-2026031500001</HotelSearchCode>
<ClientBookingCode>TQ-BK-2026-0001</ClientBookingCode>
<Leader>
<Title>MR.</Title>
<FirstName>John</FirstName>
<LastName>Smith</LastName>
</Leader>
<Email>john.smith@example.com</Email>
<Phone>+44123456789</Phone>
</Main>
</Root>
GoGlobal Response (XML):
<Root>
<Header>
<Agency>AGENCY_ID</Agency>
<Operation>2</Operation>
<OperationType>Response</OperationType>
</Header>
<Main>
<GoBookingCode>GG-123456</GoBookingCode>
<GoReference>REF-789012</GoReference>
<BookingStatus>C</BookingStatus>
<TotalPrice>450.00</TotalPrice>
<Currency>USD</Currency>
<HotelId>56789</HotelId>
<HotelName>Grand Hotel Dubai</HotelName>
<HotelConfirmation>HC-GRAND-2026-001</HotelConfirmation>
<Rooms>
<Room Id="1" Adults="2" RoomBasis="BB">
<Pax Title="MR." FirstName="John" LastName="Smith"/>
<Pax Title="MRS." FirstName="Jane" LastName="Smith"/>
</Room>
</Rooms>
<Leader Title="MR." FirstName="John" LastName="Smith"/>
<PaymentTransactions/>
</Main>
</Root>
TQPro API Request:
{
"session": "user-session-token",
"hotelSearchCode": "HSC-2026031500001",
"clientRef": "TQ-BK-2026-0001",
"title": "MR.",
"firstName": "John",
"lastName": "Smith",
"email": "john.smith@example.com",
"phone": "+44123456789",
"rooms": [
{
"adults": 2,
"pax": [
{ "title": "MR.", "firstName": "John", "lastName": "Smith" },
{ "title": "MRS.", "firstName": "Jane", "lastName": "Smith" }
]
}
]
}
TQPro API Response:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": {
"goBookingCode": "GG-123456",
"goReference": "REF-789012",
"bookingStatus": "C",
"totalPrice": 450.00,
"currency": "USD",
"hotelId": "56789",
"hotelName": "Grand Hotel Dubai",
"hotelConfirmation": "HC-GRAND-2026-001",
"clientBookingCode": "TQ-BK-2026-0001"
}
}
Booking Status Codes: C = Confirmed, RQ = On Request (pending hotel confirmation), RJ = Rejected. See Section 14 for the full booking status state machine.
Key Implementation Files:
- tqapi/src/main/java/com/perun/tlinq/api/HotelApi.java -- createBooking() endpoint
- tqgglbl/src/main/java/com/perun/tlinq/client/goglobal/service/hotel/GoGlobalHotelFacade.java -- createBooking()
- tqgglbl/src/main/java/com/perun/tlinq/client/goglobal/remote/GoGlobalRequestBuilder.java -- buildBookingRequest()
- tqgglbl/src/main/java/com/perun/tlinq/client/goglobal/remote/dto/GGBookingResponse.java -- response DTO
- tqgglbl/src/main/java/com/perun/tlinq/client/goglobal/entity/GGBooking.java -- native entity
- config/goglobal-client.xml -- service createBooking definition
18.7 Canonical Entities for the Booking Journey¶
The booking journey introduces or reuses the following canonical entities:
| Canonical Entity | Source Step | GoGlobal Op | Native Entity | Purpose |
|---|---|---|---|---|
CHotelOffer |
Step 1 (Info), Step 2 (Valuate) | 6, 9 | GGHotelInfo, GGHotelOffer |
Hotel details and validated offer pricing |
GGPriceBreakdown |
Step 3 (Breakdown) | 14 | GGPriceBreakdown |
Nightly rate breakdown per room |
GGBooking |
Step 4 (Book) | 2 | GGBooking |
Booking confirmation with GoGlobal codes |
CHotelOffer (tqapp/src/main/java/com/perun/tlinq/entity/hotel/CHotelOffer.java) is the primary canonical entity used across the search and booking journey. Key fields relevant to the booking journey:
| Field | Type | Journey Step | Description |
|---|---|---|---|
offerId |
String | All steps | Maps to GoGlobal HotelSearchCode |
hotelId |
String | Step 1, 4 | GoGlobal hotel ID |
hotelName |
String | Step 1, 4 | Hotel name |
totalAmount |
Double | Step 2 | Validated total price |
currencyCode |
String | Step 2, 3 | Currency code |
cancellationType |
String | Step 2 | REFUNDABLE or NON_REFUNDABLE |
cancellationDeadline |
String | Step 2 | Free cancellation deadline |
boardType |
String | Step 1, 2 | Meal plan code |
18.8 Entity Configuration in hotel-entities.xml¶
The GoGlobal factory is registered for the HotelOffer entity in config/entities/hotel-entities.xml:
<Entity name="HotelOffer" class="com.perun.tlinq.entity.hotel.CHotelOffer"
idField="offerId" defaultFactory="GoGlobalServiceFactory">
<EntityFactoryList>
<Factory name="AmadeusServiceFactory" enabled="false"
nativeEntity="com.perun.tlinq.client.amadeus.entity.AmdHotelOffer">
<!-- Amadeus factory disabled -->
</Factory>
<Factory name="GoGlobalServiceFactory"
nativeEntity="com.perun.tlinq.client.goglobal.entity.GGHotelOffer">
<ServiceList>
<Service name="searchHotelOffers" action="search"
returnClass="com.perun.tlinq.entity.hotel.CHotelOffer"/>
</ServiceList>
<FieldMappingList>
<FieldMapping targetField="offerId" sourceField="hotelSearchCode"
mapping="DirectMapping"/>
<FieldMapping targetField="hotelId" sourceField="hotelCode"
mapping="DirectMapping"/>
<FieldMapping targetField="hotelName" sourceField="hotelName"
mapping="DirectMapping"/>
<FieldMapping targetField="cityCode" sourceField="cityId"
mapping="DirectMapping"/>
<FieldMapping targetField="available" sourceField="available"
mapping="DirectMapping"/>
<FieldMapping targetField="checkInDate" sourceField="checkInDate"
mapping="DirectMapping"/>
<FieldMapping targetField="checkOutDate" sourceField="checkOutDate"
mapping="DirectMapping"/>
<FieldMapping targetField="roomType" sourceField="roomBasis"
mapping="DirectMapping"/>
<FieldMapping targetField="roomCategory" sourceField="category"
mapping="DirectMapping"/>
<FieldMapping targetField="currencyCode" sourceField="currency"
mapping="DirectMapping"/>
<FieldMapping targetField="totalAmount" sourceField="totalPrice"
mapping="DirectMapping"/>
<FieldMapping targetField="cancellationType" sourceField="nonRef"
mapping="DirectMapping"/>
<FieldMapping targetField="cancellationDeadline" sourceField="cxlDeadline"
mapping="DirectMapping"/>
<FieldMapping targetField="boardType" sourceField="boardBasis"
mapping="DirectMapping"/>
</FieldMappingList>
</Factory>
</EntityFactoryList>
</Entity>
18.9 API Endpoint Permissions¶
The booking journey endpoints require the following roles in config/api-roles.properties:
# Online hotel booking journey endpoints
hotel/getHotel=guest,agent,admin
hotel/searchOffers=guest,agent,admin
hotel/fetchOfferResults=guest,agent,admin
hotel/searchByName=guest,agent,admin
hotel/getByCity=guest,agent,admin
# Booking journey endpoints (agent+ only for write operations)
hotel/valuateOffer=agent,admin
hotel/getPriceBreakdown=guest,agent,admin
hotel/createBooking=agent,admin
Note: hotel/valuateOffer and hotel/createBooking require at least agent role since they are pre-booking and booking write operations. hotel/getPriceBreakdown allows guest access as it is a read-only pricing query.
18.10 End-to-End Booking Journey Sequence¶
The complete sequence showing all four steps with their data flow:
User: Search for hotels in Dubai, Mar 15-18, 2 adults
|
v
[1] POST /hotel/searchOffers
{ cityCode: "1234", checkInDate: "2026-03-15",
checkOutDate: "2026-03-18", adults: 2, searchMode: "online" }
--> GoGlobal Op 11 (JSON response)
--> Returns: offers[] with HotelSearchCode per offer
|
| User selects offer HSC-2026031500001 at Grand Hotel Dubai
|
v
[2] POST /hotel/getHotel (searchMode="online")
{ id: 56789, searchMode: "online" }
--> GoGlobal Op 6 (XML response)
--> Returns: hotel description, facilities, images, coordinates
|
| User reviews hotel details and proceeds
|
v
[3] POST /hotel/valuateOffer
{ hotelSearchCode: "HSC-2026031500001" }
--> GoGlobal Op 9 (XML response)
--> Returns: confirmed price $450, CXL deadline Mar 13, policies
|
| User confirms price is acceptable
|
v
[4] POST /hotel/getPriceBreakdown
{ hotelSearchCode: "HSC-2026031500001" }
--> GoGlobal Op 14 (XML response)
--> Returns: $150/night x 3 nights for Room 1
|
| User reviews nightly pricing and enters guest details
|
v
[5] POST /hotel/createBooking
{ hotelSearchCode: "HSC-2026031500001", firstName: "John",
lastName: "Smith", email: "john@example.com", ... }
--> GoGlobal Op 2 (XML response)
--> Returns: GoBookingCode=GG-123456, Status=C (Confirmed)
|
v
Booking confirmed. GoBookingCode used for subsequent operations
(status check Op 5, details Op 4, cancellation Op 3, voucher Op 8)
Thread Safety and Concurrency (TQ-52)¶
The following thread safety improvements were applied to the GoGlobal plugin:
| Component | Improvement |
|---|---|
GoGlobalPlugin |
Scheduled executor uses daemon threads to prevent JVM shutdown hang. Plugin shutdown follows ordered lifecycle (LIFO) with shutdownNow() on the executor. |
GoGlobalSoapClient |
Uses a shared java.net.http.HttpClient instance instead of creating per-request instances. Thread-safe by design. |
GoGlobalDBSession |
Hibernate sessions are always closed in finally blocks to prevent session leaks. Transactions include proper rollback() on exception. |
SDRefreshRunner |
AtomicBoolean guard prevents concurrent static data refreshes if a previous refresh is still running when the next scheduled invocation fires. |
GoGlobalServiceFactory |
Volatile double-checked locking for singleton instance. |
Document Information¶
Document Version: 1.3 Date: 2026-02-19 Status: Implementation (aligned with GoGlobal API spec v3.16.8) Author: System Architecture Team
Changelog:
- v1.3 (2026-02-19): Added thread safety and concurrency section documenting TQ-52 improvements (daemon threads, shared HttpClient, Hibernate session safety, AtomicBoolean refresh guard).
- v1.2 (2026-02-10): Added Section 18: Hotel Booking Journey Wiring. Documents the end-to-end wiring of 4 booking journey steps (getHotel Op 6, valuateOffer Op 9, getPriceBreakdown Op 14, createBooking Op 2) through the GoGlobal online supplier, including entity conversion chains, API request/response examples, canonical entity mappings, entity configuration, API permissions, and the full end-to-end sequence diagram.
- v1.1 (2026-02-10): Updated SOAP envelope structure to reflect correct <gg:requestType> / <gg:xmlRequest> separation. Updated all request XML examples to show complete <Root><Header><Main> structure with spec-mandated operation names and version attributes. Corrected Rooms field type from List<Object> to List<String> (room type names). Updated GGAvailabilityResponse DTO to match actual Gson-based implementation. Clarified JSON response format is Op 11 only. Updated sendRequest signature from String to int operation code. Updated OnlineHotelSupplierI method signatures to match implementation. Added operation name and version reference table.
- v1.0 (2026-02-09): Initial document.
Related Documents: - Hotel Booking Use Cases - Use case specification - Integration Plan - Implementation plan - Testing Guide - Testing guide with API simulator - Hotel API Specification - Hotel API specification