Skip to content

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

  • 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:

  1. Booking Lifecycle Management
  2. Create booking requests from cart
  3. Submit to appropriate supplier
  4. Track booking status
  5. Handle confirmations and failures

  6. Ticket Generation and Delivery

  7. Generate PDF vouchers
  8. Generate HTML tickets
  9. Create barcodes (QR, PDF417)
  10. Email tickets to customers

  11. Booking Operations

  12. Amendment processing
  13. Cancellation handling
  14. Status updates
  15. 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:

  1. Product Catalog Access
  2. Retrieve products by ID or code
  3. List products by category
  4. Search products by criteria

  5. Product Configuration

  6. Get product attributes
  7. Get product variants
  8. Get variant by attribute selection

  9. Availability and Pricing

  10. Check product availability
  11. Get real-time pricing
  12. Get variant timeslots
  13. 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:

  1. Fetch from Supplier API
  2. Countries and cities
  3. Tour list
  4. Tour details (descriptions, terms, images)
  5. Tour options
  6. Tour transfers
  7. Pricing information

  8. Transform Data

  9. Convert supplier format to database entities
  10. Handle data type conversions
  11. Validate data integrity

  12. Update Database

  13. Upsert tour records (INSERT or UPDATE)
  14. Update related entities
  15. Update refresh status

  16. Error Handling

  17. Log sync errors
  18. Continue with partial updates
  19. 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:

{
  "session": "session-token"
}

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:

{
  "session": "session-token",
  "catId": 101
}

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:

{
  "session": "session-token",
  "productId": 5001
}

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:

{
  "session": "session-token",
  "productId": 5001
}

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:

{
  "session": "session-token",
  "productId": 5001
}

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:

{
  "session": "session-token",
  "orderId": 12345
}

Response:

{
  "status": "OK",
  "data": {
    "bookingRequestId": 789,
    "refnum": "BK20251215-001",
    "reqstatus": "PENDING",
    "amount": 420.00
  }
}

POST /booking/submit

Purpose: Submit booking to supplier

Request:

{
  "session": "session-token",
  "bookingRequestId": 789
}

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:

{
  "session": "session-token",
  "bookingRequestId": 789
}

Response:

{
  "status": "OK",
  "data": {
    "statusCode": "CANCELLED",
    "statusMessage": "Booking cancelled successfully",
    "refundAmount": 350.00
  }
}

POST /booking/get

Purpose: Retrieve booking details

Request:

{
  "session": "session-token",
  "bookingReference": "BK20251215-001"
}

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