Skip to content

Activity Supplier Integration: Tiqets.com

This document provides comprehensive implementation details for the Tiqets.com supplier integration in TQPro. Tiqets is a leading online ticketing platform for museums, attractions, and tours worldwide.

Table of Contents

  1. Overview
  2. Architecture
  3. Module Structure
  4. Configuration
  5. Database Schema
  6. Entity Model
  7. Service Layer
  8. REST API Endpoints
  9. Data Synchronization
  10. Caching Strategy
  11. Frontend Integration
  12. Security
  13. Error Handling
  14. Testing

Overview

Purpose

The Tiqets integration enables TQPro to: - Browse and display activity catalog (experiences, products, variants) - Check real-time availability and pricing - Process bookings through Tiqets API - Synchronize catalog data for offline/cached access - Filter and categorize activities using tags

Key Features

Feature Description
Catalog Browsing Countries, cities, experiences, products, variants
Real-time Availability Dynamic pricing and timeslot availability
Tag-based Filtering Activity, venue, and service category tags
Automated Sync Scheduled catalog refresh (configurable interval)
In-memory Caching Fast catalog access with reverse indexes
JWT-signed Booking Secure booking operations with RS256 signatures

Integration Points

┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│   Frontend      │────▶│   TQPro API     │────▶│  Tiqets API     │
│   (tqweb/pub)   │     │   (tqapi)       │     │  (External)     │
└─────────────────┘     └─────────────────┘     └─────────────────┘
                        ┌─────────────────┐
                        │  Local Cache    │
                        │  (PostgreSQL +  │
                        │   In-Memory)    │
                        └─────────────────┘

Architecture

Three-Layer Pattern

The integration follows TQPro's standard three-layer architecture:

┌─────────────────────────────────────────────────────────────────┐
│                        REST API Layer                           │
│  TiqetsApi.java - JAX-RS endpoints for catalog and booking      │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│                        Facade Layer                             │
│  TiqetsFacade.java - Canonical entity operations                │
│  TiqetsCatalogFacade.java - Direct catalog access               │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│                       Service Layer                             │
│  TiqetsCatalogService.java - Business logic                     │
│  TiqetsDataRefresher.java - Data synchronization                │
│  TiqetsCacheManager.java - In-memory caching                    │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│                     Remote API Layer                            │
│  TiqetsHttpClient.java - HTTP client with JWT signing           │
│  ExperiencesService, ProductsService, OrdersService, etc.       │
└─────────────────────────────────────────────────────────────────┘

Plugin Architecture

The Tiqets integration is implemented as a TQPro plugin (TiqetsPlugin.java) that: 1. Loads configuration from tiqets-client.xml 2. Initializes database connectivity via TiqetsDBSession 3. Sets up TiqetsServiceFactory for entity operations 4. Initializes TiqetsCacheManager for fast lookups 5. Schedules periodic data refresh (default: every 6 hours)


Module Structure

Package Organization

tqtiqets/src/main/java/com/perun/tlinq/client/tiqets/
├── config/
│   └── TiqetsPluginConfig.java       # JAXB configuration class
├── db/
│   ├── TiqetsDBSession.java          # Hibernate session factory
│   ├── CountryEntity.java            # JPA entity for countries
│   ├── CityEntity.java               # JPA entity for cities
│   ├── ExperienceEntity.java         # JPA entity for experiences
│   ├── ProductEntity.java            # JPA entity for products
│   ├── ProductVariantEntity.java     # JPA entity for variants
│   ├── TagEntity.java                # JPA entity for tags
│   ├── TagTypeEntity.java            # JPA entity for tag types
│   ├── TiqetsOrderEntity.java        # JPA entity for orders
│   ├── TiqetsOrderItemEntity.java    # JPA entity for order items
│   └── RefreshStatusEntity.java      # JPA entity for sync tracking
├── entity/
│   ├── TqEntity.java                 # Base native entity class
│   ├── TqExperience.java             # Native experience entity
│   ├── TqProduct.java                # Native product entity
│   ├── TqProductVariant.java         # Native variant entity
│   ├── TqTag.java                    # Native tag entity
│   ├── TqTagType.java                # Native tag type entity
│   ├── TqAvailability.java           # Native availability entity
│   ├── TqCountry.java                # Native country entity
│   └── TqCity.java                   # Native city entity
├── framework/
│   └── TiqetsPlugin.java             # Plugin initialization
├── remote/
│   ├── TiqetsHttpClient.java         # HTTP client with JWT
│   ├── entity/                       # API response DTOs
│   │   ├── TiqetsExperience.java
│   │   ├── TiqetsProduct.java
│   │   ├── TiqetsVariant.java
│   │   └── ...
│   └── service/                      # API wrapper services
│       ├── ExperiencesService.java
│       ├── ProductsService.java
│       ├── OrdersService.java
│       ├── TagsService.java
│       ├── request/                  # Request DTOs
│       └── response/                 # Response DTOs
├── service/
│   ├── TiqetsEntityService.java      # Base service class
│   ├── TiqetsServiceFactory.java     # Service factory
│   ├── TiqetsDataRefresher.java      # Data sync service
│   ├── TiqetsResponseMapper.java     # Response mapping
│   └── product/
│       ├── TiqetsCatalogFacade.java  # Catalog operations
│       └── TiqetsCatalogService.java # Catalog service
└── util/
    ├── TiqetsClientConfig.java       # Configuration loader
    ├── TiqetsCacheManager.java       # In-memory cache
    └── TiqetsInputValidator.java     # Input validation

