Skip to content

Booking Lifecycle Management - Technical Implementation

Status: M1-M9 Complete -- This document covers M1 (Core Booking) through M9 (Admin Maintenance & Data Health).

Table of Contents

  1. Architecture Overview
  2. Database Schema
  3. Entity Classes
  4. API Layer
  5. Business Logic (Facade)
  6. Status State Machine
  7. ERP Integration
  8. Background Jobs
  9. Configuration Files
  10. Actions Framework (M5)
  11. Voucher Generation (M6)
  12. Booking Terms Tab (M6.5)
  13. TripMaker Integration (M7)
  14. Inbound Group Integration (M7)
  15. Outbound Group Sales (M8)

1. Architecture Overview

The Booking Lifecycle Management module follows the standard TQPro three-layer architecture:

+-------------------------------------------------------------+
|                    REST API Layer (tqapi)                    |
|                  BookingLifecycleApi.java                    |
|   Jersey JAX-RS endpoints, request/response handling         |
+-------------------------------------------------------------+
                              |
                              v
+-------------------------------------------------------------+
|                Business Logic Layer (tqapp)                  |
|               BookingLifecycleFacade.java                    |
|   CRUD, status management, amendments, totals, ERP          |
+-------------------------------------------------------------+
                              |
                              v
+-------------------------------------------------------------+
|                  Entity Framework (tqcommon)                 |
|   EntityFacade, EntityTransformer, NTSServiceFactory         |
+-------------------------------------------------------------+
                              |
                              v
+-------------------------------------------------------------+
|                PostgreSQL Database (nts schema)              |
|       Native JPA entities via Hibernate ORM                  |
+-------------------------------------------------------------+

Key Components

Component Location Purpose
BookingLifecycleApi.java tqapi/src/main/java/com/perun/tlinq/api/ REST endpoints (28 endpoints under /blm/)
BookingLifecycleFacade.java tqapp/src/main/java/com/perun/tlinq/entity/booking/ Business logic, status management, ERP integration
BookingServiceI.java tqapp/src/main/java/com/perun/tlinq/entity/booking/ Facade interface
BookingCreationRequest.java tqapp/src/main/java/com/perun/tlinq/entity/booking/ Composite creation request DTO
OptionExpiryRunner.java tqapp/src/main/java/com/perun/tlinq/sched/ Background job for option hold expiry
CBooking.java tqapp/src/main/java/com/perun/tlinq/entity/booking/ Canonical booking entity
CServiceLine.java tqapp/src/main/java/com/perun/tlinq/entity/booking/ Canonical service line entity
CBookingPassenger.java tqapp/src/main/java/com/perun/tlinq/entity/booking/ Canonical passenger entity
CBookingStatusHistory.java tqapp/src/main/java/com/perun/tlinq/entity/booking/ Canonical status history entity
CBookingAmendment.java tqapp/src/main/java/com/perun/tlinq/entity/booking/ Canonical amendment entity
CBookingActivityLog.java tqapp/src/main/java/com/perun/tlinq/entity/booking/ Canonical activity log entity
booking-entities.xml config/entities/ Entity-to-native field mapping configuration
0044-booking-core.sql config/db-changes/ Database schema (6 tables)

2. Database Schema

The BLM module uses 6 database tables in the nts schema, defined in config/db-changes/0044-booking-core.sql.

2.1 Entity Relationship Diagram

+---------------------------+
|        booking            |
|  ---------------------    |
|  bookingid (PK)           |
|  bookingref (UQ)          |     +---------------------------+
|  status                   |     |      serviceline          |
|  createddate              |     |  ---------------------    |
|  updateddate              |     |  servicelineid (PK)       |
|  travelstartdate          |<----+  bookingid (FK)           |
|  travelenddate            |     |  servicetype              |
|  leadpassengerid          |     |  description              |
|  assignedagentid          |     |  erpproductid             |
|  sourcetype               |     |  erpvariantid             |
|  sourceref                |     |  supplierid               |
|  customerid               |     |  suppliername             |
|  currency                 |     |  startdate / enddate      |
|  totalcost                |     |  cost / costlocal         |
|  totalsell                |     |  sellprice                |
|  totalservicecharges      |     |  confirmationstatus       |
|  totalpaid                |     |  supplierref              |
|  balancedue               |     |  currency / quantity      |
|  notes                    |     |  attributes (JSON)        |
|  erpcustomerid            |     |  passengerids             |
|  erpleadid                |     |  specialrequests          |
|  erpquoteid               |     |  cancellationterms        |
|  invoicenumber            |     |  isservicecharge          |
|  cartreference            |     |  sortorder                |
|  optionexpirydate         |     +---------------------------+
|  paymentstatus            |
|  version                  |
+---------------------------+
      |           |           |
      | 1:N       | 1:N       | 1:N
      v           v           v
+------------+ +---------------+ +-------------------+
| booking-   | | booking-      | | booking-          |
| passenger  | | statushistory | | amendment         |
+------------+ +---------------+ +-------------------+
| passengerid| | historyid(PK) | | amendmentid (PK)  |
| bookingid  | | bookingid(FK) | | bookingid (FK)    |
| title      | | previousstatus| | amendmentnumber   |
| firstname  | | newstatus     | | amendmentdate     |
| lastname   | | changedby     | | userid            |
| dateofbirth| | changedat     | | description       |
| nationality| | notes         | | fieldchanges      |
| passport-  | +---------------+ | previoustotal     |
|  number    |                    | newtotal          |
| passport-  |                    +-------------------+
|  expiry    |
| gender     |     +---------------------------+
| phone      |     |   bookingactivitylog      |
| email      |     |  ---------------------    |
| islead     |     |  activityid (PK)          |
| customer-  |     |  bookingid (FK)           |
|  profileid |     |  activitytype             |
+------------+     |  description              |
                   |  userid                   |
                   |  username                 |
                   |  createdat                |
                   |  details (JSON)           |
                   |  relatedentitytype        |
                   |  relatedentityid          |
                   +---------------------------+

2.2 Table Details

booking

Column Type Constraints Description
bookingid SERIAL PRIMARY KEY Auto-generated ID
bookingref VARCHAR(30) NOT NULL, UNIQUE System-generated reference (TQ-YYYY-NNNNN)
status VARCHAR(30) NOT NULL, DEFAULT 'ENQUIRY' Current lifecycle status
createddate TIMESTAMP NOT NULL, DEFAULT NOW() Creation timestamp
updateddate TIMESTAMP NOT NULL, DEFAULT NOW() Last update timestamp
travelstartdate DATE Start of travel
travelenddate DATE End of travel
leadpassengerid INTEGER FK to bookingpassenger
assignedagentid VARCHAR(100) Agent user identifier
sourcetype VARCHAR(30) Origin (MANUAL, CART, TRIPMAKER)
sourceref VARCHAR(100) Source reference ID
customerid INTEGER Customer profile ID
currency VARCHAR(3) DEFAULT 'AED' Booking currency
totalcost NUMERIC(12,2) DEFAULT 0 Sum of service line costs
totalsell NUMERIC(12,2) DEFAULT 0 Sum of service line sell prices
totalservicecharges NUMERIC(12,2) DEFAULT 0 Sum of service charge lines
totalpaid NUMERIC(12,2) DEFAULT 0 Total payments received
balancedue NUMERIC(12,2) DEFAULT 0 totalsell - totalpaid
notes TEXT Agent/booking notes
erpcustomerid INTEGER Odoo customer ID
erpleadid INTEGER Odoo CRM lead ID
erpquoteid INTEGER Odoo quotation ID
invoicenumber VARCHAR(50) ERP invoice reference
cartreference VARCHAR(100) Cart session ID (M2)
optionexpirydate TIMESTAMP Option hold deadline
paymentstatus VARCHAR(30) DEFAULT 'UNPAID' UNPAID, PARTIAL, PAID, REFUNDED
version INTEGER DEFAULT 0 Optimistic concurrency version

Indexes: bookingref, status, customerid, assignedagentid, (travelstartdate, travelenddate)

Sequences: booking_seq (entity IDs), bookingref_seq (reference number generation)

serviceline

Column Type Constraints Description
servicelineid SERIAL PRIMARY KEY Auto-generated ID
bookingid INTEGER NOT NULL, FK to booking Parent booking
servicetype VARCHAR(30) NOT NULL HOTEL, FLIGHT, TRANSFER, ACTIVITY, CRUISE, VISA, OTHER, SERVICE_CHARGE
description VARCHAR(500) Human-readable description
erpproductid INTEGER Odoo product ID
erpvariantid INTEGER Odoo product variant ID
supplierid INTEGER Supplier record ID
suppliername VARCHAR(200) Supplier display name
startdate DATE Service start date
enddate DATE Service end date
cost NUMERIC(12,2) DEFAULT 0 Cost in original currency
costlocal NUMERIC(12,2) DEFAULT 0 Cost converted to local currency
sellprice NUMERIC(12,2) DEFAULT 0 Customer sell price
confirmationstatus VARCHAR(30) DEFAULT 'PENDING' PENDING, CONFIRMED, CANCELLED
supplierref VARCHAR(200) Supplier confirmation reference
notes TEXT Service-specific notes
sortorder INTEGER DEFAULT 0 Display ordering
isservicecharge BOOLEAN DEFAULT FALSE Distinguishes service charges from services
currency VARCHAR(3) DEFAULT 'AED' Cost/sell currency
quantity INTEGER DEFAULT 1 Number of units
attributes TEXT JSON attributes bag
passengerids TEXT Comma-separated passenger IDs
specialrequests TEXT Customer special requests
cancellationterms TEXT Cancellation T&C (M4)

Indexes: bookingid, confirmationstatus

bookingpassenger

Column Type Constraints Description
passengerid SERIAL PRIMARY KEY Auto-generated ID
bookingid INTEGER NOT NULL, FK to booking Parent booking
title VARCHAR(10) Mr, Mrs, Ms, etc.
firstname VARCHAR(100) NOT NULL First name
lastname VARCHAR(100) NOT NULL Last name
dateofbirth DATE Date of birth
nationality VARCHAR(50) Nationality / country
passportnumber VARCHAR(30) Passport document number
passportexpiry DATE Passport expiry date
gender VARCHAR(10) Male, Female, Other
phone VARCHAR(50) Contact phone
email VARCHAR(200) Contact email
islead BOOLEAN DEFAULT FALSE Lead passenger flag
customerprofileid INTEGER Link to customer profile

