Hotel Management Module - API Implementation Specification¶
Overview¶
This document provides detailed technical documentation for all REST API endpoints in the Hotel Management module. The API is implemented in the HotelApi class using Jakarta EE (JAX-RS) annotations.
File: tqapi/src/main/java/com/perun/tlinq/api/HotelApi.java (1247 lines)
API Architecture¶
Base Configuration¶
@Path("/hotel")
public class HotelApi extends HttpServlet {
private static final Logger logger = Logger.getLogger(HotelApi.class.getName());
// All endpoints follow POST method pattern
// Base URL: http://server:port/api/hotel
}
Common Patterns¶
Request/Response Pattern¶
All endpoints follow a consistent pattern:
@POST
@Path("/endpointName")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response endpointName(Map reqData) {
Response resp = null;
TlinqApiResponse ar = null;
String session = "N/A";
try {
// 1. Extract session token
session = ApiUtil.gmp(reqData, "session", String.class, true);
// 2. Log begin
logger.info("BEGIN endpointName for " + session);
// 3. Delegate to private method
ar = doEndpointName(session, reqData);
} catch (Exception ex) {
// 4. Handle exceptions
ar = new TlinqApiResponse(TlinqErr.GENERAL,
ex.getMessage() == null ? ex.getClass().getName() : ex.getMessage());
}
// 5. Log end
logger.info("END endpointName for " + session);
// 6. Build and return response
resp = Response.ok(ar).build();
return resp;
}
TlinqApiResponse Structure¶
// Success response
{
"success": true,
"data": { ... } // Entity or list of entities
}
// Error response
{
"success": false,
"error": {
"code": "ERROR_CODE",
"message": "Error description"
}
}
Authentication Pattern¶
private CRegUser checkIfEmployee(String session, boolean mustBeEmployee)
throws TlinqClientException {
UserFacade uf = new UserFacade();
CRegUser user = uf.getUser(session);
if(mustBeEmployee && (!user.getEmployee()))
throw new TlinqClientException(TlinqErr.SESSION_ERROR,
"This API is available for employees only.");
return user;
}
Usage in endpoints:
- All write operations: checkIfEmployee(session, true) - employees only
- Read operations: May allow broader access or use system session
API Endpoints¶
1. Hotel Management Endpoints¶
1.1 POST /hotel/listHotels¶
Purpose: Search hotels by name and/or area
Implementation: HotelApi.java:38-59 (endpoint), 520-547 (logic)
Authorization: Employees only
Request:
{
"session": "user_session_token",
"shDes": "ATLJMB", // Optional: Search by hotel code (exact match)
"name": "Atlantis", // Optional: Search by name (partial match)
"area": "DXB" // Optional: Filter by area
}
Query Logic:
- If shDes provided: Exact match on hotel code
- Otherwise: Combine name (LIKE) and area (=) filters
- If no criteria: Return all hotels
Implementation:
private TlinqApiResponse doListHotels(String session, Map reqData) {
TlinqApiResponse response = null;
try {
checkIfEmployee(session, true);
EntityFacade ef = new EntityFacade(session);
SelectCriteriaList scl = new SelectCriteriaList();
String shdes = ApiUtil.gmp(reqData, "shDes", String.class, false);
if(TypeUtil.isEmptyString(shdes)) {
String namePart = ApiUtil.sval(
ApiUtil.gmp(reqData, "name", String.class, false), "");
String area = ApiUtil.sval(
ApiUtil.gmp(reqData, "area", String.class, false), "");
if (!namePart.isEmpty())
scl.addCriterion("name", "like", "%" + namePart + "%", null);
if (!area.isEmpty())
scl.addCriterion("area", "=", area, null);
} else {
scl.addCriterion("shDes", "=", shdes, null);
}
List<TlinqEntity> hotels = ef.search("Hotel",
(!scl.getCriteria().isEmpty()) ? scl : null);
response = new TlinqApiResponse(hotels);
} catch (TlinqClientException e) {
logger.severe(e.getMessage());
response = new TlinqApiResponse(e.getErrorCode(), e.getMessage());
}
return response;
}
Response:
{
"success": true,
"data": [
{
"hotelId": 1042,
"shDes": "ATLJMB",
"name": "Atlantis The Palm",
"shortName": "Atlantis Palm",
"country": "UAE",
"area": "DXB",
"stars": 5,
"chkIn": "15:00",
"chkOut": "12:00",
"direct": true,
"indirect": false,
...
}
]
}
1.2 POST /hotel/hotelLookup¶
Purpose: Autocomplete lookup for hotel selection (returns simplified list)
Implementation: HotelApi.java:62-83 (endpoint), 495-518 (logic)
Authorization: Employees only
Request:
Implementation:
private TlinqApiResponse doLookupHotel(String session, Map reqData) {
TlinqApiResponse response = null;
try {
checkIfEmployee(session, true);
EntityFacade ef = new EntityFacade(session);
SelectCriteriaList scl = new SelectCriteriaList();
String namePart = ApiUtil.sval(
ApiUtil.gmp(reqData, "name", String.class, false), "");
scl.addCriterion("name", "like", "'%" + namePart + "%'", null);
List<TlinqEntity> hotels = ef.search("Hotel",
(!scl.getCriteria().isEmpty()) ? scl : null);
// Build simplified result: [name, id] pairs
ArrayList<String[]> ret = new ArrayList<>();
for(TlinqEntity hotel : hotels) {
String[] ht = {
((CHotel)hotel).getName(),
((CHotel)hotel).getHotelId().toString()
};
ret.add(ht);
}
response = new TlinqApiResponse(ret);
} catch (TlinqClientException e) {
logger.severe(e.getMessage());
response = new TlinqApiResponse(e.getErrorCode(), e.getMessage());
}
return response;
}
Response:
Usage: Powers autocomplete dropdowns in UI
1.3 POST /hotel/getHotel¶
Purpose: Retrieve single hotel by ID
Implementation: HotelApi.java:86-107 (endpoint), 549-570 (logic)
Authorization: Employees only
Request:
Implementation:
private TlinqApiResponse doGetHotel(String session, Map reqData) {
TlinqApiResponse response = null;
try {
checkIfEmployee(session, true);
EntityFacade ef = new EntityFacade(session);
SelectCriteriaList scl = new SelectCriteriaList();
Integer id = ApiUtil.gmp(reqData, "id", Integer.class, true);
scl.addCriterion("hotelId", "=", id, null);
List<TlinqEntity> hotels = ef.search("Hotel", scl);
if(hotels.size() > 0)
response = new TlinqApiResponse(hotels.get(0));
else
response = null; // Hotel not found
} catch (TlinqClientException e) {
e.printStackTrace();
response = new TlinqApiResponse(e.getErrorCode(), e.getMessage());
}
return response;
}
Response:
{
"success": true,
"data": {
"hotelId": 1042,
"shDes": "ATLJMB",
"name": "Atlantis The Palm",
"shortName": "Atlantis Palm",
"hotelDesc": "Iconic 5-star resort...",
"productId": 2045,
"direct": true,
"indirect": false,
"country": "UAE",
"stars": 5,
"infant": 2,
"child": 12,
"chkIn": "15:00",
"chkOut": "12:00",
"fees": 50.00,
"feesDesc": "Tourism dirham per room per night",
"terms": "Cancellation: 72 hours before arrival...",
"area": "DXB",
"city": "Dubai",
"release": 7,
"reservationContact": "reservations@atlantis.com"
}
}
1.4 POST /hotel/saveHotel¶
Purpose: Create or update hotel record
Implementation: HotelApi.java:109-130 (endpoint), 675-691 (logic)
Authorization: Employees only
Request:
{
"session": "user_session_token",
"hotel": {
"hotelId": null, // null for create, ID for update
"shDes": "ATLJMB",
"name": "Atlantis The Palm",
"shortName": "Atlantis Palm",
"country": "UAE",
"area": "DXB",
"city": "Dubai",
"stars": 5,
"infant": 2,
"child": 12,
"chkIn": "15:00",
"chkOut": "12:00",
"direct": true,
"indirect": false,
"fees": 50.00,
"feesDesc": "Tourism dirham per room per night",
"terms": "Cancellation policy...",
"release": 7,
"reservationContact": "reservations@atlantis.com"
}
}
Implementation:
private TlinqApiResponse doSaveHotel(String session, Map reqData) {
TlinqApiResponse response = null;
try {
checkIfEmployee(session, true);
Map hotelInfo = ApiUtil.gmp(reqData, "hotel", Map.class, true);
EntityFacade ef = new EntityFacade(session);
// Create entity from map using build() method
CHotel hotel = TlinqEntityFactory.instance()
.newEntity(CHotel.class, null)
.build(hotelInfo);
// Write to database (insert if hotelId=null, update otherwise)
CHotel newHotel = (CHotel) ef.write(hotel);
response = new TlinqApiResponse(newHotel);
} catch (TlinqClientException e) {
response = new TlinqApiResponse(e.getErrorCode(), e.getMessage());
}
return response;
}
Response:
{
"success": true,
"data": {
"hotelId": 1042, // Generated ID for new hotel
"shDes": "ATLJMB",
"name": "Atlantis The Palm",
...
}
}
Behavior:
- If hotelId is null: INSERT new hotel, return with generated ID
- If hotelId exists: UPDATE existing hotel, return updated entity
- EntityFacade.write() handles both cases automatically
2. Room Management Endpoints¶
2.1 POST /hotel/listHotelRooms¶
Purpose: List all room types for a hotel
Implementation: HotelApi.java:155-177 (endpoint), 572-591 (logic)
Authorization: Employees only
Request:
Implementation:
private TlinqApiResponse doListHotelRooms(String session, Map reqData) {
TlinqApiResponse response = null;
try {
checkIfEmployee(session, true);
EntityFacade ef = new EntityFacade(session);
SelectCriteriaList scl = new SelectCriteriaList();
Integer hotelId = ApiUtil.gmp(reqData, "hotelId", Integer.class, true);
scl.addCriterion("hotelId", "=", hotelId, null);
List<TlinqEntity> rooms = ef.search("HotelRoom", scl);
response = new TlinqApiResponse(rooms);
} catch (TlinqClientException e) {
e.printStackTrace();
response = new TlinqApiResponse(e.getErrorCode(), e.getMessage());
}
return response;
}
Response:
{
"success": true,
"data": [
{
"roomId": 5012,
"hotelId": 1042,
"name": "Deluxe Room - King Bed",
"roomDesc": "35 sqm with king bed, city view",
"maxOccupancy": 3,
"maxAdults": 2,
"extraBed": true,
"extraBedRequired": true,
"bedding": "1 King",
"notes": "Non-smoking"
},
{
"roomId": 5013,
"hotelId": 1042,
"name": "Deluxe Room - Twin Beds",
"roomDesc": "35 sqm with two twin beds, city view",
"maxOccupancy": 2,
"maxAdults": 2,
"extraBed": false,
"extraBedRequired": false,
"bedding": "2 Twin",
"notes": "Non-smoking"
}
]
}
2.2 POST /hotel/saveRoom¶
Purpose: Create or update room type
Implementation: HotelApi.java:132-153 (endpoint), 693-709 (logic)
Authorization: Employees only
Request:
{
"session": "user_session_token",
"room": {
"roomId": null, // null for create, ID for update
"hotelId": 1042,
"name": "Deluxe Ocean View",
"roomDesc": "40 sqm, king bed, ocean view",
"maxOccupancy": 3,
"maxAdults": 2,
"extraBed": true,
"extraBedRequired": true,
"bedding": "1 King or 2 Twin",
"notes": "Non-smoking, suitable for families"
}
}
Implementation:
private TlinqApiResponse doSaveRom(String session, Map reqData) {
TlinqApiResponse response = null;
try {
checkIfEmployee(session, true);
Map roomInfo = ApiUtil.gmp(reqData, "room", Map.class, true);
EntityFacade ef = new EntityFacade(session);
// Create entity from map
CHotelRoom room = TlinqEntityFactory.instance()
.newEntity(CHotelRoom.class, null)
.build(roomInfo);
// Write to database
CHotelRoom newRoom = (CHotelRoom) ef.write(room);
response = new TlinqApiResponse(newRoom);
} catch (TlinqClientException e) {
response = new TlinqApiResponse(e.getErrorCode(), e.getMessage());
}
return response;
}
Response:
{
"success": true,
"data": {
"roomId": 5014, // Generated ID for new room
"hotelId": 1042,
"name": "Deluxe Ocean View",
...
}
}
3. Calendar Management Endpoints¶
3.1 POST /hotel/createRoomCalendar¶
Purpose: Bulk create calendar entries for a date range
Implementation: HotelApi.java:252-275 (endpoint), 731-752 (logic)
Authorization: Employees only
Request:
{
"session": "user_session_token",
"template": {
"roomId": 5012,
"market": "UAE",
"stayDate": "01-12-2025", // Start date (dd-MM-yyyy)
"bookFrom": "01-10-2025",
"bookTo": "01-12-2025",
"vendorId": null,
"promo": "EARLY2025",
"baseRate": 500.00,
"specDayRate": 150.00, // Special day rate (weekend)
"sdRateType": "ADD", // ADD means add to baseRate
"adultRate": 100.00,
"adultRateType": "ADD",
"childRate": 50.00,
"childRateType": "ADD",
"adultSdRate": 120.00, // Special day adult rate
"childSdRate": 60.00, // Special day child rate
"extraBedRate": 75.00,
"rateBase": "DBL", // Double occupancy
"mealBase": 1, // Room Only (planId)
"mealSupplements": "[{\"mealPlan\":\"BB\",\"adlRate\":[60,70],\"chldRate\":[30,35]}]",
"stopSale": false,
"available": true,
"onRequest": false,
"mlos": 1,
"sdmlos": 3,
"mlosDesc": "3 nights min on weekends",
"notes": null
},
"stayTo": "31-12-2025", // End date (dd-MM-yyyy)
"specDays": "WEEKENDS" // WEEKENDS | ALL | NONE
}
Implementation:
private TlinqApiResponse doCreateRoomCalendar(String session, Map reqData) {
TlinqApiResponse response = null;
try {
checkIfEmployee(session, true);
Map templateMap = ApiUtil.gmp(reqData, "template", Map.class, true);
String stayTo = ApiUtil.gmp(reqData, "stayTo", String.class, true);
String specDaySetting = ApiUtil.gmp(reqData, "specDays", String.class, true);
// Create template entity
CRoomCalendarEntry template = TlinqEntityFactory.instance()
.newEntity(CRoomCalendarEntry.class, null)
.build(templateMap);
List params = Arrays.asList(stayTo, specDaySetting);
EntityFacade ef = new EntityFacade(session);
// Call custom service to create entries
List<CRoomCalendarEntry> calRes = (List<CRoomCalendarEntry>)
ef.callCustomService(template, "createRoomCalendar", params);
response = new TlinqApiResponse(calRes);
} catch (TlinqClientException e) {
response = new TlinqApiResponse(e.getErrorCode(), e.getMessage());
}
return response;
}
Business Logic (Custom Service):
1. Parse stayDate (from) and stayTo (to) dates
2. Loop through each date in range
3. For each date:
- Copy template entry
- Set stayDate to current date
- Determine if special day based on specDays setting:
- WEEKENDS: Friday and Saturday (UAE/GCC calendar)
- ALL: Every day is special
- NONE: No special days
- Set specialDay flag accordingly
- Insert entry into database
4. Return list of created entries
Response:
{
"success": true,
"data": [
{
"calendarEntryId": 78901,
"roomId": 5012,
"market": "UAE",
"stayDate": "2025-12-01T00:00:00.000Z",
"baseRate": 500.00,
"specialDay": false,
...
},
{
"calendarEntryId": 78902,
"roomId": 5012,
"market": "UAE",
"stayDate": "2025-12-02T00:00:00.000Z",
"baseRate": 500.00,
"specialDay": false,
...
},
...
{
"calendarEntryId": 78906,
"roomId": 5012,
"market": "UAE",
"stayDate": "2025-12-06T00:00:00.000Z", // Friday
"baseRate": 500.00,
"specialDay": true, // Weekend
...
}
]
}
3.2 POST /hotel/listRoomCalendar¶
Purpose: Retrieve calendar entries for a room/market/date range
Implementation: HotelApi.java:179-201 (endpoint), 593-623 (logic)
Authorization: Employees only
Request:
{
"session": "user_session_token",
"roomId": 5012, // Optional if hotelId provided
"hotelId": 1042, // Optional if roomId provided
"fromDate": "01-12-2025",
"toDate": "31-12-2025",
"market": "UAE" // Optional, defaults to "UAE"
}
Implementation:
private TlinqApiResponse doListRoomCalendar(String session, Map reqData) {
TlinqApiResponse response = null;
try {
checkIfEmployee(session, true);
EntityFacade ef = new EntityFacade(session);
Integer roomId = ApiUtil.gmp(reqData, "roomId", Integer.class, false);
Integer hotelId = ApiUtil.gmp(reqData, "hotelId", Integer.class, false);
if((roomId == null) && (hotelId == null)) {
throw new TlinqClientException(TlinqErr.MISSING_PARAMETER,
"Room ID or Hotel ID must be provided!");
}
String fromDt = ApiUtil.gmp(reqData, "fromDate", String.class, true);
String toDt = ApiUtil.gmp(reqData, "toDate", String.class, true);
String market = ApiUtil.gmp(reqData, "market", String.class, false);
if((market == null) || (market.isEmpty()))
market = "UAE"; // Default market
List params = Arrays.asList(hotelId, roomId, fromDt, toDt, market);
// Call custom service
List<CRoomCalendarEntry> cal = (List<CRoomCalendarEntry>)
ef.callCustomService(CRoomCalendarEntry.class,
"listRoomCalendar", params);
response = new TlinqApiResponse(cal);
} catch (TlinqClientException e) {
response = new TlinqApiResponse(e.getErrorCode(), e.getMessage());
}
return response;
}
Response:
{
"success": true,
"data": [
{
"calendarEntryId": 78901,
"roomId": 5012,
"market": "UAE",
"vendorId": null,
"promo": "EARLY2025",
"bookFrom": "2025-10-01T00:00:00.000Z",
"bookTo": "2025-12-01T00:00:00.000Z",
"stayDate": "2025-12-01T00:00:00.000Z",
"baseRate": 500.00,
"specDayRate": 150.00,
"specialDay": false,
"sdRateType": "ADD",
"adultRate": 100.00,
"adultRateType": "ADD",
"childRate": 50.00,
"childRateType": "ADD",
"adultSdRate": 120.00,
"childSdRate": 60.00,
"extraBedRate": 75.00,
"rateBase": "DBL",
"mealBase": 1,
"mealSupplements": "[{\"mealPlan\":\"BB\",\"adlRate\":[60,70],\"chldRate\":[30,35]}]",
"stopSale": false,
"available": true,
"onRequest": false,
"mlos": 1,
"sdmlos": 3,
"mlosDesc": "3 nights min on weekends",
"notes": null
}
// ... more entries
]
}
3.3 POST /hotel/updateRoomCalendar¶
Purpose: Bulk update calendar entries for a date range
Implementation: HotelApi.java:277-300 (endpoint), 754-782 (logic)
Authorization: Employees only
Request:
{
"session": "user_session_token",
"template": {
"roomId": 5012,
"market": "UAE",
"baseRate": 550.00, // Updated rate
"specDayRate": 180.00, // Updated special rate
"stopSale": false,
"available": true
// Only fields to be updated need to be included
},
"stayFrom": "15-12-2025",
"stayTo": "20-12-2025"
}
Implementation:
private TlinqApiResponse doUpdateRoomCalendar(String session, Map reqData) {
TlinqApiResponse response = null;
String dateS = "";
try {
checkIfEmployee(session, true);
Map templateMap = ApiUtil.gmp(reqData, "template", Map.class, true);
Integer roomId = ApiUtil.gmp(templateMap, "roomId", Integer.class, true);
dateS = ApiUtil.gmp(reqData, "stayFrom", String.class, true);
Date stayFrom = DateUtil.getInstance()
.fromString(dateS, DateUtil.WEB_DATE_FORMAT);
dateS = ApiUtil.gmp(reqData, "stayTo", String.class, true);
Date stayTo = DateUtil.getInstance()
.fromString(dateS, DateUtil.WEB_DATE_FORMAT);
// Create template entity using update() method
// update() only sets fields present in map
CRoomCalendarEntry template = TlinqEntityFactory.instance()
.newEntity(CRoomCalendarEntry.class, null)
.update(templateMap);
List params = Arrays.asList(roomId, stayFrom, stayTo);
EntityFacade ef = new EntityFacade(session);
// Call custom service
List<CRoomCalendarEntry> calRes = (List<CRoomCalendarEntry>)
ef.callCustomService(template, "updateRoomCalendar", params);
response = new TlinqApiResponse(calRes);
} catch (TlinqClientException e) {
response = new TlinqApiResponse(e.getErrorCode(), e.getMessage());
} catch (ParseException ex) {
response = new TlinqApiResponse(TlinqErr.INVALID_FORMAT,
"Invalid date format - " + dateS);
}
return response;
}
Business Logic (Custom Service):
1. Query all calendar entries WHERE roomId = ? AND market = ? AND stayDate BETWEEN stayFrom AND stayTo
2. For each existing entry:
- Apply updates from template (only non-null fields)
- NOTE: stayDate is NOT updateable (security feature)
- Save updated entry to database
3. Return list of updated entries
Response:
{
"success": true,
"data": [
{
"calendarEntryId": 78905,
"roomId": 5012,
"market": "UAE",
"stayDate": "2025-12-15T00:00:00.000Z",
"baseRate": 550.00, // Updated
"specDayRate": 180.00, // Updated
...
}
// ... more updated entries
]
}
3.4 POST /hotel/copyRoomCalendar¶
Purpose: Copy calendar entries from one room to another
Implementation: HotelApi.java:348-369 (endpoint), 829-854 (logic)
Authorization: Employees only
Request:
{
"session": "user_session_token",
"fromRoom": 5012,
"toRoom": 5013,
"market": "UAE",
"fromDate": "01-12-2025",
"toDate": "31-12-2025"
}
Implementation:
private TlinqApiResponse doCopyCalendar(String session, Map reqData) {
TlinqApiResponse response = null;
try {
checkIfEmployee(session, true);
Integer fromRoom = ApiUtil.gmp(reqData, "fromRoom", Integer.class, true);
Integer toRoom = ApiUtil.gmp(reqData, "toRoom", Integer.class, true);
String market = ApiUtil.gmp(reqData, "market", String.class, true);
String fromDate = ApiUtil.gmp(reqData, "fromDate", String.class, true);
String toDate = ApiUtil.gmp(reqData, "toDate", String.class, true);
CHotelRoom rm = TlinqEntityFactory.instance()
.newEntity(CHotelRoom.class, null);
rm.setRoomId(toRoom);
EntityFacade ef = new EntityFacade(session);
// Call custom service
Integer copied = (Integer) ef.callCustomService(rm,
"copyRoomCalendar",
Arrays.asList(fromRoom, market, fromDate, toDate));
HashMap<String, Object> res = new HashMap<>();
res.put("recordsCopied", copied);
response = new TlinqApiResponse(res);
} catch (TlinqClientException ex) {
response = new TlinqApiResponse(ex.getErrorCode(), ex.getMessage());
}
return response;
}
Business Logic (Custom Service): 1. Query source entries: WHERE roomId = fromRoom AND market = ? AND stayDate BETWEEN fromDate AND toDate 2. For each source entry: - Create new entry with roomId = toRoom - Copy all rate and configuration fields - Check if entry already exists (roomId, market, stayDate unique key) - If exists: Skip or update based on business rule - If not exists: Insert new entry 3. Return count of copied entries
Response:
3.5 POST /hotel/readRoomPeriods¶
Purpose: Read distinct rate periods for a room (groups consecutive dates with same rates)
Implementation: HotelApi.java:371-392 (endpoint), 856-876 (logic)
Authorization: Employees only
Request:
{
"session": "user_session_token",
"roomId": 5012,
"market": "UAE",
"fromDate": "01-12-2025",
"toDate": "31-12-2025"
}
Implementation:
private TlinqApiResponse doReadRoomPeriods(String session, Map reqData)
throws Exception {
TlinqApiResponse response = null;
try {
checkIfEmployee(session, true);
String fdStr = ApiUtil.gmp(reqData, "fromDate", String.class, true);
String tdStr = ApiUtil.gmp(reqData, "toDate", String.class, true);
CRoomPeriod per = new CRoomPeriod()
.setRoomId(ApiUtil.gmp(reqData, "roomId", Integer.class, true))
.setPeriodStart(DateUtil.getInstance()
.fromString(fdStr, DateUtil.WEB_DATE_FORMAT))
.setPeriodEnd(DateUtil.getInstance()
.fromString(tdStr, DateUtil.WEB_DATE_FORMAT))
.setMarket(ApiUtil.gmp(reqData, "market", String.class, true));
EntityFacade ef = new EntityFacade(session);
List<CRoomPeriod> periods = (List<CRoomPeriod>)
ef.callCustomService(per, "findRoomPeriods", null);
response = new TlinqApiResponse(periods);
} catch (TlinqClientException ex) {
response = new TlinqApiResponse(ex.getErrorCode(), ex.getMessage());
}
return response;
}
Business Logic (Custom Service): Analyzes calendar entries and groups consecutive dates with identical rates into periods:
Example: - Dec 1-4: baseRate=500, specDayRate=150 → Period 1 - Dec 5-7: baseRate=500, specDayRate=150 (weekend) → Period 2 (different specialDay flag) - Dec 8-14: baseRate=550, specDayRate=180 → Period 3 (rate change)
Response:
{
"success": true,
"data": [
{
"roomId": 5012,
"market": "UAE",
"periodStart": "2025-12-01T00:00:00.000Z",
"periodEnd": "2025-12-04T00:00:00.000Z",
"baseRate": 500.00,
"specDayRate": 150.00,
"sdRateType": "ADD",
"rateBase": "DBL",
"mlos": 1,
"sdmlos": 3
},
{
"roomId": 5012,
"market": "UAE",
"periodStart": "2025-12-08T00:00:00.000Z",
"periodEnd": "2025-12-14T00:00:00.000Z",
"baseRate": 550.00,
"specDayRate": 180.00,
"sdRateType": "ADD",
"rateBase": "DBL",
"mlos": 1,
"sdmlos": 3
}
]
}
Usage: Powers the "Quick Edit" calendar view showing rate periods instead of individual dates
4. Meal Plan Endpoints¶
4.1 POST /hotel/listMealPlans¶
Purpose: List all meal plans or meal plans available for a hotel
Implementation: HotelApi.java:227-250 (endpoint), 711-729 (logic)
Authorization: Any authenticated user
Request:
{
"session": "user_session_token",
"hotelId": 1042 // Optional: if provided, returns hotel-specific meal plans
}
Implementation:
private TlinqApiResponse doListMealPlans(String session, Map reqData) {
TlinqApiResponse response = null;
try {
Integer htlId = ApiUtil.gmp(reqData, "hotelId", Integer.class, false);
EntityFacade ef = new EntityFacade(session);
List plans = null;
if(null == htlId) {
// List all meal plans
plans = ef.search("MealPlan", null);
} else {
// List meal plans available for specific hotel
plans = (List) ef.callCustomService(CMealPlan.class,
"listAvailableMealPlans", Arrays.asList(htlId));
}
response = new TlinqApiResponse(plans);
} catch (TlinqClientException e) {
response = new TlinqApiResponse(e.getErrorCode(), e.getMessage());
}
return response;
}
Response (All Plans):
{
"success": true,
"data": [
{
"planId": 1,
"plan": "RO",
"name": "Room Only",
"planDesc": "No meals included",
"planOrder": 1,
"adultCost": 0.00,
"childCost": 0.00
},
{
"planId": 2,
"plan": "BB",
"name": "Bed & Breakfast",
"planDesc": "Daily breakfast included",
"planOrder": 2,
"adultCost": 50.00,
"childCost": 25.00
},
{
"planId": 3,
"plan": "HB",
"name": "Half Board",
"planDesc": "Breakfast and dinner included",
"planOrder": 3,
"adultCost": 100.00,
"childCost": 50.00
}
]
}
4.2 POST /hotel/updateMealPlans¶
Purpose: Update meal plan costs for a hotel
Implementation: HotelApi.java:302-323 (endpoint), 784-800 (logic)
Authorization: Employees only
Request:
{
"session": "user_session_token",
"hotelId": 1042,
"plans": [
{
"planId": 2,
"adultCost": 55.00,
"childCost": 27.50
},
{
"planId": 3,
"adultCost": 110.00,
"childCost": 55.00
}
]
}
Implementation:
private TlinqApiResponse doUpdateMealPlans(String session, Map reqData) {
TlinqApiResponse response = null;
try {
checkIfEmployee(session, true);
Integer hotelId = ApiUtil.gmp(reqData, "hotelId", Integer.class, true);
ArrayList plans = ApiUtil.gmp(reqData, "plans", ArrayList.class, true);
List params = Arrays.asList(hotelId, plans);
EntityFacade ef = new EntityFacade(session);
CMealPlan tmpl = TlinqEntityFactory.instance()
.newEntity(CMealPlan.class, null);
// Call custom service
List newPlans = (List) ef.callCustomService(tmpl,
"updateHotelMealPlans", params);
response = new TlinqApiResponse(newPlans);
} catch (TlinqClientException e) {
response = new TlinqApiResponse(e.getErrorCode(), e.getMessage());
}
return response;
}
Response:
{
"success": true,
"data": [
{
"planId": 2,
"plan": "BB",
"name": "Bed & Breakfast",
"adultCost": 55.00,
"childCost": 27.50
},
{
"planId": 3,
"plan": "HB",
"name": "Half Board",
"adultCost": 110.00,
"childCost": 55.00
}
]
}
Note: This updates hotel-specific meal plan costs, which may differ from system defaults
5. Search and Booking Endpoints¶
5.1 POST /hotel/searchAccommodation¶
Purpose: Search for available rooms across all hotels
Implementation: HotelApi.java:203-225 (endpoint), 625-673 (logic)
Authorization: Any user (employees or logged-in customers)
Request:
{
"session": "user_session_token", // Can be null/empty for anonymous search
"checkin": "15-12-2025",
"checkout": "20-12-2025",
"adults": 2,
"children": 1,
"meals": "BB", // Optional: Meal plan code
"area": "DXB", // Optional: Filter by area
"market": "UAE", // Optional: Market segment (UAE/GCC/ROW)
"maxBudget": 3000.00, // Optional: Maximum budget filter
"extraBed": false, // Optional: Require extra bed
"hotelId": 1042, // Optional: Search specific hotel only
"roomId": 5012, // Optional: Search specific room only
"context": "config" // Optional: "config" for employee price view
}
Implementation:
private TlinqApiResponse doSearchAccommodation(String session, Map reqData) {
TlinqApiResponse response = null;
try {
boolean isEmployee = false;
// Handle anonymous searches
if(TypeUtil.isEmptyString(session))
session = ClientConfig.instance().getDefaultFactory()
.getPropertyList().getProperty("system.session");
else {
try {
CRegUser regUser = new UserFacade().getUser(session);
isEmployee = regUser.getEmployee();
} catch (TlinqClientException nex) {
logger.fine("doSearchAccommodation: running for user that is not logged in!");
}
}
EntityFacade ef = new EntityFacade(session);
// Build search input
CHotelSearchInput input = new CHotelSearchInput();
input.setCheckIn(DateUtil.getInstance().fromString(
ApiUtil.gmp(reqData, "checkin", String.class, true),
DateUtil.WEB_DATE_FORMAT));
input.setCheckOut(DateUtil.getInstance().fromString(
ApiUtil.gmp(reqData, "checkout", String.class, true),
DateUtil.WEB_DATE_FORMAT));
input.setAdults(TypeUtil.extractInteger(
ApiUtil.gmp(reqData, "adults", Integer.class, true)));
input.setChildren(TypeUtil.extractInteger(
ApiUtil.gmp(reqData, "children", Integer.class, true)));
input.setMealPlans(ApiUtil.gmp(reqData, "meals", String.class, false));
input.setArea(ApiUtil.gmp(reqData, "area", String.class, false));
input.setMarket(ApiUtil.gmp(reqData, "market", String.class, false));
input.setMaxBudget(TypeUtil.extractDouble(reqData.get("maxBudget")));
input.setExtraBed(TypeUtil.extractBool(reqData.get("extraBed")));
input.setHotelId(ApiUtil.gmp(reqData, "hotelId", Integer.class, false));
input.setRoomId(ApiUtil.gmp(reqData, "roomId", Integer.class, false));
String context = ApiUtil.gmp(reqData, "context", String.class, false);
isEmployee = isEmployee && "config".equalsIgnoreCase(context);
// Create result container
CHotelSearchResult res = TlinqEntityFactory.instance()
.newEntity(CHotelSearchResult.class, null);
res.setInput(input);
// Call custom service
List<CHotelSearchResult> sres = (List<CHotelSearchResult>)
ef.callCustomService(res, "searchAllHotels",
Arrays.asList(isEmployee));
if((sres != null) && (sres.size() > 0))
response = new TlinqApiResponse(sres.get(0));
else
response = new TlinqApiResponse(res);
} catch (TlinqClientException e) {
response = new TlinqApiResponse(e.getErrorCode(), e.getMessage());
} catch (ParseException pe) {
response = new TlinqApiResponse(TlinqErr.INVALID_FORMAT,
"Check in / out date in invalid format!");
}
return response;
}
Complex Pricing Algorithm (Custom Service - see pricing algorithm in separate section below)
Response:
{
"success": true,
"data": {
"input": {
"checkIn": "2025-12-15T00:00:00.000Z",
"checkOut": "2025-12-20T00:00:00.000Z",
"adults": 2,
"children": 1,
"mealPlans": "BB",
"market": "UAE"
},
"results": [
{
"hotelId": 1042,
"hotelName": "Atlantis The Palm",
"hotelCode": "ATLJMB",
"area": "DXB",
"stars": 5,
"roomId": 5012,
"roomName": "Deluxe Room - King Bed",
"nights": 5,
"cost": 3125.00, // Agency cost (contract rate)
"price": 3750.00, // Customer price (with margin)
"margin": 20.00, // Margin percentage
"breakdown": [
"Night 1 (15-Dec): 525.00 (Base: 500, Adult: 0, Child: 50, Meal: 60)",
"Night 2 (16-Dec): 525.00",
"Night 3 (17-Dec): 525.00",
"Night 4 (18-Dec): 525.00",
"Night 5 (19-Dec): 650.00 (Weekend: base 500 + special 150 + adult 0 + child 60 + meal 70)"
],
"available": true,
"onRequest": false
},
{
"hotelId": 1043,
"hotelName": "Jumeirah Beach Hotel",
...
}
]
}
}
Price Calculation Details: For each hotel → each room → each night: 1. Query calendar entry for (roomId, market, stayDate) 2. Skip if any night missing 3. For each night: - Determine base occupancy from rateBase (DBL = 2 persons) - Get effective base rate: * If specialDay: Apply sdRateType calculation (ABS/ADD/PCT/PCO) * Else: Use baseRate - Calculate extra adults: adults - base occupancy (e.g., 2 - 2 = 0) - Calculate extra children: children count (e.g., 1) - Add extra adult charges (use adultSdRate if special day) - Add extra child charges (use childSdRate if special day) - Add extra bed charge if required - Add meal supplement: Parse mealSupplements JSON, find requested meal plan, add costs - Sum = night cost 4. Total cost = sum of all nights 5. Apply margin: sellPrice = cost × (1 + margin% / 100) 6. Sort results by price ascending
5.2 POST /hotel/createBookingRequest¶
Purpose: Create a booking request (lead) for a hotel
Implementation: HotelApi.java:325-346 (endpoint), 802-827 (logic)
Authorization: Any authenticated user
Request:
{
"session": "user_session_token",
"tripRequest": {
"requestType": "HOTEL",
"status": "NEW",
"adults": 2,
"children": 1,
"checkIn": "2025-12-15T00:00:00.000Z",
"checkOut": "2025-12-20T00:00:00.000Z",
"hotelId": 1042,
"roomId": 5012,
"mealPlan": "BB",
"totalCost": 3125.00,
"totalPrice": 3750.00,
"customerName": "John Smith",
"customerEmail": "john.smith@example.com",
"customerPhone": "+971501234567",
"notes": "Late check-in requested",
"market": "UAE"
}
}
Implementation:
private TlinqApiResponse doCreateBookingRequest(String session, Map reqData) {
TlinqApiResponse response = null;
try {
ServiceFactoryConfig ff = ClientConfig.instance().getDefaultFactory();
Map tripReq = (Map)reqData.get("tripRequest");
String trip = TypeUtil.extractJsonFromMap(tripReq);
// Deserialize JSON to CTripRequest entity
CTripRequest req = TypeUtil.extractFromJson(trip, CTripRequest.class);
// Save using TripRequestFacade
TripRequestFacade tf = new TripRequestFacade(session);
tf.setCurrentRequest(req);
tf.save();
// Optionally send notification (commented out)
// tf.sendLeadCreatedNotification(theUser);
CTripRequest savedReq = tf.getCurrentRequest();
response = new TlinqApiResponse(savedReq);
} catch (TlinqClientException ex) {
response = new TlinqApiResponse(ex.getErrorCode(), ex.getMessage());
}
return response;
}
Response:
{
"success": true,
"data": {
"requestId": 78456,
"requestType": "HOTEL",
"status": "NEW",
"createdDate": "2025-11-25T14:30:00.000Z",
"adults": 2,
"children": 1,
"checkIn": "2025-12-15T00:00:00.000Z",
"checkOut": "2025-12-20T00:00:00.000Z",
"hotelId": 1042,
"roomId": 5012,
"mealPlan": "BB",
"totalCost": 3125.00,
"totalPrice": 3750.00,
"customerName": "John Smith",
"customerEmail": "john.smith@example.com",
"customerPhone": "+971501234567",
"notes": "Late check-in requested",
"market": "UAE"
}
}
Note: This creates a "lead" or "inquiry" record, not a confirmed booking. Follow-up workflow handled by separate system.
Price Calculation Algorithm¶
Overview¶
The most complex logic in the Hotel API is the accommodation search price calculation. This algorithm is executed in the custom service searchAllHotels called by doSearchAccommodation.
Input Parameters¶
- checkIn, checkOut dates
- adults, children counts
- mealPlan requested
- market segment (UAE/GCC/ROW)
- Optional filters: hotelId, roomId, area, maxBudget
Algorithm Steps¶
FOR each hotel in database (filtered by area, hotelId if provided):
FOR each room in hotel (filtered by roomId if provided):
// Step 1: Validate room capacity
IF room.maxAdults < adults:
SKIP room (too many adults)
IF room.maxOccupancy < (adults + children):
SKIP room (too many people)
// Step 2: Query calendar entries
nights = []
FOR each night FROM checkIn TO (checkOut - 1):
entry = QUERY calendar_entry WHERE:
roomId = room.roomId
AND market = search.market
AND stayDate = night
AND available = true
AND stopSale = false
IF entry NOT FOUND:
SKIP room (not available for all nights)
nights.ADD(entry)
END FOR
// Step 3: Calculate cost for each night
totalCost = 0
breakdown = []
FOR each night_entry IN nights:
nightCost = 0
// 3a. Determine base occupancy
baseOccupancy = PARSE_OCCUPANCY(night_entry.rateBase)
// SGL=1, DBL=2, TRPL=3, QPL=4
// 3b. Calculate effective base rate
IF night_entry.specialDay == true:
baseRate = CALCULATE_SPECIAL_RATE(
night_entry.baseRate,
night_entry.specDayRate,
night_entry.sdRateType
)
// ABS: use specDayRate directly
// ADD: baseRate + specDayRate
// PCT: baseRate + (baseRate × specDayRate / 100)
// PCO: baseRate × (specDayRate / 100)
ELSE:
baseRate = night_entry.baseRate
END IF
nightCost += baseRate
// 3c. Calculate extra adult charges
extraAdults = MAX(0, adults - baseOccupancy)
IF extraAdults > 0:
adultRate = night_entry.specialDay ?
night_entry.adultSdRate : night_entry.adultRate
adultCharge = CALCULATE_SUPPLEMENT(
baseRate,
adultRate,
night_entry.adultRateType,
extraAdults
)
nightCost += adultCharge
END IF
// 3d. Calculate child charges
IF children > 0:
childRate = night_entry.specialDay ?
night_entry.childSdRate : night_entry.childRate
childCharge = CALCULATE_SUPPLEMENT(
baseRate,
childRate,
night_entry.childRateType,
children
)
nightCost += childCharge
END IF
// 3e. Add extra bed charge if required
IF room.extraBedRequired AND adults > room.maxAdults:
nightCost += night_entry.extraBedRate
END IF
// 3f. Add meal supplement
IF search.mealPlan != night_entry.mealBase:
mealSupps = PARSE_JSON(night_entry.mealSupplements)
mealSuppObj = FIND_IN_ARRAY(mealSupps,
WHERE mealPlan = search.mealPlan)
IF mealSuppObj FOUND:
dayIndex = night_entry.specialDay ? 1 : 0
adultMealCost = mealSuppObj.adlRate[dayIndex] × adults
childMealCost = mealSuppObj.chldRate[dayIndex] × children
nightCost += adultMealCost + childMealCost
END IF
END IF
// 3g. Add to total
totalCost += nightCost
breakdown.ADD("Night " + nightDate + ": " + nightCost + " ...")
END FOR
// Step 4: Apply margin (if employee context, show cost; else show price)
IF isEmployee == false:
margin = GET_HOTEL_MARGIN(hotel.hotelId) // e.g., 20%
sellPrice = totalCost × (1 + margin / 100)
ELSE:
sellPrice = totalCost // Employees see cost
END IF
// Step 5: Apply max budget filter
IF search.maxBudget IS NOT NULL:
IF sellPrice > search.maxBudget:
SKIP room
END IF
// Step 6: Add to results
results.ADD({
hotel: hotel,
room: room,
cost: totalCost,
price: sellPrice,
breakdown: breakdown,
available: true,
onRequest: false
})
END FOR (rooms)
END FOR (hotels)
// Step 7: Sort results by price ascending
results.SORT_BY(price ASC)
RETURN results
Helper Functions¶
CALCULATE_SPECIAL_RATE(baseRate, specDayRate, sdRateType)¶
SWITCH sdRateType:
CASE "ABS": RETURN specDayRate
CASE "ADD": RETURN baseRate + specDayRate
CASE "PCT": RETURN baseRate + (baseRate × specDayRate / 100)
CASE "PCO": RETURN baseRate × (specDayRate / 100)
DEFAULT: RETURN baseRate
END SWITCH
CALCULATE_SUPPLEMENT(baseRate, supplementRate, rateType, quantity)¶
perPersonCharge = 0
SWITCH rateType:
CASE "ABS": perPersonCharge = supplementRate
CASE "ADD": perPersonCharge = supplementRate
CASE "PCT": perPersonCharge = baseRate × (supplementRate / 100)
CASE "PCO": perPersonCharge = baseRate × (supplementRate / 100)
DEFAULT: perPersonCharge = supplementRate
END SWITCH
RETURN perPersonCharge × quantity
PARSE_OCCUPANCY(rateBase)¶
SWITCH rateBase:
CASE "SGL": RETURN 1
CASE "DBL": RETURN 2
CASE "TRPL": RETURN 3
CASE "QPL": RETURN 4
DEFAULT: RETURN 2
END SWITCH
Example Calculation¶
Input: - Check-in: Dec 15, 2025 (Monday) - Check-out: Dec 20, 2025 (Saturday) - Adults: 2, Children: 1 - Meal plan: BB - Market: UAE
Room Configuration: - Rate base: DBL (2 persons) - Base rate: 500 AED - Special day rate: 150 AED (ADD type) - Adult rate: 100 AED (ADD type) - Child rate: 50 AED (ADD type) - Special days: Friday & Saturday
Meal Supplements:
Calculation:
Monday-Thursday (4 nights, normal days): - Base rate: 500 - Extra adults: 2 - 2 = 0 (no charge) - Extra children: 1 × 50 = 50 - Meal supplement: (2 × 60) + (1 × 30) = 150 - Night cost: 500 + 0 + 50 + 150 = 700 AED - 4 nights: 700 × 4 = 2,800 AED
Friday (1 night, special day): - Base rate: 500 + 150 = 650 (ADD type) - Extra adults: 0 - Extra children: 1 × 60 (childSdRate) = 60 - Meal supplement: (2 × 70) + (1 × 35) = 175 - Night cost: 650 + 0 + 60 + 175 = 885 AED
Total Cost: 2,800 + 885 = 3,685 AED
Sell Price (with 20% margin): 3,685 × 1.20 = 4,422 AED
Error Handling¶
Error Response Structure¶
{
"success": false,
"error": {
"code": "ERROR_CODE",
"message": "Human-readable error description"
}
}
Common Error Codes¶
| Error Code | Description | Example Scenario |
|---|---|---|
| SESSION_ERROR | Invalid or expired session | User not logged in |
| MISSING_PARAMETER | Required parameter not provided | Missing hotelId in request |
| INVALID_FORMAT | Data format error | Date in wrong format |
| GENERAL | Unhandled exception | Database connection error |
| NOT_FOUND | Entity not found | Hotel ID does not exist |
| PERMISSION_DENIED | Insufficient permissions | Non-employee accessing employee-only endpoint |
Exception Handling Pattern¶
try {
// Business logic
} catch (TlinqClientException e) {
// Business exception (expected errors)
logger.severe(e.getMessage());
response = new TlinqApiResponse(e.getErrorCode(), e.getMessage());
} catch (ParseException e) {
// Date parsing error
response = new TlinqApiResponse(TlinqErr.INVALID_FORMAT,
"Invalid date format: " + e.getMessage());
} catch (Exception ex) {
// Unexpected errors
response = new TlinqApiResponse(TlinqErr.GENERAL,
ex.getMessage() == null ? ex.getClass().getName() : ex.getMessage());
}
Logging¶
Logging Pattern¶
Every endpoint logs: 1. BEGIN: When request starts 2. END: When request completes (success or failure)
logger.info("BEGIN endpointName for " + session);
// ... business logic ...
logger.info("END endpointName for " + session);
Log Levels¶
- INFO: Request start/end, normal operations
- FINE: Debug information, search details
- SEVERE: Errors, exceptions
Example Log Output¶
2025-11-25 14:30:15.123 [INFO] BEGIN listHotels for 7f8a9b2c3d4e5f6g
2025-11-25 14:30:15.345 [INFO] END listHotels for 7f8a9b2c3d4e5f6g
2025-11-25 14:30:20.567 [INFO] BEGIN searchAccommodation for 7f8a9b2c3d4e5f6g
2025-11-25 14:30:21.890 [INFO] END searchAccommodation for 7f8a9b2c3d4e5f6g
2025-11-25 14:31:05.234 [SEVERE] TlinqClientException: Hotel ID 9999 not found
Performance Considerations¶
Database Query Optimization¶
- Indexed Searches:
- Hotel search by name: Uses
LIKE '%name%'on indexed column -
Calendar queries: Composite index on (roomId, market, stayDate)
-
Batch Operations:
- Calendar creation: Bulk insert for date ranges
-
Calendar update: Batch update with single transaction
-
Custom Services:
- Complex queries delegated to database layer
- Use of stored procedures or prepared statements (via EntityFacade)
Caching Opportunities¶
Currently no caching implemented. Potential improvements:
- Hotel Master Data: Cache hotel list (low churn rate)
- Meal Plans: Cache meal plan list (rarely changes)
- Calendar Lookups: Cache frequently accessed date ranges
- Search Results: Short-lived cache (5 minutes) for identical search criteria
Response Time Targets¶
| Endpoint | Target | Notes |
|---|---|---|
| listHotels | < 500ms | List operations |
| getHotel | < 200ms | Single read |
| saveHotel | < 1s | Write operation |
| listRoomCalendar | < 1s | Date range query |
| createRoomCalendar | < 3s | Bulk insert (90 days) |
| updateRoomCalendar | < 2s | Bulk update |
| searchAccommodation | < 5s | Complex price calculation |
Summary¶
The Hotel Management API consists of 16+ endpoints organized into 5 functional areas:
- Hotel Management (4 endpoints): CRUD operations for hotel master data
- Room Management (2 endpoints): CRUD operations for room types
- Calendar Management (5 endpoints): Bulk calendar operations and queries
- Meal Plans (2 endpoints): Meal plan configuration
- Search & Booking (2 endpoints): Accommodation search and booking requests
Key Implementation Patterns: - Consistent POST-only REST design - Session-based authentication - Standard request/response structure (TlinqApiResponse) - Comprehensive error handling - Request/response logging - Custom service delegation for complex operations - EntityFacade ORM abstraction
Complex Features: - Multi-market pricing (UAE/GCC/ROW) - Special day rate calculations (4 types: ABS/ADD/PCT/PCO) - Occupancy-based pricing - Meal plan supplements with JSON storage - Comprehensive availability filtering - Detailed price breakdown generation
This API design prioritizes: - ✅ Consistency: All endpoints follow same pattern - ✅ Security: Employee-only write operations - ✅ Flexibility: Support for anonymous searches - ✅ Traceability: Comprehensive logging - ✅ Error Handling: Structured error responses - ✅ Performance: Batch operations, indexed queries