Dependencies

The tqtiqets module depends on: - tqapp - Core application framework - tqcommon - Common utilities and base classes - jjwt-api, jjwt-impl, jjwt-jackson (v0.11.5) - JWT signing


Configuration

Configuration File: tiqets-client.xml

Located in TLINQ_HOME/config/, this file contains:

<?xml version="1.0" encoding="UTF-8"?>
<TiqetsConfig>
    <!-- Database Configuration -->
    <Properties>
        <Property name="db.name">tiqets</Property>
        <Property name="db.host">localhost</Property>
        <Property name="db.port">5432</Property>
        <Property name="db.user">tqpro</Property>
        <Property name="db.password">...</Property>
    </Properties>

    <!-- API Configuration -->
    <Properties>
        <Property name="api.key">your-api-key</Property>
        <Property name="api.baseUrl.test">https://api.tiqets.com/v2</Property>
        <Property name="api.baseUrl.prod">https://api.tiqets.com/v2</Property>
        <Property name="jwt.privateKeyFile">config/tiqets-private-key.pem</Property>
        <Property name="jwt.keyId">your-key-id</Property>
    </Properties>

    <!-- Defaults -->
    <Properties>
        <Property name="default.currency">AED</Property>
        <Property name="default.language">en</Property>
        <Property name="refresh.intervalHours">6</Property>
        <Property name="refresh.autoEnabled">true</Property>
        <Property name="supplier.id">TIQETS</Property>
    </Properties>

    <!-- Service Definitions -->
    <ServiceList>
        <Service name="TiqetsCatalogService"
                 class="com.perun.tlinq.client.tiqets.service.product.TiqetsCatalogService"/>
        <!-- Additional services... -->
    </ServiceList>
</TiqetsConfig>

Configuration Access

Use TiqetsClientConfig.instance() to access configuration:

TiqetsClientConfig config = TiqetsClientConfig.instance();
String apiKey = config.getApiKey();
String baseUrl = config.getApiBaseUrl();
int refreshInterval = config.getRefreshIntervalHours();

Database Schema

Schema: tiqets

All tables are created in the tiqets schema within PostgreSQL.

Core Tables

tiqets.country

CREATE TABLE tiqets.country (
    id SERIAL PRIMARY KEY,
    supplier_country_id VARCHAR(50) UNIQUE NOT NULL,
    country_name VARCHAR(255),
    country_code VARCHAR(10),
    hash BIGINT,
    last_update TIMESTAMP
);

tiqets.city

CREATE TABLE tiqets.city (
    id SERIAL PRIMARY KEY,
    supplier_city_id VARCHAR(50) UNIQUE NOT NULL,
    city_name VARCHAR(255),
    supplier_country_id VARCHAR(50) REFERENCES tiqets.country(supplier_country_id),
    timezone VARCHAR(100),
    latitude DECIMAL(10, 7),
    longitude DECIMAL(10, 7),
    currency VARCHAR(10) DEFAULT 'AED',
    enabled BOOLEAN DEFAULT FALSE,
    hash BIGINT,
    last_update TIMESTAMP
);

CREATE INDEX idx_city_country ON tiqets.city(supplier_country_id);
CREATE INDEX idx_city_enabled ON tiqets.city(enabled);

tiqets.experience

