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¶
- Overview
- Architecture
- Module Structure
- Configuration
- Database Schema
- Entity Model
- Service Layer
- REST API Endpoints
- Data Synchronization
- Caching Strategy
- Frontend Integration
- Security
- Error Handling
- 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¶
- Initialization: Lazy-loaded on first access from database
- Refresh: Triggered after data sync completes
- Updates: Individual put methods for incremental updates
- 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:
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¶
- Unit Tests: Individual class testing
- Integration Tests: Database and API integration
- 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¶
Related Documentation¶
- Tiqets API Specification - API endpoint specifications
- PCN Webhooks - Webhook integration plan
- Tags Implementation - Tags feature implementation
External Resources¶
Version History¶
| Version | Date | Changes |
|---|---|---|
| 1.0 | 2024-Q4 | Initial implementation |
| 1.1 | 2025-Q1 | Added tag filtering, input validation |