Activity Ticketing System - Implementation Specification¶
1. Introduction¶
1.1 Purpose¶
This document describes the technical implementation of the Activity Ticketing System in the TQPro platform. It details the architecture, components, classes, services, APIs, and data flows used to integrate third-party ticket suppliers.
1.2 Scope¶
This document covers: - Overall system architecture and design patterns - Core framework components (tqapp module) - Supplier plugin architecture (tqryb2b as reference implementation) - Database schema and entity model - Service layer implementation - REST API endpoints - Frontend integration - Data and control flow through the system
1.3 Related Documents¶
- Activity_Ticketing_Requirement_Spec.md - Functional requirements
- RAYNA_B2B_FUNCTIONAL_DESCRIPTION.md - Example supplier integration
- ENTITY_MANAGEMENT_GUIDE.md - Entity framework documentation
- DEVELOPER_GUIDE_ADDING_FEATURES.md - Development procedures
2. Architecture Overview¶
2.1 Architectural Style¶
The Activity Ticketing System follows a layered architecture with plugin-based supplier integrations:
┌─────────────────────────────────────────────────────────────┐
│ Frontend Layer │
│ (tqweb-pub: HTML/JavaScript/ES6 Modules) │
└─────────────────────────────────────────────────────────────┘
↓ REST/JSON
┌─────────────────────────────────────────────────────────────┐
│ API Layer (tqapi) │
│ JAX-RS REST Controllers │
│ ProductApi, BookingApi, CommonApi │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Facade Layer (tqapp) │
│ ProductFacade, BookingRequestFacade │
│ (Business Logic & Orchestration) │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Ticketing Service Framework (tqapp) │
│ TicketingServiceI, TicketingRequestI, etc. │
│ (Abstract interfaces & base entities) │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Supplier Plugin Layer (tqryb2b, etc.) │
│ RaynaB2BActPlugin, RaynaServiceFactory │
│ RaynaTicketingService, RaynaProductService │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Local Database Cache Layer │
│ PostgreSQL (nts schema + rayna schema) │
│ Booking Requests, Tickets, Supplier Catalog Cache │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ External Supplier APIs (3rd Party) │
│ REST/SOAP Services, Various Protocols │
└─────────────────────────────────────────────────────────────┘
2.2 Design Patterns¶
Factory Pattern
- TicketingServiceFactoryI - Creates ticketing service instances per supplier
- RaynaServiceFactory - Rayna-specific service factory implementation
- TicketRequestFactory - Creates ticketing request objects
Facade Pattern
- ProductFacade - Unified product operations interface
- BookingRequestFacade - Booking lifecycle management
- CatalogFacade - Supplier catalog operations (Rayna)
Strategy Pattern
- TicketingServiceI implementations per supplier
- Different strategies for availability checks, pricing, booking
Adapter Pattern - Supplier plugins adapt third-party APIs to common interfaces - Entity transformers convert between native and canonical formats
Plugin Architecture
- AbstractPlugin base class for supplier integrations
- Dynamic loading via configuration
- Isolated supplier-specific logic
2.3 Key Principles¶
Separation of Concerns - Framework (tqapp) defines contracts and common logic - Plugins (tqryb2b) implement supplier-specific behavior - API layer handles HTTP/REST concerns - Facades orchestrate business workflows
Dependency Inversion - Framework depends on abstractions (interfaces) - Plugins implement interfaces, injected via factory pattern - Loose coupling enables supplier independence
Configuration-Driven - XML configuration for entity mappings - Properties files for supplier settings - No hardcoding of supplier specifics in framework
3. Module Structure¶
3.1 Core Framework Module (tqapp)¶
Package: com.perun.tlinq.entity.tkt
Core ticketing entities and interfaces:
| Class/Interface | Purpose |
|---|---|
TicketingServiceI |
Main service interface for ticketing operations |
TicketingServiceFactoryI |
Factory for creating ticketing service instances |
TicketingRequestI |
Interface for ticketing request data |
TicketingResponseI |
Interface for ticketing response data |
TicketRequestItemI |
Interface for individual ticket items |
TicketingStatusI |
Interface for booking status information |
TicketingDataI |
Interface for ticket/voucher data |
CBookingRequest |
Booking request entity (master record) |
CTicketingRequest |
Ticketing request entity (supplier communication) |
CTicketItem |
Individual ticket/voucher entity |
CTicketConfirmation |
Ticket confirmation details |
CBookingResult |
Booking operation result |
CTicketingResult |
Ticketing operation result |
BookingRequestFacade |
Facade for booking operations |
TicketRequestFactory |
Factory for creating ticket requests |
TicketRequestWrapper |
Wrapper for ticket request processing |
CBookingAccessAuth |
Access control for guest booking retrieval |
CBookingSearchResult |
Booking search result entity |
Package: com.perun.tlinq.service
| Interface | Purpose |
|---|---|
TicketingServiceI |
Core service operations interface |
TicketingServiceFactoryI |
Service factory interface |
RemoteServiceI |
Base remote service interface |
RemoteServiceFactoryI |
Base factory interface |
3.2 Supplier Plugin Module (tqryb2b - Reference Implementation)¶
Package: com.perun.tlinq.framework
| Class | Purpose |
|---|---|
RaynaB2BActPlugin |
Main plugin entry point, extends AbstractPlugin |
SDRefreshRunner |
Scheduled task runner for catalog sync |
Package: com.perun.tlinq.client.ryb2b.service
| Class | Purpose |
|---|---|
RaynaServiceFactory |
Implements supplier-specific service factory |
RaynaTicketingService |
Implements TicketingServiceI for Rayna bookings |
RaynaEntityService |
Base service for Rayna entity operations |
RaynaRemoteServiceFactory |
Factory for remote API services |
SDRefresher |
Static data refresh orchestrator |
StaticDataRefresher |
Catalog synchronization logic |
Package: com.perun.tlinq.client.ryb2b.service.product
| Class | Purpose |
|---|---|
CatalogFacade |
Facade for product catalog operations |
RaynaProductService |
Product-related service operations |
Package: com.perun.tlinq.client.ryb2b.remoteservice
Remote API client services:
| Class | Purpose |
|---|---|
AbstractRemoteService |
Base class for API communication |
TourOptionService |
Fetches pricing and options |
TourAvailabilityService |
Checks real-time availability |
TourTimeslotService |
Retrieves available time slots |
TourBookService |
Creates bookings with supplier |
TourCancelService |
Handles cancellations |
TourPriceService |
Gets pricing information |
Package: com.perun.tlinq.client.ryb2b.remote.service
Higher-level remote services:
| Class | Purpose |
|---|---|
TourInfoService |
Fetches tour static data |
TourStaticDataService |
Retrieves catalog information |
TourBookingService |
Booking submission to supplier |
CountriesService |
Gets destination countries |
CitiesService |
Gets destination cities |
Package: com.perun.tlinq.client.ryb2b.db
JPA entities for local cache (PostgreSQL, schema: rayna):
| Entity | Table | Purpose |
|---|---|---|
TourEntity |
tour |
Tour master data |
TouroptionEntity |
touroption |
Tour options/variants |
TourtransferEntity |
tourtransfer |
Transfer mappings |
TransferEntity |
transfer |
Transfer types |
TourpriceEntity |
tourprice |
Pricing information |
TourtimeEntity |
tourtime |
Tour schedules |
TourimageEntity |
tourimage |
Image references |
TourinfoEntity |
tourinfo |
Descriptive content |
TourtermsEntity |
tourterms |
Terms and conditions |
CancelpolicyEntity |
cancelpolicy |
Cancellation policies |
CountryEntity |
country |
Countries catalog |
CityEntity |
city |
Cities catalog |
RefreshstatusEntity |
refreshstatus |
Sync tracking |
Package: com.perun.tlinq.client.ryb2b.entity
Canonical entities for data interchange:
| Class | Purpose |
|---|---|
RyProduct |
Canonical product representation |
RyProductAttribute |
Product attributes (options, transfers) |
RyProductVariant |
Product variants with pricing |
RyProductAttrValue |
Attribute value with dependencies |
RyAttrValueDependency |
Attribute dependency rules |
RyTimeslot |
Available time slots |
RyTransfer |
Transfer option details |
RyActEntity |
Base activity entity |
RyActTicketingRequest |
Rayna-specific booking request |
RyActTicketingResponse |
Rayna-specific booking response |
RyActTicketingData |
Rayna ticket data |
RyTourOptionCacheEntry |
Cached pricing options |
Package: com.perun.tlinq.client.ryb2b.util
| Class | Purpose |
|---|---|
RaynaClientConfig |
Configuration management |
RaynaDBSession |
Hibernate session factory |
RaynaCacheManager |
Hazelcast distributed cache |
3.3 API Module (tqapi)¶
Package: com.perun.tlinq.api
REST API controllers (JAX-RS):
| Class | Endpoint Base | Purpose |
|---|---|---|
ProductApi |
/product |
Product catalog operations |
BookingApi |
/booking |
Booking management |
CommonApi |
/common |
Common utilities |
CartApi |
/cart |
Shopping cart operations |
CustomerApi |
/customer |
Customer management |
3.4 Frontend Module (tqweb-pub)¶
JavaScript ES6 Modules (js/modules/):
| Module | Purpose |
|---|---|
globals.js |
Core utilities (tlinq API caller, session management) |
products.js |
Product catalog operations |
productbook.js |
Product booking workflow |
ticketbook.js |
Ticket checkout and confirmation |
carts.js |
Shopping cart management |
allservices.js |
Service discovery and browsing |
4. Core Components¶
4.1 Ticketing Service Interface¶
Interface: TicketingServiceI
Location: tqapp/src/main/java/com/perun/tlinq/service/TicketingServiceI.java
Key Methods:
public interface TicketingServiceI extends RemoteServiceI {
// Initialize booking request with supplier-specific data
TicketingRequestI initTicketRequest(TicketingRequestI request)
throws TlinqClientException;
// Submit booking to supplier
TicketingResponseI sendTicketRequest(TicketingRequestI req)
throws TlinqClientException;
// Check status of submitted booking
TicketingResponseI checkTicketRequest(TicketingRequestI req);
// Confirm provisional booking
TicketingResponseI confirmTicketRequest(TicketingRequestI req);
// Cancel confirmed booking
TicketingResponseI cancelTicketRequest(
TicketingRequestI req,
TicketingDataI prevResponse
);
// Modify existing booking
TicketingResponseI amendTicketingRequest(
TicketingRequestI req,
TicketingDataI prevResponse
);
}
Implementation Pattern:
Each supplier plugin provides a concrete implementation:
- RaynaTicketingService for Rayna B2B
- Future: ViatorTicketingService, GetYourGuideTicketingService, etc.
4.2 Ticketing Request Interface¶
Interface: TicketingRequestI
Location: tqapp/src/main/java/com/perun/tlinq/entity/tkt/TicketingRequestI.java
Key Methods:
public interface TicketingRequestI {
Integer getBookingRequestId(); // Internal booking ID
String getBookingRequestCode(); // Customer-facing reference
Integer getOriginRequestId(); // Original request (for amendments)
Integer getSupplierId(); // Target supplier ID
CCustomer getCustomer(); // Customer information
TicketRequestItemI[] getTicketItems(); // Items to book
}
Concrete Implementation: CTicketingRequest
Persisted entity storing: - Supplier/vendor ID - Customer ID - Booking request ID (link to master booking) - Extended info fields (extinfo1-5) for supplier-specific data - Status tracking (trqstatus) - Error codes and messages - Request/response timestamps
4.3 Ticket Request Item Interface¶
Interface: TicketRequestItemI
Location: tqapp/src/main/java/com/perun/tlinq/entity/tkt/TicketRequestItemI.java
Key Methods:
public interface TicketRequestItemI {
Integer getProductId(); // Internal product template ID
Integer getSupplierProductId(); // Supplier's product ID
Integer getOptionId(); // Selected option/variant
String getTimeslotId(); // Selected time slot
String getAttributes(); // JSON-encoded attributes
Double getQuantity(); // Number of units
Integer getUnitId(); // Unit type (adult/child/infant)
Double getPurchaseCost(); // Supplier cost per unit
Date getBookingStartDate(); // Service date
Date getBookingEndDate(); // End date (multi-day)
String getStartTime(); // Service start time
String getPickupLocation(); // Pickup point (if applicable)
Map getExtraInfo(); // Additional data
Integer getTicketItemId(); // Unique item ID
String getProductName(); // Product description
String getReservationName(); // Name for reservation
Integer getSupplierBookingId(); // Supplier confirmation ID
}
Purpose: Represents individual bookable items within a booking request.
4.4 Ticketing Response Interface¶
Interface: TicketingResponseI
Location: tqapp/src/main/java/com/perun/tlinq/entity/tkt/TicketingResponseI.java
Key Methods:
public interface TicketingResponseI {
// Status information for each booked item
TicketingStatusI[] getTicketingStatus();
// Ticket/voucher data
TicketingDataI[] getTicketData();
// Overall booking result
String getBookingResult();
// Status code
String getStatusCode();
// Status message
String getStatusMessage();
}
Concrete Implementation: RyActTicketingResponse (Rayna)
4.5 Booking Request Facade¶
Class: BookingRequestFacade
Location: tqapp/src/main/java/com/perun/tlinq/entity/tkt/BookingRequestFacade.java
Key Responsibilities:
- Booking Lifecycle Management
- Create booking requests from cart
- Submit to appropriate supplier
- Track booking status
-
Handle confirmations and failures
-
Ticket Generation and Delivery
- Generate PDF vouchers
- Generate HTML tickets
- Create barcodes (QR, PDF417)
-
Email tickets to customers
-
Booking Operations
- Amendment processing
- Cancellation handling
- Status updates
- Access code management
Key Methods:
public class BookingRequestFacade extends EntityFacade {
// Create booking request from order
public CBookingRequest createBookingRequest(
Integer orderId,
String sessionToken
) throws TlinqClientException;
// Submit to supplier
public List<TicketingResponseI> submitToSupplier(
Integer bookingRequestId
) throws TlinqClientException;
// Process supplier response
public void processTicketingResponse(
Integer bookingRequestId,
List<TicketingResponseI> responses
) throws TlinqClientException;
// Generate and send tickets
public void generateAndSendTickets(
CBookingRequest request
) throws TlinqClientException;
// Cancel booking
public TicketingResponseI cancelBooking(
Integer bookingRequestId,
String sessionToken
) throws TlinqClientException;
// Get booking by reference
public CBookingRequest getBookingByReference(
String referenceNumber
) throws TlinqClientException;
// Update booking status
public void updateBookingStatus(
Integer bookingRequestId,
String status
) throws TlinqClientException;
}
4.6 Product Facade¶
Class: ProductFacade
Location: tqapp/src/main/java/com/perun/tlinq/entity/product/ProductFacade.java
Key Responsibilities:
- Product Catalog Access
- Retrieve products by ID or code
- List products by category
-
Search products by criteria
-
Product Configuration
- Get product attributes
- Get product variants
-
Get variant by attribute selection
-
Availability and Pricing
- Check product availability
- Get real-time pricing
- Get variant timeslots
- Get product terms and conditions
Key Methods:
public class ProductFacade extends EntityFacade {
// Get product by ID
public CProduct getProduct(Integer productId)
throws TlinqClientException;
// Get product categories
public CProductCategory[] getCategories()
throws TlinqClientException;
// Get products in category
public List<CProduct> getCategoryProducts(Integer categoryId)
throws TlinqClientException;
// Get product attributes
public List<CProductAttribute> getProductAttributes(Integer productId)
throws TlinqClientException;
// Get all variants
public List<CProductVariant> getProductVariants(
Integer productId,
Map bookInfo
) throws TlinqClientException;
// Get specific variant by attributes
public List<CProductVariant> getProductVariant(
Integer productId,
Map bookInfo,
ArrayList<Integer> attrVals
) throws TlinqClientException;
// Check availability and get pricing
public CItemPriceInfo checkProductAvailable(Map bookInfo)
throws TlinqClientException;
// Get product terms
public CProductTerms getProductTerms(Integer productId)
throws TlinqClientException;
// Get variant timeslots
public List<CTimeslot> getVariantTimeslots(
CProductVariant variant,
Map bookInfo
) throws TlinqClientException;
}
5. Database Schema¶
5.1 Booking Management (Schema: nts)¶
Table: bookingrequest
Master booking record:
| Column | Type | Purpose |
|---|---|---|
bookingrequestid |
INTEGER | Primary key |
refnum |
VARCHAR | Customer reference number |
rqdate |
TIMESTAMP | Request creation date |
reqstatus |
VARCHAR | Status (PENDING, CONFIRMED, CANCELLED) |
ordernum |
VARCHAR | Associated order number |
rqmoddate |
TIMESTAMP | Last modification date |
customerid |
INTEGER | Customer ID (FK) |
accesscode |
VARCHAR | Guest access code |
accessmaxdate |
TIMESTAMP | Access expiration |
orderdate |
TIMESTAMP | Order placement date |
invoicenum |
VARCHAR | Invoice number |
amount |
DECIMAL | Total booking amount |
Table: ticketingrequest
Ticketing transaction record:
| Column | Type | Purpose |
|---|---|---|
tixrequestid |
INTEGER | Primary key |
vendorid |
INTEGER | Supplier ID (FK) |
customerid |
INTEGER | Customer ID (FK) |
bookingrequestid |
INTEGER | Booking request ID (FK) |
extinfo1-5 |
TEXT | Supplier-specific data |
trqdate |
TIMESTAMP | Transaction date |
trqmoddate |
TIMESTAMP | Last modification |
trqstatus |
VARCHAR | Transaction status |
resultcode |
VARCHAR | Supplier result code |
errorcode |
VARCHAR | Error code (if failed) |
errormessage |
TEXT | Error details |
requesttimestamp |
TIMESTAMP | Request sent timestamp |
responsetimestamp |
TIMESTAMP | Response received timestamp |
Table: ticketitem
Individual ticket/voucher records:
| Column | Type | Purpose |
|---|---|---|
ticketitemid |
INTEGER | Primary key |
mainticket |
INTEGER | Main ticket ID (grouping) |
extproductid |
VARCHAR | Supplier product ID |
orderitemid |
INTEGER | Order item ID (FK) |
paxcount |
VARCHAR | Passenger counts (JSON) |
costs |
VARCHAR | Cost breakdown (JSON) |
extoptionid |
VARCHAR | Supplier option ID |
barcode |
VARCHAR | Barcode value |
barcodeimg |
VARCHAR | Barcode image path |
producttype |
VARCHAR | Product type |
timeslot |
VARCHAR | Service time slot |
startdate |
DATE | Service start date |
enddate |
DATE | Service end date |
tixterms |
TEXT | Terms and conditions |
tixrequestid |
INTEGER | Ticketing request ID (FK) |
extinfo1-5 |
TEXT | Extended information |
prodtemplateid |
INTEGER | Product template ID (FK) |
confirmationid |
INTEGER | Confirmation ID |
supplierbookingid |
INTEGER | Supplier booking ID |
reservationname |
VARCHAR | Reservation name |
Table: ticketconfirmation
Booking confirmation details:
| Column | Type | Purpose |
|---|---|---|
confirmationid |
INTEGER | Primary key |
bookingrequestid |
INTEGER | Booking request ID (FK) |
supplierreference |
VARCHAR | Supplier booking reference |
suppliervoucher |
VARCHAR | Supplier voucher number |
ticketdescription |
TEXT | Ticket description |
barcode |
VARCHAR | Barcode value |
barcodeimage |
VARCHAR | Barcode image reference |
timeslot |
VARCHAR | Service time |
startdate |
DATE | Service date |
enddate |
DATE | End date |
terms |
TEXT | Terms and conditions |
ticketresvname |
VARCHAR | Ticket holder name |
pickuplocation |
VARCHAR | Pickup point |
extinfo1-4 |
TEXT | Additional information |
5.2 Supplier Catalog Cache (Schema: rayna - Example)¶
Table: tour
Tour master data:
| Column | Type | Purpose |
|---|---|---|
tourid |
INTEGER | Primary key (supplier ID) |
tourtypeid |
INTEGER | Tour type ID |
tourtype |
VARCHAR | Tour type name |
tourname |
VARCHAR | Tour name |
iscombo |
BOOLEAN | Combo tour flag |
istimeslot |
BOOLEAN | Has timeslots flag |
childfrom |
VARCHAR | Child age from |
duration |
VARCHAR | Duration |
infantfrom |
VARCHAR | Infant age from |
tourlanguage |
VARCHAR | Available languages |
note |
TEXT | Notes |
cityid |
INTEGER | City ID (FK) |
countryid |
INTEGER | Country ID (FK) |
faqdetails |
TEXT | FAQ content |
tourterms |
TEXT | Terms and conditions |
itinerary |
TEXT | Itinerary details |
cancelpolicyname |
VARCHAR | Cancellation policy |
importantinfo |
TEXT | Important information |
description |
TEXT | Tour description |
inclusion |
TEXT | Inclusions |
exclusion |
TEXT | Exclusions |
departpoint |
VARCHAR | Departure point |
childpolicydesc |
TEXT | Child policy |
cancelpolicydesc |
TEXT | Cancellation details |
Table: touroption
Tour options/variants:
| Column | Type | Purpose |
|---|---|---|
touroptionid |
INTEGER | Primary key |
tourid |
INTEGER | Tour ID (FK) |
optionname |
VARCHAR | Option name |
optiondesc |
TEXT | Option description |
active |
BOOLEAN | Active flag |
cancelpolicydesc |
TEXT | Option-specific cancellation |
Table: tourtransfer
Transfer mappings:
| Column | Type | Purpose |
|---|---|---|
tourtransferid |
INTEGER | Primary key |
tourid |
INTEGER | Tour ID (FK) |
touroptionid |
INTEGER | Tour option ID (FK) |
transferid |
INTEGER | Transfer ID (FK) |
Table: transfer
Transfer types:
| Column | Type | Purpose |
|---|---|---|
transferid |
INTEGER | Primary key |
name |
VARCHAR | Transfer name |
xfercode |
VARCHAR | Transfer code (SH, PV, etc.) |
sortorder |
INTEGER | Display order |
Table: tourprice
Pricing information:
| Column | Type | Purpose |
|---|---|---|
tourpriceid |
INTEGER | Primary key |
tourid |
INTEGER | Tour ID (FK) |
touroptionid |
INTEGER | Option ID (FK) |
transferid |
INTEGER | Transfer ID (FK) |
contractid |
INTEGER | Contract ID |
validfrom |
DATE | Valid from date |
validto |
DATE | Valid to date |
adultprice |
DECIMAL | Adult price |
childprice |
DECIMAL | Child price |
infantprice |
DECIMAL | Infant price |
Table: tourimage
Image references:
| Column | Type | Purpose |
|---|---|---|
tourimageid |
INTEGER | Primary key |
tourid |
INTEGER | Tour ID (FK) |
imageurl |
VARCHAR | Image URL |
imagetype |
VARCHAR | Image type |
sortorder |
INTEGER | Display order |
Table: refreshstatus
Synchronization tracking:
| Column | Type | Purpose |
|---|---|---|
refreshstatusid |
INTEGER | Primary key |
entitytype |
VARCHAR | Entity type (tour, option, etc.) |
lastrefresh |
TIMESTAMP | Last successful sync |
status |
VARCHAR | Sync status |
errordetails |
TEXT | Error information |
6. Service Layer Implementation¶
6.1 Service Factory Pattern¶
Interface: TicketingServiceFactoryI
public interface TicketingServiceFactoryI {
// Get ticketing service instance
TicketingServiceI getTicketingService();
}
Implementation: RaynaServiceFactory
Location: tqryb2b/.../RaynaServiceFactory.java
public class RaynaServiceFactory implements
RemoteServiceFactoryI, TicketingServiceFactoryI {
private static volatile RaynaServiceFactory _instance;
// Singleton access
public static RaynaServiceFactory getInstance() {
if(_instance == null)
synchronized (RaynaServiceFactory.class) {
if(_instance == null) {
_instance = new RaynaServiceFactory();
}
}
return _instance;
}
// Create entity service by name
@Override
public RemoteServiceI createService(
String serviceName,
String sessionToken
) throws TlinqClientException {
RaynaClientConfig cfg = RaynaClientConfig.instance();
ServiceConfig scfg = cfg.getService(serviceName);
// Use reflection to instantiate service class
Class sclass = Class.forName(scfg.getServiceClass());
RaynaEntityService service =
(RaynaEntityService) sclass.getDeclaredConstructor().newInstance();
Properties prop = new Properties();
prop.put("service-config", scfg);
service.initialize(prop);
return service;
}
// Get ticketing service
@Override
public TicketingServiceI getTicketingService() {
return new RaynaTicketingService();
}
}
6.2 Ticketing Service Implementation¶
Class: RaynaTicketingService
Location: tqryb2b/.../RaynaTicketingService.java
Key Implementation Details:
public class RaynaTicketingService extends RaynaEntityService
implements TicketingServiceI {
private GetTourBookingRequest booking = null;
// Initialize booking data structure
@Override
public TicketingRequestI initTicketRequest(TicketingRequestI treq)
throws TlinqClientException {
if(booking == null) {
booking = initBookingData(treq);
}
return treq;
}
// Transform platform request to supplier format
private GetTourBookingRequest initBookingData(TicketingRequestI treq) {
HashMap<String, TourBookingDetail> tourMap = new HashMap<>();
ProductCache pc = ProductCache.instance();
CCustomer customer = treq.getCustomer();
// Iterate through ticket items
for(TicketRequestItemI item: treq.getTicketItems()) {
Integer activityId = item.getSupplierProductId();
Integer templateId = item.getProductId();
String timeSlotId = item.getTimeslotId();
// Extract attributes (option, transfer)
Integer productOptionId = 0;
Integer transferId = null;
String attrs = item.getAttributes();
List attrList = TypeUtil.extractObjectFromJson(attrs);
for(Object ao : attrList) {
Map am = (Map) ao;
Integer attrId = TypeUtil.extractInteger(am.get("attributeId"));
String attrVal = TypeUtil.extractString(am.get("attributeValueId"));
switch (attrId) {
case CatalogFacade.ATTRID_PRODOPTION:
productOptionId = TypeUtil.extractInteger(attrVal);
break;
case CatalogFacade.ATTRID_TIMESLOT:
timeSlotId = attrVal;
break;
case CatalogFacade.ATTRID_TRANSFER:
transferId = TypeUtil.extractInteger(attrVal);
break;
}
}
// Create map key for consolidation
String bkDate = DateUtil.getInstance().toString(
item.getBookingStartDate(),
RaynaClientConfig.CLIENT_DATE_FORMAT
);
String mapKey = activityId + "-" + bkDate + "-" +
productOptionId + "-" + timeSlotId + "-" + transferId;
// Get or create TourBookingDetail
TourBookingDetail tbd = tourMap.get(mapKey);
if(null == tbd) {
tbd = new TourBookingDetail();
tbd.setTourId(activityId);
tbd.setTourDate(bkDate);
tbd.setOptionId(productOptionId);
tbd.setTimeSlotId(timeSlotId);
tbd.setTransferId(transferId);
tbd.setPickup(item.getPickupLocation());
}
// Aggregate quantities by passenger type
tbd.addSourceItemId(item.getTicketItemId());
Double newQty = item.getQuantity();
if(item.getUnitId().equals(pc.getChildUnit(templateId))) {
newQty += tbd.getChild();
tbd.setChild(newQty.intValue());
tbd.setChildRate(item.getPurchaseCost());
} else if (item.getUnitId().equals(pc.getInfantUnit(templateId))) {
newQty += tbd.getInfant();
tbd.setInfant(newQty.intValue());
} else {
newQty += tbd.getAdult();
tbd.setAdult(newQty.intValue());
tbd.setAdultRate(item.getPurchaseCost());
}
// Calculate service total
Double ad = tbd.getAdult() * tbd.getAdultRate();
Double ch = tbd.getChild() * tbd.getChildRate();
tbd.setServiceTotal(Math.round((ad+ch)*100)/100.0);
tourMap.put(mapKey, tbd);
}
// Create passenger list
ArrayList<TourBookingPassenger> pax = new ArrayList<>();
ArrayList<TourBookingDetail> tours = new ArrayList<>();
TourBookingPassenger rbpd = new TourBookingPassenger();
rbpd.setEmail(customer.getEmail());
rbpd.setMobile(customer.getMobilePhone());
rbpd.setPrefix("Mr/Ms");
rbpd.setPaxType("adult");
rbpd.setLeadPassenger(1);
rbpd.setNationality("UAE");
// Create passenger and tour entries
for(TourBookingDetail tour : tourMap.values()) {
TourBookingPassenger pd = rbpd.copy();
pd.setPickup(tour.getPickup());
String fullName = TypeUtil.isEmptyString(tour.getReservationName())
? customer.getFullName()
: tour.getReservationName();
String[] parts = fullName.split(" ",2);
pd.setServiceType("Tour");
pd.setFirstName(parts.length > 0 ? parts[0] : null);
pd.setLastName(parts.length > 1? parts[1] : null);
pd.setAdultRate(tour.getAdultRate());
pd.setClientReferenceNo("PTCR-"+TypeUtil.createShortUID(8));
pax.add(pd);
tours.add(tour);
}
// Create booking request
GetTourBookingRequest bookingReq = new GetTourBookingRequest();
bookingReq.setPassengers(pax.toArray(new TourBookingPassenger[0]));
bookingReq.setTourDetails(tours.toArray(new TourBookingDetail[0]));
bookingReq.setUniqueNo((100000 + treq.getBookingRequestId()) % 1000000);
return bookingReq;
}
// Send booking to supplier
@Override
public TicketingResponseI sendTicketRequest(TicketingRequestI req)
throws TlinqClientException {
initTicketRequest(req);
GetTourBookingResponse tbr;
TourBookingService trbs = new TourBookingService();
try {
tbr = trbs.getTourBooking(
req.getBookingRequestId(),
booking.getTourDetails(),
booking.getPassengers()
);
} catch (Exception ex) {
throw new TlinqClientException(
TlinqErr.REMOTE_ERROR,
ex.getMessage()
);
}
return new RyActTicketingResponse(booking, tbr);
}
// Other methods throw RuntimeException (not implemented)
@Override
public TicketingResponseI checkTicketRequest(TicketingRequestI req) {
throw new RuntimeException("Calling unimplemented function!");
}
@Override
public TicketingResponseI confirmTicketRequest(TicketingRequestI req) {
throw new RuntimeException("Calling unimplemented function!");
}
@Override
public TicketingResponseI cancelTicketRequest(
TicketingRequestI req,
TicketingDataI prevResponse
) {
throw new RuntimeException("Calling unimplemented function!");
}
@Override
public TicketingResponseI amendTicketingRequest(
TicketingRequestI req,
TicketingDataI prevResponse
) {
throw new RuntimeException("Calling unimplemented function!");
}
}
6.3 Product Service Implementation¶
Class: RaynaProductService
Location: tqryb2b/.../service/product/RaynaProductService.java
Key Methods:
public class RaynaProductService extends RaynaEntityService {
// Get product attributes (options, transfers)
public RyProductAttribute[] getAttributes(RemoteEntityI product) {
Integer productId = product.getId();
return CatalogFacade.instance().getProductAttributes(productId);
}
// Get available timeslots
public RyTimeslot[] getTimeslots(RemoteEntityI notUsed)
throws TlinqClientException {
Integer productId = (Integer) getNamedParam("productId");
Integer optionId = (Integer) getNamedParam("optionId");
Integer transferId = (Integer) getNamedParam("transferId");
Integer contractId = (Integer) getNamedParam("contractId");
Date travelDate = (Date) getNamedParam("travelDate");
return CatalogFacade.instance().getTimeslots(
productId, contractId, optionId, transferId, travelDate
);
}
// Get all variants for a product
public List getAllVariants(RemoteEntityI notUsed)
throws TlinqClientException {
Integer templateId = (Integer)getNamedParam("productId");
Map<String, Object> bookInfo =
(Map<String, Object>) getNamedParam("bookInfo");
Date travelDate = extractDate(bookInfo);
Integer numA = extractInteger(bookInfo, "adults", 1);
Integer numC = extractInteger(bookInfo, "children", 0);
Integer numI = extractInteger(bookInfo, "infants", 0);
RyProductVariant[] pvs = CatalogFacade.instance()
.getAllVariants(templateId, travelDate, numA, numC, numI);
return Arrays.asList(pvs);
}
// Get variant by selected attributes
public List getVariantByAttr(RemoteEntityI notused)
throws TlinqClientException {
Integer option = null, xfer = null;
String tslot = null;
Integer templateId = (Integer)getNamedParam("productId");
Map bookInfo = (Map)getNamedParam("bookInfo");
Date travelDate = extractDate(bookInfo);
// Extract attribute selections
List attrMapList = (List)getNamedParam("attributeMap");
for(Object o:attrMapList) {
Map attrMap = (Map)o;
Integer attrId = TypeUtil.extractInteger(attrMap.get("attributeId"));
String attrValId = TypeUtil.extractString(attrMap.get("attributeValueId"));
if(attrId == CatalogFacade.ATTRID_PRODOPTION)
option = TypeUtil.extractInteger(attrValId);
else if(attrId == CatalogFacade.ATTRID_TRANSFER)
xfer = TypeUtil.extractInteger(attrValId);
else if(attrId == CatalogFacade.ATTRID_TIMESLOT)
tslot = attrValId;
}
Integer adults = extractInteger(bookInfo, "adults", 1);
Integer children = extractInteger(bookInfo, "children", 0);
Integer infants = extractInteger(bookInfo, "infants", 0);
RyProductVariant[] pvs = CatalogFacade.instance().getVariantByAttr(
templateId, option, xfer, tslot, travelDate,
adults, children, infants
);
return Arrays.asList(pvs);
}
// Get timeslots for a variant
public List getVariantTimeslots(RemoteEntityI variant)
throws TlinqClientException {
CatalogFacade cf = CatalogFacade.instance();
RyProductVariant theVar = (RyProductVariant) variant;
Integer optionId = theVar.getOptionId();
Integer transferId = theVar.getTransferId();
Integer tourId = theVar.getProductId();
Date travelDate = theVar.getServiceDate();
if(travelDate == null) {
Map bookInfo = (Map) getNamedParam("bookInfo");
String dateStr = (String) bookInfo.get("dateFrom");
travelDate = DateUtil.getInstance().fromString(
dateStr, DateUtil.API_DATE_FORMAT
);
}
RyTimeslot[] timeslots = cf.getTimeslots(
tourId, 300, optionId, transferId, travelDate
);
return Arrays.asList(timeslots);
}
// Get product terms and conditions
public CProductTerms getProductTerms(RemoteEntityI notUsed) {
CatalogFacade cf = CatalogFacade.instance();
ProductCache pc = ProductCache.instance();
Integer templateId = (Integer)getNamedParam("productId");
Integer tourId = pc.getVendorProductId(templateId);
String[] tandf = cf.getProductTermsAndInfo(tourId);
CProductTerms pt = new CProductTerms();
pt.setInfo(tandf[CatalogFacade.IDX_INFO]);
pt.setTerms(tandf[CatalogFacade.IDX_TERMS]);
pt.setInclusions(tandf[CatalogFacade.IDX_INCL]);
pt.setCancelPolicy(tandf[CatalogFacade.IDX_CANCELP]);
return pt;
}
// Check availability and get pricing
public CItemPriceInfo getTourAvailability(RemoteEntityI notused)
throws TlinqClientException {
Map requestList = (Map) getNamedParam("pricingRequest");
return getAvailability(requestList);
}
private CItemPriceInfo getAvailability(Map pricingRequest)
throws TlinqClientException {
ProductCache pc = ProductCache.instance();
Integer templateId = (Integer) pricingRequest.get("templateId");
Integer tourId = pc.getVendorProductId(templateId);
Map attrs = (Map) pricingRequest.get("attributes");
Integer option = TypeUtil.extractInteger(attrs.get(ATTRID_PRODOPTION));
Integer transfer = TypeUtil.extractInteger(attrs.get(ATTRID_TRANSFER));
String slot = TypeUtil.extractString(pricingRequest.get("slotId"));
String fromDt = TypeUtil.extractString(pricingRequest.get("dateFrom"));
Date date = TypeUtil.extractDate(fromDt, DateUtil.API_DATE_FORMAT);
Integer adlts = TypeUtil.extractInteger(pricingRequest.get("adults"));
Integer chld = TypeUtil.extractInteger(pricingRequest.get("children"));
Integer inf = TypeUtil.extractInteger(pricingRequest.get("infants"));
CatalogFacade cf = CatalogFacade.instance();
RyProductVariant rpvar = cf.checkAvailability(
date, tourId, option, slot, transfer, adlts, chld, inf
);
if(rpvar == null)
throw new TlinqClientException(
TlinqErr.REMOTE_ERROR,
"No variants found!"
);
CItemPriceInfo pi = new CItemPriceInfo();
pi.setAvailable(rpvar.getAvailable());
pi.setStatusReason(rpvar.getAvailReason());
pi.setStartTime(rpvar.getStartTime());
pi.setTotalCost(rpvar.getSupplierTotal());
pi.setVpc(new Double[]{
rpvar.getAdultPrice(),
rpvar.getChildPrice(),
0.0
});
// Check if pickup location needed
TransferEntity xfer = (TransferEntity) RaynaCacheManager.instance()
.getCacheEntry(RaynaCacheManager.XFER_REVCACHE, transfer);
if(xfer != null && xfer.getXfercode() != null) {
String xferCode = xfer.getXfercode().trim();
if("SH".equals(xferCode) || "PV".equals(xferCode))
pi.setNeedsLocation(Boolean.TRUE);
}
return pi;
}
}
6.4 Catalog Facade¶
Class: CatalogFacade
Location: tqryb2b/.../service/product/CatalogFacade.java
Key Responsibilities: - Interface between product service and local database - Interact with remote supplier APIs for real-time data - Transform between database entities and canonical entities
Key Methods:
public class CatalogFacade {
// Attribute IDs
public static final int ATTRID_PRODOPTION = 0;
public static final int ATTRID_TIMESLOT = 1;
public static final int ATTRID_TRANSFER = 2;
private static volatile CatalogFacade instance;
// Singleton access
public static synchronized CatalogFacade instance() {
if(instance == null) {
instance = new CatalogFacade();
}
return instance;
}
// Get product from local database
public RyProduct getProduct(Integer productId) {
Session dbs = RaynaDBSession.getSession();
TourEntity te = dbs.get(TourEntity.class, productId);
if(null != te) {
RyProduct product = new RyProduct();
product.initFromEntity(te);
return product;
}
return null;
}
// Get product attributes (options + transfers)
public RyProductAttribute[] getProductAttributes(Integer productId) {
ArrayList<RyProductAttribute> attrList = new ArrayList<>();
Session dbs = RaynaDBSession.getSession();
// Get tour options
Query<TouroptionEntity> getOptions = dbs.createQuery(
"from TouroptionEntity where tourid=:tid and active=1"
);
getOptions.setParameter("tid", productId);
List<TouroptionEntity> res = getOptions.getResultList();
if(!res.isEmpty()) {
RyProductAttribute pa = new RyProductAttribute(
ATTRID_PRODOPTION, "Product option"
);
for(TouroptionEntity te : res) {
Integer id = te.getTouroptionid();
String name = te.getOptionname();
// Get transfers for this option
RyAttrValueDependency[] deps = getAttrDeps(productId, id);
RyProductAttrValue pav = new RyProductAttrValue(id, name, deps);
pa.addValue(pav);
}
attrList.add(pa);
}
// Get tour transfers
Query<TourtransferEntity> xfrQry = dbs.createQuery(
"from TourtransferEntity where tourid=:trid"
).setParameter("trid", productId);
List<TourtransferEntity> tourTransfers = xfrQry.getResultList();
if(!tourTransfers.isEmpty()) {
RyProductAttribute pa = new RyProductAttribute(
ATTRID_TRANSFER, "Transfer"
);
// Get unique transfers
HashMap<Integer, TransferEntity> xfers = new HashMap<>();
for(TourtransferEntity te:tourTransfers) {
TransferEntity tte = (TransferEntity) RaynaCacheManager.instance()
.getCacheEntry(RaynaCacheManager.XFER_REVCACHE, te.getTransferId());
xfers.put(te.getTransferId(), tte);
}
// Sort by display order
ArrayList<TransferEntity> xfrlist = new ArrayList<>(xfers.values());
xfrlist.sort(Comparator.comparingInt(TransferEntity::getSortorder));
xfrlist.forEach(transferEntity ->
pa.addValue(transferEntity.getTransferid(), transferEntity.getName())
);
attrList.add(pa);
}
return attrList.toArray(new RyProductAttribute[0]);
}
// Get attribute dependencies (which transfers valid for which options)
private RyAttrValueDependency[] getAttrDeps(
Integer tourId,
Integer tourOptionId
) {
Session dbs = RaynaDBSession.getSession();
Query<TourtransferEntity> xfrQry = dbs.createQuery(
"from TourtransferEntity where tourid=:trid and touroptionid=:optid"
).setParameter("trid", tourId).setParameter("optid", tourOptionId);
List<TourtransferEntity> tourTransfers = xfrQry.getResultList();
ArrayList<Integer> vals = new ArrayList<>();
for(TourtransferEntity te:tourTransfers)
vals.add(te.getTransferId());
RyAttrValueDependency dep = new RyAttrValueDependency();
dep.depAttributeId = ATTRID_TRANSFER;
dep.depAttribValues = vals.toArray(new Integer[0]);
return new RyAttrValueDependency[]{dep};
}
// Get product terms and info
public String[] getProductTermsAndInfo(Integer productId) {
Session dbs = RaynaDBSession.getSession();
TourEntity te = dbs.get(TourEntity.class, productId);
String[] termsAndInfo = new String[4];
if(te != null) {
String tourInfo = TypeUtil.nvl(te.getImportantinfo(), "") + "<br>" +
TypeUtil.nvl(te.getDepartpoint(),"");
String terms = te.getTourterms();
termsAndInfo[IDX_INFO] = TypeUtil.nvl(tourInfo, "Not applicable.");
termsAndInfo[IDX_TERMS] = TypeUtil.nvl(terms, "Not applicable.");
termsAndInfo[IDX_CANCELP] = TypeUtil.nvl(
te.getChildpolicydesc(), "Not applicable."
);
termsAndInfo[IDX_INCL] = TypeUtil.nvl(
te.getInclusion(), "Not applicable."
);
}
return termsAndInfo;
}
// Get all variants (all option/transfer combinations)
public RyProductVariant[] getAllVariants(
Integer templateId,
Date travelDate,
Integer numAdults,
Integer numChildren,
Integer numInfants
) throws TlinqClientException {
ProductCache pc = ProductCache.instance();
Integer tourId = pc.getVendorProductId(templateId);
RyProductAttribute[] attrs = getProductAttributes(tourId);
// Generate all combinations
ArrayList<RyProductVariant> variants = new ArrayList<>();
for(RyProductAttribute attr : attrs) {
if(attr.attributeId == ATTRID_PRODOPTION) {
for(RyProductAttrValue optVal : attr.values) {
Integer optionId = optVal.attributeValueId;
// For each option, get valid transfers
if(optVal.dependencies != null && optVal.dependencies.length > 0) {
Integer[] xferIds = optVal.dependencies[0].depAttribValues;
for(Integer xferId : xferIds) {
// Get pricing for this combination
TourPricedOption tpo = getPricedOption(
tourId, 300, optionId, xferId, travelDate,
numAdults, numChildren, numInfants
);
if(tpo != null) {
RyProductVariant variant = new RyProductVariant();
variant.setProductId(tourId);
variant.setOptionId(optionId);
variant.setTransferId(xferId);
variant.setServiceDate(travelDate);
variant.setAdultPrice(tpo.getAdultPrice());
variant.setChildPrice(tpo.getChildPrice());
// ... set other fields
variants.add(variant);
}
}
}
}
}
}
return variants.toArray(new RyProductVariant[0]);
}
// Check availability for specific variant
public RyProductVariant checkAvailability(
Date date,
Integer tourId,
Integer optionId,
String slotId,
Integer transferId,
Integer adults,
Integer children,
Integer infants
) throws TlinqClientException {
// Call remote supplier API for real-time availability
TourAvailabilityService availSvc = new TourAvailabilityService();
GetTourAvailabilityResponse resp = availSvc.checkAvailability(
date, tourId, optionId, slotId, transferId, adults, children, infants
);
TourAvailability avail = resp.getAvailability();
if(avail != null) {
RyProductVariant variant = new RyProductVariant();
variant.setProductId(tourId);
variant.setOptionId(optionId);
variant.setTransferId(transferId);
variant.setTimeslotId(slotId);
variant.setServiceDate(date);
variant.setAvailable(avail.isAvailable());
variant.setAvailReason(avail.getAvailabilityStatus());
variant.setAdultPrice(avail.getAdultPrice());
variant.setChildPrice(avail.getChildPrice());
variant.setSupplierTotal(avail.getTotalCost());
variant.setStartTime(avail.getStartTime());
return variant;
}
return null;
}
// Get timeslots from supplier
public RyTimeslot[] getTimeslots(
Integer tourId,
Integer contractId,
Integer optionId,
Integer transferId,
Date travelDate
) throws TlinqClientException {
TourTimeslotService tsSvc = new TourTimeslotService();
GetTourTimeslotResponse resp = tsSvc.getTimeslots(
tourId, contractId, optionId, transferId, travelDate
);
TourTimeslot[] slots = resp.getTimeslots();
if(slots != null && slots.length > 0) {
RyTimeslot[] rySlots = new RyTimeslot[slots.length];
for(int i = 0; i < slots.length; i++) {
RyTimeslot rys = new RyTimeslot();
rys.setTimeSlotId(slots[i].getTimeSlotId());
rys.setStartTimestamp(slots[i].getStartTime());
rys.setAvailable(slots[i].isAvailable());
rySlots[i] = rys;
}
return rySlots;
}
return new RyTimeslot[0];
}
}
6.5 Catalog Synchronization¶
Class: SDRefresher (Static Data Refresher)
Location: tqryb2b/.../service/SDRefresher.java
Purpose: Synchronizes supplier catalog data to local database
Key Process:
- Fetch from Supplier API
- Countries and cities
- Tour list
- Tour details (descriptions, terms, images)
- Tour options
- Tour transfers
-
Pricing information
-
Transform Data
- Convert supplier format to database entities
- Handle data type conversions
-
Validate data integrity
-
Update Database
- Upsert tour records (INSERT or UPDATE)
- Update related entities
-
Update refresh status
-
Error Handling
- Log sync errors
- Continue with partial updates
- Track failed entities
Scheduled Execution:
public class RaynaB2BActPlugin extends AbstractPlugin {
@Override
public void initializePlugin() {
// Schedule refresh every hour
ScheduledExecutorService executorService =
Executors.newScheduledThreadPool(3);
executorService.scheduleAtFixedRate(
new SDRefreshRunner(),
0, // Initial delay
1, // Period
TimeUnit.HOURS
);
// Initialize cache manager
RaynaCacheManager rcm = RaynaCacheManager.instance();
// Initialize service factory
RaynaServiceFactory rsf = RaynaServiceFactory.getInstance();
}
}
7. REST API Layer¶
7.1 Product API¶
Class: ProductApi
Location: tqapi/src/main/java/com/perun/tlinq/api/ProductApi.java
Base Path: /tlinq-api/product
Endpoints:
GET/POST /product/getCategories¶
Purpose: Retrieve all product categories
Request:
Response:
{
"status": "OK",
"data": [
{
"categoryId": 101,
"categoryName": "Desert Safari",
"parentId": null,
"sortOrder": 1
},
{
"categoryId": 102,
"categoryName": "Water Sports",
"parentId": null,
"sortOrder": 2
}
]
}
Implementation:
@POST
@Path("/getCategories")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response getAllCategories(Map reqData) {
String sessionToken = (String)reqData.get("session");
try {
CProductCategory[] allCategories = getCategories(sessionToken);
TlinqApiResponse ar = new TlinqApiResponse(allCategories);
return Response.status(Response.Status.OK).entity(ar).build();
} catch (Exception ex) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(ex).build();
}
}
private CProductCategory[] getCategories(String session)
throws TlinqClientException {
if (session == null) {
ServiceFactoryConfig sf = ClientConfig.instance().getDefaultFactory();
session = sf.getPropertyList().getProperty("system.session");
}
ProductFacade productFacade = new ProductFacade(session);
return productFacade.getCategories();
}
POST /product/getCategoryProducts¶
Purpose: Get products in a category
Request:
Response:
{
"status": "OK",
"data": [
{
"productId": 5001,
"productCode": "DSF-001",
"productName": "Morning Desert Safari",
"description": "Experience the desert at sunrise...",
"categoryId": 101,
"imageUrl": "https://cdn.example.com/desert-safari.jpg",
"startingPrice": 150.00,
"currency": "AED"
}
]
}
Implementation:
@Path("/getCategoryProducts")
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response getCategoryProducts(Map reqData) {
String sessionToken = (String) reqData.get("session");
Integer catId = (Integer) reqData.get("catId");
return _getCategoryProducts(sessionToken, catId);
}
private Response _getCategoryProducts(String session, Integer categoryId) {
TlinqApiResponse rsp;
try {
if (TypeUtil.isEmptyString(session)) {
ServiceFactoryConfig sf = ClientConfig.instance().getDefaultFactory();
session = sf.getPropertyList().getProperty("system.session");
}
ProductFacade facade = new ProductFacade(session);
List productList = facade.getCategoryProducts(categoryId);
CProduct[] products = (CProduct[]) productList.toArray(new CProduct[0]);
rsp = new TlinqApiResponse(products);
} catch (TlinqClientException ex) {
rsp = new TlinqApiResponse(ex.getErrorCode(), ex.getMessage());
} catch (Exception ex) {
rsp = new TlinqApiResponse("SRV0005", ex.getMessage());
}
return Response.status(Response.Status.OK).entity(rsp).build();
}
POST /product/getProduct¶
Purpose: Get detailed product information
Request:
Response:
{
"status": "OK",
"data": {
"productId": 5001,
"productCode": "DSF-001",
"productName": "Morning Desert Safari",
"description": "Experience the desert at sunrise with dune bashing...",
"longDescription": "<p>Full HTML description...</p>",
"categoryId": 101,
"supplierId": 2,
"supplierProductId": 7856,
"duration": "4 hours",
"images": [
{
"imageUrl": "https://cdn.example.com/desert-1.jpg",
"imageType": "primary"
}
],
"hasTimeSlot": true,
"minAdults": 1,
"maxAdults": 15,
"childFromAge": 3,
"infantFromAge": 0
}
}
POST /product/getProductAttributes¶
Purpose: Get configurable attributes for a product
Request:
Response:
{
"status": "OK",
"data": [
{
"attributeId": 0,
"name": "Product option",
"values": [
{
"attributeValueId": 101,
"name": "Standard Safari",
"dependencies": [
{
"depAttributeId": 2,
"depAttribValues": [201, 202, 203]
}
]
},
{
"attributeValueId": 102,
"name": "VIP Safari",
"dependencies": [
{
"depAttributeId": 2,
"depAttribValues": [202, 203]
}
]
}
]
},
{
"attributeId": 2,
"name": "Transfer",
"values": [
{
"attributeValueId": 201,
"name": "No Transfer"
},
{
"attributeValueId": 202,
"name": "Shared Transfer"
},
{
"attributeValueId": 203,
"name": "Private Transfer"
}
]
}
]
}
Implementation:
@Path("/getProductAttributes")
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response getProductAttributes(Map reqData) {
String sessionToken = (String) reqData.get("session");
Integer prodId = (Integer) reqData.get("productId");
return _getProductAttributes(sessionToken, prodId);
}
private Response _getProductAttributes(String session, Integer productId) {
TlinqApiResponse rsp;
try {
if (TypeUtil.isEmptyString(session)) {
ServiceFactoryConfig sf = ClientConfig.instance().getDefaultFactory();
session = sf.getPropertyList().getProperty("system.session");
}
ProductFacade facade = new ProductFacade(session);
List productList = facade.getProductAttributes(productId);
CProductAttribute[] attrs =
(CProductAttribute[]) productList.toArray(new CProductAttribute[0]);
rsp = new TlinqApiResponse(attrs);
} catch (TlinqClientException ex) {
rsp = new TlinqApiResponse(ex.getErrorCode(), ex.getMessage());
} catch (Exception ex) {
rsp = new TlinqApiResponse("SRV0005", ex.getMessage());
}
return Response.ok().entity(rsp).build();
}
POST /product/getProductVariants¶
Purpose: Get all possible variants for a product
Request:
{
"session": "session-token",
"productId": 5001,
"bookInfo": {
"dateFrom": "2025-12-15",
"adults": 2,
"children": 1,
"infants": 0
}
}
Response:
{
"status": "OK",
"data": [
{
"variantId": "5001-101-201",
"productId": 5001,
"optionId": 101,
"optionName": "Standard Safari",
"transferId": 201,
"transferName": "No Transfer",
"adultPrice": 150.00,
"childPrice": 100.00,
"infantPrice": 0.00,
"available": true,
"timeSlots": "[{\"timeSlotId\":\"TS001\",\"startTime\":\"06:00\"}]"
}
]
}
POST /product/getVariant¶
Purpose: Get specific variant by selected attributes
Request:
{
"session": "session-token",
"productId": 5001,
"bookInfo": {
"dateFrom": "2025-12-15",
"adults": 2,
"children": 1
},
"attrValues": [
{
"attributeId": 0,
"attributeValueId": 101
},
{
"attributeId": 2,
"attributeValueId": 202
}
]
}
Response:
{
"status": "OK",
"data": {
"variantId": "5001-101-202",
"productId": 5001,
"optionId": 101,
"transferId": 202,
"adultPrice": 180.00,
"childPrice": 120.00,
"timeSlots": "[...]",
"servdesc": "Standard Safari with Shared Transfer"
}
}
POST /product/getTimeslots¶
Purpose: Get available time slots for a variant
Request:
{
"session": "session-token",
"bookInfo": {
"dateFrom": "2025-12-15"
},
"variant": {
"productId": 5001,
"optionId": 101,
"transferId": 202
}
}
Response:
{
"status": "OK",
"data": [
{
"timeSlotId": "TS001",
"startTimestamp": "2025-12-15T06:00:00",
"available": true
},
{
"timeSlotId": "TS002",
"startTimestamp": "2025-12-15T08:00:00",
"available": true
}
]
}
POST /product/getAvailability¶
Purpose: Check availability and get final pricing
Request:
{
"session": "session-token",
"bookingRequest": {
"templateId": 5001,
"dateFrom": "2025-12-15",
"adults": 2,
"children": 1,
"infants": 0,
"attributes": {
"0": 101,
"2": 202
},
"slotId": "TS001"
}
}
Response:
{
"status": "OK",
"data": {
"available": true,
"statusReason": "Available",
"startTime": "06:00:00",
"vpc": [180.00, 120.00, 0.00],
"totalCost": 420.00,
"needsLocation": true
}
}
Implementation:
@POST
@Path("/getAvailability")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response getAvailability(Map reqData) {
String sessionToken = (String) reqData.get("session");
TlinqApiResponse ar;
try {
ar = doGetAvailability(reqData);
} catch (Exception ex) {
ar = new TlinqApiResponse(
TlinqErr.GENERAL,
ex.getMessage()
);
}
return Response.ok(ar).build();
}
private TlinqApiResponse doGetAvailability(Map reqData) {
Map bookInfo = (Map)reqData.get("bookingRequest");
if(null == bookInfo)
return new TlinqApiResponse(
TlinqErr.MISSING_PARAMETER,
"Booking request not properly initialized!"
);
// Transform attribute arrays to map
List attrIds = (List)bookInfo.get("attributeids");
List attrVals = (List)bookInfo.get("attributevals");
if((null != attrIds) && (null != attrVals)) {
if((attrIds.size() > 0) && (attrVals.size() == attrIds.size())) {
HashMap realAttrMap = new HashMap();
for(int i = 0; i<attrIds.size(); i++) {
realAttrMap.put(attrIds.get(i), attrVals.get(i));
}
bookInfo.put("attributes", realAttrMap);
}
}
try {
ServiceFactoryConfig sf = ClientConfig.instance().getDefaultFactory();
String session = sf.getPropertyList().getProperty("system.session");
ProductFacade pf = new ProductFacade(session);
CItemPriceInfo pi = pf.checkProductAvailable(bookInfo);
return new TlinqApiResponse(pi);
} catch (TlinqClientException ex) {
return new TlinqApiResponse(ex.getErrorCode(), ex.getMessage());
}
}
POST /product/getTerms¶
Purpose: Get product terms and conditions
Request:
Response:
{
"status": "OK",
"data": {
"info": "<p>Important information...</p>",
"terms": "<p>Terms and conditions...</p>",
"inclusions": "<p>What's included...</p>",
"cancelPolicy": "<p>Cancellation policy...</p>"
}
}
7.2 Booking API¶
Class: BookingApi
Location: tqapi/src/main/java/com/perun/tlinq/api/BookingApi.java
Base Path: /tlinq-api/booking
Key Endpoints:
POST /booking/create¶
Purpose: Create booking request from cart
Request:
Response:
{
"status": "OK",
"data": {
"bookingRequestId": 789,
"refnum": "BK20251215-001",
"reqstatus": "PENDING",
"amount": 420.00
}
}
POST /booking/submit¶
Purpose: Submit booking to supplier
Request:
Response:
{
"status": "OK",
"data": {
"bookingResult": "CONFIRMED",
"statusCode": "OK",
"statusMessage": "Booking confirmed successfully",
"ticketingStatus": [
{
"bookingId": 7856,
"bookingRefNo": "RYN123456",
"voucherNo": "VCH789012",
"startTime": "06:00:00"
}
]
}
}
POST /booking/cancel¶
Purpose: Cancel confirmed booking
Request:
Response:
{
"status": "OK",
"data": {
"statusCode": "CANCELLED",
"statusMessage": "Booking cancelled successfully",
"refundAmount": 350.00
}
}
POST /booking/get¶
Purpose: Retrieve booking details
Request:
Response:
{
"status": "OK",
"data": {
"bookingrequestid": 789,
"refnum": "BK20251215-001",
"rqdate": "2025-11-23T10:30:00",
"reqstatus": "CONFIRMED",
"amount": 420.00,
"customerid": 456,
"items": [...]
}
}
8. Data Flow Diagrams¶
8.1 Product Search and Selection Flow¶
User Action Frontend API Layer Facade Layer Service Layer Database/Supplier
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
1. Browse Categories
│
├─> loadCategories()
│ │
│ ├─> GET /product/getCategories
│ │ │
│ │ ├─> ProductFacade.getCategories()
│ │ │ │
│ │ │ ├─> ServiceFactory.createService()
│ │ │ │ │
│ │ │ │ └─> Query local cache
│ │ │ │ │
│ │ │ ├─< CProductCategory[] │
│ │ ├─< TlinqApiResponse │
│ ├─< JSON Response │
├─< Display categories │
│ │
2. View Products in Category │
│ │
├─> loadCategoryProducts(101) │
│ │ │
│ ├─> POST /product/getCategoryProducts │
│ │ {catId: 101} │
│ │ │ │
│ │ ├─> ProductFacade.getCategoryProducts(101) │
│ │ │ │ │
│ │ │ ├─> RaynaProductService.read() │
│ │ │ │ │ │
│ │ │ │ └─> Query local DB │
│ │ │ │ (rayna.tour) │
│ │ │ ├─< List<CProduct> │
│ │ ├─< TlinqApiResponse │
│ ├─< JSON Response │
├─< Display product list │
│ │
3. View Product Details │
│ │
├─> getProduct(5001) │
│ │ │
│ ├─> POST /product/getProduct │
│ │ {productId: 5001} │
│ │ │ │
│ │ ├─> ProductFacade.getProduct(5001) │
│ │ │ │ │
│ │ │ ├─> CatalogFacade.getProduct(5001) │
│ │ │ │ │ │
│ │ │ │ └─> Query local DB │
│ │ │ ├─< RyProduct │
│ │ │ │ │
│ │ │ ├─> Transform to CProduct │
│ │ ├─< TlinqApiResponse │
│ ├─< JSON Response │
├─< Display product details │
│ │
4. Get Product Attributes │
│ │
├─> getProductAttributes(5001) │
│ │ │
│ ├─> POST /product/getProductAttributes │
│ │ {productId: 5001} │
│ │ │ │
│ │ ├─> ProductFacade.getProductAttributes(5001) │
│ │ │ │ │
│ │ │ ├─> RaynaProductService.getAttributes() │
│ │ │ │ │ │
│ │ │ │ ├─> CatalogFacade │
│ │ │ │ │ .getProductAttributes()
│ │ │ │ │ │
│ │ │ │ │ Query: │
│ │ │ │ │ - touroption │
│ │ │ │ │ - tourtransfer │
│ │ │ │ │ - transfer │
│ │ │ │ │ │
│ │ │ ├─< RyProductAttribute[] │
│ │ ├─< TlinqApiResponse │
│ ├─< JSON Response │
├─< Populate attribute dropdowns │
│ │
5. User Selects Attributes │
│ │
├─> refreshVariant() │
│ (on attribute change) │
│ │ │
│ ├─> POST /product/getVariant │
│ │ {productId: 5001, │
│ │ bookInfo: {dateFrom, adults, children}, │
│ │ attrValues: [{attrId:0, valId:101}, │
│ │ {attrId:2, valId:202}]} │
│ │ │ │
│ │ ├─> ProductFacade.getProductVariant() │
│ │ │ │ │
│ │ │ ├─> RaynaProductService │
│ │ │ │ .getVariantByAttr() │
│ │ │ │ │ │
│ │ │ │ ├─> CatalogFacade │
│ │ │ │ │ .getVariantByAttr()
│ │ │ │ │ │
│ │ │ │ │ Query pricing: │
│ │ │ │ │ - tourprice │
│ │ │ │ │ │
│ │ │ ├─< RyProductVariant │
│ │ ├─< TlinqApiResponse │
│ ├─< JSON Response │
├─< Update pricing display │
│ Update timeslot dropdown │
│ │
6. Get Available Timeslots │
│ │
├─> (automatic after variant selection) │
│ │ │
│ ├─> POST /product/getTimeslots │
│ │ {variant: {...}, bookInfo: {...}} │
│ │ │ │
│ │ ├─> ProductFacade.getVariantTimeslots() │
│ │ │ │ │
│ │ │ ├─> RaynaProductService │
│ │ │ │ .getVariantTimeslots() │
│ │ │ │ │ │
│ │ │ │ ├─> CatalogFacade │
│ │ │ │ │ .getTimeslots() │
│ │ │ │ │ │
│ │ │ │ └─> TourTimeslotService
│ │ │ │ (Remote API Call)
│ │ │ │ ─────────────────> Supplier API
│ │ │ │ <───────────────── Timeslot data
│ │ │ ├─< RyTimeslot[] │
│ │ ├─< TlinqApiResponse │
│ ├─< JSON Response │
├─< Populate timeslot dropdown │
│ │
7. Check Final Availability │
│ │
├─> checkProductAvailability() │
│ (before adding to cart) │
│ │ │
│ ├─> POST /product/getAvailability │
│ │ {bookingRequest: { │
│ │ templateId, dateFrom, adults, children, │
│ │ attributes: {0:101, 2:202}, │
│ │ slotId: "TS001" │
│ │ }} │
│ │ │ │
│ │ ├─> ProductFacade.checkProductAvailable() │
│ │ │ │ │
│ │ │ ├─> RaynaProductService │
│ │ │ │ .getTourAvailability() │
│ │ │ │ │ │
│ │ │ │ ├─> CatalogFacade │
│ │ │ │ │ .checkAvailability()
│ │ │ │ │ │
│ │ │ │ └─> TourAvailabilityService
│ │ │ │ (Remote API Call)
│ │ │ │ ─────────────────> Supplier API
│ │ │ │ <───────────────── Availability + Pricing
│ │ │ ├─< CItemPriceInfo │
│ │ │ │ {available:true, │
│ │ │ │ vpc:[180,120,0], │
│ │ │ │ totalCost:420, │
│ │ │ │ needsLocation:true} │
│ │ ├─< TlinqApiResponse │
│ ├─< JSON Response │
├─< Show availability status │
│ Enable "Add to Cart" button │
8.2 Booking Creation and Confirmation Flow¶
User Action Frontend API Layer Facade Layer Service Layer Database/Supplier
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
1. Add to Cart
│
├─> addItemToCart()
│ │
│ ├─> POST /cart/addItem
│ │ {productId, variant, quantity, bookInfo, ...}
│ │ │
│ │ ├─> CartFacade.addItem()
│ │ │ │
│ │ │ └─> Insert into cartitem table
│ │ ├─< CCartItem
│ ├─< JSON Response
├─< Update cart display
│
2. Proceed to Checkout
│
├─> checkout()
│ │
│ ├─> POST /cart/checkout
│ │ {cartId, customer info, payment info}
│ │ │
│ │ ├─> CartFacade.checkout()
│ │ │ │
│ │ │ ├─> Create order
│ │ │ │ Insert: salesorder
│ │ │ │ Insert: orderitem(s)
│ │ │ │
│ │ │ ├─> Process payment
│ │ │ │
│ │ │ └─> Clear cart
│ │ ├─< Order created
│ ├─< JSON Response
│ │ {orderId: 12345}
├─< Redirect to confirmation
│
3. Create Booking Request
│
├─> (automatic after payment)
│ │
│ ├─> POST /booking/create
│ │ {orderId: 12345}
│ │ │
│ │ ├─> BookingRequestFacade
│ │ │ .createBookingRequest(12345)
│ │ │ │
│ │ │ ├─> Query order items
│ │ │ │ SELECT * FROM orderitem
│ │ │ │ WHERE orderid = 12345
│ │ │ │
│ │ │ ├─> Create booking request
│ │ │ │ INSERT INTO bookingrequest
│ │ │ │ (refnum, rqdate, reqstatus='PENDING',
│ │ │ │ ordernum, customerid, amount)
│ │ │ │
│ │ │ └─> Generate access code
│ │ ├─< CBookingRequest
│ │ │ {bookingRequestId: 789,
│ │ │ refnum: "BK20251215-001"}
│ ├─< JSON Response
├─< Display booking reference
│
4. Submit to Supplier
│
├─> (automatic after booking creation)
│ │
│ ├─> POST /booking/submit
│ │ {bookingRequestId: 789}
│ │ │
│ │ ├─> BookingRequestFacade
│ │ │ .submitToSupplier(789)
│ │ │ │
│ │ │ ├─> Get booking request
│ │ │ │ SELECT * FROM bookingrequest
│ │ │ │ WHERE bookingrequestid = 789
│ │ │ │
│ │ │ ├─> Update status = 'PENDING'
│ │ │ │ UPDATE bookingrequest
│ │ │ │ SET reqstatus = 'PENDING'
│ │ │ │
│ │ │ ├─> Get order items
│ │ │ │ (grouped by supplier)
│ │ │ │
│ │ │ ├─> For each supplier:
│ │ │ │ │
│ │ │ │ ├─> Get ServiceFactory
│ │ │ │ │ (e.g., RaynaServiceFactory)
│ │ │ │ │
│ │ │ │ ├─> Get TicketingService
│ │ │ │ │ ticketingSvc = factory.getTicketingService()
│ │ │ │ │ │
│ │ │ │ │ └─> new RaynaTicketingService()
│ │ │ │ │
│ │ │ │ ├─> Create TicketingRequest
│ │ │ │ │ INSERT INTO ticketingrequest
│ │ │ │ │ (vendorid, customerid,
│ │ │ │ │ bookingrequestid, trqstatus='NEW')
│ │ │ │ │
│ │ │ │ │ Populate:
│ │ │ │ │ - TicketRequestItems[]
│ │ │ │ │ - Customer info
│ │ │ │ │ - Booking details
│ │ │ │ │
│ │ │ │ ├─> Initialize request
│ │ │ │ │ ticketingSvc.initTicketRequest(req)
│ │ │ │ │ │
│ │ │ │ │ └─> RaynaTicketingService
│ │ │ │ │ .initBookingData()
│ │ │ │ │ │
│ │ │ │ │ ├─> Group items by
│ │ │ │ │ │ tour/date/option/transfer
│ │ │ │ │ │
│ │ │ │ │ ├─> Aggregate quantities
│ │ │ │ │ │ by pax type
│ │ │ │ │ │
│ │ │ │ │ ├─> Create TourBookingDetail[]
│ │ │ │ │ │
│ │ │ │ │ └─> Create TourBookingPassenger[]
│ │ │ │ │
│ │ │ │ ├─> Send to supplier
│ │ │ │ │ response = ticketingSvc.sendTicketRequest(req)
│ │ │ │ │ │
│ │ │ │ │ └─> RaynaTicketingService
│ │ │ │ │ .sendTicketRequest()
│ │ │ │ │ │
│ │ │ │ │ ├─> TourBookingService
│ │ │ │ │ │ .getTourBooking()
│ │ │ │ │ │ │
│ │ │ │ │ │ └─> HTTP POST
│ │ │ │ │ │ to Supplier API
│ │ │ │ │ │ ─────────────────>
│ │ │ │ │ │ Supplier processes
│ │ │ │ │ │ <─────────────────
│ │ │ │ │ │ Booking confirmed
│ │ │ │ │ │
│ │ │ │ │ ├─> Parse response
│ │ │ │ │ │ - Supplier booking ID
│ │ │ │ │ │ - Voucher numbers
│ │ │ │ │ │ - Confirmation details
│ │ │ │ │ │
│ │ │ │ │ └─> Return RyActTicketingResponse
│ │ │ │ │
│ │ │ │ ├─> Process response
│ │ │ │ │ BookingRequestFacade
│ │ │ │ │ .processTicketingResponse()
│ │ │ │ │ │
│ │ │ │ │ ├─> UPDATE ticketingrequest
│ │ │ │ │ │ SET trqstatus = 'CONFIRMED',
│ │ │ │ │ │ resultcode = ...,
│ │ │ │ │ │ responsetimestamp = NOW()
│ │ │ │ │ │
│ │ │ │ │ ├─> For each confirmed item:
│ │ │ │ │ │ │
│ │ │ │ │ │ ├─> INSERT INTO ticketitem
│ │ │ │ │ │ │ (tixrequestid, orderitemid,
│ │ │ │ │ │ │ extproductid, barcode,
│ │ │ │ │ │ │ supplierbookingid,
│ │ │ │ │ │ │ startdate, timeslot, ...)
│ │ │ │ │ │ │
│ │ │ │ │ │ └─> INSERT INTO ticketconfirmation
│ │ │ │ │ │ (bookingrequestid,
│ │ │ │ │ │ supplierreference,
│ │ │ │ │ │ suppliervoucher,
│ │ │ │ │ │ barcode, startdate, ...)
│ │ │ │ │ │
│ │ │ │ │ └─> UPDATE bookingrequest
│ │ │ │ │ SET reqstatus = 'CONFIRMED'
│ │ │ │ │
│ │ │ │ └─> Return response
│ │ │ │
│ │ │ └─> Collect all responses
│ │ ├─< List<TicketingResponseI>
│ │ │
│ │ ├─> Generate tickets
│ │ │ BookingRequestFacade
│ │ │ .generateAndSendTickets()
│ │ │ │
│ │ │ ├─> Get ticket confirmations
│ │ │ │ SELECT * FROM ticketconfirmation
│ │ │ │ WHERE bookingrequestid = 789
│ │ │ │
│ │ │ ├─> Generate barcodes
│ │ │ │ (QR codes, PDF417)
│ │ │ │
│ │ │ ├─> Generate HTML tickets
│ │ │ │ (from template)
│ │ │ │
│ │ │ ├─> Generate PDF vouchers
│ │ │ │ (wkhtmltopdf)
│ │ │ │
│ │ │ └─> Send email
│ │ │ - Tickets attached
│ │ │ - Booking details
│ │ │ - Access link
│ │ │
│ ├─< JSON Response
│ │ {bookingResult: "CONFIRMED",
│ │ ticketingStatus: [...]}
├─< Display confirmation
│ "Booking confirmed! Check email for tickets."
8.3 Catalog Synchronization Flow¶
Scheduled Task Refresh Service Remote API Local Database Cache
────────────────────────────────────────────────────────────────────────────────────────────────
1. Scheduled Trigger (Every 1 hour)
│
├─> SDRefreshRunner.run()
│ │
│ ├─> SDRefresher.refreshAllAct()
│ │ │
│ │ ├─> Log: "Starting catalog refresh"
│ │ │
│ │ ├─> CountriesService.getCountries()
│ │ │ │
│ │ │ └─────────────────> GET /countries
│ │ │ <───────────────── Country[]
│ │ │
│ │ ├─> For each country:
│ │ │ │
│ │ │ ├─> MERGE INTO rayna.country
│ │ │ │ (countryid, name, code)
│ │ │ │ VALUES (...)
│ │ │ │ ON CONFLICT UPDATE
│ │ │
│ │ ├─> CitiesService.getCities()
│ │ │ │
│ │ │ └─────────────────> GET /cities
│ │ │ <───────────────── City[]
│ │ │
│ │ ├─> For each city:
│ │ │ │
│ │ │ ├─> MERGE INTO rayna.city
│ │ │ │ (cityid, name, countryid)
│ │ │ │ VALUES (...)
│ │ │
│ │ ├─> TourInfoService.getTourList()
│ │ │ │
│ │ │ └─────────────────> GET /tours
│ │ │ │ ?cityId=X&contractId=Y
│ │ │ <───────────────── TourBase[]
│ │ │
│ │ ├─> For each tour:
│ │ │ │
│ │ │ ├─> TourStaticDataService
│ │ │ │ .getTourDetails(tourId)
│ │ │ │ │
│ │ │ │ └─────────────> GET /tour/{tourId}
│ │ │ │ <─────────────── Full tour details
│ │ │ │
│ │ │ ├─> MERGE INTO rayna.tour
│ │ │ │ (tourid, tourname, description,
│ │ │ │ tourtype, duration, cityid,
│ │ │ │ importantinfo, tourterms,
│ │ │ │ cancelpolicydesc, ...)
│ │ │ │ VALUES (...)
│ │ │ │
│ │ │ ├─> For each tour image:
│ │ │ │ │
│ │ │ │ └─> MERGE INTO rayna.tourimage
│ │ │ │ (tourid, imageurl, imagetype)
│ │ │ │
│ │ │ ├─> For each tour option:
│ │ │ │ │
│ │ │ │ ├─> MERGE INTO rayna.touroption
│ │ │ │ │ (touroptionid, tourid,
│ │ │ │ │ optionname, active)
│ │ │ │ │
│ │ │ │ ├─> For each transfer in option:
│ │ │ │ │ │
│ │ │ │ │ ├─> MERGE INTO rayna.transfer
│ │ │ │ │ │ (transferid, name, xfercode)
│ │ │ │ │ │
│ │ │ │ │ └─> MERGE INTO rayna.tourtransfer
│ │ │ │ │ (tourid, touroptionid, transferid)
│ │ │ │ │
│ │ │ │ └─> Get pricing for option
│ │ │ │ TourPriceService.getPrices()
│ │ │ │ │
│ │ │ │ └─────────────> GET /prices
│ │ │ │ │ ?tourId=X&optionId=Y
│ │ │ │ <─────────────── Price list
│ │ │ │
│ │ │ ├─> For each price:
│ │ │ │ │
│ │ │ │ └─> MERGE INTO rayna.tourprice
│ │ │ │ (tourid, touroptionid, transferid,
│ │ │ │ contractid, validfrom, validto,
│ │ │ │ adultprice, childprice)
│ │ │ │
│ │ │ └─> Get tour times/schedule
│ │ │ TourTimeslotService.getTimes()
│ │ │ │
│ │ │ └─────────────────> GET /timeslots
│ │ │ <───────────────── Timeslot[]
│ │ │ │
│ │ │ └─> MERGE INTO rayna.tourtime
│ │ │ (tourid, touroptionid,
│ │ │ starttime, endtime)
│ │ │
│ │ ├─> UPDATE rayna.refreshstatus
│ │ │ SET lastrefresh = NOW(),
│ │ │ status = 'SUCCESS'
│ │ │ WHERE entitytype = 'tour'
│ │ │
│ │ ├─> Refresh cache
│ │ │ RaynaCacheManager.refresh()
│ │ │ │
│ │ │ ├─> Load transfers to Hazelcast
│ │ │ │ SELECT * FROM rayna.transfer
│ │ │ │ │
│ │ │ │ └─> Cache put
│ │ │ │
│ │ │ └─> Load other frequently-accessed data
│ │ │
│ │ └─> Log: "Refresh completed successfully"
│ │ - X tours updated
│ │ - Y new tours added
│ │ - Z images processed
9. Configuration¶
9.1 Plugin Configuration¶
File: config/rayna-plugin-config.xml
<RaynaPluginConfig>
<PluginProperties>
<Property name="api.base.url" value="https://api.raynatours.com/v1"/>
<Property name="api.key" value="ENCRYPTED_API_KEY"/>
<Property name="api.timeout" value="30000"/>
<Property name="contract.id" value="300"/>
<Property name="currency" value="AED"/>
</PluginProperties>
<Services>
<Service name="Product"
class="com.perun.tlinq.client.ryb2b.service.product.RaynaProductService"
singleton="false"/>
<Service name="Ticketing"
class="com.perun.tlinq.client.ryb2b.service.RaynaTicketingService"
singleton="false"/>
</Services>
</RaynaPluginConfig>
9.2 Database Configuration¶
File: config/hibernate.cfg.xml
<hibernate-configuration>
<session-factory name="rayna">
<property name="hibernate.connection.driver_class">
org.postgresql.Driver
</property>
<property name="hibernate.connection.url">
jdbc:postgresql://localhost:5432/tlinq
</property>
<property name="hibernate.connection.username">tquser</property>
<property name="hibernate.connection.password">ENCRYPTED_PASSWORD</property>
<property name="hibernate.default_schema">rayna</property>
<property name="hibernate.dialect">
org.hibernate.dialect.PostgreSQLDialect
</property>
<!-- Entity mappings -->
<mapping class="com.perun.tlinq.client.ryb2b.db.TourEntity"/>
<mapping class="com.perun.tlinq.client.ryb2b.db.TouroptionEntity"/>
<mapping class="com.perun.tlinq.client.ryb2b.db.TourtransferEntity"/>
<!-- ... more mappings ... -->
</session-factory>
</hibernate-configuration>
9.3 Cache Configuration¶
File: config/hazelcast.xml
<hazelcast>
<map name="transfer-cache">
<time-to-live-seconds>3600</time-to-live-seconds>
<max-idle-seconds>1800</max-idle-seconds>
<eviction-policy>LRU</eviction-policy>
<max-size policy="PER_NODE">1000</max-size>
</map>
<map name="tour-option-cache">
<time-to-live-seconds>300</time-to-live-seconds>
<max-idle-seconds>180</max-idle-seconds>
<eviction-policy>LRU</eviction-policy>
<max-size policy="PER_NODE">5000</max-size>
</map>
</hazelcast>
10. Error Handling¶
10.1 Exception Hierarchy¶
TlinqClientException (Base)
│
├── RemoteException (Supplier API errors)
│ ├── TimeoutException
│ ├── AuthenticationException
│ └── SupplierErrorException
│
├── ConfigException (Configuration errors)
│
├── InvalidParameterException (Invalid input)
│
└── DataException (Database errors)
10.2 Error Codes¶
| Code | Category | Description |
|---|---|---|
API00001 |
API | Invalid parameters |
SRV0001 |
Service | Service unavailable |
SRV0002 |
Service | Configuration error |
SRV0003 |
Service | Database error |
SRV0004 |
Service | Remote service error |
SRV0005 |
Service | General service error |
TKT0001 |
Ticketing | Booking failed |
TKT0002 |
Ticketing | Cancellation failed |
TKT0003 |
Ticketing | Amendment failed |
PRD0001 |
Product | Product not found |
PRD0002 |
Product | Variant not found |
PRD0003 |
Product | Not available |
RMT0001 |
Remote | Supplier timeout |
RMT0002 |
Remote | Supplier authentication failed |
RMT0003 |
Remote | Supplier error |
11. Security Considerations¶
11.1 Authentication¶
- Session-based authentication for API calls
- API keys for supplier integration
- Encrypted credentials storage
- Token expiration and renewal
11.2 Authorization¶
- Role-based access control (RBAC)
- API endpoint authorization
- Booking access codes for guest retrieval
- Time-limited access links
11.3 Data Protection¶
- TLS 1.2+ for all communications
- Encrypted personal data at rest
- PCI DSS compliance for payment data
- Audit logging of all transactions
11.4 Input Validation¶
- Parameter validation in API layer
- SQL injection prevention (parameterized queries)
- XSS prevention (HTML sanitization)
- CSRF protection (tokens)
12. Performance Optimization¶
12.1 Caching Strategy¶
Level 1: Hazelcast Distributed Cache - Frequently accessed reference data - Transfer types, categories - 1-hour TTL, LRU eviction
Level 2: Local Database Cache - Complete product catalog - Pricing within validity period - Hourly synchronization
Level 3: HTTP Response Caching - Product listings (5 minutes) - Category structure (1 hour) - ETag/Last-Modified headers
12.2 Database Optimization¶
- Indexes on frequently queried columns
- Connection pooling (HikariCP)
- Query optimization and pagination
- Materialized views for complex queries
12.3 API Optimization¶
- Gzip compression
- JSON response minification
- Pagination for large result sets
- Async processing for long operations
Document Information¶
Document Version: 1.0 Date: 2025-11-23 Status: Final Author: System Architecture Team Related Requirements: Activity_Ticketing_Requirement_Spec.md