CREATE TABLE tiqets.experience (
    id SERIAL PRIMARY KEY,
    supplier_experience_id VARCHAR(50) UNIQUE NOT NULL,
    experience_type VARCHAR(50),
    title VARCHAR(500),
    tagline TEXT,
    description TEXT,
    supplier_city_id VARCHAR(50),
    supplier_country_id VARCHAR(50),
    city_name VARCHAR(255),
    country_name VARCHAR(255),
    content_language VARCHAR(10),
    street_address VARCHAR(500),
    postal_code VARCHAR(50),
    latitude DECIMAL(10, 7),
    longitude DECIMAL(10, 7),
    google_place_id VARCHAR(255),
    image_small VARCHAR(1000),
    image_medium VARCHAR(1000),
    image_large VARCHAR(1000),
    image_xlarge VARCHAR(1000),
    image_alt_text VARCHAR(500),
    image_credit VARCHAR(500),
    rating_average DECIMAL(3, 2),
    rating_count INTEGER,
    from_price DECIMAL(10, 2),
    currency VARCHAR(10),
    tag_ids TEXT,           -- Comma-separated tag IDs
    product_ids TEXT,       -- Comma-separated product IDs
    experience_url VARCHAR(1000),
    active BOOLEAN DEFAULT TRUE,
    hash BIGINT,
    last_update TIMESTAMP
);

CREATE INDEX idx_exp_city ON tiqets.experience(supplier_city_id);
CREATE INDEX idx_exp_country ON tiqets.experience(supplier_country_id);
CREATE INDEX idx_exp_active ON tiqets.experience(active);

tiqets.product

CREATE TABLE tiqets.product (
    id SERIAL PRIMARY KEY,
    supplier_product_id VARCHAR(50) UNIQUE NOT NULL,
    supplier_experience_id VARCHAR(50),
    title VARCHAR(500),
    description TEXT,
    highlights TEXT,
    inclusions TEXT,
    exclusions TEXT,
    important_info TEXT,
    cancellation_policy TEXT,

    -- Duration
    duration_minutes INTEGER,
    duration_formatted VARCHAR(100),
    starting_time VARCHAR(50),
    product_timezone VARCHAR(100),

    -- Pricing
    min_price DECIMAL(10, 2),
    currency VARCHAR(10),
    retail_price DECIMAL(10, 2),
    price_supplier_currency DECIMAL(10, 2),
    supplier_currency VARCHAR(10),
    prediscount_price DECIMAL(10, 2),
    discount_percentage DECIMAL(5, 2),
    booking_fee DECIMAL(10, 2),

    -- Booking Configuration
    has_timeslots BOOLEAN DEFAULT FALSE,
    has_dynamic_pricing BOOLEAN DEFAULT FALSE,
    max_tickets_per_order INTEGER,
    booking_cutoff_minutes INTEGER,
    instant_confirmation BOOLEAN DEFAULT TRUE,

    -- Sale Status
    sale_status VARCHAR(50),
    sale_status_reason TEXT,
    sale_status_expected_reopen DATE,

    -- Content
    tagline VARCHAR(500),
    promo_label VARCHAR(255),
    summary TEXT,
    whats_included TEXT,
    whats_excluded TEXT,
    product_slug VARCHAR(255),
    content_language VARCHAR(10),

    -- Features
    supports_cancellation_insurance BOOLEAN,
    instant_ticket_delivery BOOLEAN,
    wheelchair_access BOOLEAN,
    smartphone_ticket BOOLEAN,
    is_package BOOLEAN,
    in_package_ids TEXT,
    package_product_ids TEXT,

    -- Language Support
    supported_languages TEXT,
    live_guide_languages TEXT,
    audio_guide_languages TEXT,
    language_selection_options TEXT,

    -- Location
    latitude DECIMAL(10, 7),
    longitude DECIMAL(10, 7),
    venue_id VARCHAR(50),
    venue_name VARCHAR(255),
    venue_address TEXT,

    -- Images
    image_small VARCHAR(1000),
    image_medium VARCHAR(1000),
    image_large VARCHAR(1000),
    image_xlarge VARCHAR(1000),

    -- URLs
    product_url VARCHAR(1000),
    checkout_url VARCHAR(1000),

    -- Metadata
    tag_ids TEXT,
    active BOOLEAN DEFAULT TRUE,
    hash BIGINT,
    last_update TIMESTAMP
);

CREATE INDEX idx_prod_exp ON tiqets.product(supplier_experience_id);
CREATE INDEX idx_prod_city ON tiqets.product(supplier_city_id);
CREATE INDEX idx_prod_active ON tiqets.product(active);