Indexes: bookingid

bookingstatushistory

Column Type Constraints Description
historyid SERIAL PRIMARY KEY Auto-generated ID
bookingid INTEGER NOT NULL, FK to booking Parent booking
previousstatus VARCHAR(30) Status before transition
newstatus VARCHAR(30) NOT NULL Status after transition
changedby VARCHAR(100) User who made the change
changedat TIMESTAMP NOT NULL, DEFAULT NOW() Timestamp of change
notes TEXT Reason / notes

Indexes: bookingid

bookingamendment

Column Type Constraints Description
amendmentid SERIAL PRIMARY KEY Auto-generated ID
bookingid INTEGER NOT NULL, FK to booking Parent booking
amendmentnumber INTEGER NOT NULL Sequential per booking
amendmentdate TIMESTAMP NOT NULL, DEFAULT NOW() Amendment timestamp
userid VARCHAR(100) User who made the change
description VARCHAR(500) What was changed
fieldchanges TEXT JSON field-level changes
previoustotal NUMERIC(12,2) Booking total before
newtotal NUMERIC(12,2) Booking total after

Indexes: bookingid

bookingactivitylog

Column Type Constraints Description
activityid SERIAL PRIMARY KEY Auto-generated ID
bookingid INTEGER NOT NULL, FK to booking Parent booking
activitytype VARCHAR(30) NOT NULL Event type code
description VARCHAR(500) NOT NULL Human-readable description
userid VARCHAR(100) User who triggered the event
username VARCHAR(200) Display name of user
createdat TIMESTAMP NOT NULL, DEFAULT NOW() Event timestamp
details TEXT JSON additional details
relatedentitytype VARCHAR(30) Related entity (ServiceLine, BookingPassenger, etc.)
relatedentityid INTEGER Related entity ID

Indexes: bookingid, createdat


3. Entity Classes

3.1 Canonical Entities (tqapp)

All canonical entities are in tqapp/src/main/java/com/perun/tlinq/entity/booking/:

Class Extends ID Field Description
CBooking TlinqEntity bookingId Central booking record
CServiceLine TlinqEntity serviceLineId Service within a booking
CBookingPassenger TlinqEntity passengerId Traveller on a booking
CBookingStatusHistory TlinqEntity historyId Status transition audit
CBookingAmendment TlinqEntity amendmentId Amendment audit record
CBookingActivityLog TlinqEntity activityId Unified event log

3.2 Native JPA Entities (tqapp - NTS plugin)

All native entities are in tqapp/src/main/java/com/perun/tlinq/client/nts/db/booking/:

Class Table ID Field
BookingEntity nts.booking id (maps to bookingid)
ServicelineEntity nts.serviceline id (maps to servicelineid)
BookingpassengerEntity nts.bookingpassenger id (maps to passengerid)
BookingstatushistoryEntity nts.bookingstatushistory id (maps to historyid)
BookingamendmentEntity nts.bookingamendment id (maps to amendmentid)
BookingactivitylogEntity nts.bookingactivitylog id (maps to activityid)

3.3 Entity Configuration

Entity-to-native field mappings are defined in config/entities/booking-entities.xml, included in config/tourlinq-config.xml via XInclude. All 6 entities use DirectMapping for all fields, with the NTSServiceFactory as the default factory.

Each entity has read/search services (using NTSEntityReadService) and create/update/delete services (using NTSEntityWriteService) defined in config/nts-client.xml.


4. API Layer

All endpoints are in BookingLifecycleApi.java, registered under the /blm/ path prefix. All endpoints use POST method with JSON request/response.

4.1 Booking Endpoints

# Path Required Params Optional Params Response
1 blm/booking/create session customerId, travelStartDate, travelEndDate, currency, notes, assignedAgentId, sourceType, sourceRef, customerName, email, phone CBooking
2 blm/booking/read session, bookingId CBooking
3 blm/booking/readByRef session, bookingRef CBooking
4 blm/booking/list session status, agentId, customerId, bookingRef, dateFrom, dateTo List\<CBooking>
5 blm/booking/update session, bookingId notes, assignedAgentId, travelStartDate, travelEndDate CBooking
6 blm/booking/changeStatus session, bookingId, newStatus notes, optionExpiryDate CBooking
7 blm/booking/cancel session, bookingId reasonText CBooking
8 blm/booking/expiring session hoursAhead (default 48) List\<CBooking>
9 blm/booking/extendHold session, bookingId, newExpiryDate CBooking
10 blm/booking/sendConfirmation session, bookingId String
11 blm/booking/sendReminder session, bookingId String
12 blm/booking/export session (stub -- M3)

4.2 Service Line Endpoints

# Path Required Params Optional Params Response
13 blm/serviceline/add session, bookingId, serviceType description, erpProductId, erpVariantId, supplierId, supplierName, startDate, endDate, cost, sellPrice, currency, quantity, attributes, notes, supplierRef, specialRequests, isServiceCharge, sortOrder CServiceLine
14 blm/serviceline/update session, serviceLineId serviceType, description, startDate, endDate, cost, sellPrice, currency, notes, supplierName, supplierRef, erpProductId, erpVariantId, quantity, sortOrder CServiceLine
15 blm/serviceline/remove session, serviceLineId reason String
16 blm/serviceline/list session, bookingId List\<CServiceLine>
17 blm/serviceline/confirm session, serviceLineId, supplierRef CServiceLine
18 blm/serviceline/duplicate session, serviceLineId CServiceLine

4.3 Passenger Endpoints

# Path Required Params Optional Params Response
19 blm/passenger/add session, bookingId, firstName, lastName title, dateOfBirth, nationality, passportNumber, passportExpiry, gender, phone, email, isLead, customerProfileId CBookingPassenger
20 blm/passenger/update session, passengerId firstName, lastName, title, dateOfBirth, nationality, passportNumber, passportExpiry, gender, phone, email CBookingPassenger
21 blm/passenger/remove session, passengerId String
22 blm/passenger/list session, bookingId List\<CBookingPassenger>
23 blm/passenger/setLead session, bookingId, passengerId String
24 blm/passenger/validate session, bookingId List\<Map> (warnings)

4.4 History / Audit Endpoints

# Path Required Params Optional Params Response
25 blm/statushistory/list session, bookingId List\<CBookingStatusHistory>
26 blm/amendment/list session, bookingId List\<CBookingAmendment>
27 blm/activity/list session, bookingId limit (default 50) List\<CBookingActivityLog>
28 blm/activity/add session, bookingId, note String

4.5 Security

All 28 endpoints require authentication. The role configuration in config/api-roles.properties:

