Skip to content

Tiqets.com Integration Plugin - Complete Implementation Plan

Overview

This document provides a complete implementation plan for integrating Tiqets.com worldwide activity/ticketing into the TQPro platform. The implementation is divided into three phases:

  1. Phase 1: Backend Integration (TQPro APIs) - Fully detailed
  2. Phase 2: Public Website (BookMyHoliday integration) - High-level outline
  3. Phase 3: Trip Management Integration (tqweb-adm) - High-level outline

Phase 1: Backend Integration

1. Architecture Overview

1.1 Reusing Existing Infrastructure

The Tiqets integration will leverage existing TQPro booking infrastructure:

Existing Component Full Path Reuse Strategy
TicketingServiceI tqapp/src/main/java/com/perun/tlinq/service/TicketingServiceI.java Implement interface in TiqetsTicketingService
BookingRequestFacade tqapp/src/main/java/com/perun/tlinq/entity/tkt/BookingRequestFacade.java Extend for direct B2C flow
CBookingRequest tqapp/src/main/java/com/perun/tlinq/entity/tkt/CBookingRequest.java Use as-is
CTicketingRequest tqapp/src/main/java/com/perun/tlinq/entity/tkt/CTicketingRequest.java Use as-is
CTicketItem tqapp/src/main/java/com/perun/tlinq/entity/tkt/CTicketItem.java Use as-is
CTicketConfirmation tqapp/src/main/java/com/perun/tlinq/entity/tkt/CTicketConfirmation.java Use as-is
InvoiceFacade tqapp/src/main/java/com/perun/tlinq/entity/document/InvoiceFacade.java Use for invoice generation
PGWFacade tqapp/src/main/java/com/perun/tlinq/pgw/PGWFacade.java Use for payment processing
MailUtil tqcommon/src/main/java/com/perun/tlinq/util/MailUtil.java Use for email delivery

1.2 New Components to Create

Component Location Purpose
tqtiqets module tqtiqets/ New Gradle module
TiqetsPlugin tqtiqets/.../framework/ Plugin initialization
TiqetsServiceFactory tqtiqets/.../service/ Service factory
TiqetsTicketingService tqtiqets/.../service/ Implements TicketingServiceI
TiqetsHttpClient tqtiqets/.../remote/ HTTP client with JWT
TiqetsCatalogFacade tqtiqets/.../service/product/ Experience/product catalog
DirectBookingFacade tqapp/.../entity/tkt/ B2C booking without sales order
TicketingApi tqapi/.../api/ REST endpoints
JPA entities tqtiqets/.../db/ Database caching

2. Module Structure

2.1 Gradle Module Configuration

File: tqtiqets/build.gradle.kts

plugins {
    id("java")
}

group = "com.perun.tqpro"
version = "1.0-SNAPSHOT"

repositories {
    mavenCentral()
}

dependencies {
    implementation(project(":tqapp"))
    implementation(project(":tqcommon"))

    // JWT signing for booking APIs (RS256)
    implementation("io.jsonwebtoken:jjwt-api:0.11.5")
    runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.5")
    runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.5")

    testImplementation(platform("org.junit:junit-bom:5.10.0"))
    testImplementation("org.junit.jupiter:junit-jupiter")
}

tasks.test {
    useJUnitPlatform()
}

tasks.register<Copy>("copyDependencies") {
    val outputDir = layout.buildDirectory.dir("libs/lib")
    into(outputDir)
    from(configurations.runtimeClasspath) { include("*.jar") }
}

tasks.named<Jar>("jar") {
    archiveFileName.set("tqtiqets.jar")
}

File: settings.gradle.kts (modification)

// Add "tqtiqets" to the include list
include("tqapi","tqapp","tqcommon","tqodoo","tqamds","tqryb2b","tqtiqets")

2.2 Package Structure

tqtiqets/
└── src/main/java/com/perun/tlinq/
    ├── framework/
    │   ├── TiqetsPlugin.java                    # Plugin initialization
    │   └── TiqetsRefreshRunner.java             # Scheduled refresh job
    └── client/tiqets/
        ├── config/
        │   └── TiqetsPluginConfig.java          # JAXB config class
        ├── util/
        │   ├── TiqetsClientConfig.java          # Configuration singleton
        │   ├── TiqetsDBSession.java             # Hibernate session factory
        │   └── TiqetsCacheManager.java          # In-memory cache
        ├── service/
        │   ├── TiqetsServiceFactory.java        # Service factory
        │   ├── TiqetsEntityService.java         # Base service class
        │   ├── TiqetsTicketingService.java      # TicketingServiceI impl
        │   ├── TiqetsResponseMapper.java        # Response mapping
        │   ├── StaticDataRefresher.java         # Data sync logic
        │   └── product/
        │       └── TiqetsCatalogFacade.java     # Catalog operations
        ├── db/
        │   ├── CountryEntity.java               # JPA entity
        │   ├── CityEntity.java                  # JPA entity
        │   ├── ExperienceEntity.java            # JPA entity
        │   ├── ProductEntity.java               # JPA entity
        │   ├── ProductVariantEntity.java        # JPA entity
        │   ├── TiqetsOrderEntity.java           # JPA entity
        │   └── RefreshStatusEntity.java         # JPA entity
        ├── entity/
        │   ├── TqExperience.java                # Native entity
        │   ├── TqProduct.java                   # Native entity
        │   ├── TqProductVariant.java            # Native entity
        │   └── TqAvailability.java              # Native entity
        └── remote/
            ├── TiqetsHttpClient.java            # HTTP client with JWT
            ├── entity/
            │   ├── TiqetsExperience.java        # API DTO
            │   ├── TiqetsProduct.java           # API DTO
            │   ├── TiqetsVariant.java           # API DTO
            │   ├── TiqetsAvailability.java      # API DTO
            │   ├── TiqetsOrder.java             # API DTO
            │   └── TiqetsTicket.java            # API DTO
            └── service/
                ├── ExperiencesService.java      # API wrapper
                ├── ProductsService.java         # API wrapper
                ├── AvailabilityService.java     # API wrapper
                ├── OrdersService.java           # API wrapper
                ├── request/
                │   ├── CreateOrderRequest.java
                │   └── ConfirmOrderRequest.java
                └── response/
                    ├── ExperiencesResponse.java
                    ├── ProductsResponse.java
                    ├── AvailabilityResponse.java
                    ├── CreateOrderResponse.java
                    ├── ConfirmOrderResponse.java
                    └── TicketsResponse.java