tiqets.product_variant

CREATE TABLE tiqets.product_variant (
    id SERIAL PRIMARY KEY,
    supplier_variant_id VARCHAR(50) UNIQUE NOT NULL,
    supplier_product_id VARCHAR(50) REFERENCES tiqets.product(supplier_product_id),
    label VARCHAR(255),
    description TEXT,
    variant_type VARCHAR(50),
    max_visitors_per_ticket INTEGER,
    max_tickets INTEGER,
    valid_with_variant_ids TEXT,
    cancellation_window_hours INTEGER,
    cancellation_policy TEXT,
    active BOOLEAN DEFAULT TRUE,
    hash BIGINT,
    last_update TIMESTAMP
);

CREATE INDEX idx_variant_product ON tiqets.product_variant(supplier_product_id);

tiqets.tag_type

CREATE TABLE tiqets.tag_type (
    id SERIAL PRIMARY KEY,
    supplier_tag_type_id VARCHAR(50) UNIQUE NOT NULL,
    name VARCHAR(255),
    group_name VARCHAR(255),
    icon_url VARCHAR(1000),
    last_update TIMESTAMP
);

tiqets.tag

CREATE TABLE tiqets.tag (
    id SERIAL PRIMARY KEY,
    supplier_tag_id VARCHAR(50) UNIQUE NOT NULL,
    name VARCHAR(255),
    supplier_tag_type_id VARCHAR(50),
    type_name VARCHAR(255),
    type_group_name VARCHAR(255),
    icon_url VARCHAR(1000),
    last_update TIMESTAMP
);

CREATE INDEX idx_tag_type ON tiqets.tag(supplier_tag_type_id);
CREATE INDEX idx_tag_group ON tiqets.tag(type_group_name);

tiqets.tiqets_order

CREATE TABLE tiqets.tiqets_order (
    id SERIAL PRIMARY KEY,
    tiqets_order_reference VARCHAR(100) UNIQUE,
    tiqets_external_reference VARCHAR(100),
    tqpro_booking_request_id INTEGER,
    tqpro_invoice_id INTEGER,
    product_id VARCHAR(50),
    booking_date DATE,
    timeslot_time VARCHAR(50),
    status VARCHAR(50),
    total_amount DECIMAL(10, 2),
    currency VARCHAR(10),
    customer_firstname VARCHAR(255),
    customer_lastname VARCHAR(255),
    customer_email VARCHAR(255),
    customer_phone VARCHAR(50),
    payment_confirmation_token TEXT,
    tickets_pdf_url TEXT,
    error_code VARCHAR(50),
    error_message TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP
);

CREATE INDEX idx_order_status ON tiqets.tiqets_order(status);
CREATE INDEX idx_order_booking ON tiqets.tiqets_order(tqpro_booking_request_id);

tiqets.refresh_status

CREATE TABLE tiqets.refresh_status (
    ref_code VARCHAR(50) PRIMARY KEY,
    ref_status VARCHAR(20),
    ref_started TIMESTAMP,
    ref_completed TIMESTAMP,
    items_processed INTEGER,
    error_message TEXT
);

Entity Model

Canonical Entities (tqapp)

These are the standard TQPro entities used across all suppliers:

Entity Package Description
CCountry entity.common Country information
CLocation entity.flight City/location (reused for cities)
CExperience entity.product Activity/attraction grouping
CProduct entity.product Bookable product
CProductVariant entity.product Product variant (ticket type)
CTimeslot entity.product Available time slot
CTag entity.product Category tag
CTagType entity.product Tag type/group

Native Entities (tqtiqets)

Tiqets-specific entities that implement RemoteEntityI:

Entity Description
TqExperience 25 fields covering experience details
TqProduct 100+ fields for full product data
TqProductVariant Variant/ticket type information
TqAvailability Nested availability with timeslots
TqTag Tag with type group information
TqTagType Tag type classification
TqCountry Country with code
TqCity City with timezone and coordinates

Entity Field Mappings

Field mappings are defined in config/entities/product-entities.xml:

