Skip to content

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.*)     |
                          +-------------------+

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 (&lt;, &gt;, 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) from GoGlobalClientConfig
  • Setting the spec-defined <Operation> name (e.g., HOTEL_SEARCH_REQUEST)
  • Setting <OperationType>Request</OperationType>
  • Setting the <Main> tag with the correct Version attribute 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:

SELECT * FROM goglobal.hotel
WHERE city_id = ?
ORDER BY star_rating DESC, hotel_name

Enrich availability results:

SELECT * FROM goglobal.hotel
WHERE hotel_id = ?

Resolve city code:

SELECT city_id FROM goglobal.city
WHERE city_name ILIKE ? OR city_id = ?

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:

  1. Plugin registration under <Plugins> (see Section 3.2)
  2. Service factory under <ServiceFactories> (see Section 3.2)
  3. App property under <AppProperties>:
    <property name="hotel.online.supplier" value="GoGlobalServiceFactory"/>
    

11.3 hotel-entities.xml Changes

Modified file: config/entities/hotel-entities.xml

  1. Disable Amadeus factories (set enabled="false"):
  2. HotelOffer entity -> AmadeusServiceFactory
  3. HotelByName entity -> AmadeusServiceFactory
  4. HotelByCity entity -> AmadeusServiceFactory

  5. 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:

{
    "session": "user-session-token",
    "id": 56789,
    "searchMode": "online"
}

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:

{
    "session": "user-session-token",
    "hotelSearchCode": "HSC-2026031500001"
}

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:

{
    "session": "user-session-token",
    "hotelSearchCode": "HSC-2026031500001"
}

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