3. Database Schema

3.1 SQL Schema Creation Script

-- ============================================
-- Tiqets Integration Schema
-- ============================================

CREATE SCHEMA IF NOT EXISTS tiqets;

-- --------------------------------------------
-- Countries cache
-- --------------------------------------------
CREATE TABLE tiqets.country (
    country_id VARCHAR(10) PRIMARY KEY,
    country_name VARCHAR(255) NOT NULL,
    country_code VARCHAR(10),
    hash BIGINT NOT NULL DEFAULT 0,
    last_update TIMESTAMP NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_tiqets_country_name ON tiqets.country(country_name);

-- --------------------------------------------
-- Cities cache (with enabled flag)
-- --------------------------------------------
CREATE TABLE tiqets.city (
    city_id VARCHAR(20) PRIMARY KEY,
    city_name VARCHAR(255) NOT NULL,
    country_id VARCHAR(10) REFERENCES tiqets.country(country_id),
    timezone VARCHAR(50),
    latitude DECIMAL(10,7),
    longitude DECIMAL(10,7),
    currency VARCHAR(10) DEFAULT 'AED',
    enabled BOOLEAN DEFAULT false,
    hash BIGINT NOT NULL DEFAULT 0,
    last_update TIMESTAMP NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_tiqets_city_country ON tiqets.city(country_id);
CREATE INDEX idx_tiqets_city_enabled ON tiqets.city(enabled);
CREATE INDEX idx_tiqets_city_name ON tiqets.city(city_name);

-- --------------------------------------------
-- Experiences cache
-- --------------------------------------------
CREATE TABLE tiqets.experience (
    experience_id VARCHAR(30) PRIMARY KEY,
    experience_type VARCHAR(50),           -- venue, activity, service, poi
    title VARCHAR(500) NOT NULL,
    tagline VARCHAR(500),
    description TEXT,
    city_id VARCHAR(20) REFERENCES tiqets.city(city_id),
    country_id VARCHAR(10) REFERENCES tiqets.country(country_id),
    street_address VARCHAR(500),
    postal_code VARCHAR(20),
    latitude DECIMAL(10,7),
    longitude DECIMAL(10,7),
    google_place_id VARCHAR(100),
    image_small VARCHAR(500),
    image_medium VARCHAR(500),
    image_large VARCHAR(500),
    image_xlarge VARCHAR(500),
    image_alt_text VARCHAR(255),
    rating_average DECIMAL(3,2),
    rating_count INTEGER DEFAULT 0,
    from_price DECIMAL(12,2),
    currency VARCHAR(10) DEFAULT 'AED',
    tag_ids TEXT,                          -- JSON array
    product_ids TEXT,                      -- JSON array of arrays
    experience_url VARCHAR(500),
    active BOOLEAN DEFAULT true,
    hash BIGINT NOT NULL DEFAULT 0,
    last_update TIMESTAMP NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_tiqets_exp_city ON tiqets.experience(city_id);
CREATE INDEX idx_tiqets_exp_country ON tiqets.experience(country_id);
CREATE INDEX idx_tiqets_exp_active ON tiqets.experience(active);
CREATE INDEX idx_tiqets_exp_title ON tiqets.experience(title);

-- --------------------------------------------
-- Products cache
-- --------------------------------------------
CREATE TABLE tiqets.product (
    product_id VARCHAR(30) PRIMARY KEY,
    experience_id VARCHAR(30) REFERENCES tiqets.experience(experience_id),
    title VARCHAR(500) NOT NULL,
    city_id VARCHAR(20),
    city_name VARCHAR(255),
    country_id VARCHAR(10),
    country_name VARCHAR(255),
    description TEXT,
    highlights TEXT,                       -- JSON array
    inclusions TEXT,                       -- JSON array
    exclusions TEXT,                       -- JSON array
    important_info TEXT,
    cancellation_policy TEXT,
    duration_minutes INTEGER,
    min_price DECIMAL(12,2),
    currency VARCHAR(10) DEFAULT 'AED',
    has_timeslots BOOLEAN DEFAULT false,
    has_dynamic_pricing BOOLEAN DEFAULT false,
    max_tickets_per_order INTEGER DEFAULT 10,
    booking_cutoff_minutes INTEGER DEFAULT 60,
    instant_confirmation BOOLEAN DEFAULT true,
    voucher_type VARCHAR(50),
    sale_status VARCHAR(50),
    sale_status_reason VARCHAR(255),
    latitude DECIMAL(10,7),
    longitude DECIMAL(10,7),
    venue_id VARCHAR(30),
    venue_name VARCHAR(255),
    venue_address VARCHAR(500),
    image_small VARCHAR(500),
    image_medium VARCHAR(500),
    image_large VARCHAR(500),
    image_xlarge VARCHAR(500),
    product_url VARCHAR(500),
    checkout_url VARCHAR(500),
    tag_ids TEXT,                          -- JSON array
    active BOOLEAN DEFAULT true,
    hash BIGINT NOT NULL DEFAULT 0,
    last_update TIMESTAMP NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_tiqets_prod_exp ON tiqets.product(experience_id);
CREATE INDEX idx_tiqets_prod_city ON tiqets.product(city_id);
CREATE INDEX idx_tiqets_prod_active ON tiqets.product(active);
CREATE INDEX idx_tiqets_prod_title ON tiqets.product(title);

-- --------------------------------------------
-- Product variants (ticket types)
-- --------------------------------------------
CREATE TABLE tiqets.product_variant (
    variant_id VARCHAR(50) PRIMARY KEY,
    product_id VARCHAR(30) REFERENCES tiqets.product(product_id),
    label VARCHAR(255) NOT NULL,
    description TEXT,
    variant_type VARCHAR(50),              -- adult, child, infant, family, senior, etc.
    max_visitors_per_ticket INTEGER DEFAULT 1,
    max_tickets INTEGER DEFAULT 10,
    valid_with_variant_ids TEXT,           -- JSON array
    cancellation_window_hours INTEGER,
    cancellation_policy VARCHAR(50),
    active BOOLEAN DEFAULT true,
    hash BIGINT NOT NULL DEFAULT 0,
    last_update TIMESTAMP NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_tiqets_var_product ON tiqets.product_variant(product_id);
CREATE INDEX idx_tiqets_var_type ON tiqets.product_variant(variant_type);

-- --------------------------------------------
-- Tiqets orders (for tracking)
-- --------------------------------------------
CREATE TABLE tiqets.tiqets_order (
    order_id SERIAL PRIMARY KEY,
    tiqets_order_reference VARCHAR(50),
    tiqets_external_reference VARCHAR(255),
    tqpro_booking_request_id INTEGER,
    tqpro_invoice_id INTEGER,
    product_id VARCHAR(30) REFERENCES tiqets.product(product_id),
    booking_date DATE NOT NULL,
    timeslot_time VARCHAR(20),
    status VARCHAR(50) NOT NULL DEFAULT 'PENDING',
    total_amount DECIMAL(12,2),
    currency VARCHAR(10) DEFAULT 'AED',
    customer_firstname VARCHAR(255),
    customer_lastname VARCHAR(255),
    customer_email VARCHAR(255),
    customer_phone VARCHAR(50),
    payment_confirmation_token VARCHAR(255),
    tickets_pdf_url VARCHAR(500),
    error_code VARCHAR(50),
    error_message TEXT,
    created_at TIMESTAMP NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_tiqets_order_status ON tiqets.tiqets_order(status);
CREATE INDEX idx_tiqets_order_booking ON tiqets.tiqets_order(tqpro_booking_request_id);
CREATE INDEX idx_tiqets_order_ref ON tiqets.tiqets_order(tiqets_order_reference);
CREATE INDEX idx_tiqets_order_date ON tiqets.tiqets_order(booking_date);

-- --------------------------------------------
-- Order items (variants selected)
-- --------------------------------------------
CREATE TABLE tiqets.tiqets_order_item (
    order_item_id SERIAL PRIMARY KEY,
    order_id INTEGER REFERENCES tiqets.tiqets_order(order_id),
    variant_id VARCHAR(50),
    variant_label VARCHAR(255),
    quantity INTEGER NOT NULL DEFAULT 1,
    unit_price DECIMAL(12,2),
    total_price DECIMAL(12,2)
);

CREATE INDEX idx_tiqets_order_item_order ON tiqets.tiqets_order_item(order_id);

-- --------------------------------------------
-- Refresh status tracking
-- --------------------------------------------
CREATE TABLE tiqets.refresh_status (
    ref_code VARCHAR(50) PRIMARY KEY,      -- COUNTRIES, CITIES, EXP_{cityId}, PROD_{expId}
    ref_status VARCHAR(20) NOT NULL DEFAULT 'NEW',
    ref_started TIMESTAMP,
    ref_completed TIMESTAMP,
    items_processed INTEGER DEFAULT 0,
    error_message TEXT
);

-- --------------------------------------------
-- Supplier registration (if not exists)
-- --------------------------------------------
INSERT INTO public.suppliers (supplier_name, supplier_code, factory_name, active, created_at)
VALUES ('Tiqets', 'TIQETS', 'TiqetsServiceFactory', true, NOW())
ON CONFLICT DO NOTHING;

4. Configuration Files

4.1 Plugin Configuration

File: config/tiqets-client.xml

<?xml version="1.0" encoding="UTF-8"?>
<TiqetsPluginConfig xmlns="http://tlinq.perun.com/tiqets">

    <PluginProperties>
        <!-- Database connection -->
        <property name="dbname" value="local"/>

        <!-- Tiqets API Configuration -->
        <property name="tiqets.api.key" value="${TIQETS_API_KEY}"/>
        <property name="tiqets.api.baseUrl.test" value="https://api-tiqt-test.steq.it/v2"/>
        <property name="tiqets.api.baseUrl.prod" value="https://api.tiqets.com/v2"/>
        <property name="tiqets.api.baseUrl" value="https://api-tiqt-test.steq.it/v2"/>

        <!-- JWT Signing for Booking APIs -->
        <property name="tiqets.jwt.privateKeyFile" value="tiqets-private-key.pem"/>
        <property name="tiqets.jwt.keyId" value="${TIQETS_JWT_KEY_ID}"/>

        <!-- Default Settings -->
        <property name="tiqets.default.currency" value="AED"/>
        <property name="tiqets.default.language" value="en"/>
        <property name="tiqets.refresh.interval.hours" value="6"/>
        <property name="tiqets.dateformat" value="yyyy-MM-dd"/>

        <!-- Supplier ID (from suppliers table) -->
        <property name="tiqets.supplier.id" value="TIQETS_SUPPLIER_ID"/>
    </PluginProperties>

    <Databases>
        <Database name="local"
                  url="jdbc:postgresql://localhost:5432/tlinq"
                  username="tlinq"
                  password="${DB_PASSWORD}"/>
        <Database name="dev"
                  url="jdbc:postgresql://devdb1.perunapps.com:5432/tlinq"
                  username="tlinq"
                  password="${DB_PASSWORD}"/>
        <Database name="prod"
                  url="jdbc:postgresql://perun-db-prod01.cmupgeo7xxaa.us-east-1.rds.amazonaws.com:5432/tlinq"
                  username="tlinq"
                  password="${DB_PASSWORD}"/>
    </Databases>

    <Services>
        <!-- Catalog Services -->
        <Service name="listCountries"
                 class="com.perun.tlinq.client.tiqets.service.product.TiqetsCatalogService"
                 method="listCountries"/>
        <Service name="listCities"
                 class="com.perun.tlinq.client.tiqets.service.product.TiqetsCatalogService"
                 method="listCities"/>
        <Service name="listExperiences"
                 class="com.perun.tlinq.client.tiqets.service.product.TiqetsCatalogService"
                 method="listExperiences"/>
        <Service name="getExperience"
                 class="com.perun.tlinq.client.tiqets.service.product.TiqetsCatalogService"
                 method="getExperience"/>
        <Service name="listProducts"
                 class="com.perun.tlinq.client.tiqets.service.product.TiqetsCatalogService"
                 method="listProducts"/>
        <Service name="getProduct"
                 class="com.perun.tlinq.client.tiqets.service.product.TiqetsCatalogService"
                 method="getProduct"/>
        <Service name="getAvailability"
                 class="com.perun.tlinq.client.tiqets.service.product.TiqetsCatalogService"
                 method="getAvailability"/>

        <!-- Booking Services -->
        <Service name="createOrder"
                 class="com.perun.tlinq.client.tiqets.service.TiqetsTicketingService"
                 method="createOrder"/>
        <Service name="confirmOrder"
                 class="com.perun.tlinq.client.tiqets.service.TiqetsTicketingService"
                 method="confirmOrder"/>
        <Service name="getOrderStatus"
                 class="com.perun.tlinq.client.tiqets.service.TiqetsTicketingService"
                 method="getOrderStatus"/>
        <Service name="getTickets"
                 class="com.perun.tlinq.client.tiqets.service.TiqetsTicketingService"
                 method="getTickets"/>
        <Service name="cancelOrder"
                 class="com.perun.tlinq.client.tiqets.service.TiqetsTicketingService"
                 method="cancelOrder"/>
    </Services>

</TiqetsPluginConfig>

4.2 Main Config Updates

File: config/tourlinq-config.xml (additions)

Add to <Plugins> section:

<Plugin name="TiqetsPlugin" constructorClass="com.perun.tlinq.framework.TiqetsPlugin">
    <properties>
        <property name="dbname" value="##tlinq.dbname"/>
        <property name="dbpass" value="##tlinq.dbpass"/>
        <property name="configFile" value="tiqets-client.xml"/>
    </properties>
</Plugin>

Add to <ServiceFactories> section:

<ServiceFactory name="TiqetsServiceFactory"
                code="TIQETS"
                type="remote"
                enabled="true"
                class="com.perun.tlinq.client.tiqets.service.TiqetsServiceFactory">
    <properties>
        <property name="ticketing.factory" value="com.perun.tlinq.client.tiqets.service.TiqetsServiceFactory"/>
    </properties>
</ServiceFactory>

4.3 API Roles Configuration

File: config/api-roles.properties (additions)

# ============================================
# Tiqets Ticketing API - Public (Browsing)
# ============================================
ticketing/country/list=guest,agent,admin
ticketing/city/list=guest,agent,admin
ticketing/experience/list=guest,agent,admin
ticketing/experience/read=guest,agent,admin
ticketing/product/list=guest,agent,admin
ticketing/product/read=guest,agent,admin
ticketing/product/availability=guest,agent,admin

# ============================================
# Tiqets Ticketing API - Direct Booking (B2C)
# ============================================
ticketing/booking/create=guest,agent,admin
ticketing/booking/callback=guest,agent,admin
ticketing/booking/confirm=guest,agent,admin
ticketing/booking/resend=guest,agent,admin

# ============================================
# Tiqets Ticketing API - Agent Booking (B2B)
# ============================================
ticketing/order/create=agent,admin
ticketing/order/confirm=agent,admin
ticketing/order/status=agent,admin
ticketing/order/tickets=agent,admin
ticketing/order/cancel=agent,admin

# ============================================
# Tiqets Admin - City Management
# ============================================
ticketing/admin/city/enable=admin
ticketing/admin/city/disable=admin
ticketing/admin/refresh=admin


5. Key Class Specifications

5.1 TiqetsPlugin.java

package com.perun.tlinq.framework;

/**
 * Plugin initialization for Tiqets integration.
 *
 * Responsibilities:
 * - Load configuration from tiqets-client.xml
 * - Initialize TiqetsDBSession for Hibernate
 * - Initialize TiqetsServiceFactory
 * - Schedule data refresh job (every 6 hours)
 * - Initialize TiqetsCacheManager
 */
public class TiqetsPlugin extends AbstractPlugin {

    // Configuration holder
    private TiqetsClientConfig config;

    // Required methods:
    public void initializePlugin();
    public String getProperty(String propertyName);
    public void setProperty(String propertyName, String propertyValue);
}

5.2 TiqetsServiceFactory.java

package com.perun.tlinq.client.tiqets.service;

/**
 * Service factory for Tiqets services.
 *
 * Implements:
 * - RemoteServiceFactoryI - for generic service creation
 * - TicketingServiceFactoryI - for ticketing service access
 */
public class TiqetsServiceFactory implements RemoteServiceFactoryI, TicketingServiceFactoryI {

    // Singleton pattern
    private static volatile TiqetsServiceFactory _instance;
    public static TiqetsServiceFactory getInstance();

    // RemoteServiceFactoryI
    @Override
    public RemoteServiceI createService(String serviceName, String sessionToken);

    @Override
    public String authenticateUser(String user, String pwd);  // Returns null

    @Override
    public Integer getSessionId(String sessionToken);  // Returns null

    // TicketingServiceFactoryI
    @Override
    public TicketingServiceI getTicketingService();  // Returns new TiqetsTicketingService()
}

5.3 TiqetsTicketingService.java

package com.perun.tlinq.client.tiqets.service;

/**
 * Implementation of TicketingServiceI for Tiqets API.
 *
 * This is the KEY integration point that connects TQPro's
 * booking infrastructure to Tiqets' APIs.
 */
public class TiqetsTicketingService implements TicketingServiceI {

    private TiqetsHttpClient httpClient;

    /**
     * Initialize request with Tiqets-specific data.
     * Validates all required fields are present.
     */
    @Override
    public TicketingRequestI initTicketRequest(TicketingRequestI request) throws TlinqClientException;

    /**
     * Send booking request to Tiqets.
     * Maps to: POST /orders
     *
     * Process:
     * 1. Extract product_id, day, variants from request
     * 2. Build CreateOrderRequest DTO
     * 3. Call Tiqets API with JWT signing
     * 4. Store order_reference_id and payment_confirmation_token
     * 5. Map response to TicketingResponseI
     */
    @Override
    public TicketingResponseI sendTicketRequest(TicketingRequestI req) throws TlinqClientException;

    /**
     * Confirm order after payment.
     * Maps to: PUT /orders/{orderReferenceId}
     *
     * Process:
     * 1. Get stored order_reference_id
     * 2. Call confirm API with payment_confirmation_token
     * 3. Poll GET /orders/{id} until status=done (max 5 minutes)
     * 4. Call GET /orders/{id}/tickets to get PDF URL
     * 5. Map final response with ticket data
     */
    @Override
    public TicketingResponseI confirmTicketRequest(TicketingRequestI req);

    /**
     * Check order status.
     * Maps to: GET /orders/{orderReferenceId}
     */
    @Override
    public TicketingResponseI checkTicketRequest(TicketingRequestI req);

    /**
     * Cancel order.
     * Maps to: DELETE /orders/{orderReferenceId} (if supported)
     */
    @Override
    public TicketingResponseI cancelTicketRequest(TicketingRequestI req, TicketingDataI prevResponse);

    /**
     * Amend order (not supported by Tiqets - throws exception).
     */
    @Override
    public TicketingResponseI amendTicketingRequest(TicketingRequestI req, TicketingDataI prevResponse);
}

5.4 TiqetsResponseMapper.java

package com.perun.tlinq.client.tiqets.service;

/**
 * Maps Tiqets API responses to TQPro ticketing interfaces.
 */
public class TiqetsResponseMapper {

    /**
     * Map CreateOrderResponse to TicketingResponseI.
     *
     * Mapping:
     * - order_reference_id -> TicketingStatusI.bookingRefNo
     * - payment_confirmation_token -> stored for confirmation
     * - estimated_commissions -> for cost calculation
     */
    public TicketingResponseI mapCreateOrderResponse(CreateOrderResponse response);

    /**
     * Map order status to TicketingResponseI.
     *
     * Mapping:
     * - order_status -> bookingResult (done=OK, failed=FAILED, pending=PENDING)
     * - tickets_pdf_url -> TicketingDataI.ticketUrl
     * - how_to_use_info -> TicketingDataI.ticketDescription
     */
    public TicketingResponseI mapOrderStatusResponse(OrderStatusResponse response, TicketsResponse tickets);

    /**
     * Build TicketingStatusI array from order items.
     */
    public TicketingStatusI[] buildTicketingStatus(TiqetsOrder order, List<OrderItem> items);

    /**
     * Build TicketingDataI array from tickets response.
     */
    public TicketingDataI[] buildTicketingData(TicketsResponse tickets);
}

5.5 TiqetsHttpClient.java

package com.perun.tlinq.client.tiqets.remote;

/**
 * HTTP client for Tiqets API with JWT signing support.
 */
public class TiqetsHttpClient {

    private WebTarget target;
    private String apiKey;
    private PrivateKey jwtPrivateKey;
    private String jwtKeyId;
    private Gson gson;

    public TiqetsHttpClient();

    /**
     * Load RSA private key for JWT signing.
     */
    private void loadPrivateKey(String path) throws Exception;

    /**
     * Generate JWT token for secure API requests.
     * Algorithm: RS256
     * Claims: request_hash, iat, exp (5 minutes)
     */
    private String generateJwt(Map<String, Object> payload);

    /**
     * GET request (no JWT).
     */
    public <T> T get(String path, Class<T> responseClass);
    public <T> T get(String path, Class<T> responseClass, Map<String, String> queryParams);

    /**
     * POST request (with optional JWT).
     */
    public <T> T post(String path, Object requestBody, Class<T> responseClass);
    public <T> T postWithJwt(String path, Object requestBody, Class<T> responseClass);

    /**
     * PUT request (with JWT for booking confirmation).
     */
    public <T> T put(String path, Object requestBody, Class<T> responseClass);

    /**
     * DELETE request (with JWT for cancellation).
     */
    public <T> T delete(String path, Class<T> responseClass);
}

5.6 TiqetsCatalogFacade.java

package com.perun.tlinq.client.tiqets.service.product;

/**
 * Facade for catalog operations (experiences, products, availability).
 */
public class TiqetsCatalogFacade {

    private static TiqetsCatalogFacade _instance;
    public static TiqetsCatalogFacade instance();

    // Location APIs
    public TqCountry[] listCountries() throws TlinqClientException;
    public TqCity[] listCities(String countryId, Boolean enabledOnly) throws TlinqClientException;

    // Experience APIs
    public TqExperience[] listExperiences(String cityId, String countryId,
                                          String category, Integer page, Integer pageSize)
                                          throws TlinqClientException;
    public TqExperience getExperience(String experienceId) throws TlinqClientException;

    // Product APIs
    public TqProduct[] listProducts(String experienceId, String cityId) throws TlinqClientException;
    public TqProduct getProduct(String productId) throws TlinqClientException;

    // Availability API (real-time from Tiqets)
    public TqAvailability getAvailability(String productId, String dateFrom, String dateTo)
                                          throws TlinqClientException;

    // Admin operations
    public void enableCity(String cityId) throws TlinqClientException;
    public void disableCity(String cityId) throws TlinqClientException;
    public void triggerRefresh() throws TlinqClientException;
}

5.7 DirectBookingFacade.java (New in tqapp)

package com.perun.tlinq.entity.tkt;

/**
 * Facade for direct B2C bookings (without sales order).
 * Location: tqapp/src/main/java/com/perun/tlinq/entity/tkt/DirectBookingFacade.java
 *
 * This extends the booking flow for public website purchases.
 */
public class DirectBookingFacade extends EntityFacade {

    /**
     * Create a direct booking with invoice.
     *
     * @param customerId Customer ID (or null for guest checkout)
     * @param productId Tiqets product ID
     * @param bookingDate Visit date (YYYY-MM-DD)
     * @param timeslotId Optional timeslot ID
     * @param variants List of {variantId, quantity}
     * @param customerDetails {firstname, lastname, email, phone}
     * @return DirectBookingResult with bookingId, invoiceId, paymentUrl
     */
    public DirectBookingResult createDirectBooking(
        Integer customerId,
        String productId,
        String bookingDate,
        String timeslotId,
        List<VariantSelection> variants,
        Map<String, String> customerDetails
    ) throws TlinqClientException;

    /**
     * Confirm booking after successful payment.
     *
     * @param bookingId Booking request ID
     * @param paymentReference Payment gateway reference
     * @return CBookingResult with ticket details
     */
    public CBookingResult confirmDirectBooking(
        Integer bookingId,
        String paymentReference
    ) throws TlinqClientException;

    /**
     * Handle payment gateway callback.
     */
    public void handlePaymentCallback(
        String transactionRef,
        String status,
        Map<String, String> callbackData
    ) throws TlinqClientException;

    /**
     * Resend tickets and invoice to customer.
     */
    public void resendBookingEmails(Integer bookingId) throws TlinqClientException;

    // Inner classes
    public static class VariantSelection {
        public String variantId;
        public Integer quantity;
    }

    public static class DirectBookingResult {
        public Integer bookingId;
        public Integer invoiceId;
        public String paymentUrl;
        public String bookingReference;
    }
}

6. REST API Specifications

6.1 TicketingApi.java

File: tqapi/src/main/java/com/perun/tlinq/api/TicketingApi.java

@Path("/ticketing")
public class TicketingApi extends HttpServlet {

    // ==================== Location APIs ====================

    @POST @Path("/country/list")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response listCountries(Map reqData);
    // Request: { "session": "" }
    // Response: { "apiStatus": {...}, "apiData": [TqCountry...] }

    @POST @Path("/city/list")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response listCities(Map reqData);
    // Request: { "session": "", "countryId": "NL", "enabledOnly": true }
    // Response: { "apiStatus": {...}, "apiData": [TqCity...] }

    // ==================== Experience APIs ====================

    @POST @Path("/experience/list")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response listExperiences(Map reqData);
    // Request: { "session": "", "cityId": "1", "countryId": "NL", "category": "museums", "page": 1, "pageSize": 20 }
    // Response: { "apiStatus": {...}, "apiData": { "total": 100, "page": 1, "experiences": [...] } }

    @POST @Path("/experience/read")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response readExperience(Map reqData);
    // Request: { "session": "", "experienceId": "12345" }
    // Response: { "apiStatus": {...}, "apiData": TqExperience }

    // ==================== Product APIs ====================

    @POST @Path("/product/list")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response listProducts(Map reqData);
    // Request: { "session": "", "experienceId": "12345", "cityId": "1" }
    // Response: { "apiStatus": {...}, "apiData": [TqProduct...] }

    @POST @Path("/product/read")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response readProduct(Map reqData);
    // Request: { "session": "", "productId": "67890" }
    // Response: { "apiStatus": {...}, "apiData": TqProduct (with variants) }

    @POST @Path("/product/availability")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response getProductAvailability(Map reqData);
    // Request: { "session": "", "productId": "67890", "dateFrom": "2024-03-01", "dateTo": "2024-03-31" }
    // Response: { "apiStatus": {...}, "apiData": TqAvailability }

    // ==================== Direct Booking APIs (B2C) ====================

    @POST @Path("/booking/create")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response createDirectBooking(Map reqData);
    // Request: {
    //   "session": "",
    //   "productId": "67890",
    //   "bookingDate": "2024-03-15",
    //   "timeslotId": "10:00",
    //   "variants": [{"variantId": "adult", "quantity": 2}],
    //   "customer": {"firstname": "John", "lastname": "Doe", "email": "john@example.com", "phone": "+971..."}
    // }
    // Response: { "apiStatus": {...}, "apiData": { "bookingId": 123, "invoiceId": 456, "paymentUrl": "https://..." } }

    @POST @Path("/booking/callback")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response paymentCallback(Map reqData);
    // Called by payment gateway

    @POST @Path("/booking/confirm")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response confirmDirectBooking(Map reqData);
    // Request: { "session": "", "bookingId": 123, "paymentReference": "TXN123" }
    // Response: { "apiStatus": {...}, "apiData": CBookingResult }

    @POST @Path("/booking/resend")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response resendBookingEmails(Map reqData);
    // Request: { "session": "", "bookingId": 123 }
    // Response: { "apiStatus": {...}, "apiData": "Emails sent successfully" }

    // ==================== Agent Booking APIs (B2B) ====================

    @POST @Path("/order/create")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response createOrder(Map reqData);
    // Request: { "session": "agent_session", ...same as booking/create... }
    // For agent workflow (linked to trip/quote)

    @POST @Path("/order/confirm")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response confirmOrder(Map reqData);

    @POST @Path("/order/status")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response getOrderStatus(Map reqData);

    @POST @Path("/order/tickets")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response getOrderTickets(Map reqData);

    @POST @Path("/order/cancel")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response cancelOrder(Map reqData);

    // ==================== Admin APIs ====================

    @POST @Path("/admin/city/enable")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response enableCity(Map reqData);
    // Request: { "session": "admin_session", "cityId": "1" }

    @POST @Path("/admin/city/disable")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response disableCity(Map reqData);

    @POST @Path("/admin/refresh")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response triggerRefresh(Map reqData);
}

7. Email Templates

7.1 Ticket Email Template

File: config/tiqets-ticket-master.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Your Tickets - %BOOKREF%</title>
</head>
<body style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
    <div style="background: #1a73e8; color: white; padding: 20px; text-align: center;">
        <h1>Your Tickets Are Ready!</h1>
    </div>

    <div style="padding: 20px;">
        <p>Dear %USERNAME%,</p>
        <p>Thank you for your booking. Your tickets are attached to this email as a PDF.</p>

        <h2>Booking Reference: %BOOKREF%</h2>

        <div id="TIXDETAILS">
            <!-- Ticket details inserted here -->
        </div>

        <hr style="margin: 30px 0;">

        <h3>Important Information</h3>
        <ul>
            <li>Please bring a printed or digital copy of your tickets</li>
            <li>Arrive at least 15 minutes before your scheduled time</li>
            <li>Your tickets are non-transferable</li>
        </ul>

        <p>If you have any questions, please contact our support team.</p>

        <p>Best regards,<br>BookMyHoliday Team</p>
    </div>
</body>
</html>

File: config/tiqets-ticket-detail.html

<div style="border: 1px solid #ddd; border-radius: 8px; padding: 15px; margin: 15px 0;">
    <table style="width: 100%;">
        <tr>
            <td style="width: 30%; vertical-align: top;">
                <img src="%BARIMGSRC%" alt="QR Code" style="max-width: 150px;">
            </td>
            <td style="vertical-align: top;">
                <h3 style="margin-top: 0;">%TIXDESC%</h3>
                <p><strong>Date:</strong> %DATE%</p>
                <p><strong>Time:</strong> %TIME%</p>
                <p><strong>Ticket Type:</strong> %TIXTYPE%</p>
                <p><strong>Ticket Holder:</strong> %TIXHOLDER%</p>
                <p><strong>Voucher:</strong> %VOUCHER%</p>
                <div %HIDEINFO%>
                    <p><strong>Additional Info:</strong></p>
                    <p>%OTHERINFO%</p>
                </div>
            </td>
        </tr>
    </table>
    <div style="margin-top: 10px; font-size: 12px; color: #666;">
        <p><strong>Cancellation Policy:</strong> %CANCELPOLICY%</p>
    </div>
</div>

7.2 Invoice Email Template

File: config/tiqets-invoice.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Invoice %INVOICENUM%</title>
</head>
<body style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
    <div style="background: #333; color: white; padding: 20px; text-align: center;">
        <h1>Invoice</h1>
    </div>

    <div style="padding: 20px;">
        <table style="width: 100%;">
            <tr>
                <td>
                    <strong>Invoice Number:</strong> %INVOICENUM%<br>
                    <strong>Date:</strong> %INVOICEDATE%<br>
                    <strong>Booking Reference:</strong> %BOOKREF%
                </td>
                <td style="text-align: right;">
                    <strong>Bill To:</strong><br>
                    %CUSTNAME%<br>
                    %CUSTEMAIL%
                </td>
            </tr>
        </table>

        <hr style="margin: 20px 0;">

        <table style="width: 100%; border-collapse: collapse;">
            <thead>
                <tr style="background: #f5f5f5;">
                    <th style="padding: 10px; text-align: left; border-bottom: 2px solid #ddd;">Description</th>
                    <th style="padding: 10px; text-align: center; border-bottom: 2px solid #ddd;">Qty</th>
                    <th style="padding: 10px; text-align: right; border-bottom: 2px solid #ddd;">Price</th>
                    <th style="padding: 10px; text-align: right; border-bottom: 2px solid #ddd;">Total</th>
                </tr>
            </thead>
            <tbody id="INVOICEITEMS">
                <!-- Items inserted here -->
            </tbody>
        </table>

        <table style="width: 100%; margin-top: 20px;">
            <tr>
                <td></td>
                <td style="text-align: right; width: 200px;">
                    <strong>Subtotal:</strong> %SUBTOTAL% %CURRENCY%<br>
                    <strong>Tax:</strong> %TAX% %CURRENCY%<br>
                    <hr>
                    <strong style="font-size: 18px;">Total: %TOTAL% %CURRENCY%</strong>
                </td>
            </tr>
        </table>

        <div style="margin-top: 30px; padding: 15px; background: #e8f5e9; border-radius: 8px;">
            <strong>Payment Status: PAID</strong><br>
            Payment Reference: %PAYMENTREF%
        </div>

        <p style="margin-top: 30px; font-size: 12px; color: #666;">
            Thank you for your purchase. This invoice is automatically generated.
        </p>
    </div>
</body>
</html>


8. Implementation Sequence

Phase 1.A: Foundation (Steps 1-5)

Step Task Files to Create/Modify
1 Create Gradle module tqtiqets/build.gradle.kts, settings.gradle.kts
2 Create database schema Execute SQL in PostgreSQL
3 Create JAXB config class TiqetsPluginConfig.java
4 Create client config singleton TiqetsClientConfig.java
5 Create config XML config/tiqets-client.xml

Phase 1.B: HTTP Layer (Steps 6-8)

Step Task Files to Create
6 Create HTTP client TiqetsHttpClient.java with JWT signing
7 Create API DTOs remote/entity/*.java, remote/service/request/*.java, remote/service/response/*.java
8 Create API wrapper services remote/service/*Service.java

Phase 1.C: Database Layer (Steps 9-11)

Step Task Files to Create
9 Create JPA entities db/*Entity.java
10 Create Hibernate session factory TiqetsDBSession.java
11 Create native entities entity/Tq*.java

Phase 1.D: Business Logic (Steps 12-16)

Step Task Files to Create
12 Create cache manager TiqetsCacheManager.java
13 Create data refresher StaticDataRefresher.java, TiqetsRefreshRunner.java
14 Create catalog facade TiqetsCatalogFacade.java
15 Create TiqetsTicketingService TiqetsTicketingService.java (implements TicketingServiceI)
16 Create TiqetsResponseMapper TiqetsResponseMapper.java

Phase 1.E: Booking Integration (Steps 17-19)

Step Task Files to Create/Modify
17 Create DirectBookingFacade tqapp/.../entity/tkt/DirectBookingFacade.java
18 Register Tiqets supplier Add to suppliers table, update config
19 Create service factory TiqetsServiceFactory.java

Phase 1.F: API & Plugin (Steps 20-23)

Step Task Files to Create/Modify
20 Create REST API tqapi/.../api/TicketingApi.java
21 Create plugin class TiqetsPlugin.java
22 Update main config tourlinq-config.xml, api-roles.properties
23 Create email templates config/tiqets-*.html

Phase 1.G: Testing (Steps 24-26)

Step Task Description
24 Unit tests Test each service and mapper
25 Integration tests Test API endpoints
26 E2E booking flow Test complete B2C and B2B flows

9. Reference Files Summary

Plugin Patterns (Template Files)

File Full Path
Plugin example tqryb2b/src/main/java/com/perun/tlinq/framework/RaynaB2BActPlugin.java
Service factory tqryb2b/src/main/java/com/perun/tlinq/client/ryb2b/service/RaynaServiceFactory.java
Data refresher tqryb2b/src/main/java/com/perun/tlinq/client/ryb2b/service/StaticDataRefresher.java
Config XML config/rayna-client.xml

Booking Infrastructure (Reuse)

File Full Path
TicketingServiceI tqapp/src/main/java/com/perun/tlinq/service/TicketingServiceI.java
BookingRequestFacade tqapp/src/main/java/com/perun/tlinq/entity/tkt/BookingRequestFacade.java
CBookingRequest tqapp/src/main/java/com/perun/tlinq/entity/tkt/CBookingRequest.java
CTicketingRequest tqapp/src/main/java/com/perun/tlinq/entity/tkt/CTicketingRequest.java
CTicketItem tqapp/src/main/java/com/perun/tlinq/entity/tkt/CTicketItem.java
CTicketConfirmation tqapp/src/main/java/com/perun/tlinq/entity/tkt/CTicketConfirmation.java
InvoiceFacade tqapp/src/main/java/com/perun/tlinq/entity/document/InvoiceFacade.java
PGWFacade tqapp/src/main/java/com/perun/tlinq/pgw/PGWFacade.java
MailUtil tqcommon/src/main/java/com/perun/tlinq/util/MailUtil.java
BookingApi tqapi/src/main/java/com/perun/tlinq/api/BookingApi.java

Configuration Files

File Full Path
Main config config/tourlinq-config.xml
Ticketing entities config/entities/tkt-entities.xml
API roles config/api-roles.properties

Phase 2: Public Website (High Level)

Scope: BookMyHoliday destination pages with ticketing.

Components: 1. Destination page - experiences section 2. Experience detail page - dynamic from Tiqets 3. Product detail page - variant selection, availability calendar 4. Cart integration - add tickets to cart 5. Checkout flow - customer details, payment, confirmation

APIs Used: All /ticketing/* public endpoints


Phase 3: Trip Management Integration (High Level)

Scope: tqweb-adm Trip Management workspace.

Components: 1. Experience browser panel 2. Product selector popup 3. Add to itinerary as CActivity 4. Cost breakdown integration 5. PDF quote generation

Key Integration: - Extend TripMakerFacade with ticketing methods - Update tripmaker-workspace.js - Map Tiqets products to CActivity entities


Thread Safety and Concurrency (TQ-52)

The following thread safety improvements were applied to the Tiqets plugin:

Component Improvement
TiqetsPlugin Scheduled executor uses daemon threads to prevent JVM shutdown hang. Plugin shutdown cancels refresh tasks via ordered lifecycle management.
TiqetsHttpClient Uses a shared java.net.http.HttpClient instance instead of creating per-request instances. Thread-safe by design.
TiqetsCacheManager Uses copy-on-write pattern for cache refresh: builds new map, swaps atomic volatile reference. AtomicBoolean guard prevents concurrent refreshes.
TiqetsServiceFactory Volatile double-checked locking for singleton.
TiqetsDBSession Hibernate sessions closed in finally blocks to prevent leaks.