<Factory name="TiqetsServiceFactory" nativeEntity="com.perun.tlinq.client.tiqets.entity.TqProduct">
    <FieldMappingList>
        <!-- Identity -->
        <FieldMapping targetField="prodCode" sourceField="productId" mapping="DirectMapping"/>
        <FieldMapping targetField="prodName" sourceField="title" mapping="DirectMapping"/>

        <!-- Pricing -->
        <FieldMapping targetField="productPrice" sourceField="minPrice" mapping="DirectMapping"/>
        <FieldMapping targetField="retailPrice" sourceField="retailPrice" mapping="DirectMapping"/>
        <FieldMapping targetField="currency" sourceField="currency" mapping="DirectMapping"/>

        <!-- Images -->
        <FieldMapping targetField="prodImgSmall" sourceField="imageSmall" mapping="DirectMapping"/>
        <FieldMapping targetField="prodImgMed" sourceField="imageMedium" mapping="DirectMapping"/>
        <FieldMapping targetField="prodImgLarge" sourceField="imageLarge" mapping="DirectMapping"/>

        <!-- ... 80+ additional field mappings ... -->
    </FieldMappingList>
</Factory>

Service Layer

TiqetsServiceFactory

Factory for creating Tiqets service instances:

public class TiqetsServiceFactory implements RemoteServiceFactoryI, TicketingServiceFactoryI {

    private static TiqetsServiceFactory instance;

    public static TiqetsServiceFactory getInstance() {
        if (instance == null) {
            instance = new TiqetsServiceFactory();
        }
        return instance;
    }

    @Override
    public RemoteServiceI createService(String serviceName, String sessionToken) {
        // Create service via reflection from configuration
    }

    @Override
    public TicketingServiceI getTicketingService() {
        return new TiqetsTicketingService();
    }
}

TiqetsCatalogFacade

High-level catalog operations:

public class TiqetsCatalogFacade {

    // 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 (real-time from Tiqets API)
    public TqAvailability getAvailability(String productId, String dateFrom, String dateTo)
            throws TlinqClientException;

    // Tag APIs
    public TqTag[] listTagsNative(String typeGroupName) throws TlinqClientException;
    public TqTagType[] listTagTypesNative() throws TlinqClientException;
    public int syncTags() throws TlinqClientException;
    public int syncTagTypes() throws TlinqClientException;

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

TiqetsCatalogService

Business logic service implementing RemoteServiceI:

public class TiqetsCatalogService extends TiqetsEntityService {

    // Experience Methods
    public TqExperience[] listExperiences(RemoteEntityI input) throws TlinqClientException {
        String cityId = (String) getNamedParameter("cityId");
        String countryId = (String) getNamedParameter("countryId");
        // ... implementation
    }

    // Product Methods
    public TqProduct[] listProducts(RemoteEntityI input) throws TlinqClientException {
        String experienceId = (String) getNamedParameter("experienceId");
        String cityId = (String) getNamedParameter("cityId");
        // ... implementation
    }