blm/booking/*=agent,admin
blm/serviceline/*=agent,admin
blm/passenger/*=agent,admin
blm/statushistory/list=agent,admin
blm/amendment/list=agent,admin
blm/activity/*=agent,admin

No endpoints are accessible to the guest role. The assignedAgentId is auto-populated from the request context (display name or email) if not explicitly provided.


5. Business Logic (Facade)

BookingLifecycleFacade extends EntityFacade and implements BookingServiceI. It uses the NTSServiceFactory for all entity operations.

5.1 Facade Methods by Function

Booking CRUD

Method Description
createBooking(CBooking) Creates booking with reference generation, initial status, ERP integration
createBooking(CBooking, customerName, email, phone) Creates booking with customer lookup/creation
createBooking(BookingCreationRequest) Composite creation with service lines and passengers
readBooking(bookingId) Reads booking with on-read option expiry check
readByRef(bookingRef) Reads booking by reference with expiry check
listBookings(SelectCriteriaList) Searches bookings by criteria
updateBooking(CBooking) Updates booking fields

Status Management

Method Description
changeBookingStatus(bookingId, newStatus, notes, userId) Validates and executes status transition
setOptionHold(bookingId, expiryDate, userId) Sets option hold and transitions to OPTION_HELD
extendOptionHold(bookingId, newExpiryDate) Extends hold deadline (must be in OPTION_HELD)
checkOptionExpiry(CBooking) On-read expiry check, auto-transitions to EXPIRED
getExpiringBookings(hoursAhead) Queries bookings expiring within N hours
getStatusHistory(bookingId) Returns status transition history

Service Lines

Method Description
addServiceLine(CServiceLine) Adds line, converts currency, recalculates totals, records amendment if needed
updateServiceLine(CServiceLine) Updates line, reconverts currency, recalculates totals
removeServiceLine(serviceLineId, bookingId, reason) Removes line, recalculates totals
duplicateServiceLine(serviceLineId) Copies line with PENDING status
getServiceLines(bookingId) Returns lines sorted by sortOrder
confirmServiceLine(serviceLineId, supplierRef) Sets status to CONFIRMED with supplier ref

Passengers

Method Description
addPassenger(CBookingPassenger) Adds passenger, updates lead reference if lead
updatePassenger(CBookingPassenger) Updates passenger fields
removePassenger(passengerId, bookingId) Removes passenger, clears lead if was lead
getPassengers(bookingId) Returns passengers (lead first, then by last name)
setLeadPassenger(bookingId, passengerId) Sets lead flag, clears others
validatePassportExpiry(bookingId) Checks 6-month passport validity

Internal Operations

Method Description
recalculateBookingTotals(bookingId) Sums service line costs/sells, updates booking
convertCostToLocal(CServiceLine) Converts cost to local currency via ExchangeRateHelper
writeStatusHistory(...) Writes status history record
recordAmendment(...) Creates amendment record with sequential numbering
logActivity(...) Writes activity log entry
addAgentNote(bookingId, note, userId) Adds NOTE_ADDED activity entry
findOrCreateErpCustomer(...) Finds/creates customer in Odoo ERP
createCrmLead(...) Creates CRM lead in Odoo ERP

Notifications

Method Description
sendBookingConfirmation(bookingId) Sends confirmation email
sendPaymentReminder(bookingId) Sends payment reminder email
notifyOptionExpiry(bookingId, expired) Sends option expiry notification

5.2 Booking Total Recalculation

When recalculateBookingTotals() is called, the facade:

  1. Loads all service lines for the booking.
  2. Sums costLocal for non-service-charge lines into totalCost.
  3. Sums sellPrice for non-service-charge lines into totalSell.
  4. Sums sellPrice for service-charge lines (where isServiceCharge = true) into totalServiceCharges.
  5. Calculates balanceDue = totalSell + totalServiceCharges - totalPaid.
  6. Writes the updated booking.

5.3 Currency Conversion

When a service line is added or updated, convertCostToLocal() converts the cost field from the service line's currency to the system local currency using ExchangeRateHelper, storing the result in costLocal.


6. Status State Machine

                    +-------------------+
                    |                   |
                    v                   |
+--------+    +--------+    +-----------+--+    +----------+
| ENQUIRY|--->| QUOTED |--->| OPTION_HELD  |--->| EXPIRED  |
+---+----+    +---+----+    +------+-------+    +----+-----+
    |             |                |                  |
    |             |                |                  |
    |             v                v                  v
    |         +---+----------------+--+          +--------+
    +-------->|      CONFIRMED        |<---------| QUOTED |
              +---+--------+----+----+           +--------+
                  |        |    |
                  v        |    v
            +---------+    | +----------+
            | AMENDED |----+ | TICKETED |
            +----+----+     +-----+----+
                 |                |
                 v                v
            +---------+     +-----------+
            |CONFIRMED|     | COMPLETED |
            +---------+     +-----------+

    Any active status ---> CANCELLED

Transition Rules

The valid transitions are enforced in BookingLifecycleFacade.VALID_TRANSITIONS:

Map.of(
    "ENQUIRY",     Set.of("QUOTED", "CONFIRMED", "CANCELLED"),
    "QUOTED",      Set.of("OPTION_HELD", "CONFIRMED", "CANCELLED"),
    "OPTION_HELD", Set.of("CONFIRMED", "EXPIRED", "CANCELLED"),
    "CONFIRMED",   Set.of("AMENDED", "TICKETED", "COMPLETED", "CANCELLED"),
    "AMENDED",     Set.of("CONFIRMED", "TICKETED", "CANCELLED"),
    "TICKETED",    Set.of("COMPLETED", "CANCELLED"),
    "EXPIRED",     Set.of("QUOTED", "CANCELLED")
);

COMPLETED and CANCELLED are terminal states (no outbound transitions).


7. ERP Integration

7.1 Customer Synchronization

During booking creation, findOrCreateErpCustomer():

  1. Uses CustomerFacade to look up the customer by email in Odoo.
  2. If found, stores the Odoo customer ID on the booking (erpCustomerId).
  3. If not found and customer details are provided, creates a new customer in Odoo and stores the returned ID.
  4. Failures are caught, logged as warnings, and do not block booking creation.

7.2 CRM Lead Creation

After customer synchronization, createCrmLead():

  1. Creates a CRM lead/opportunity in Odoo linked to the customer.
  2. Stores the Odoo lead ID on the booking (erpLeadId).
  3. Failures are caught, logged as warnings, and do not block booking creation.

7.3 Product References

Service lines can reference Odoo products via erpProductId and erpVariantId. These are optional foreign keys used for: - Linking service lines to the ERP product catalog. - Future invoice generation (M3). - Cost/margin reporting.


8. Background Jobs

8.1 OptionExpiryRunner

Class: com.perun.tlinq.sched.OptionExpiryRunner

Schedule: Runs periodically (configured in server startup).

Behavior:

  1. Acquires a distributed lock (blm-option-expiry) via Hazelcast IMap.tryLock() with a 55-minute lease to prevent concurrent execution across cluster nodes.
  2. If the lock cannot be acquired (held by another instance), the run is skipped silently.
  3. Queries all OPTION_HELD bookings with optionExpiryDate in the past (via getExpiringBookings(0)).
  4. For each expired booking:
  5. Transitions status from OPTION_HELD to EXPIRED.
  6. Sends an option expiry notification.
  7. Logs the expiry.
  8. Releases the distributed lock in a finally block.

Error Handling: Individual booking expiry failures are caught and logged without stopping the batch. The distributed lock is always released.


9. Configuration Files

9.1 Files Created or Modified for M1

File Action Description
config/db-changes/0044-booking-core.sql Created 6 tables, 6 sequences, 10 indexes
config/entities/booking-entities.xml Created 6 entity definitions with field mappings
config/tourlinq-config.xml Modified Added XInclude for booking-entities.xml
config/nts-client.xml Modified Added 12 service definitions (read/write for each entity)
config/api-roles.properties Modified Added 28 endpoint role mappings

9.2 Entity Configuration Summary

All 6 entities use: - Factory: NTSServiceFactory - Mapping type: DirectMapping (all fields) - Read service class: NTSEntityReadService - Write service class: NTSEntityWriteService - ID field in native entity: id

9.3 NTS Service Definitions

Service Name Class Entity
saveBooking / readBooking / deleteBooking NTSEntityWriteService / NTSEntityReadService BookingEntity
saveServiceLine / readServiceLine / deleteServiceLine NTSEntityWriteService / NTSEntityReadService ServicelineEntity
saveBookingPassenger / readBookingPassenger / deleteBookingPassenger NTSEntityWriteService / NTSEntityReadService BookingpassengerEntity
saveBookingStatusHistory / readBookingStatusHistory NTSEntityWriteService / NTSEntityReadService BookingstatushistoryEntity
saveBookingAmendment / readBookingAmendment NTSEntityWriteService / NTSEntityReadService BookingamendmentEntity
saveBookingActivityLog / readBookingActivityLog NTSEntityWriteService / NTSEntityReadService BookingactivitylogEntity

Note: BookingStatusHistory, BookingAmendment, and BookingActivityLog do not have delete services as these audit records should not be deleted.


M2: Cart + Browse-First Flow — Technical Details

Database Tables (M2)

Table Purpose Key Columns
nts.bookingcart Persistent shopping cart cartid, customerid, createdby, status, totalsell, currency, version, customername, customeremail, customerphone, erpcustomerid, erpquoteid, bookingid
nts.bookingcartitem Cart line items cartitemid, cartid, servicetype, description, suppliername, servicedate, servicedateend, quantity, unitcost, unitsell, attributes (JSON), sourcemodule, sourceref

Migration scripts: 0045-booking-cart.sql, 0046-booking-customer-denorm.sql

Cart Status Lifecycle

ACTIVE → PARKED (auto-park when agent creates new cart)
ACTIVE → CHECKED_OUT (on checkout)
ACTIVE → EXPIRED (stale cart expiry)
ACTIVE → ABANDONED (manual or retirement)
PARKED → ACTIVE (transferred to another agent on customer assignment)
PARKED → ABANDONED (retired during merge)

Facade Methods (added to BookingLifecycleFacade)

Cart CRUD: createCart(), getCart(), getActiveCartByAgent(), getActiveCartForCustomer(), getResumableCartForCustomer(), listActiveCarts()

Cart Items: addCartItem(), updateCartItem() (merge pattern — reads existing, applies non-null fields), removeCartItem(), clearCart(), getCartItems()

Customer Assignment: assignCustomerToCart() — validates active, resolves ERP customer, denormalizes customer info, handles transfer/merge for agent rotation

Checkout: checkoutCart() — validates customer + items, creates CBooking (sourceType=CART), converts items to CServiceLine, sets CHECKED_OUT

Lifecycle: parkCart(), abandonCart(), expireStaleCart(), retireCart() (clear items + abandon, works on ACTIVE or PARKED)

API Endpoints (M2)

All under @Path("/blm"):

Endpoint Method Returns
cart/create POST CBookingCart
cart/read POST {cart, items}
cart/active POST {cart, items} or null
cart/forCustomer POST CBookingCart or null
cart/listActive POST List (admin only)
cart/assignCustomer POST CBookingCart
cart/addItem POST CBookingCartItem
cart/updateItem POST CBookingCartItem
cart/removeItem POST success
cart/clear POST success
cart/checkout POST CBooking

Under @Path("/customer"):

Endpoint Method Returns
customer/quickSearch POST List\<CCustomer> (deduplicated, max 15)

Agent Identity

Cart ownership uses the stable OIDC user ID (UUID from RequestContext.getUserId()), not the display name or email which can vary between requests. The display name (resolveAgentDisplayName()) is used for booking's assignedAgentId field.

Frontend Architecture (M2)

File Purpose
booking-cart.html + booking-cart.js Cart page: customer autocomplete, items table with inline edit, manual item entry, checkout
booking-common.js Cart API methods, updateCartBadge(), ensureCart()
hotel-search.html + hotel-search.js Online hotel search (GoGlobal supplier) with cart integration
hotel-list.js Contracted hotel search — [Add to Cart] on room cards
flight-search.js Flight search — round-trip as single cart item
activities-search.js Activity search — [Add to Cart] on experience cards and product modal
header_bootstrap.html Cart icon with badge, navbar reorganization
booking-list.js Customer column + customer search filter

customer/quickSearch endpoint performs 3 parallel Odoo searches (by custName, email, phone with ilike), merges and deduplicates by customerId, caps at 15. Used via setupAutocomplete() from tripmaker-common.js on all customer search pages.


10. Actions Framework (M5)

The Actions Framework provides template-driven operational task management for bookings. When a booking is confirmed, action items are automatically generated from configurable templates, giving agents a structured checklist of tasks (supplier confirmations, payment processing, document collection) required to fulfil each service line.

10.1 Database Schema

Three tables defined in config/db-changes/0050-booking-actions.sql:

actiontemplateset

Column Type Constraints Description
templatesetid SERIAL PRIMARY KEY Auto-generated ID
name VARCHAR(100) NOT NULL Template set name
servicetype VARCHAR(30) NOT NULL Service type (HOTEL, FLIGHT, TRANSFER, ACTIVITY, etc.)
description VARCHAR(500) Human-readable description
isactive BOOLEAN DEFAULT TRUE Active flag (soft-delete sets isactive=false)
sortorder INTEGER DEFAULT 0 Display ordering

Indexes: servicetype

actiontemplate

Column Type Constraints Description
actiontemplateid SERIAL PRIMARY KEY Auto-generated ID
templatesetid INTEGER NOT NULL, FK to actiontemplateset Parent template set
sequence INTEGER NOT NULL Execution order within the set
actioncode VARCHAR(50) NOT NULL Machine-readable action identifier
description VARCHAR(500) NOT NULL Human-readable description
isrequired BOOLEAN DEFAULT TRUE Required for voucher readiness
isconditional BOOLEAN DEFAULT FALSE Only create if condition field is non-empty
conditionfield VARCHAR(50) Service line field to evaluate for conditional actions
deadlinedays INTEGER DEFAULT 0 Days offset from reference date (negative = before)
deadlinerelativeto VARCHAR(30) DEFAULT 'TRAVEL_START' Reference: TRAVEL_START, SERVICE_DATE, BOOKING_CONFIRMED
defaultassigneerole VARCHAR(30) BOOKING_AGENT, FINANCE, OPERATIONS, or direct userId
resultfields TEXT JSON array of field definitions for completion data
dependsonsequence INTEGER Sequence number of the dependency template
notes TEXT Admin notes

Indexes: templatesetid

actionitem

Column Type Constraints Description
actionid SERIAL PRIMARY KEY Auto-generated ID
bookingid INTEGER NOT NULL, FK to booking Parent booking
servicelineid INTEGER FK to serviceline Associated service line
actiontemplateid INTEGER FK to actiontemplate Source template (null for custom actions)
sequence INTEGER DEFAULT 0 Execution order
actioncode VARCHAR(50) Machine-readable action identifier
description VARCHAR(500) Human-readable description
status VARCHAR(30) DEFAULT 'PENDING' PENDING, IN_PROGRESS, AWAITING_SUPPLIER, COMPLETED, FAILED, NOT_REQUIRED
assignedto VARCHAR(100) Assigned user ID
assignedtoname VARCHAR(200) Assigned user display name
duedate TIMESTAMP Calculated deadline
completedat TIMESTAMP Completion timestamp
completedby VARCHAR(100) User who completed the action
resultdata TEXT JSON structured result data
notes TEXT Action notes
isrequired BOOLEAN DEFAULT TRUE Required for voucher readiness
iscustom BOOLEAN DEFAULT FALSE True for manually created actions
dependsonactionid INTEGER FK to actionitem Dependency on another action item
createdat TIMESTAMP NOT NULL, DEFAULT NOW() Creation timestamp
sortorder INTEGER DEFAULT 0 Display ordering

Indexes: bookingid, servicelineid, status, assignedto, duedate

Seed data: The migration script includes default template sets for HOTEL (8 templates), FLIGHT (4 templates), TRANSFER (4 templates), and ACTIVITY (4 templates) service types.

10.2 Entity Classes

Canonical Entities (tqapp)

All in tqapp/src/main/java/com/perun/tlinq/entity/booking/:

Class Extends ID Field Fields
CActionTemplateSet TlinqEntity templateSetId 6 fields: templateSetId, name, serviceType, description, isActive, sortOrder
CActionTemplate TlinqEntity actionTemplateId 14 fields: actionTemplateId, templateSetId, sequence, actionCode, description, isRequired, isConditional, conditionField, deadlineDays, deadlineRelativeTo, defaultAssigneeRole, resultFields, dependsOnSequence, notes
CActionItem TlinqEntity actionId 20 fields: actionId, bookingId, serviceLineId, actionTemplateId, sequence, actionCode, description, status, assignedTo, assignedToName, dueDate, completedAt, completedBy, resultData, notes, isRequired, isCustom, dependsOnActionId, createdAt, sortOrder

Native JPA Entities (tqapp - NTS plugin)

All in tqapp/src/main/java/com/perun/tlinq/client/nts/db/booking/:

Class Table ID Field
ActiontemplatesetEntity nts.actiontemplateset id (maps to templatesetid)
ActiontemplateEntity nts.actiontemplate id (maps to actiontemplateid)
ActionitemEntity nts.actionitem id (maps to actionid)

10.3 Entity Configuration

Entity-to-native field mappings are in config/entities/booking-entities.xml under the <!-- Actions Framework (M5) --> section. All 3 entities use DirectMapping for all fields with NTSServiceFactory as the default factory.

10.4 Service Configuration

9 service entries added to config/nts-client.xml:

Service Name Class Entity
saveActionTemplateSet / readActionTemplateSet / deleteActionTemplateSet NTSEntityWriteService / NTSEntityReadService ActiontemplatesetEntity
saveActionTemplate / readActionTemplate / deleteActionTemplate NTSEntityWriteService / NTSEntityReadService ActiontemplateEntity
saveActionItem / readActionItem / deleteActionItem NTSEntityWriteService / NTSEntityReadService ActionitemEntity

10.5 Facade Methods

All methods are in BookingLifecycleFacade.java.

Template Management

Method Description
listTemplateSets(serviceType) Lists active template sets, optionally filtered by service type, sorted by sortOrder
getTemplateSet(templateSetId) Reads a single template set by ID
saveTemplateSet(CActionTemplateSet) Creates or updates a template set (validates name is non-empty)
deleteTemplateSet(templateSetId) Soft-deletes by setting isActive=false
getTemplates(templateSetId) Lists templates for a set, sorted by sequence
saveTemplate(CActionTemplate) Creates or updates a template (validates templateSetId, sequence, actionCode, description; validates resultFields as JSON array)
deleteTemplate(actionTemplateId) Hard-deletes a template (blocked if action items reference it)
findTemplateSet(serviceType) Finds the first active template set for a service type

Action Generation

Method Description
generateActionsForBooking(bookingId) Generates actions for all non-cancelled, non-service-charge service lines in a booking
generateActionsForServiceLine(bookingId, CServiceLine) Generates actions from the matching template set for a single service line
createCustomAction(bookingId, serviceLineId, description, assignedTo, dueDate, isRequired) Creates an ad-hoc action item (actionCode=CUSTOM, isCustom=true)

Helper methods: - resolveDeadlineDate(line, booking, deadlineRelativeTo, deadlineDays) -- Calculates due date from reference date + offset - resolveAssignee(bookingId, role) -- Maps role to user (BOOKING_AGENT -> booking's agent; FINANCE/OPERATIONS -> null for team pool) - resolveAssigneeName(assignedTo) -- Resolves display name from request context - isConditionEmpty(line, conditionField) -- Checks if a service line field is empty via reflection

Action Operations

Method Description
updateActionItem(CActionItem) Updates allowed fields (status, assignedTo, assignedToName, dueDate, notes); rejects updates to terminal actions
completeActionItem(actionId, completedBy, resultDataJson, notes) Completes an action: checks dependency status, validates result data against template fields, sets COMPLETED status
failActionItem(actionId, notes) Sets action to FAILED status
markNotRequired(actionId, notes) Sets action to NOT_REQUIRED status
reassignAction(actionId, newAssignee, newAssigneeName) Reassigns action to a different user
getActionItems(bookingId, status) Lists actions for a booking, optionally filtered by status, sorted by serviceLineId then sequence
getActionItemsByServiceLine(serviceLineId) Lists actions for a specific service line, sorted by sequence
getMyActionItems(agentId, status) Lists actions assigned to a specific user, sorted by dueDate
getOverdueActions() Returns all PENDING, IN_PROGRESS, and AWAITING_SUPPLIER actions with dueDate in the past
isVoucherReady(bookingId) Returns true if all required actions are COMPLETED or NOT_REQUIRED

Integration Hooks

  • changeBookingStatus(): When status transitions to CONFIRMED, calls generateActionsForBooking(). Failures are logged as warnings but do not block the status change.
  • addServiceLine(): When a service line is added to a booking in CONFIRMED or AMENDED status, calls generateActionsForServiceLine() for the new line. Failures are logged as warnings.

10.6 API Endpoints

All endpoints are in BookingLifecycleApi.java under the /blm/ path prefix. All use POST method with JSON request/response.

Template Management (admin only)

# Path Required Params Optional Params Facade Method
1 blm/actiontemplate/sets session serviceType listTemplateSets(serviceType)
2 blm/actiontemplate/set/read session, templateSetId getTemplateSet() + getTemplates()
3 blm/actiontemplate/set/save session, name, serviceType templateSetId, description, isActive, sortOrder saveTemplateSet()
4 blm/actiontemplate/set/delete session, templateSetId deleteTemplateSet()
5 blm/actiontemplate/save session, templateSetId, sequence, actionCode, description actionTemplateId, isRequired, isConditional, conditionField, deadlineDays, deadlineRelativeTo, defaultAssigneeRole, resultFields, dependsOnSequence, notes saveTemplate()
6 blm/actiontemplate/delete session, actionTemplateId deleteTemplate()

Action Operations (agent + admin)

# Path Required Params Optional Params Facade Method
7 blm/action/create session, bookingId, description serviceLineId, assignedTo, dueDate, isRequired createCustomAction()
8 blm/action/update session, actionId status, assignedTo, dueDate, notes updateActionItem()
9 blm/action/complete session, actionId resultData, notes completeActionItem()
10 blm/action/fail session, actionId notes failActionItem()
11 blm/action/markNotRequired session, actionId notes markNotRequired()
12 blm/action/reassign session, actionId, assignedTo assignedToName reassignAction()
13 blm/action/list session bookingId, serviceLineId, status getActionItems() or getActionItemsByServiceLine()
14 blm/action/myItems session status getMyActionItems()
15 blm/action/overdue session getOverdueActions()
16 blm/action/voucherReady session, bookingId isVoucherReady()

10.7 Frontend

File Purpose
tqweb-adm/booking-detail.html Actions tab added to booking detail page
tqweb-adm/booking-detail.js Actions tab logic: load, display, complete, fail, reassign actions per service line
tqweb-adm/action-templates.html Admin page for managing action template sets and templates
tqweb-adm/js/modules/action-templates.js Template management: guided wizard for creating/editing template sets, template CRUD with inline editing
tqweb-adm/js/modules/booking-common.js BookingAPI methods for action endpoints
tqweb-adm/header_bootstrap.html Navigation link to action-templates admin page

10.8 Configuration

api-roles.properties

# Action Templates -- admin only (M5)
blm/actiontemplate/sets=admin
blm/actiontemplate/set/read=admin
blm/actiontemplate/set/save=admin
blm/actiontemplate/set/delete=admin
blm/actiontemplate/save=admin
blm/actiontemplate/delete=admin

# Action Items -- agent + admin (M5)
blm/action/create=agent,admin
blm/action/update=agent,admin
blm/action/complete=agent,admin
blm/action/fail=agent,admin
blm/action/markNotRequired=agent,admin
blm/action/reassign=agent,admin
blm/action/list=agent,admin
blm/action/myItems=agent,admin
blm/action/overdue=agent,admin
blm/action/voucherReady=agent,admin

Action Status Constants

Defined in BookingLifecycleFacade:

Constant Value Terminal
ACTION_PENDING PENDING No
ACTION_IN_PROGRESS IN_PROGRESS No
ACTION_AWAITING_SUPPLIER AWAITING_SUPPLIER No
ACTION_COMPLETED COMPLETED Yes
ACTION_FAILED FAILED Yes
ACTION_NOT_REQUIRED NOT_REQUIRED Yes

Result Fields Schema

The resultFields column on actiontemplate stores a JSON array defining structured data fields:

[
    {
        "name": "supplierReference",
        "label": "Confirmation Reference",
        "type": "text",
        "required": true,
        "showOnVoucher": true,
        "voucherLabel": "Confirmation No.",
        "voucherOrder": 1
    }
]

Field types: text, number, date. The showOnVoucher/voucherLabel/voucherOrder properties are used by the voucher generation system (M6) to extract action results onto customer vouchers.


11. Vouchers + Document Management (M6)

11.1 Architecture

M6 adds two concerns to the BLM module:

  1. Voucher generation — A pipeline that collects data from the booking, service lines, completed actions, and T&C templates, renders them into an HTML template, converts to PDF via OpenHTMLtoPDF, optionally merges e-ticket PDFs via PDFBox, and uploads the result to S3.
  2. Document storage — A generic S3-backed file store for booking-related documents (vouchers, e-tickets, confirmations, invoices, other).
BookingLifecycleApi (9 new endpoints)
    |
BookingLifecycleFacade
    |-- VoucherRenderer        (HTML → PDF)
    |-- PdfIngestionService    (PDF merge, reused from offline-tickets)
    |-- BookingDocumentStorageService  (S3 ops)
    |-- resolveBookingTerms()  (from M4)
    |-- getActionItems()       (from M5)

11.2 New Entities

VoucherSectionTemplate

Defines which fields appear in a voucher section for a given service type.

Field Type Description
voucherSectionTemplateId Integer Primary key (sequence)
serviceType String(30) HOTEL, FLIGHT, TRANSFER, ACTIVITY, CRUISE, VISA (unique)
sectionTitle String(200) Display title for the section
fieldsDefinition Text (JSON) Array of field definitions (see schema below)
headerTemplate Text (HTML) Section header HTML with %DESCRIPTION% and %ICON% placeholders
isActive Boolean Soft-delete flag

Field Definition Schema (each element in fieldsDefinition JSON array):

{
    "label": "Hotel Name",
    "source": "serviceline | attribute | action | attachment",
    "field": "description",
    "path": "checkIn",
    "actionCode": "HOTEL_CONF_NUMBER",
    "showEmpty": false
}
  • source=serviceline: resolves field via CServiceLine getter (description, supplierRef, supplierName, notes, specialRequests, startDate, endDate, serviceType, currency)
  • source=attribute: resolves path from service line's attributes JSON
  • source=action: finds completed action by actionCode, extracts field from its resultData JSON
  • source=attachment: finds action by actionCode, extracts fileName from resultData

BookingDocument

An S3-backed file associated with a booking.

Field Type Description
documentId Integer Primary key (sequence)
bookingId Integer FK to booking
serviceLineId Integer FK to serviceline (nullable)
docType String(30) VOUCHER, VOUCHER_ARCHIVE, ETICKET, CONFIRMATION, INVOICE, OTHER
fileName String(500) Original file name
s3Key String(500) S3 object key
contentType String(100) MIME type
fileSize Long Size in bytes
uploadedBy String(100) Agent who uploaded
uploadedAt Timestamp Upload time
notes Text Optional notes
version Integer Version counter (for voucher re-generation)

11.3 Backend Services

BookingDocumentStorageService

tqapp/.../client/nts/service/media/BookingDocumentStorageService.java

Follows the VisaDocumentStorageService pattern. S3 key format: {prefix}bookings/{bookingId}/documents/{randomSuffix}.{ext}.

Method Description
uploadDocument(bookingId, filename, bytes) MIME validation via Tika, size check, KMS-encrypted upload
uploadPdfBytes(bookingId, filename, pdfBytes) Convenience for generated PDFs (skips MIME detection)
downloadDocument(s3Key) Returns byte array
generatePresignedUrl(s3Key) Time-limited download URL
deleteDocument(s3Key) Removes S3 object
copyObject(sourceKey, destKey) Copies for voucher version archiving

VoucherRenderer (File-Based Templates)

tqapp/.../entity/booking/VoucherRenderer.java

Static utility class that renders a booking voucher as a PDF. All HTML comes from external template files — zero hardcoded HTML in the renderer.

Method Description
render(...) Main entry point; loads templates, resolves placeholders, returns PDF byte array
loadSectionTemplate(serviceType, default) Tries sections/section-{TYPE}.html, falls back to default
resolveFieldValue(fieldDef, line, actions) Resolves field from 4 sources (serviceline, attribute, action, attachment)

Template files (all in config/templates/booking/):

File Purpose
voucher-master.html Page shell with %SECTIONS%, %PASSENGERS%, %TERMS%
voucher-section.html Default section layout
voucher-passengers.html Passengers table with %PASSENGER_ROWS%
voucher-terms.html Terms wrapper with %TERMS_BLOCKS%
sections/section-{TYPE}.html Per-service-type override (optional, e.g., section-HOTEL.html)

Rendering pipeline:

  1. Load voucher-master.html and voucher-section.html (default section template)
  2. Replace company placeholders (%COMPANY_NAME%, %COMPANY_LOGO%, etc.) from AppConfig
  3. Replace booking placeholders (%BOOKING_REF%, %CLIENT_NAME%, %TRAVEL_DATES%)
  4. Load voucher-passengers.html, build %PASSENGER_ROWS%, replace into %PASSENGERS%
  5. For each service line: a. Load sections/section-{TYPE}.html (falls back to default) b. Resolve field values from fieldsDefinition JSON via resolveFieldValue() c. Build %FIELD_ROWS% (label/value table rows) d. Replace structural placeholders: %SECTION_TITLE%, %DESCRIPTION%, %SERVICE_DATES%, etc. e. Replace %LabelName% placeholders with resolved field values (for custom field placement) f. Strip unresolved placeholders
  6. Load voucher-terms.html, build %TERMS_BLOCKS%, replace into %TERMS%
  7. Replace footer placeholders (%GENERATED_DATE%, %GENERATED_BY%)
  8. Convert HTML to PDF via PdfRendererBuilder (OpenHTMLtoPDF)

Serviceline field resolution — 21 fields supported:

description, serviceType, supplierRef, supplierName, supplierId, notes, specialRequests, startDate, endDate, currency, cost, costLocal, sellPrice, quantity, confirmationStatus, sortOrder, erpProductId, erpVariantId, passengerIds, cancellationTerms, isServiceCharge

11.4 Facade Methods

Added to BookingLifecycleFacade:

Voucher Template CRUD:

Method Description
listVoucherSectionTemplates() Returns all active templates
getVoucherSectionTemplateByType(serviceType) Returns template for a service type
saveVoucherSectionTemplate(template) Creates or updates a template

Document Management:

Method Description
uploadDocument(bookingId, serviceLineId, docType, fileName, fileBytes, uploadedBy, notes) Validates, uploads to S3, creates entity, logs activity
readDocument(documentId) Returns CBookingDocument entity
downloadDocumentBytes(documentId) Downloads bytes from S3
listDocuments(bookingId) Returns documents sorted by uploadedAt (descending)
deleteDocument(documentId, deletedBy) Deletes from S3 and database, logs activity
emailDocument(documentId, recipientEmail, subject, body, senderAgent) Downloads to temp file, sends via MailUtil, logs activity

Voucher Generation:

Method Description
generateVoucher(bookingId, agent) Full pipeline (see below)

generateVoucher steps:

  1. Verify isVoucherReady(bookingId) — reject if pending required actions
  2. Load booking, active service lines (non-cancelled, non-service-charge), passengers, completed actions
  3. Load matching voucher section templates per service type present
  4. Call resolveBookingTerms(bookingId) for T&C
  5. Call VoucherRenderer.render() to produce voucher PDF bytes
  6. Scan completed actions for ETICKET action codes with documentId in result data; download and merge via PdfIngestionService.mergePdfs()
  7. Archive previous VOUCHER documents (copy S3 object with -v{N} suffix, update record to VOUCHER_ARCHIVE)
  8. Upload final PDF via BookingDocumentStorageService.uploadPdfBytes()
  9. Create CBookingDocument entity (docType=VOUCHER)
  10. Log VOUCHER_GENERATED activity

11.5 API Endpoints

Endpoint Method Description
/blm/vouchertemplate/list POST List active voucher section templates
/blm/vouchertemplate/read POST Read template by serviceType
/blm/vouchertemplate/save POST Create/update template (params: voucherSectionTemplateId, serviceType, sectionTitle, fieldsDefinition, headerTemplate, isActive)
/blm/booking/generateVoucher POST Generate voucher (params: bookingId). Returns CBookingDocument
/blm/document/upload POST Upload document (params: bookingId, serviceLineId, docType, fileName, fileData [base64], notes)
/blm/document/download POST Download document (params: documentId). Returns binary application/octet-stream with Content-Disposition header
/blm/document/list POST List documents (params: bookingId). Returns List<CBookingDocument>
/blm/document/delete POST Delete document (params: documentId)
/blm/document/email POST Email document (params: documentId, recipientEmail, subject, body)

11.6 Frontend

Documents Tab (booking-detail.html / booking-detail.js)

  • Toolbar: Generate Voucher button (disabled until isVoucherReady returns true), Upload Document button
  • Document table: icon by docType, file name, type badge, size, uploaded by, date, action buttons (download, email, delete)
  • Upload modal: file input (PDF/JPEG/PNG), docType select, optional service line, notes
  • Email modal: recipient (pre-filled from booking's customerEmail), subject, body
  • Download: uses requestBlob() to fetch binary and trigger browser save

Voucher Template Editor (action-templates.html / action-templates.js)

  • Table listing all voucher section templates with service type, title, field count, active status
  • Edit modal: service type (read-only), section title, header template (HTML textarea), structured field rows (label, source dropdown, field/path, actionCode), active toggle

11.7 Files

File Purpose
config/db-changes/0051-booking-vouchers.sql Migration (2 tables, 6 seed templates)
config/templates/booking/voucher-master.html A4 voucher PDF template
config/entities/booking-entities.xml VoucherSectionTemplate + BookingDocument entity definitions
config/nts-client.xml 6 NTS service definitions
config/api-roles.properties 9 endpoint permissions
tqapp/.../db/booking/VouchersectiontemplateEntity.java JPA entity
tqapp/.../db/booking/BookingdocumentEntity.java JPA entity
tqapp/.../entity/booking/CVoucherSectionTemplate.java Canonical entity
tqapp/.../entity/booking/CBookingDocument.java Canonical entity
tqapp/.../service/media/BookingDocumentStorageService.java S3 storage service
tqapp/.../entity/booking/VoucherRenderer.java HTML-to-PDF renderer
tqapp/.../entity/booking/BookingLifecycleFacade.java Facade methods (M6 additions)
tqapi/.../api/BookingLifecycleApi.java API endpoints (M6 additions)
tqweb-adm/booking-detail.html Documents tab HTML
tqweb-adm/js/modules/booking-detail.js Documents tab JS
tqweb-adm/js/modules/booking-common.js API methods + requestBlob
tqweb-adm/action-templates.html Voucher template editor HTML
tqweb-adm/js/modules/action-templates.js Voucher template editor JS

11.8 Configuration

api-roles.properties

# Voucher Templates -- admin only (M6)
blm/vouchertemplate/list=admin
blm/vouchertemplate/read=admin
blm/vouchertemplate/save=admin

# Voucher Generation (M6)
blm/booking/generateVoucher=agent,admin

# Document Management (M6)
blm/document/upload=agent,admin
blm/document/download=agent,admin
blm/document/list=agent,admin
blm/document/delete=agent,admin
blm/document/email=agent,admin

Document Type Constants

Value Usage
VOUCHER Generated travel voucher PDF
VOUCHER_ARCHIVE Previous voucher version (after re-generation)
ETICKET Airline e-ticket PDF
CONFIRMATION Supplier confirmation document
INVOICE Invoice document
OTHER Any other document

12. Booking Terms Tab (M6.5)

12.1 Architecture

M6.5 adds a per-booking terms management layer between the T&C templates (M4) and voucher generation (M6). Instead of dynamically resolving all applicable terms at voucher time, agents can now curate which terms appear, add custom entries, and reorder them.

T&C Templates (M4, database)
    |
    v
initializeBookingTerms() — seeds BookingTermsItem records on first tab open
    |
    v
BookingTermsItem table — per-booking list with include/exclude flags
    |
    v
generateVoucher() — reads only included items (fallback to dynamic if none saved)
    |
    v
VoucherRenderer — file-based section templates (config/templates/booking/)

12.2 New Entity: BookingTermsItem

Field Type Description
bookingTermsItemId Integer Primary key (sequence)
bookingId Integer FK to booking
termsTemplateId Integer FK to termstemplate (null for custom items)
title String(200) Display title
contentHtml Text Resolved HTML content
termsType String(20) SERVICE, CANCELLATION, GLOBAL, or CUSTOM
serviceType String(30) HOTEL, FLIGHT, etc. (null for GLOBAL/CUSTOM)
sortOrder Integer Display order (increments of 10)
isIncluded Boolean Include in voucher (default true)
isCustom Boolean Agent-entered free-text (default false)

12.3 Facade Methods

Method Description
getBookingTermsItems(bookingId) List items sorted by sortOrder
initializeBookingTerms(bookingId) Idempotent seed from resolveBookingTerms()
saveBookingTermsItems(bookingId, items) Batch update include flags and sort order
addCustomBookingTerm(bookingId, title, contentHtml) Add free-text item (max 30 custom)
refreshBookingTermsFromTemplates(bookingId) Add new template items without disturbing existing
deleteBookingTermsItem(id) Delete custom items only (template-based items rejected)

12.4 Voucher Generation Integration

In generateVoucher(), the terms resolution was changed from:

List<ResolvedTerms> terms = resolveBookingTerms(bookingId);
To:
List<CBookingTermsItem> savedTerms = getBookingTermsItems(bookingId);
if (savedTerms != null && !savedTerms.isEmpty()) {
    terms = savedTerms.stream()
        .filter(t -> Boolean.TRUE.equals(t.getIsIncluded()))
        .map(t -> new ResolvedTerms(t.getTitle(), t.getContentHtml(), ...))
        .collect(Collectors.toList());
} else {
    terms = resolveBookingTerms(bookingId);  // backwards-compatible fallback
}

12.5 API Endpoints

Endpoint Method Description
/blm/terms/booking/list POST List terms items for a booking
/blm/terms/booking/initialize POST Seed items from templates (idempotent)
/blm/terms/booking/save POST Batch update (items JSON array with include/sortOrder)
/blm/terms/booking/addCustom POST Add custom term (title, contentHtml)
/blm/terms/booking/refresh POST Merge new template items
/blm/terms/booking/deleteCustom POST Delete custom item by bookingTermsItemId

12.6 Frontend

The Terms tab is positioned between Passengers and Amendments. It lazy-loads via shown.bs.tab event.

  • Auto-initialization: On first open, if no items exist, calls initialize to seed from templates.
  • Table: Include checkbox, title, type badge (SERVICE=blue, CANCELLATION=amber, GLOBAL=green, CUSTOM=cyan), service type, actions (delete for custom only).
  • Reorder: Up/down arrow buttons swap array positions locally; Save persists new sortOrder values.
  • Add Custom Term: Modal with title + HTML textarea. Max 30 custom items enforced.
  • Refresh from Templates: Adds terms for new service types without disturbing existing items or custom entries.

12.7 Files

File Purpose
config/db-changes/0052-booking-terms-items.sql Migration
tqapp/.../db/booking/BookingtermsitemEntity.java JPA entity
tqapp/.../entity/booking/CBookingTermsItem.java Canonical entity
tqapp/.../entity/booking/BookingLifecycleFacade.java 6 methods + generateVoucher modification
tqapi/.../api/BookingLifecycleApi.java 6 endpoints
tqweb-adm/booking-detail.html Terms tab + custom term modal
tqweb-adm/js/modules/booking-detail.js Terms tab JS
tqweb-adm/js/modules/booking-common.js 6 API methods

13. TripMaker Integration (M7)

13.1 Architecture

M7 follows a source-agnostic design: each external module builds a BookingCreationRequest DTO and calls BookingServiceI.createBooking(). The booking module never imports TripMaker or Group classes.

Integration contract: - BookingServiceI interface (single method: createBooking(BookingCreationRequest)) - BookingCreationRequest DTO with customer info, travel dates, source tracking, service lines, and passengers - ServiceLineRequest and PassengerRequest DTOs for child data

13.2 TripMakerFacade.convertToBooking()

File: tqapp/src/main/java/com/perun/tlinq/entity/tripmaker/TripMakerFacade.java

Signature: public CBooking convertToBooking(Integer projectId, Integer itineraryId)

Process: 1. Validates project status is QUOTED and itinerary belongs to project 2. Loads cost breakdown via getItineraryCostBreakdown(itineraryId) -- includes margin overrides 3. Builds BookingCreationRequest: - Customer name from first TravelerDetail.name - Travel dates from datePeriodStart / datePeriodEnd - Currency from ExchangeRateHelper.getLocalCurrency() - sourceType = "TRIPMAKER", sourceRef = projectId - assignedAgentId = project.getAgentId() 4. Maps cost breakdown components to service lines: - ACCOMMODATION -> HOTEL, FLIGHT -> FLIGHT, ACTIVITY -> ACTIVITY, OTHER_SERVICE -> OTHER - cost = baseCost (supplier cost), sellPrice = finalPrice (with margin applied) - Loads source entities for dates and attributes (check-in/out, flight times, etc.) 5. Creates lead passenger from first traveler detail (name split into first/last) 6. Delegates to BookingServiceI.createBooking(request) -> booking created in ENQUIRY status 7. Sets project status to CLOSED

13.3 API Endpoint

Method Path Parameters Response
POST /tripmaker/project/convertToBooking projectId (Integer, required), itineraryId (Integer, required) CBooking

Roles: agent, admin

13.4 Frontend

  • File: tqweb-adm/js/modules/tripmaker-costs.js
  • "Convert to Booking" button visible when project.status === 'QUOTED'
  • Uses TQ.confirm() for confirmation dialog
  • Uses TQ.loading.button() for loading state
  • On success: redirects to booking-detail.html?id={bookingId} after 800ms delay

14. Inbound Group Integration (M7)

14.1 GroupManagerFacade.createBookingFromInboundGroup()

File: tqapp/src/main/java/com/perun/tlinq/entity/group/GroupManagerFacade.java

Signature: public CBooking createBookingFromInboundGroup(Integer groupId)

Process: 1. Validates group exists and groupType = "INBOUND" 2. Checks for existing booking (sourceType=GROUP_INBOUND, sourceRef=groupId): - No existing booking -> creates new (step 3) - Existing in ENQUIRY/QUOTED -> updates in-place (step 4) - Existing CONFIRMED or beyond -> rejects with error 3. Create path: Builds BookingCreationRequest with partner agency as customer (partnerName, partnerId), resolves agency email/phone from ERP via CustomerFacade. Loads all group items as service lines, passengers as booking passengers. Delegates to BookingServiceI.createBooking(). 4. Update path: Updates booking header (customer, dates, currency, agent). Removes all existing service lines and passengers, then re-adds from current group state.

Service line mapping: - Hotels: One service line per hotel. Room costs (roomCost/roomPrice) summed across all rooms for that hotel. Room count as quantity. - Transports: One service line per transport. Uses cost/price directly. Type = TRANSFER. - Services/Activities: One per service. Cost = adultCost * numAdults + childCost * numChildren. Sell = adultPrice * numAdults + childPrice * numChildren. Type = ACTIVITY.

Passenger mapping: paxFirstName -> firstName, paxLastName -> lastName, birthDate -> dateOfBirth, passportNum -> passportNumber, passportExpiryDate -> passportExpiry, contactTel -> phone, contactEmail -> email, nationalityCode -> nationality. First passenger marked as lead.

14.2 API Endpoint

Method Path Parameters Response
POST /groups/group/createBooking groupId (Integer, required) CBooking

Roles: agent, admin

14.3 Frontend

  • File: tqweb-adm/js/modules/groups-summary.js
  • "Create Booking" button visible when currentGroup.groupType === 'INBOUND'
  • Uses TQ.confirm() with message covering both create and update scenarios
  • Uses TQ.loading.button() for loading state
  • On success: redirects to booking-detail.html?id={bookingId} after 800ms delay

14.4 Source Type Display

Booking list page shows source type badge below booking reference: - SOURCE_TYPES map in booking-common.js defines icon and label for each source - getSourceTypeInfo(sourceType) helper returns display info - Rendered as: <icon> Label #sourceRef (e.g., "TripMaker #15", "Inbound Group #7")

14.5 Configuration

api-roles.properties additions:

tripmaker/project/convertToBooking=agent,admin
groups/group/createBooking=agent,admin

No database migration required -- M7 uses existing M1 tables.


15. Outbound Group Sales (M8)

15.1 Architecture

M8 adds a group sales layer on top of the existing BLM booking infrastructure. Group pricing, variations, and sales are managed through a dedicated GroupSalesFacade, while sale confirmation delegates to BookingServiceI to create standard BLM bookings.

GroupApi.java (9 endpoints under /groups/)
BookingLifecycleApi.java (13 endpoints under /blm/group/)
    |
GroupSalesFacade.java
    |-- Cost aggregation (hotels, activities, transports, flights, expenses)
    |-- Price calculation engine (variations: FIXED/PERCENT, automatic/optional)
    |-- Sale lifecycle (DRAFT -> CONFIRMED -> CANCELLED)
    |-- Occupancy tracking
    |
    v
BookingServiceI.createBooking() -- standard booking creation
    |-- ERP customer sync, CRM lead
    |-- Service lines (GROUP_TRAVEL + HOTEL per room)
    |-- Passengers from group sale

15.2 New Entities

Canonical Entities (tqapp)

All in tqapp/src/main/java/com/perun/tlinq/entity/booking/:

Class Extends ID Field Fields
CGroupPricing TlinqEntity groupPricingId 11 fields: groupPricingId, groupId, assumedPaxCount, totalGroupCost, baseCostPerPax, marginPercent, baseSellingPrice, currency, maxPaxCount, notes, updatedAt
CGroupPricingVariation TlinqEntity variationId 11 fields: variationId, groupPricingId, variationCode, description, adjustmentType, adjustmentValue, appliesTo, isOptional, isAutomatic, paxType, sortOrder
CGroupSale TlinqEntity groupSaleId 18 fields: groupSaleId, groupId, groupPricingId, customerId, erpCustomerId, bookingId, paxCount, adultCount, childCount, infantCount, basePriceTotal, adjustmentsTotal, finalPrice, overridePrice, overrideReason, status, createdAt, createdBy
CGroupSaleVariation TlinqEntity id 5 fields: id, groupSaleId, variationId, paxCount, adjustmentAmount
CGroupSalePax TlinqEntity groupSalePaxId 6 fields: groupSalePaxId, groupSaleId, paxId, paxType, individualPrice, roomId, bookingPassengerId

All in tqapp/src/main/java/com/perun/tlinq/entity/group/:

Class Extends ID Field Fields
CTripFlightBlock TlinqEntity flightBlockId blockSize, costPerTicket, pricePerTicket, minCommitment, travelClass, status, groupId, notes
CTripFlightLeg TlinqEntity flightLegId flightBlockId, direction (OUTBOUND/RETURN), departureAirport, arrivalAirport, departureTime, arrivalTime, airline, flightNumber
CTripExpense TlinqEntity expenseId groupId, amount, currencyCode, category (VISA/INSURANCE/FEE/OTHER), description, notes

DTO: GroupOccupancy

public record GroupOccupancy(
    int assumedPax,
    int soldPax,
    int remainingPax,
    double fillPercentage,
    boolean isOverbooked,
    boolean atHardLimit,
    Integer maxPaxCount,
    List<CGroupSale> sales
) {}

Native JPA Entities

All in tqapp/src/main/java/com/perun/tlinq/client/nts/db/booking/:

Class Table Sequence
GrouppricingEntity nts.grouppricing nts.grouppricing_seq
GrouppricingvariationEntity nts.grouppricingvariation nts.grouppricingvariation_seq
GroupsaleEntity nts.groupsale nts.groupsale_seq
GroupsalevariationEntity nts.groupsalevariation nts.groupsalevariation_seq
GroupsalepaxEntity nts.groupsalepax nts.groupsalepax_seq
TripflightblockEntity nts.tripflightblock nts.tripflightblock_seq
TripflightlegEntity nts.tripflightleg nts.tripflightleg_seq
TripexpenseEntity nts.tripexpense nts.tripexpense_seq

15.3 Database Schema

Four migration scripts:

Script Tables Description
0048-outbound-groups.sql grouppricing, grouppricingvariation, groupsale, groupsalevariation Core group sales tables
0053-group-sale-pax.sql groupsalepax Sale passenger assignments
0054-trip-flight-blocks.sql tripflightblock, tripflightleg Flight block inventory
0055-trip-expenses.sql tripexpense General expenses

Additional schema changes: - bookingpassenger table: added grouppaxid column for bidirectional sync with CGroupSalePax

15.4 Facade: GroupSalesFacade

File: tqapp/src/main/java/com/perun/tlinq/entity/booking/GroupSalesFacade.java

Pricing Methods

Method Description
getOrCreatePricing(Integer groupId) Loads existing pricing or creates default with cost aggregation from all group services
recalculateBasePricing(Integer groupId) Re-aggregates totalGroupCost from 5 sources (hotels, activities, transports, flight blocks, expenses) with currency conversion, recalculates baseCostPerPax and baseSellingPrice
updatePricingFields(CGroupPricing) Updates agent-editable fields (assumedPaxCount, marginPercent, maxPaxCount, notes), recalculates derived fields

Cost Aggregation

Costs are aggregated from 5 sources with currency conversion to the group pricing currency:

  1. Hotels: room-level costs (roomCost multiplied by number of nights per room)
  2. Activities: per-activity total cost
  3. Transports: per-transport cost
  4. Flight blocks: blockSize multiplied by costPerTicket per block
  5. Expenses: expense amount per expense record

All costs are converted from their source currency to the group pricing currency using ExchangeRateHelper.

Variation Methods

Method Description
addVariation(CGroupPricingVariation) Validates adjustmentType (FIXED/PERCENT), writes variation
updateVariation(CGroupPricingVariation) Validates and writes updated variation
removeVariation(Integer variationId) Deletes variation (checks no confirmed sales reference it)
getVariations(Integer groupPricingId) Lists variations ordered by sortOrder

Price Calculation

calculateSalePrice(groupPricingId, adults, children, infants, variationIds) implements the full pricing engine:

  1. basePriceTotal = baseSellingPrice * totalPax
  2. For each automatic variation: applies to matching pax type count
  3. For each agent-selected variation (from variationIds): applies to totalPax
  4. FIXED: amount = adjustmentValue * applicablePax
  5. PERCENT: amount = (adjustmentValue / 100) * baseSellingPrice * applicablePax
  6. finalPrice = basePriceTotal + adjustmentsTotal

Returns SalePriceResult with breakdown of applied variations.

Sales Methods

Method Description
createSale(groupId, sale, passengers, variationIds) Calculates price, sets DRAFT status, writes sale + variation assignments + sale pax records
confirmSale(groupSaleId, agentId) Validates DRAFT status, creates booking via BookingServiceI (GROUP_TRAVEL service line + HOTEL lines per room), links bookingId, sets CONFIRMED
cancelSale(groupSaleId, reason) Releases room assignments (CTripRoomPax), clears CGroupSalePax references, cancels linked booking, sets CANCELLED
getGroupSales(groupId) Lists sales ordered by createdAt
getOccupancy(groupId) Calculates occupancy metrics from non-cancelled sales

Booking Bridge (confirmSale)

On confirmation, the facade:

  1. Builds BookingCreationRequest with customer from sale, sourceType=GROUP_OUTBOUND, sourceRef=groupId
  2. Creates one GROUP_TRAVEL service line with sellPrice = sale.finalPrice (or overridePrice if set)
  3. Creates one HOTEL service line per distinct room assignment: cost = room cost multiplied by nights, converted to group currency
  4. Imports all sale passengers as booking passengers
  5. Calls BookingServiceI.createBooking(request)
  6. Links bookingId on the sale record

Bidirectional Cancellation Sync

  • Sale cancel -> booking cancel: cancelSale() calls BookingServiceI.changeBookingStatus(bookingId, CANCELLED) if a linked booking exists
  • Booking cancel -> sale cancel: BookingLifecycleFacade.changeBookingStatus() checks if booking has sourceType=GROUP_OUTBOUND and calls GroupSalesFacade.cancelSale() for the linked sale (skips re-cancelling the booking to avoid loops)

15.5 BookingServiceI Expansion

The BookingServiceI interface was expanded with methods needed by external modules (GroupSalesFacade) to manipulate bookings without directly accessing the facade internals:

Method Description
addServiceLine(CServiceLine) Add a service line to a booking
updateServiceLine(CServiceLine) Update a service line
removeServiceLine(serviceLineId, bookingId, reason) Remove a service line
addPassenger(CBookingPassenger) Add a passenger to a booking
updatePassenger(CBookingPassenger) Update a passenger
removePassenger(passengerId, bookingId) Remove a passenger
readBooking(bookingId) Read a booking by ID
getServiceLines(bookingId) Get service lines for a booking
getPassengers(bookingId) Get passengers for a booking
changeBookingStatus(bookingId, newStatus, notes, userId) Change booking status

15.6 API Endpoints (22 new)

Group Pricing and Sales (BookingLifecycleApi.java, under /blm/group/)

# Path Required Params Optional Params Facade Method
1 blm/group/pricing/read session, groupId -- getOrCreatePricing()
2 blm/group/pricing/update session, groupPricingId, assumedPaxCount marginPercent, maxPaxCount, notes updatePricingFields()
3 blm/group/pricing/recalculate session, groupId -- recalculateBasePricing()
4 blm/group/variation/add session, groupPricingId, variationCode, adjustmentType, adjustmentValue description, appliesTo, isOptional, isAutomatic, paxType, sortOrder addVariation()
5 blm/group/variation/update session, variationId any variation fields updateVariation()
6 blm/group/variation/remove session, variationId -- removeVariation()
7 blm/group/variation/list session, groupPricingId -- getVariations()
8 blm/group/sale/create session, groupId, customerId, paxCount, adultCount, childCount, infantCount variationIds, passengers, overridePrice, overrideReason createSale()
9 blm/group/sale/confirm session, groupSaleId -- confirmSale()
10 blm/group/sale/cancel session, groupSaleId reason cancelSale()
11 blm/group/sale/list session, groupId -- getGroupSales()
12 blm/group/sale/calculatePrice session, groupPricingId, adults, children, infants variationIds calculateSalePrice()
13 blm/group/occupancy session, groupId -- getOccupancy()

Flight Blocks, Legs, Expenses (GroupApi.java, under /groups/)

# Path Required Params Optional Params Facade Method
14 groups/flightblock/create session, groupId blockSize, costPerTicket, pricePerTicket, minCommitment, travelClass, notes createFlightBlock()
15 groups/flightblock/update session, flightBlockId any block fields updateFlightBlock()
16 groups/flightblock/list session, groupId -- getFlightBlocks()
17 groups/flightleg/create session, flightBlockId, direction airports, times, airline, flightNumber createFlightLeg()
18 groups/flightleg/update session, flightLegId any leg fields updateFlightLeg()
19 groups/flightleg/list session, flightBlockId -- getFlightLegs()
20 groups/expense/create session, groupId, amount, currencyCode, category description, notes createExpense()
21 groups/expense/update session, expenseId any expense fields updateExpense()
22 groups/expense/list session, groupId -- getExpenses()

15.7 Configuration

api-roles.properties

# Group pricing (M8)
blm/group/pricing/read=agent,admin
blm/group/pricing/update=agent,admin
blm/group/pricing/recalculate=agent,admin

# Group variations (M8)
blm/group/variation/add=agent,admin
blm/group/variation/update=agent,admin
blm/group/variation/remove=admin
blm/group/variation/list=agent,admin

# Group sales (M8)
blm/group/sale/create=agent,admin
blm/group/sale/confirm=agent,admin
blm/group/sale/cancel=agent,admin
blm/group/sale/list=agent,admin
blm/group/sale/calculatePrice=agent,admin

# Group occupancy (M8)
blm/group/occupancy=agent,admin

# Flight blocks, legs, expenses (M8)
groups/flightblock/create=agent,admin
groups/flightblock/update=agent,admin
groups/flightblock/list=agent,admin
groups/flightleg/create=agent,admin
groups/flightleg/update=agent,admin
groups/flightleg/list=agent,admin
groups/expense/create=agent,admin
groups/expense/update=agent,admin
groups/expense/list=agent,admin

erp-booking.properties

# Product code for outbound group sale bookings (in config/properties.d/erp-booking.properties)
erp.default.product.GROUP_TRAVEL=GRPTRAVEL

The ERP product resolution uses erp.default.product.{SERVICE_TYPE} — when a service line has no explicit erpProductId, resolveDefaultProduct() looks up the product code by service type and resolves it to an Odoo product variant ID via ProductFacade.getProductByCode().

GROUP_TRAVEL Service Type

A new service type GROUP_TRAVEL is used for the primary service line in group sale bookings. This distinguishes group bookings from standard service types in reporting and ERP integration.

See Configuration Reference for the complete list of ERP product codes and configuration properties.

15.8 Entity Configuration

All M8 entities use DirectMapping for all fields with NTSServiceFactory as the default factory. Entity configurations are in config/entities/booking-entities.xml (pricing and sales entities) and config/entities/group-entities.xml (flight blocks, legs, expenses).

NTS service definitions added to config/nts-client.xml:

Service Name Class Entity
saveGroupPricing / readGroupPricing NTSEntityWriteService / NTSEntityReadService GrouppricingEntity
saveGroupPricingVariation / readGroupPricingVariation / deleteGroupPricingVariation NTSEntityWriteService / NTSEntityReadService GrouppricingvariationEntity
saveGroupSale / readGroupSale NTSEntityWriteService / NTSEntityReadService GroupsaleEntity
saveGroupSaleVariation / readGroupSaleVariation NTSEntityWriteService / NTSEntityReadService GroupsalevariationEntity
saveGroupSalePax / readGroupSalePax NTSEntityWriteService / NTSEntityReadService GroupsalepaxEntity
saveTripFlightBlock / readTripFlightBlock NTSEntityWriteService / NTSEntityReadService TripflightblockEntity
saveTripFlightLeg / readTripFlightLeg NTSEntityWriteService / NTSEntityReadService TripflightlegEntity
saveTripExpense / readTripExpense NTSEntityWriteService / NTSEntityReadService TripexpenseEntity

15.9 Frontend

File Purpose
tqweb-adm/js/modules/groups-pricing.js Group pricing panel, variations table, cost recalculation
tqweb-adm/js/modules/groups-sales.js Sales list, occupancy bar, 5-step sale wizard
tqweb-adm/js/modules/groups-flights.js Flight blocks and legs management (two-panel layout)
tqweb-adm/js/modules/groups-expenses.js Expense list management
tqweb-adm/group-detail.html Updated nav tabs: Itinerary, Passengers, Accommodation, Activities, Transport, Flights, Expenses, Pricing, Rooming, Sales, Invoicing

15.10 Bug Fixes Applied During M8

Fix Description
Rooming pax assignment roomPaxMap not mapped to room objects; fixed mapping
Duplicate room pax Prevention of duplicate room-pax assignments
Default product ID GENACTVOUT default for outbound group activities
GroupSalesFacade writes Using wrong service factory for write operations
Cost aggregation Room-level costs not included; fixed to use roomCost multiplied by nights
Currency conversion Costs converted to group currency before summing
API param extraction Switched to explicit ApiUtil.gmp() instead of Gson deserialization
JS typed params Added parseInt/parseFloat for numeric form values
Currency display formatMoney already includes currency code; removed double display
Hotel costs Displayed in hotel currency not default AED
Sale wizard Next button Hidden on Pricing step due to CSS issue
Cancel agent name Shows display name not UID
Multi-room creation Added quantity input to room creation dialog

16. Admin Maintenance & Data Health (M9)

16.1 Overview

M9 adds a dedicated BookingMaintenanceFacade and BookingMaintenanceApi for admin-only maintenance operations, plus two background jobs for automated monitoring and cleanup.

16.2 BookingMaintenanceFacade

Location: tqapp/src/main/java/com/perun/tlinq/entity/booking/BookingMaintenanceFacade.java

Extends EntityFacade. All methods are admin-only (enforced at API layer).

Method Group Methods
Cart Cleanup findStaleCarts(int ageInDays, String status), bulkExpireCarts(List<Integer> cartIds), bulkAbandonCarts(List<Integer> cartIds), getCartStatistics()
Booking Health findZombieBookings(String status, int ageInDays), findBookingsWithoutServiceLines(), getBookingStatusDistribution()
Option Expiry getExpiryRunnerStatus(), findMissedExpiries(), manualTriggerExpiry(Integer bookingId)
Orphan Detection findOrphanedServiceLines(), findOrphanedPassengers(), findOrphanedCartItems(), deleteOrphans(String entityType, List<Integer> ids)
ERP Sync Audit findBookingsMissingErpCustomer(int ageInDays), findBookingsMissingErpLead(int ageInDays), retryErpSync(Integer bookingId)
Notification Audit findFailedNotifications(int days), retryNotification(Integer bookingId, String type), getNotificationStatistics()
Activity Logs getActivityLogStatistics(), archiveActivityLogs(int olderThanDays), findExcessiveActivityBookings(int threshold)
System Health getSystemHealthSummary()

16.3 BookingMaintenanceApi

Location: tqapi/src/main/java/com/perun/tlinq/api/BookingMaintenanceApi.java

@Path("/blm/maintenance"). All 20 endpoints are admin-only. See requirements section 9.13 for the full endpoint table.

16.4 Background Jobs

Job Location Schedule Lock
CartCleanupRunner tqapp/.../sched/CartCleanupRunner.java Daily via scheduleAtFixedRate in NTSPlugin Hazelcast blm-cart-cleanup
BookingHealthRunner tqapp/.../sched/BookingHealthRunner.java Daily via scheduleAtFixedRate in NTSPlugin Hazelcast blm-booking-health

Both jobs use Hazelcast distributed locks to prevent duplicate execution in clustered deployments. Results are logged to the maintenancejoblog table.

16.5 New JPA Entities

Entity Class Table ID Field
MaintenancejoblogEntity nts.maintenancejoblog joblogid
NotificationstatusEntity nts.notificationstatus notificationid

Location: tqapp/src/main/java/com/perun/tlinq/client/nts/db/booking/

16.6 Entity Configuration

Added to config/entities/booking-entities.xml:

  • CMaintenanceJobLog canonical entity mapped to MaintenancejoblogEntity
  • CNotificationStatus canonical entity mapped to NotificationstatusEntity

16.7 NTS Services (4 new)

Service Name Class Entity
saveMaintenanceJobLog / readMaintenanceJobLog NTSEntityWriteService / NTSEntityReadService MaintenancejoblogEntity
saveNotificationStatus / readNotificationStatus NTSEntityWriteService / NTSEntityReadService NotificationstatusEntity

16.8 Database Migration

config/db-changes/0057-maintenance-tables.sql -- Creates maintenancejoblog and notificationstatus tables with sequences and indexes.

16.9 Frontend

File Purpose
tqweb-adm/booking-maintenance.html Admin-only maintenance page
tqweb-adm/js/modules/booking-maintenance.js Dashboard cards, tabbed interface (Carts, Bookings, ERP Sync, Notifications, Activity Logs), bulk actions

16.10 Bug Fixes Applied During M9

Fix Description
Silent error handling Maintenance dashboard silently swallowed API errors; added proper error display
notify() API calls Used notify() as object instead of function; fixed invocation pattern