Booking Lifecycle Management - Technical Implementation¶
Status: M1-M9 Complete -- This document covers M1 (Core Booking) through M9 (Admin Maintenance & Data Health).
Table of Contents¶
- Architecture Overview
- Database Schema
- Entity Classes
- API Layer
- Business Logic (Facade)
- Status State Machine
- ERP Integration
- Background Jobs
- Configuration Files
- Actions Framework (M5)
- Voucher Generation (M6)
- Booking Terms Tab (M6.5)
- TripMaker Integration (M7)
- Inbound Group Integration (M7)
- 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 | |
| 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:
- Loads all service lines for the booking.
- Sums
costLocalfor non-service-charge lines intototalCost. - Sums
sellPricefor non-service-charge lines intototalSell. - Sums
sellPricefor service-charge lines (whereisServiceCharge = true) intototalServiceCharges. - Calculates
balanceDue = totalSell + totalServiceCharges - totalPaid. - 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():
- Uses
CustomerFacadeto look up the customer by email in Odoo. - If found, stores the Odoo customer ID on the booking (
erpCustomerId). - If not found and customer details are provided, creates a new customer in Odoo and stores the returned ID.
- Failures are caught, logged as warnings, and do not block booking creation.
7.2 CRM Lead Creation¶
After customer synchronization, createCrmLead():
- Creates a CRM lead/opportunity in Odoo linked to the customer.
- Stores the Odoo lead ID on the booking (
erpLeadId). - 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:
- Acquires a distributed lock (
blm-option-expiry) via HazelcastIMap.tryLock()with a 55-minute lease to prevent concurrent execution across cluster nodes. - If the lock cannot be acquired (held by another instance), the run is skipped silently.
- Queries all OPTION_HELD bookings with
optionExpiryDatein the past (viagetExpiringBookings(0)). - For each expired booking:
- Transitions status from OPTION_HELD to EXPIRED.
- Sends an option expiry notification.
- Logs the expiry.
- Releases the distributed lock in a
finallyblock.
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 Quick Search¶
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, callsgenerateActionsForBooking(). 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, callsgenerateActionsForServiceLine()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:
- 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.
- 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: resolvesfieldvia CServiceLine getter (description, supplierRef, supplierName, notes, specialRequests, startDate, endDate, serviceType, currency)source=attribute: resolvespathfrom service line'sattributesJSONsource=action: finds completed action byactionCode, extractsfieldfrom itsresultDataJSONsource=attachment: finds action byactionCode, extractsfileNamefromresultData
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:
- Load
voucher-master.htmlandvoucher-section.html(default section template) - Replace company placeholders (
%COMPANY_NAME%,%COMPANY_LOGO%, etc.) fromAppConfig - Replace booking placeholders (
%BOOKING_REF%,%CLIENT_NAME%,%TRAVEL_DATES%) - Load
voucher-passengers.html, build%PASSENGER_ROWS%, replace into%PASSENGERS% - For each service line:
a. Load
sections/section-{TYPE}.html(falls back to default) b. Resolve field values fromfieldsDefinitionJSON viaresolveFieldValue()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 - Load
voucher-terms.html, build%TERMS_BLOCKS%, replace into%TERMS% - Replace footer placeholders (
%GENERATED_DATE%,%GENERATED_BY%) - 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:
- Verify
isVoucherReady(bookingId)— reject if pending required actions - Load booking, active service lines (non-cancelled, non-service-charge), passengers, completed actions
- Load matching voucher section templates per service type present
- Call
resolveBookingTerms(bookingId)for T&C - Call
VoucherRenderer.render()to produce voucher PDF bytes - Scan completed actions for
ETICKETaction codes withdocumentIdin result data; download and merge viaPdfIngestionService.mergePdfs() - Archive previous VOUCHER documents (copy S3 object with
-v{N}suffix, update record to VOUCHER_ARCHIVE) - Upload final PDF via
BookingDocumentStorageService.uploadPdfBytes() - Create
CBookingDocumententity (docType=VOUCHER) - 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
isVoucherReadyreturns 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<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
initializeto 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:
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:
- Hotels: room-level costs (roomCost multiplied by number of nights per room)
- Activities: per-activity total cost
- Transports: per-transport cost
- Flight blocks: blockSize multiplied by costPerTicket per block
- 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:
basePriceTotal = baseSellingPrice * totalPax- For each automatic variation: applies to matching pax type count
- For each agent-selected variation (from variationIds): applies to totalPax
- FIXED:
amount = adjustmentValue * applicablePax - PERCENT:
amount = (adjustmentValue / 100) * baseSellingPrice * applicablePax 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:
- Builds
BookingCreationRequestwith customer from sale,sourceType=GROUP_OUTBOUND,sourceRef=groupId - Creates one
GROUP_TRAVELservice line withsellPrice = sale.finalPrice(or overridePrice if set) - Creates one
HOTELservice line per distinct room assignment: cost = room cost multiplied by nights, converted to group currency - Imports all sale passengers as booking passengers
- Calls
BookingServiceI.createBooking(request) - Links
bookingIdon the sale record
Bidirectional Cancellation Sync¶
- Sale cancel -> booking cancel:
cancelSale()callsBookingServiceI.changeBookingStatus(bookingId, CANCELLED)if a linked booking exists - Booking cancel -> sale cancel:
BookingLifecycleFacade.changeBookingStatus()checks if booking hassourceType=GROUP_OUTBOUNDand callsGroupSalesFacade.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:
CMaintenanceJobLogcanonical entity mapped toMaintenancejoblogEntityCNotificationStatuscanonical entity mapped toNotificationstatusEntity
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 |