    // Tag Methods
    public TqTag[] listTags(RemoteEntityI input) throws TlinqClientException {
        String typeGroupName = (String) getNamedParameter("typeGroupName");
        // ... implementation
    }
}

REST API Endpoints

Base Path: /api/tiqets

All endpoints accept POST requests with JSON body.

Location Endpoints

List Countries

POST /tiqets/countries
Request: { "session": "..." }
Response: { "apiStatus": {...}, "apiData": [CCountry, ...] }

List Cities

POST /tiqets/cities
Request: {
    "session": "...",
    "countryId": "50001",      // Optional
    "enabledOnly": true        // Optional, default false
}
Response: { "apiStatus": {...}, "apiData": [CLocation, ...] }

Enable/Disable City

POST /tiqets/city/enable
POST /tiqets/city/disable
Request: { "cityId": "dubai" }
Response: { "apiStatus": {...}, "apiData": "City enabled/disabled successfully" }

Experience Endpoints

List Experiences

POST /tiqets/experiences
Request: {
    "session": "...",
    "cityId": "dubai",          // Optional
    "countryId": "50001",       // Optional
    "categoryId": "123",        // Optional
    "page": 1,                  // Optional, default 1
    "pageSize": 50              // Optional, default 50, max 100
}
Response: { "apiStatus": {...}, "apiData": [CExperience, ...] }

Get Experience

POST /tiqets/experience
Request: { "session": "...", "experienceId": "12345" }
Response: { "apiStatus": {...}, "apiData": CExperience }

Product Endpoints

List Products

POST /tiqets/products
Request: {
    "session": "...",
    "experienceId": "12345",    // Required if no cityId
    "cityId": "dubai"           // Required if no experienceId
}
Response: { "apiStatus": {...}, "apiData": [CProduct, ...] }

Get Product

POST /tiqets/product
Request: { "session": "...", "productId": "67890" }
Response: { "apiStatus": {...}, "apiData": CProduct }

Get Product Variants

POST /tiqets/product/variants
Request: { "session": "...", "productId": "67890" }
Response: { "apiStatus": {...}, "apiData": [CProductVariant, ...] }

Availability Endpoints

Get Availability (Native Structure)

POST /tiqets/availability
Request: {
    "session": "...",
    "productId": "67890",
    "dateFrom": "2024-01-15",   // Required, yyyy-MM-dd
    "dateTo": "2024-01-31"      // Required, yyyy-MM-dd, max 90 days range
}
Response: {
    "apiStatus": {...},
    "apiData": TqAvailability {
        productId, dateFrom, dateTo,
        availableDates: [{
            date, isAvailable, capacity,
            timeslots: [{ time, label, capacity }, ...]
        }, ...]
    }
}

Get Timeslots (Flattened)

POST /tiqets/timeslots
Request: {
    "session": "...",
    "productId": "67890",
    "dateFrom": "2024-01-15",
    "dateTo": "2024-01-31"
}
Response: { "apiStatus": {...}, "apiData": [CTimeslot, ...] }

Tag Endpoints

List Tags

POST /tiqets/tags
Request: {
    "session": "...",
    "typeGroupName": "Activity categories"  // Optional filter
}
Response: { "apiStatus": {...}, "apiData": [CTag, ...] }

List Tag Types

POST /tiqets/tag-types
Request: { "session": "..." }
Response: { "apiStatus": {...}, "apiData": [CTagType, ...] }

Sync Tags (Admin)

POST /tiqets/tags/sync
Request: {}
Response: { "apiStatus": {...}, "apiData": "Synced X tag types and Y tags" }

Input Validation

All inputs are validated using TiqetsInputValidator:

Validation Rules
Date Format yyyy-MM-dd pattern
Date Range Maximum 90 days
Pagination Page >= 1, PageSize <= 100
ID Format Alphanumeric, max 50 chars

Data Synchronization

TiqetsDataRefresher

Handles catalog data synchronization from Tiqets API to local database.

Refresh Methods

public class TiqetsDataRefresher {

    // Full refresh for all enabled cities
    public int[] refreshAll() throws TlinqClientException;

    // Optimized batch refresh (~100x faster)
    public int[] refreshAllOptimized() throws TlinqClientException;

    // Single city refresh
    public int refreshCity(String cityId) throws TlinqClientException;

    // Experience-only sync (fast)
    public int quickExperienceSync(int maxPages) throws TlinqClientException;

    // Initial bootstrap
    public int initialSync(int maxPages) throws TlinqClientException;
}

Refresh Schedule

Configured in TiqetsPlugin: - Default Interval: 6 hours - No Initial Refresh: First check after interval - Configurable: Via refresh.intervalHours property - Disable: Set refresh.autoEnabled=false

Refresh Status Tracking

// Status codes
RefreshStatusEntity.STATUS_NEW = "NEW"
RefreshStatusEntity.STATUS_RUNNING = "RUNNING"
RefreshStatusEntity.STATUS_COMPLETED = "COMPLETED"
RefreshStatusEntity.STATUS_FAILED = "FAILED"

// Reference codes
"GLOBAL" - Overall refresh status
"CITY_dubai" - City-specific refresh
"EXP_12345" - Experience-specific refresh

Caching Strategy

TiqetsCacheManager

Thread-safe singleton providing in-memory caching with reverse indexes.

Cache Stores

private final Map<String, CountryEntity> countryCache;
private final Map<String, CityEntity> cityCache;
private final Map<String, ExperienceEntity> experienceCache;
private final Map<String, ProductEntity> productCache;
private final Map<String, ProductVariantEntity> variantCache;

Reverse Indexes

private final Map<String, List<String>> citiesByCountry;
private final Map<String, List<String>> experiencesByCity;
private final Map<String, List<String>> productsByExperience;
private final Map<String, List<String>> productsByCity;
private final Map<String, List<String>> variantsByProduct;

Usage Example

TiqetsCacheManager cache = TiqetsCacheManager.getInstance();

// Get all enabled cities
List<CityEntity> cities = cache.getEnabledCities();

// Get experiences for a city
List<ExperienceEntity> experiences = cache.getExperiencesByCity("dubai");

// Get products for an experience
List<ProductEntity> products = cache.getProductsByExperience("12345");

Cache Lifecycle

