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:
- Phase 1: Backend Integration (TQPro APIs) - Fully detailed
- Phase 2: Public Website (BookMyHoliday integration) - High-level outline
- 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. |