  1. Initialization: Lazy-loaded on first access from database
  2. Refresh: Triggered after data sync completes
  3. Updates: Individual put methods for incremental updates
  4. Clear: refresh() clears and reloads all data

Frontend Integration

Public Website (tqweb/pub)

The UAE Experiences page (uae-experiences.html) provides the public-facing catalog.

Page Structure

<!-- Filter Section -->
<div class="grid-container">
    <select id="citySelect">
        <option value="">All UAE</option>
        <!-- Cities loaded dynamically -->
    </select>
    <input type="text" id="srchbox" placeholder="Search experiences...">
</div>

<!-- Tag Strip Section -->
<div class="grid-container">
    <div id="tagStrip" class="perun-tag-strip">
        <!-- Tags loaded dynamically -->
    </div>
</div>

<!-- Experience Cards -->
<div id="carddiv" class="grid-x grid-margin-x grid-margin-y">
    <!-- Cards loaded dynamically -->
</div>

JavaScript Module: uaeexp.js

// State variables
let experiences = new Map();    // Map<experienceId, CExperience>
let allExperiences = [];        // Full list for filtering
let currentCityCode = null;     // null = all UAE
let uaeCities = [];             // cached CLocation entities
let allTags = [];               // all loaded CTag entities (unfiltered)
let activityTags = [];          // filtered CTag entities
let selectedTagId = null;       // currently selected tag

// Exported functions
export function initPage();
export function filterExperiences();
export function onCitySelectChange(event);
export function onTagClick(event);

Tag Loading

Tags are loaded from three type groups: 1. Activity categories 2. Venue categories 3. Service categories

function loadActivityTags() {
    const typeGroups = [
        "Activity categories",
        "Venue categories",
        "Service categories"
    ];

    const promises = typeGroups.map(typeGroupName =>
        glb.tlinq("tiqets/tags", { typeGroupName })
    );

    return Promise.all(promises).then(results => {
        // Merge and deduplicate by tagId
        // Sort alphabetically
        // Return combined list
    });
}

Tag Filtering

Tags are filtered to only show those present in loaded experiences:

function filterTagsByLoadedExperiences() {
    // Collect tagIds from all experiences
    const experienceTagIds = new Set();
    allExperiences.forEach(exp => {
        // Handle both array and comma-separated formats
        if (exp.tagIds) {
            // Parse and add to set
        }
    });

    // Filter tags to only include those in experiences
    activityTags = allTags.filter(tag =>
        experienceTagIds.has(String(tag.tagId))
    );

    buildTagStrip(activityTags);
}

Experience Filtering by Tag

function applyTagFilter() {
    $("div[data-experience-name]").each(function(idx, elem) {
        const expId = $(elem).find('.perun-exp-card').data('experienceId');
        const exp = experiences.get(String(expId));

        let hasTag = false;
        if (exp && exp.tagIds) {
            // Check if experience has selected tag
            let tagIdList = [];
            if (Array.isArray(exp.tagIds)) {
                exp.tagIds.forEach(id => {
                    String(id).split(',').forEach(part => tagIdList.push(part.trim()));
                });
            } else if (typeof exp.tagIds === 'string') {
                tagIdList = exp.tagIds.split(',').map(id => id.trim());
            }
            hasTag = tagIdList.some(id => id === selectedTagIdStr);
        }

        elem.style.display = hasTag ? "block" : "none";
    });
}

CSS Styling

Tag strip styling in css/app.css:

.perun-tag-strip {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    justify-content: center;
    padding: 0.5rem 0 1rem 0;
}

.perun-tag-chip {
    font-family: "Montserrat", sans-serif;
    font-size: 0.8rem;
    font-weight: 500;
    color: #555;
    background-color: #f5f5f5;
    border: 1px solid #ddd;
    border-radius: 20px;
    padding: 0.4rem 0.9rem;
    cursor: pointer;
    transition: all 0.2s ease;
}

.perun-tag-chip:hover {
    background-color: #e8f4fc;
    border-color: #1e90fb;
    color: #1e90fb;
}

.perun-tag-chip.active {
    background-color: #1e90fb;
    border-color: #1e90fb;
    color: #fff;
}

/* Mobile: horizontal scrolling */
@media screen and (max-width: 39.9375em) {
    .perun-tag-strip {
        overflow-x: auto;
        flex-wrap: nowrap;
        justify-content: flex-start;
        -webkit-overflow-scrolling: touch;
    }
    .perun-tag-chip {
        flex-shrink: 0;
    }
}

Security

API Key Authentication

Read-only catalog APIs use API key header:

Authorization: Token <api_key>

JWT-Signed Requests

Booking operations require JWT signatures:

public class TiqetsHttpClient {

    public String generateJwt(String payload) {
        // Header
        String header = Base64.encode({
            "alg": "RS256",
            "typ": "JWT",
            "kid": keyId
        });

        // Claims
        String claims = Base64.encode({
            "request_hash": SHA256(payload),
            "iat": currentTimestamp,
            "exp": currentTimestamp + 300  // 5 minutes
        });

        // Signature
        String signature = RS256.sign(header + "." + claims, privateKey);

        return header + "." + claims + "." + signature;
    }

    public <T> T postSigned(String path, Object body, Class<T> responseClass) {
        String payload = gson.toJson(body);
        String jwt = generateJwt(payload);

        Request request = new Request.Builder()
            .url(baseUrl + path)
            .header("Authorization", "Token " + apiKey)
            .header("X-Signature", jwt)
            .post(RequestBody.create(payload, JSON))
            .build();

        return executeRequest(request, responseClass);
    }
}

Input Validation

TiqetsInputValidator provides security-focused validation:

public class TiqetsInputValidator {

    public static final int MAX_PAGE_SIZE = 100;
    public static final int MAX_DATE_RANGE_DAYS = 90;

    private static final Pattern DATE_PATTERN = Pattern.compile("^\\d{4}-\\d{2}-\\d{2}$");
    private static final Pattern ID_PATTERN = Pattern.compile("^[a-zA-Z0-9_-]{1,50}$");

    public static boolean validateDateFormat(String date, String fieldName, ValidationResult result);
    public static boolean validateDateRange(String dateFrom, String dateTo, ValidationResult result);
    public static boolean validateIdFormat(String id, String fieldName, ValidationResult result);
    public static boolean validateRequired(String value, String fieldName, ValidationResult result);
    public static int[] validatePagination(Integer page, Integer pageSize);
}

Error Handling

Error Response Format

{
    "apiStatus": {
        "errorCode": "ERR00014",
        "errorMessage": "Invalid parameter: dateFrom must be in yyyy-MM-dd format"
    },
    "apiData": null
}

Error Codes

Code Name Description
OK Success Operation completed successfully
ERR00001 GENERAL Generic error (internal details hidden)
ERR00003 NOTFOUND Resource not found
ERR00014 INVALID_PARAMETER Validation failure
ERR00015 UNAUTHORIZED Authentication failed
ERR00016 SERVICE_UNAVAILABLE Tiqets API unavailable

Exception Handling

private TlinqApiResponse handleException(String operation, Exception ex) {
    if (ex instanceof TlinqClientException) {
        TlinqClientException tce = (TlinqClientException) ex;
        logger.log(Level.SEVERE, "Error in " + operation + ": " + tce.getMessage(), ex);
        return new TlinqApiResponse(tce.getErrorCode(), tce.getMessage());
    } else {
        logger.log(Level.SEVERE, "Error in " + operation, ex);
        // Return generic message, don't expose internal details
        return new TlinqApiResponse(TlinqErr.GENERAL, "An error occurred processing your request");
    }
}

Testing

Test Categories

  1. Unit Tests: Individual class testing
  2. Integration Tests: Database and API integration
  3. E2E Tests: Full API endpoint testing

Test Setup

@BeforeAll
static void setUp() {
    // Set TLINQ_HOME for configuration loading
    System.setProperty("TLINQ_HOME", "path/to/config");
}

Example Test Cases

@Test
void testListExperiences() {
    TiqetsCatalogFacade facade = TiqetsCatalogFacade.instance();
    TqExperience[] experiences = facade.listExperiences("dubai", null, null, 1, 50);

    assertNotNull(experiences);
    assertTrue(experiences.length > 0);
    assertNotNull(experiences[0].getExperienceId());
    assertNotNull(experiences[0].getTitle());
}

@Test
void testInputValidation() {
    ValidationResult vr = TiqetsInputValidator.newResult();

    // Valid date
    assertTrue(TiqetsInputValidator.validateDateFormat("2024-01-15", "dateFrom", vr));

    // Invalid date
    assertFalse(TiqetsInputValidator.validateDateFormat("15-01-2024", "dateFrom", vr));
    assertFalse(vr.isValid());
}

Appendix

External Resources

Version History

Version Date Changes
1.0 2024-Q4 Initial implementation
1.1 2025-Q1 Added tag filtering, input validation