Skip to content

Cruise Offer Management - Implementation Plan

Status: COMPLETED (2026-01-21)

All phases of this implementation plan have been completed. See Implementation Status below for details.

1. Introduction

1.1 Purpose

This document provides a detailed implementation plan for enhancing the Cruise Offer Management module to meet the requirements specified in Cruise-Offer-Management-Requirements.md. It analyzes the gap between current implementation and requirements, and outlines the changes needed across all layers.

1.2 Current State Summary

As of 2026-01-21, the cruise module is fully implemented with: - 54 entity classes (canonical, NTS, and database layers) - CruiseFacade with 100+ methods covering all CRUD, delete, status change, tree, and booking operations - NTSCruiseService with search, list, and custom operations - 69 API endpoints (full CRUD for all entities plus booking interface) - Complete database schema with all tables and new status/name fields - Full entity configuration in cruise-entities.xml with all field mappings - CruiseValidationUtil for input validation and security - CruisePricingUtil for retail price calculations - Integration tests in CruiseFacadeTest.java

1.3 Gap Summary (RESOLVED)

The gaps identified in the original plan have all been addressed: 1. ~~API Layer: Only 3 of ~55 required endpoints implemented~~ → 69 endpoints implemented 2. ~~Database Schema: Missing 4 columns (status fields, name, description)~~ → All columns added 3. ~~Facade Layer: Missing delete methods, cascade operations, tree/hierarchy methods~~ → All methods implemented 4. ~~Validation: No business rule validation or error code handling~~ → CruiseValidationUtil implemented 5. ~~New Features: Booking interface endpoints, price calculation, point regeneration~~ → All features implemented

Implementation Status

Phase Description Status
Phase 1 Database Schema Updates ✅ COMPLETE
Phase 2 Entity Class Updates ✅ COMPLETE
Phase 3 Facade Layer Enhancements ✅ COMPLETE
Phase 4 API Endpoints - Dimension Data ✅ COMPLETE
Phase 5 API Endpoints - Itinerary & Cruise Management ✅ COMPLETE
Phase 6 API Endpoints - Pricing Management ✅ COMPLETE
Phase 7 API Endpoints - Search & Booking ✅ COMPLETE
Phase 8 Error Handling & Validation ✅ COMPLETE
Phase 9 Integration Testing ✅ COMPLETE

Key Implementation Files

Component File Lines
API Endpoints tqapi/.../CruiseApi.java ~3000
Facade Layer tqapp/.../CruiseFacade.java ~2000
Validation tqapp/.../CruiseValidationUtil.java 322
Pricing tqapp/.../CruisePricingUtil.java 186
Entity Config config/entities/cruise-entities.xml 411
Service Config config/nts-client.xml (cruise section) ~300
API Roles config/api-roles.properties (cruise section) 103
Integration Tests tqapp/.../CruiseFacadeTest.java ~800

API Endpoints Summary (69 total)

  • Legacy endpoints (3): listCompanies, searchCruises, getCruiseArea
  • Company CRUD (4): list, read, write, delete
  • Area CRUD (4): list, read, write, delete
  • Ship CRUD (4): list, read, write, delete
  • CabinType CRUD (4): list, read, write, delete
  • ChargeType CRUD (4): list, read, write, delete
  • Port CRUD (4): list, read, write, delete
  • ShipCabin (5): list, read, write, delete, bulkAssign
  • Itinerary (6): list, read, write, delete, changeStatus, tree
  • Template CRUD (4): list, read, write, delete
  • Cruise (5): list, read, write, delete, changeStatus
  • CruisePoint (4): list, write, delete, regenerate
  • CabinCharge (4): list, write, delete, initForCruise
  • OtherCharge (3): list, write, delete
  • CabinPriceTemplate (4): list, write, delete, initForItinerary
  • OtherChargeTemplate (3): list, write, delete
  • Booking (2): availability, calculatePrice
  • Other (2): itinerary/cruises, detail

2. Implementation Phases (Reference)

Phase 1: Database Schema Updates

Phase 2: Entity Class Updates

Phase 3: Facade Layer Enhancements

Phase 4: API Endpoints - Dimension Data

Phase 5: API Endpoints - Itinerary & Cruise Management

Phase 6: API Endpoints - Pricing Management

Phase 7: API Endpoints - Search & Booking

Phase 8: Error Handling & Validation

Phase 9: Integration Testing


3. Phase 1: Database Schema Updates

3.1 Required Schema Changes

Change ID Table Column Type Description Priority
SCH-001 itinerary status varchar(20) Active/Inactive status [M]
SCH-002 itinerary name varchar(100) Display name [M]
SCH-003 cruise status varchar(20) available/cancelled/sold-out [M]
SCH-004 shipcabin description varchar(500) Cabin amenities description [O]
SCH-005 cruisetemplate description varchar(200) Stop description [O]
SCH-006 cruisepoint stopseq integer Stop sequence for ordering [O]
SCH-007 othercharge paxtype varchar(20) adult/child/all classification [O]

3.2 SQL Migration Script

File: config/db-changes/cruise-schema-updates.sql

-- Phase 1: Schema Updates for Cruise Offer Management
-- Version: 1.0
-- Date: 2025-01

-- SCH-001: Add status to itinerary
ALTER TABLE nts.itinerary
ADD COLUMN IF NOT EXISTS status varchar(20) DEFAULT 'active';

-- SCH-002: Add name to itinerary
ALTER TABLE nts.itinerary
ADD COLUMN IF NOT EXISTS name varchar(100);

-- SCH-003: Add status to cruise
ALTER TABLE nts.cruise
ADD COLUMN IF NOT EXISTS status varchar(20) DEFAULT 'available';

-- SCH-004: Add description to shipcabin (optional)
ALTER TABLE nts.shipcabin
ADD COLUMN IF NOT EXISTS description varchar(500);

-- SCH-005: Add description to cruisetemplate (optional)
ALTER TABLE nts.cruisetemplate
ADD COLUMN IF NOT EXISTS description varchar(200);

-- SCH-006: Add stopseq to cruisepoint (optional)
ALTER TABLE nts.cruisepoint
ADD COLUMN IF NOT EXISTS stopseq integer;

-- SCH-007: Add paxtype to othercharge (optional)
ALTER TABLE nts.othercharge
ADD COLUMN IF NOT EXISTS paxtype varchar(20) DEFAULT 'all';

-- Add unique constraints for codes
ALTER TABLE nts.cabintype
ADD CONSTRAINT IF NOT EXISTS uq_cabintypecode UNIQUE (code);

ALTER TABLE nts.cruiseport
ADD CONSTRAINT IF NOT EXISTS uq_cruiseportcode UNIQUE (code);

ALTER TABLE nts.cruiseship
ADD CONSTRAINT IF NOT EXISTS uq_cruiseshipcode UNIQUE (code);

-- Add index for status filtering
CREATE INDEX IF NOT EXISTS idx_itinerary_status ON nts.itinerary(status);
CREATE INDEX IF NOT EXISTS idx_cruise_status ON nts.cruise(status);

-- Add unique constraint for ship cabin (one cabin type per ship)
ALTER TABLE nts.shipcabin
ADD CONSTRAINT IF NOT EXISTS uq_shipcabin_ship_type UNIQUE (shipid, cabintypeid);

-- Add unique constraint for cabin charge (one per cruise/cabin)
ALTER TABLE nts.cabincharge
ADD CONSTRAINT IF NOT EXISTS uq_cabincharge_cruise_cabin UNIQUE (cruiseid, shipcabinid);

-- Add unique constraint for other charge (one charge type per cruise)
ALTER TABLE nts.othercharge
ADD CONSTRAINT IF NOT EXISTS uq_othercharge_cruise_type UNIQUE (cruiseid, chargetypeid);

3.3 Implementation Tasks

Task ID Description Effort
DB-001 Create migration SQL script 1h
DB-002 Test migration on dev database 1h
DB-003 Update existing data with default values 0.5h
DB-004 Verify constraints don't break existing data 1h

4. Phase 2: Entity Class Updates

4.1 Database Entity Updates (JPA)

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

4.1.1 ItineraryEntity.java

Change Description
Add field private String status;
Add field private String name;
Add getter/setter getStatus(), setStatus()
Add getter/setter getName(), setName()
Update @Column Add column annotations
// Add to ItineraryEntity.java
@Column(name = "status")
private String status;

@Column(name = "name")
private String name;

// Add getters and setters

4.1.2 CruiseEntity.java

Change Description
Add field private String status;
Add getter/setter getStatus(), setStatus()

4.1.3 ShipcabinEntity.java

Change Description
Add field private String description;
Add getter/setter getDescription(), setDescription()

4.1.4 CruisetemplateEntity.java

Change Description
Add field private String description;
Add getter/setter getDescription(), setDescription()

4.1.5 CruisepointEntity.java

Change Description
Add field private Integer stopseq;
Add getter/setter getStopseq(), setStopseq()

4.1.6 OtherchargeEntity.java

Change Description
Add field private String paxtype;
Add getter/setter getPaxtype(), setPaxtype()

4.2 Canonical Entity Updates

Location: tqapp/src/main/java/com/perun/tlinq/entity/cruise/

4.2.1 CCruiseItinerary.java

// Add fields
private String status;      // "active" or "inactive"
private String name;        // Display name

// Add getters/setters
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }

4.2.2 CCruise.java

// Add field
private String status;      // "available", "cancelled", "sold-out"

// Add getter/setter
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }

4.2.3 CShipCabin.java

// Add field
private String description;

// Add getter/setter

4.2.4 CCruiseTemplate.java

// Add field
private String description;

// Add getter/setter

4.2.5 CCruisePoint.java

// Add field
private Integer stopSeq;

// Add getter/setter

4.2.6 COtherCharge.java

// Add field
private String paxType;     // "adult", "child", "all"

// Add getter/setter

4.3 New Canonical Entity Classes

4.3.1 CCruiseAvailability.java (NEW)

Location: tqapp/src/main/java/com/perun/tlinq/entity/cruise/CCruiseAvailability.java

package com.perun.tlinq.entity.cruise;

import com.perun.tlinq.entity.TlinqEntity;
import java.util.List;

/**
 * Cruise availability response for booking interface
 */
public class CCruiseAvailability extends TlinqEntity {
    private CCruise cruise;
    private CCruiseItinerary itinerary;
    private CCruiseShip ship;
    private CCruiseCompany company;
    private List<CCruiseCabinRecord> availableCabins;
    private List<CCruiseChargeRecord> mandatoryCharges;
    private List<CCruiseChargeRecord> optionalCharges;
    private List<CCruisePointRecord> cruisePoints;

    // Getters and setters
}

4.3.2 CPriceCalculation.java (NEW)

Location: tqapp/src/main/java/com/perun/tlinq/entity/cruise/CPriceCalculation.java

package com.perun.tlinq.entity.cruise;

import com.perun.tlinq.entity.TlinqEntity;
import java.util.List;

/**
 * Price calculation result for booking
 */
public class CPriceCalculation extends TlinqEntity {
    private Integer cruiseId;
    private Integer shipCabinId;
    private Integer adults;
    private Integer children;

    private Double cabinCharge;
    private String cabinCurrency;

    private Double mandatoryChargesTotal;
    private Double optionalChargesTotal;
    private Double grandTotal;

    private List<CPriceLineItem> lineItems;

    // Getters and setters
}

4.3.3 CPriceLineItem.java (NEW)

package com.perun.tlinq.entity.cruise;

import com.perun.tlinq.entity.TlinqEntity;

/**
 * Individual price line item
 */
public class CPriceLineItem extends TlinqEntity {
    private String description;
    private Double unitPrice;
    private Integer quantity;
    private Double totalPrice;
    private String currency;
    private String chargeType;  // "cabin", "mandatory", "optional"

    // Getters and setters
}

4.3.4 CItineraryTreeNode.java (NEW)

package com.perun.tlinq.entity.cruise;

import com.perun.tlinq.entity.TlinqEntity;
import java.util.List;

/**
 * Hierarchical tree node for company/itinerary display
 */
public class CItineraryTreeNode extends TlinqEntity {
    private Integer companyId;
    private String companyCode;
    private String companyName;
    private List<CItineraryTreeItem> itineraries;

    // Getters and setters
}

4.3.5 CItineraryTreeItem.java (NEW)

package com.perun.tlinq.entity.cruise;

import com.perun.tlinq.entity.TlinqEntity;

/**
 * Itinerary item within tree node
 */
public class CItineraryTreeItem extends TlinqEntity {
    private Integer itineraryId;
    private String code;
    private String name;
    private Integer duration;
    private String status;
    private String shipName;
    private String areaName;
    private Integer cruiseCount;

    // Getters and setters
}

4.4 Entity Configuration Updates

File: config/entities/cruise-entities.xml

Add field mappings for new columns:

<!-- Update CCruiseItinerary mapping -->
<FieldMapping targetField="status" sourceField="status" mapping="DirectMapping"/>
<FieldMapping targetField="name" sourceField="name" mapping="DirectMapping"/>

<!-- Update CCruise mapping -->
<FieldMapping targetField="status" sourceField="status" mapping="DirectMapping"/>

<!-- Update CShipCabin mapping -->
<FieldMapping targetField="description" sourceField="description" mapping="DirectMapping"/>

<!-- Update CCruiseTemplate mapping -->
<FieldMapping targetField="description" sourceField="description" mapping="DirectMapping"/>

<!-- Update CCruisePoint mapping -->
<FieldMapping targetField="stopSeq" sourceField="stopseq" mapping="DirectMapping"/>

<!-- Update COtherCharge mapping -->
<FieldMapping targetField="paxType" sourceField="paxtype" mapping="DirectMapping"/>

Add new entity configurations for tree/availability entities:

<!-- CItineraryTreeNode - for hierarchical display -->
<Entity name="ItineraryTreeNode" class="com.perun.tlinq.entity.cruise.CItineraryTreeNode" defaultFactory="NTSServiceFactory">
    <EntityFactoryList>
        <Factory name="NTSServiceFactory" nativeEntity="com.perun.tlinq.client.nts.entity.cruise.NTSItineraryTreeNode">
            <ServiceList>
                <Service name="getItineraryTree" action="search" returnClass="com.perun.tlinq.entity.cruise.CItineraryTreeNode"/>
            </ServiceList>
            <FieldMappingList>
                <FieldMapping targetField="companyId" sourceField="companyId" mapping="DirectMapping"/>
                <FieldMapping targetField="companyCode" sourceField="companyCode" mapping="DirectMapping"/>
                <FieldMapping targetField="companyName" sourceField="companyName" mapping="DirectMapping"/>
                <FieldMapping targetField="itineraries" targetFieldEntity="ItineraryTreeItem" sourceField="itineraries" mapping="ArrayMapping"/>
            </FieldMappingList>
        </Factory>
    </EntityFactoryList>
</Entity>

<!-- CItineraryTreeItem -->
<Entity name="ItineraryTreeItem" class="com.perun.tlinq.entity.cruise.CItineraryTreeItem" defaultFactory="NTSServiceFactory">
    <EntityFactoryList>
        <Factory name="NTSServiceFactory" nativeEntity="com.perun.tlinq.client.nts.entity.cruise.NTSItineraryTreeItem">
            <FieldMappingList>
                <FieldMapping targetField="itineraryId" sourceField="itineraryId" mapping="DirectMapping"/>
                <FieldMapping targetField="code" sourceField="code" mapping="DirectMapping"/>
                <FieldMapping targetField="name" sourceField="name" mapping="DirectMapping"/>
                <FieldMapping targetField="duration" sourceField="duration" mapping="DirectMapping"/>
                <FieldMapping targetField="status" sourceField="status" mapping="DirectMapping"/>
                <FieldMapping targetField="shipName" sourceField="shipName" mapping="DirectMapping"/>
                <FieldMapping targetField="areaName" sourceField="areaName" mapping="DirectMapping"/>
                <FieldMapping targetField="cruiseCount" sourceField="cruiseCount" mapping="DirectMapping"/>
            </FieldMappingList>
        </Factory>
    </EntityFactoryList>
</Entity>

<!-- CCruiseAvailability - for booking interface -->
<Entity name="CruiseAvailability" class="com.perun.tlinq.entity.cruise.CCruiseAvailability" defaultFactory="NTSServiceFactory">
    <EntityFactoryList>
        <Factory name="NTSServiceFactory" nativeEntity="com.perun.tlinq.client.nts.entity.cruise.NTSCruiseAvailability">
            <ServiceList>
                <Service name="getCruiseAvailability" action="read" returnClass="com.perun.tlinq.entity.cruise.CCruiseAvailability">
                    <NamedParams>
                        <Param name="cruiseId" source="input"/>
                    </NamedParams>
                </Service>
            </ServiceList>
            <FieldMappingList>
                <FieldMapping targetField="cruise" targetFieldEntity="Cruise" sourceField="cruise" mapping="DirectMapping"/>
                <FieldMapping targetField="itinerary" targetFieldEntity="CruiseItinerary" sourceField="itinerary" mapping="DirectMapping"/>
                <FieldMapping targetField="ship" targetFieldEntity="CruiseShip" sourceField="ship" mapping="DirectMapping"/>
                <FieldMapping targetField="company" targetFieldEntity="CruiseCompany" sourceField="company" mapping="DirectMapping"/>
                <FieldMapping targetField="availableCabins" targetFieldEntity="CruiseCabinRecord" sourceField="availableCabins" mapping="ArrayMapping"/>
                <FieldMapping targetField="mandatoryCharges" targetFieldEntity="CruiseChargeRecord" sourceField="mandatoryCharges" mapping="ArrayMapping"/>
                <FieldMapping targetField="optionalCharges" targetFieldEntity="CruiseChargeRecord" sourceField="optionalCharges" mapping="ArrayMapping"/>
                <FieldMapping targetField="cruisePoints" targetFieldEntity="CruisePointRecord" sourceField="cruisePoints" mapping="ArrayMapping"/>
            </FieldMappingList>
        </Factory>
    </EntityFactoryList>
</Entity>

<!-- CPriceCalculation - for booking price calculation -->
<Entity name="PriceCalculation" class="com.perun.tlinq.entity.cruise.CPriceCalculation" defaultFactory="NTSServiceFactory">
    <EntityFactoryList>
        <Factory name="NTSServiceFactory" nativeEntity="com.perun.tlinq.client.nts.entity.cruise.NTSPriceCalculation">
            <ServiceList>
                <Service name="calculateCruisePrice" action="read" returnClass="com.perun.tlinq.entity.cruise.CPriceCalculation">
                    <NamedParams>
                        <Param name="cruiseId" source="input"/>
                        <Param name="shipCabinId" source="input"/>
                        <Param name="adults" source="input"/>
                        <Param name="children" source="input"/>
                        <Param name="optionalChargeIds" source="input"/>
                    </NamedParams>
                </Service>
            </ServiceList>
            <FieldMappingList>
                <FieldMapping targetField="cruiseId" sourceField="cruiseId" mapping="DirectMapping"/>
                <FieldMapping targetField="shipCabinId" sourceField="shipCabinId" mapping="DirectMapping"/>
                <FieldMapping targetField="adults" sourceField="adults" mapping="DirectMapping"/>
                <FieldMapping targetField="children" sourceField="children" mapping="DirectMapping"/>
                <FieldMapping targetField="cabinCharge" sourceField="cabinCharge" mapping="DirectMapping"/>
                <FieldMapping targetField="cabinCurrency" sourceField="cabinCurrency" mapping="DirectMapping"/>
                <FieldMapping targetField="mandatoryChargesTotal" sourceField="mandatoryChargesTotal" mapping="DirectMapping"/>
                <FieldMapping targetField="optionalChargesTotal" sourceField="optionalChargesTotal" mapping="DirectMapping"/>
                <FieldMapping targetField="grandTotal" sourceField="grandTotal" mapping="DirectMapping"/>
                <FieldMapping targetField="lineItems" targetFieldEntity="PriceLineItem" sourceField="lineItems" mapping="ArrayMapping"/>
            </FieldMappingList>
        </Factory>
    </EntityFactoryList>
</Entity>

<!-- CPriceLineItem -->
<Entity name="PriceLineItem" class="com.perun.tlinq.entity.cruise.CPriceLineItem" defaultFactory="NTSServiceFactory">
    <EntityFactoryList>
        <Factory name="NTSServiceFactory" nativeEntity="com.perun.tlinq.client.nts.entity.cruise.NTSPriceLineItem">
            <FieldMappingList>
                <FieldMapping targetField="description" sourceField="description" mapping="DirectMapping"/>
                <FieldMapping targetField="unitPrice" sourceField="unitPrice" mapping="DirectMapping"/>
                <FieldMapping targetField="quantity" sourceField="quantity" mapping="DirectMapping"/>
                <FieldMapping targetField="totalPrice" sourceField="totalPrice" mapping="DirectMapping"/>
                <FieldMapping targetField="currency" sourceField="currency" mapping="DirectMapping"/>
                <FieldMapping targetField="chargeType" sourceField="chargeType" mapping="DirectMapping"/>
            </FieldMappingList>
        </Factory>
    </EntityFactoryList>
</Entity>

4.5 NTS Service Plugin Configuration

File: config/nts-client.xml

The NTS plugin configuration maps service names to implementation classes, methods, and native entities. The existing cruise services cover basic CRUD operations. Additional services are needed for delete operations and custom functionality.

4.5.1 Existing Cruise Services (No Changes Required)

The following services are already configured: - saveCruiseCompany, readCruiseCompany - saveCruisePort, readCruisePort - saveCabinType, readCabinType - saveChargeType, readChargeType - saveCruiseArea, readCruiseArea - saveCruiseShip, readCruiseShip - saveShipCabin, readShipCabin, listShipCabins - saveCruiseItinerary, readCruiseItinerary - saveCruise, readCruise - saveCruiseTemplate, readCruiseTemplate - saveCabinCharge, readCabinCharge - saveCruisePoint, readCruisePoint - saveOtherCharge, readOtherCharge - searchCruises

4.5.2 New Services to Add

Add the following service configurations to config/nts-client.xml in the Cruises section:

<!-- Delete Services for Cruise Entities -->

<Service name="deleteCruiseCompany"
         class="com.perun.tlinq.client.nts.service.NTSEntityDeleteService"
         method=""
         entity="com.perun.tlinq.client.nts.db.cruise.CruisecompanyEntity"
         idField="id"/>

<Service name="deleteCruisePort"
         class="com.perun.tlinq.client.nts.service.NTSEntityDeleteService"
         method=""
         entity="com.perun.tlinq.client.nts.db.cruise.CruiseportEntity"
         idField="id"/>

<Service name="deleteCabinType"
         class="com.perun.tlinq.client.nts.service.NTSEntityDeleteService"
         method=""
         entity="com.perun.tlinq.client.nts.db.cruise.CabintypeEntity"
         idField="id"/>

<Service name="deleteChargeType"
         class="com.perun.tlinq.client.nts.service.NTSEntityDeleteService"
         method=""
         entity="com.perun.tlinq.client.nts.db.cruise.ChargetypeEntity"
         idField="id"/>

<Service name="deleteCruiseArea"
         class="com.perun.tlinq.client.nts.service.NTSEntityDeleteService"
         method=""
         entity="com.perun.tlinq.client.nts.db.cruise.CruiseareaEntity"
         idField="id"/>

<Service name="deleteCruiseShip"
         class="com.perun.tlinq.client.nts.service.NTSEntityDeleteService"
         method=""
         entity="com.perun.tlinq.client.nts.db.cruise.CruiseshipEntity"
         idField="id"/>

<Service name="deleteShipCabin"
         class="com.perun.tlinq.client.nts.service.NTSEntityDeleteService"
         method=""
         entity="com.perun.tlinq.client.nts.db.cruise.ShipcabinEntity"
         idField="id"/>

<Service name="deleteCruiseItinerary"
         class="com.perun.tlinq.client.nts.service.NTSEntityDeleteService"
         method=""
         entity="com.perun.tlinq.client.nts.db.cruise.ItineraryEntity"
         idField="id"/>

<Service name="deleteCruise"
         class="com.perun.tlinq.client.nts.service.NTSEntityDeleteService"
         method=""
         entity="com.perun.tlinq.client.nts.db.cruise.CruiseEntity"
         idField="id"/>

<Service name="deleteCruiseTemplate"
         class="com.perun.tlinq.client.nts.service.NTSEntityDeleteService"
         method=""
         entity="com.perun.tlinq.client.nts.db.cruise.CruisetemplateEntity"
         idField="id"/>

<Service name="deleteCabinCharge"
         class="com.perun.tlinq.client.nts.service.NTSEntityDeleteService"
         method=""
         entity="com.perun.tlinq.client.nts.db.cruise.CabinchargeEntity"
         idField="id"/>

<Service name="deleteCruisePoint"
         class="com.perun.tlinq.client.nts.service.NTSEntityDeleteService"
         method=""
         entity="com.perun.tlinq.client.nts.db.cruise.CruisepointEntity"
         idField="id"/>

<Service name="deleteOtherCharge"
         class="com.perun.tlinq.client.nts.service.NTSEntityDeleteService"
         method=""
         entity="com.perun.tlinq.client.nts.db.cruise.OtherchargeEntity"
         idField="id"/>

<!-- Custom Cruise Services -->

<Service name="getItineraryTree"
         class="com.perun.tlinq.client.nts.service.cruise.NTSCruiseService"
         method="getItineraryTree"
         entity="com.perun.tlinq.client.nts.entity.cruise.NTSItineraryTreeNode"
         idField=""/>

<Service name="getCruiseAvailability"
         class="com.perun.tlinq.client.nts.service.cruise.NTSCruiseService"
         method="getCruiseAvailability"
         entity="com.perun.tlinq.client.nts.entity.cruise.NTSCruiseAvailability"
         idField=""/>

<Service name="calculateCruisePrice"
         class="com.perun.tlinq.client.nts.service.cruise.NTSCruiseService"
         method="calculateCruisePrice"
         entity="com.perun.tlinq.client.nts.entity.cruise.NTSPriceCalculation"
         idField=""/>

<Service name="regenerateCruisePoints"
         class="com.perun.tlinq.client.nts.service.cruise.NTSCruiseService"
         method="regenerateCruisePoints"
         entity="com.perun.tlinq.client.nts.db.cruise.CruisepointEntity"
         idField="id"/>

<Service name="initCabinCharges"
         class="com.perun.tlinq.client.nts.service.cruise.NTSCruiseService"
         method="initCabinCharges"
         entity="com.perun.tlinq.client.nts.db.cruise.CabinchargeEntity"
         idField="id"/>

4.5.3 NTS Service Configuration Summary

Service Name Class Method Entity Purpose
deleteCruiseCompany NTSEntityDeleteService - CruisecompanyEntity Delete company
deleteCruisePort NTSEntityDeleteService - CruiseportEntity Delete port
deleteCabinType NTSEntityDeleteService - CabintypeEntity Delete cabin type
deleteChargeType NTSEntityDeleteService - ChargetypeEntity Delete charge type
deleteCruiseArea NTSEntityDeleteService - CruiseareaEntity Delete area
deleteCruiseShip NTSEntityDeleteService - CruiseshipEntity Delete ship
deleteShipCabin NTSEntityDeleteService - ShipcabinEntity Delete ship cabin
deleteCruiseItinerary NTSEntityDeleteService - ItineraryEntity Delete itinerary
deleteCruise NTSEntityDeleteService - CruiseEntity Delete cruise
deleteCruiseTemplate NTSEntityDeleteService - CruisetemplateEntity Delete template
deleteCabinCharge NTSEntityDeleteService - CabinchargeEntity Delete cabin charge
deleteCruisePoint NTSEntityDeleteService - CruisepointEntity Delete cruise point
deleteOtherCharge NTSEntityDeleteService - OtherchargeEntity Delete other charge
getItineraryTree NTSCruiseService getItineraryTree NTSItineraryTreeNode Get tree hierarchy
getCruiseAvailability NTSCruiseService getCruiseAvailability NTSCruiseAvailability Get booking availability
calculateCruisePrice NTSCruiseService calculateCruisePrice NTSPriceCalculation Calculate total price
regenerateCruisePoints NTSCruiseService regenerateCruisePoints CruisepointEntity Regenerate from template
initCabinCharges NTSCruiseService initCabinCharges CabinchargeEntity Initialize cabin charges

4.5.4 NTSCruiseService Methods to Add

File: tqapp/src/main/java/com/perun/tlinq/client/nts/service/cruise/NTSCruiseService.java

Add the following methods to the existing NTSCruiseService class:

/**
 * Get hierarchical tree of companies and itineraries
 */
public List<NTSItineraryTreeNode> getItineraryTree(Map<String, Object> params) {
    boolean includeInactive = params.containsKey("includeInactive") &&
        (Boolean) params.get("includeInactive");
    // Implementation using JPA queries to build tree structure
    // ...
}

/**
 * Get cruise availability with all cabin/charge details
 */
public NTSCruiseAvailability getCruiseAvailability(Map<String, Object> params) {
    Integer cruiseId = (Integer) params.get("cruiseId");
    // Implementation combining cruise, itinerary, ship, cabins, charges, points
    // ...
}

/**
 * Calculate cruise price for booking
 */
public NTSPriceCalculation calculateCruisePrice(Map<String, Object> params) {
    Integer cruiseId = (Integer) params.get("cruiseId");
    Integer shipCabinId = (Integer) params.get("shipCabinId");
    Integer adults = (Integer) params.get("adults");
    Integer children = (Integer) params.get("children");
    List<Integer> optionalChargeIds = (List<Integer>) params.get("optionalChargeIds");
    // Implementation calculating total price with line items
    // ...
}

/**
 * Regenerate cruise points from template
 */
public List<CruisepointEntity> regenerateCruisePoints(Map<String, Object> params) {
    Integer cruiseId = (Integer) params.get("cruiseId");
    Boolean confirmOverwrite = (Boolean) params.get("confirmOverwrite");
    // Implementation deleting existing points and creating new ones from template
    // ...
}

/**
 * Initialize cabin charges for all ship cabins on a cruise
 */
public List<CabinchargeEntity> initCabinCharges(Map<String, Object> params) {
    Integer cruiseId = (Integer) params.get("cruiseId");
    // Implementation creating cabin charge records for all ship cabins
    // ...
}

4.5.5 New NTS Native Entity Classes Required

Location: tqapp/src/main/java/com/perun/tlinq/client/nts/entity/cruise/

Create the following NTS native entity classes to support the new services:

// NTSItineraryTreeNode.java
package com.perun.tlinq.client.nts.entity.cruise;

public class NTSItineraryTreeNode implements RemoteEntityI {
    private Integer companyId;
    private String companyCode;
    private String companyName;
    private List<NTSItineraryTreeItem> itineraries;
    // getters/setters
}

// NTSItineraryTreeItem.java
public class NTSItineraryTreeItem implements RemoteEntityI {
    private Integer itineraryId;
    private String code;
    private String name;
    private Integer duration;
    private String status;
    private String shipName;
    private String areaName;
    private Integer cruiseCount;
    // getters/setters
}

// NTSCruiseAvailability.java
public class NTSCruiseAvailability implements RemoteEntityI {
    private CruiseEntity cruise;
    private ItineraryEntity itinerary;
    private CruiseshipEntity ship;
    private CruisecompanyEntity company;
    private List<NTSCruiseCabinRecord> availableCabins;
    private List<NTSCruiseChargeRecord> mandatoryCharges;
    private List<NTSCruiseChargeRecord> optionalCharges;
    private List<NTSCruisePointRecord> cruisePoints;
    // getters/setters
}

// NTSPriceCalculation.java
public class NTSPriceCalculation implements RemoteEntityI {
    private Integer cruiseId;
    private Integer shipCabinId;
    private Integer adults;
    private Integer children;
    private Double cabinCharge;
    private String cabinCurrency;
    private Double mandatoryChargesTotal;
    private Double optionalChargesTotal;
    private Double grandTotal;
    private List<NTSPriceLineItem> lineItems;
    // getters/setters
}

// NTSPriceLineItem.java
public class NTSPriceLineItem implements RemoteEntityI {
    private String description;
    private Double unitPrice;
    private Integer quantity;
    private Double totalPrice;
    private String currency;
    private String chargeType;
    // getters/setters
}

4.6 Implementation Tasks

Task ID Description Effort
ENT-001 Update ItineraryEntity with status, name 0.5h
ENT-002 Update CruiseEntity with status 0.5h
ENT-003 Update other DB entities (shipcabin, template, point, othercharge) 1h
ENT-004 Update canonical entities (CCruiseItinerary, CCruise, etc.) 1h
ENT-005 Create CCruiseAvailability class 0.5h
ENT-006 Create CPriceCalculation and CPriceLineItem classes 0.5h
ENT-007 Create CItineraryTreeNode and CItineraryTreeItem classes 0.5h
ENT-008 Update cruise-entities.xml with new field mappings 1h
ENT-009 Add new entity configurations to cruise-entities.xml 1.5h
ENT-010 Add delete services to nts-client.xml (13 services) 1h
ENT-011 Add custom services to nts-client.xml (5 services) 0.5h
ENT-012 Create NTS native entity classes (5 classes) 2h
ENT-013 Implement NTSCruiseService custom methods (5 methods) 3h

5. Phase 3: Facade Layer Enhancements

5.1 CruiseFacade Existing Methods Status

Method Category Existing To Add/Modify
Company CRUD ✅ create, update, search, getById ❌ delete, validateDelete
Area CRUD ✅ create, update, search, getById ❌ delete, validateDelete
Ship CRUD ✅ create, update, search, getById ❌ delete, validateDelete
CabinType CRUD ✅ create, update, search, getById ❌ delete, validateDelete
ChargeType CRUD ✅ create, update, search, getById ❌ delete, validateDelete
Port CRUD ✅ create, update, search, getById ❌ delete, validateDelete
ShipCabin CRUD ✅ create, update, search, getById ❌ delete, bulkAssign, cascadeDelete
Itinerary CRUD ✅ create, update, search, getById ❌ delete, changeStatus, getTree
Template CRUD ✅ create, update, search, getById ❌ delete
Cruise CRUD ✅ create, update, search, getById ❌ delete, changeStatus, cascadeDelete
CruisePoint CRUD ✅ create, update, list ❌ delete, regenerate
CabinCharge CRUD ✅ create, update, search, getById ❌ delete, initForCruise
OtherCharge CRUD ✅ create, update, list, getById ❌ delete
Search ✅ executeCruiseSearch ❌ enhanced filters
Booking ❌ getAvailability, calculatePrice

5.2 New Facade Methods Required

File: tqapp/src/main/java/com/perun/tlinq/entity/cruise/CruiseFacade.java

5.2.1 Delete Methods with Dependency Validation

// ============== DELETE METHODS ==============

/**
 * Delete cruise company after validating no dependencies
 * @throws CruiseException if company has associated ships
 */
public void deleteCompany(Integer companyId) throws CruiseException {
    // Check for associated ships
    List<CCruiseShip> ships = getCompanyShips(companyId);
    if (ships != null && !ships.isEmpty()) {
        throw new CruiseException("CRU0005",
            "Cannot delete company with " + ships.size() + " associated ships");
    }
    // Perform delete via EntityFacade
    entityFacade.delete(CCruiseCompany.class, companyId);
}

/**
 * Delete cruise area after validating no dependencies
 */
public void deleteArea(Integer areaId) throws CruiseException {
    // Check for associated itineraries
    List<CCruiseItinerary> itineraries = searchItinerary(areaId, null, null, null, null);
    if (itineraries != null && !itineraries.isEmpty()) {
        throw new CruiseException("CRU0005",
            "Cannot delete area with " + itineraries.size() + " associated itineraries");
    }
    entityFacade.delete(CCruiseArea.class, areaId);
}

/**
 * Delete cruise ship after validating no dependencies
 */
public void deleteShip(Integer shipId) throws CruiseException {
    // Check for associated itineraries
    List<CCruiseItinerary> itineraries = searchItinerary(null, shipId, null, null, null);
    if (itineraries != null && !itineraries.isEmpty()) {
        throw new CruiseException("CRU0005",
            "Cannot delete ship with associated itineraries");
    }
    // Check for ship cabins
    List<CShipCabin> cabins = getAllShipCabins(shipId);
    if (cabins != null && !cabins.isEmpty()) {
        throw new CruiseException("CRU0005",
            "Cannot delete ship with cabin assignments");
    }
    entityFacade.delete(CCruiseShip.class, shipId);
}

/**
 * Delete cabin type after validating no dependencies
 */
public void deleteCabinType(Integer cabinTypeId) throws CruiseException {
    // Check for ship cabin assignments using this type
    // Implementation depends on search capability
    entityFacade.delete(CCabinType.class, cabinTypeId);
}

/**
 * Delete charge type after validating no dependencies
 */
public void deleteChargeType(Integer chargeTypeId) throws CruiseException {
    // Check for other charges using this type
    entityFacade.delete(CChargeType.class, chargeTypeId);
}

/**
 * Delete cruise port after validating no dependencies
 */
public void deleteCruisePort(Integer portId) throws CruiseException {
    // Check for template and cruise point references
    entityFacade.delete(CCruisePort.class, portId);
}

/**
 * Delete ship cabin assignment
 * @param confirmCascade if true, also delete related cabin charges
 */
public void deleteShipCabin(Integer shipCabinId, boolean confirmCascade) throws CruiseException {
    // Check for cabin charges
    List<CCabinCharge> charges = searchCabinCharge(null, shipCabinId, null);
    if (charges != null && !charges.isEmpty()) {
        if (!confirmCascade) {
            throw new CruiseException("CRU0010",
                "Ship cabin has " + charges.size() + " cabin charges. Confirm cascade delete.");
        }
        // Delete cabin charges first
        for (CCabinCharge charge : charges) {
            entityFacade.delete(CCabinCharge.class, charge.getCabinChargeId());
        }
    }
    entityFacade.delete(CShipCabin.class, shipCabinId);
}

/**
 * Delete itinerary after validating no cruises exist
 */
public void deleteItinerary(Integer itineraryId) throws CruiseException {
    // Check for cruises
    List<CCruise> cruises = searchCruises(itineraryId, null, null);
    if (cruises != null && !cruises.isEmpty()) {
        throw new CruiseException("CRU0005",
            "Cannot delete itinerary with " + cruises.size() + " existing cruises");
    }
    // Delete template entries first
    List<CCruiseTemplate> templates = searchCruiseTemplate(itineraryId, null);
    if (templates != null) {
        for (CCruiseTemplate template : templates) {
            entityFacade.delete(CCruiseTemplate.class, template.getCruiseTemplateId());
        }
    }
    entityFacade.delete(CCruiseItinerary.class, itineraryId);
}

/**
 * Delete cruise template entry
 */
public void deleteCruiseTemplate(Integer templateId) throws CruiseException {
    entityFacade.delete(CCruiseTemplate.class, templateId);
}

/**
 * Delete cruise with cascade delete of all related records
 * @param confirmCascade must be true to proceed
 */
public void deleteCruise(Integer cruiseId, boolean confirmCascade) throws CruiseException {
    if (!confirmCascade) {
        throw new CruiseException("CRU0010",
            "Cruise delete requires cascade confirmation");
    }

    // Delete other charges
    List<COtherCharge> otherCharges = getCruiseOtherCharges(cruiseId, null);
    if (otherCharges != null) {
        for (COtherCharge charge : otherCharges) {
            entityFacade.delete(COtherCharge.class, charge.getOtherChargeId());
        }
    }

    // Delete cabin charges
    List<CCabinCharge> cabinCharges = searchCabinCharge(cruiseId, null, null);
    if (cabinCharges != null) {
        for (CCabinCharge charge : cabinCharges) {
            entityFacade.delete(CCabinCharge.class, charge.getCabinChargeId());
        }
    }

    // Delete cruise points
    List<CCruisePoint> points = listCruisePoints(cruiseId);
    if (points != null) {
        for (CCruisePoint point : points) {
            entityFacade.delete(CCruisePoint.class, point.getCruisePointId());
        }
    }

    // Delete cruise
    entityFacade.delete(CCruise.class, cruiseId);
}

/**
 * Delete cruise point
 */
public void deleteCruisePoint(Integer pointId) throws CruiseException {
    entityFacade.delete(CCruisePoint.class, pointId);
}

/**
 * Delete cabin charge
 */
public void deleteCabinCharge(Integer chargeId) throws CruiseException {
    entityFacade.delete(CCabinCharge.class, chargeId);
}

/**
 * Delete other charge
 */
public void deleteOtherCharge(Integer chargeId) throws CruiseException {
    entityFacade.delete(COtherCharge.class, chargeId);
}

5.2.2 Status Change Methods

// ============== STATUS CHANGE METHODS ==============

/**
 * Change itinerary status (active/inactive)
 */
public CCruiseItinerary changeItineraryStatus(Integer itineraryId, String status)
        throws CruiseException {
    if (!status.equals("active") && !status.equals("inactive")) {
        throw new CruiseException("CRU0014", "Invalid status: " + status);
    }
    CCruiseItinerary itinerary = getItinerary(itineraryId);
    if (itinerary == null) {
        throw new CruiseException("CRU0003", "Itinerary not found: " + itineraryId);
    }
    itinerary.setStatus(status);
    return updateItinerary(itinerary);
}

/**
 * Change cruise status (available/cancelled/sold-out)
 */
public CCruise changeCruiseStatus(Integer cruiseId, String status) throws CruiseException {
    if (!status.equals("available") && !status.equals("cancelled") && !status.equals("sold-out")) {
        throw new CruiseException("CRU0014", "Invalid status: " + status);
    }
    CCruise cruise = getCruise(cruiseId);
    if (cruise == null) {
        throw new CruiseException("CRU0003", "Cruise not found: " + cruiseId);
    }
    cruise.setStatus(status);
    return updateCruise(cruise);
}

5.2.3 Bulk and Special Operations

// ============== BULK OPERATIONS ==============

/**
 * Bulk assign cabin types to a ship
 * @param shipId the ship
 * @param cabinTypeIds array of cabin type IDs to assign
 * @param defaultMaxPax default max passengers if not specified
 * @return list of created ship cabin assignments
 */
public List<CShipCabin> bulkAssignCabinTypes(Integer shipId, List<Integer> cabinTypeIds,
        Integer defaultMaxPax) throws CruiseException {
    List<CShipCabin> results = new ArrayList<>();
    for (Integer cabinTypeId : cabinTypeIds) {
        CShipCabin cabin = createShipCabin(shipId, cabinTypeId,
            defaultMaxPax != null ? defaultMaxPax : 2);
        results.add(cabin);
    }
    return results;
}

/**
 * Initialize cabin charges for all ship cabins on a cruise
 */
public List<CCabinCharge> initCabinChargesForCruise(Integer cruiseId) throws CruiseException {
    CCruise cruise = getCruise(cruiseId);
    if (cruise == null) {
        throw new CruiseException("CRU0003", "Cruise not found: " + cruiseId);
    }

    CCruiseItinerary itinerary = getItinerary(cruise.getItineraryId());
    List<CShipCabin> shipCabins = getAllShipCabins(itinerary.getCruiseShipId());

    List<CCabinCharge> charges = new ArrayList<>();
    for (CShipCabin cabin : shipCabins) {
        // Check if charge already exists
        List<CCabinCharge> existing = searchCabinCharge(cruiseId, cabin.getShipCabinId(), null);
        if (existing == null || existing.isEmpty()) {
            CCabinCharge charge = createCabinCharge(cruiseId, cabin.getShipCabinId(),
                0.0, 1.0, "USD", true);
            charges.add(charge);
        }
    }
    return charges;
}

/**
 * Regenerate cruise points from template
 * @param confirmOverwrite must be true to delete existing points
 */
public List<CCruisePoint> regenerateCruisePoints(Integer cruiseId, boolean confirmOverwrite)
        throws CruiseException {
    CCruise cruise = getCruise(cruiseId);
    if (cruise == null) {
        throw new CruiseException("CRU0003", "Cruise not found: " + cruiseId);
    }

    // Check for existing points
    List<CCruisePoint> existingPoints = listCruisePoints(cruiseId);
    if (existingPoints != null && !existingPoints.isEmpty()) {
        if (!confirmOverwrite) {
            throw new CruiseException("CRU0010",
                "Cruise has " + existingPoints.size() + " points. Confirm overwrite.");
        }
        // Delete existing points
        for (CCruisePoint point : existingPoints) {
            entityFacade.delete(CCruisePoint.class, point.getCruisePointId());
        }
    }

    // Get template entries
    List<CCruiseTemplate> templates = searchCruiseTemplate(cruise.getItineraryId(), null);
    if (templates == null || templates.isEmpty()) {
        throw new CruiseException("CRU0011", "No template found for itinerary");
    }

    // Sort by stopSeq
    templates.sort((a, b) -> a.getStopSeq().compareTo(b.getStopSeq()));

    // Generate points from template
    List<CCruisePoint> newPoints = new ArrayList<>();
    LocalDate startDate = cruise.getStartDate();

    for (CCruiseTemplate template : templates) {
        LocalDate arriveDate = startDate.plusDays(template.getDayOffsetArr());
        LocalDate departDate = startDate.plusDays(template.getDayOffsetDep());

        String arriveDateTime = arriveDate.toString() + " " + template.getArriveTime();
        String departDateTime = departDate.toString() + " " + template.getDepartTime();

        CCruisePoint point = createCruisePoint(
            template.getPortId(),
            cruiseId,
            arriveDateTime,
            departDateTime,
            template.getStopSeq(),
            template.getDescription()
        );
        newPoints.add(point);
    }

    return newPoints;
}

5.2.4 Tree and Hierarchy Methods

// ============== TREE/HIERARCHY METHODS ==============

/**
 * Get hierarchical tree of companies with their itineraries
 * @param includeInactive if true, include inactive itineraries
 */
public List<CItineraryTreeNode> getItineraryTree(boolean includeInactive) throws CruiseException {
    List<CItineraryTreeNode> tree = new ArrayList<>();

    // Get all companies
    List<CCruiseCompany> companies = getCruiseCompanies(null, null);

    for (CCruiseCompany company : companies) {
        CItineraryTreeNode node = new CItineraryTreeNode();
        node.setCompanyId(company.getCompanyId());
        node.setCompanyCode(company.getCompanyCode());
        node.setCompanyName(company.getCompanyName());

        // Get ships for this company
        List<CCruiseShip> ships = getCompanyShips(company.getCompanyId());
        List<CItineraryTreeItem> itineraryItems = new ArrayList<>();

        for (CCruiseShip ship : ships) {
            // Get itineraries for this ship
            List<CCruiseItinerary> itineraries = searchItinerary(null, ship.getCruiseShipId(),
                null, null, null);

            for (CCruiseItinerary itin : itineraries) {
                // Filter by status if needed
                if (!includeInactive && "inactive".equals(itin.getStatus())) {
                    continue;
                }

                CItineraryTreeItem item = new CItineraryTreeItem();
                item.setItineraryId(itin.getItineraryId());
                item.setCode(itin.getItineraryCode());
                item.setName(itin.getName());
                item.setDuration(itin.getDuration());
                item.setStatus(itin.getStatus());
                item.setShipName(ship.getShipName());

                // Get area name
                CCruiseArea area = getArea(itin.getCruiseAreaId());
                item.setAreaName(area != null ? area.getAreaName() : null);

                // Count cruises
                List<CCruise> cruises = searchCruises(itin.getItineraryId(), null, null);
                item.setCruiseCount(cruises != null ? cruises.size() : 0);

                itineraryItems.add(item);
            }
        }

        node.setItineraries(itineraryItems);
        tree.add(node);
    }

    return tree;
}

5.2.5 Booking Interface Methods

// ============== BOOKING INTERFACE METHODS ==============

/**
 * Get cruise availability for booking
 */
public CCruiseAvailability getCruiseAvailability(Integer cruiseId) throws CruiseException {
    CCruise cruise = getCruise(cruiseId);
    if (cruise == null) {
        throw new CruiseException("CRU0003", "Cruise not found: " + cruiseId);
    }

    CCruiseAvailability availability = new CCruiseAvailability();
    availability.setCruise(cruise);

    // Get itinerary
    CCruiseItinerary itinerary = getItinerary(cruise.getItineraryId());
    availability.setItinerary(itinerary);

    // Get ship and company
    CCruiseShip ship = getShip(itinerary.getCruiseShipId());
    availability.setShip(ship);

    CCruiseCompany company = getCruiseCompany(ship.getCompanyId());
    availability.setCompany(company);

    // Get cabin availability with pricing
    // Uses existing searchCabinCharge and joins with cabin types
    List<CCabinCharge> cabinCharges = searchCabinCharge(cruiseId, null, null);
    List<CCruiseCabinRecord> cabinRecords = new ArrayList<>();
    for (CCabinCharge charge : cabinCharges) {
        CShipCabin cabin = getShipCabin(charge.getShipCabinId());
        CCabinType cabinType = getCabinType(cabin.getCabinTypeId());

        CCruiseCabinRecord record = new CCruiseCabinRecord();
        record.setCabin(cabin);
        record.setCabinType(cabinType);
        record.setCabinCharge(charge);
        cabinRecords.add(record);
    }
    availability.setAvailableCabins(cabinRecords);

    // Get mandatory and optional charges
    List<COtherCharge> allCharges = getCruiseOtherCharges(cruiseId, null);
    List<CCruiseChargeRecord> mandatoryCharges = new ArrayList<>();
    List<CCruiseChargeRecord> optionalCharges = new ArrayList<>();

    for (COtherCharge charge : allCharges) {
        CChargeType chargeType = getChargeType(charge.getChargeTypeId());
        CCruiseChargeRecord record = new CCruiseChargeRecord();
        record.setChargeType(chargeType);
        record.setCharge(charge);

        if (charge.getMandatory()) {
            mandatoryCharges.add(record);
        } else {
            optionalCharges.add(record);
        }
    }
    availability.setMandatoryCharges(mandatoryCharges);
    availability.setOptionalCharges(optionalCharges);

    // Get cruise points
    List<CCruisePoint> points = listCruisePoints(cruiseId);
    List<CCruisePointRecord> pointRecords = new ArrayList<>();
    for (CCruisePoint point : points) {
        CCruisePort port = getCruisePort(point.getPortId());
        CCruisePointRecord record = new CCruisePointRecord();
        record.setPort(port);
        record.setPoint(point);
        pointRecords.add(record);
    }
    availability.setCruisePoints(pointRecords);

    return availability;
}

/**
 * Calculate total price for a cruise booking
 */
public CPriceCalculation calculateCruisePrice(Integer cruiseId, Integer shipCabinId,
        Integer adults, Integer children, List<Integer> optionalChargeIds) throws CruiseException {

    // Validate cabin is available
    List<CCabinCharge> cabinCharges = searchCabinCharge(cruiseId, shipCabinId, null);
    if (cabinCharges == null || cabinCharges.isEmpty()) {
        throw new CruiseException("CRU0003", "Cabin charge not found");
    }
    CCabinCharge cabinCharge = cabinCharges.get(0);
    if (!cabinCharge.getAvailable()) {
        throw new CruiseException("CRU0003", "Selected cabin is not available");
    }

    int totalPax = adults + children;

    CPriceCalculation calc = new CPriceCalculation();
    calc.setCruiseId(cruiseId);
    calc.setShipCabinId(shipCabinId);
    calc.setAdults(adults);
    calc.setChildren(children);

    List<CPriceLineItem> lineItems = new ArrayList<>();

    // Cabin charge (per cabin, not per person)
    calc.setCabinCharge(cabinCharge.getAmount());
    calc.setCabinCurrency(cabinCharge.getCurrency());

    CPriceLineItem cabinItem = new CPriceLineItem();
    cabinItem.setDescription("Cabin");
    cabinItem.setUnitPrice(cabinCharge.getAmount());
    cabinItem.setQuantity(1);
    cabinItem.setTotalPrice(cabinCharge.getAmount());
    cabinItem.setCurrency(cabinCharge.getCurrency());
    cabinItem.setChargeType("cabin");
    lineItems.add(cabinItem);

    double mandatoryTotal = 0.0;
    double optionalTotal = 0.0;

    // Mandatory charges (per person)
    List<COtherCharge> mandatoryCharges = getCruiseOtherCharges(cruiseId, true);
    for (COtherCharge charge : mandatoryCharges) {
        CChargeType chargeType = getChargeType(charge.getChargeTypeId());
        double total = charge.getAmount() * totalPax;
        mandatoryTotal += total;

        CPriceLineItem item = new CPriceLineItem();
        item.setDescription(chargeType.getChargeTypeName());
        item.setUnitPrice(charge.getAmount());
        item.setQuantity(totalPax);
        item.setTotalPrice(total);
        item.setCurrency(charge.getCurrency());
        item.setChargeType("mandatory");
        lineItems.add(item);
    }

    // Optional charges (per person, only if selected)
    if (optionalChargeIds != null && !optionalChargeIds.isEmpty()) {
        for (Integer chargeId : optionalChargeIds) {
            COtherCharge charge = getOtherCharge(chargeId);
            if (charge != null && charge.getCruiseId().equals(cruiseId)) {
                CChargeType chargeType = getChargeType(charge.getChargeTypeId());
                double total = charge.getAmount() * totalPax;
                optionalTotal += total;

                CPriceLineItem item = new CPriceLineItem();
                item.setDescription(chargeType.getChargeTypeName());
                item.setUnitPrice(charge.getAmount());
                item.setQuantity(totalPax);
                item.setTotalPrice(total);
                item.setCurrency(charge.getCurrency());
                item.setChargeType("optional");
                lineItems.add(item);
            }
        }
    }

    calc.setMandatoryChargesTotal(mandatoryTotal);
    calc.setOptionalChargesTotal(optionalTotal);
    calc.setGrandTotal(cabinCharge.getAmount() + mandatoryTotal + optionalTotal);
    calc.setLineItems(lineItems);

    return calc;
}

/**
 * Get itineraries for a cruise area with lowest price (for booking interface)
 */
public List<CCruiseItinerary> getItinerariesForBooking(Integer areaId) throws CruiseException {
    List<CCruiseItinerary> itineraries = searchItinerary(areaId, null, null, null, null);

    // Filter to active only and with future cruises
    List<CCruiseItinerary> result = new ArrayList<>();
    LocalDate today = LocalDate.now();

    for (CCruiseItinerary itin : itineraries) {
        if (!"active".equals(itin.getStatus())) continue;

        // Check for future cruises
        List<CCruise> cruises = searchCruises(itin.getItineraryId(), today, null);
        if (cruises != null && !cruises.isEmpty()) {
            // TODO: Add lowest price calculation
            result.add(itin);
        }
    }

    return result;
}

5.3 CruiseException Class (NEW)

File: tqapp/src/main/java/com/perun/tlinq/entity/cruise/CruiseException.java

package com.perun.tlinq.entity.cruise;

/**
 * Exception class for cruise management operations
 */
public class CruiseException extends Exception {
    private final String errorCode;

    public CruiseException(String errorCode, String message) {
        super(message);
        this.errorCode = errorCode;
    }

    public String getErrorCode() {
        return errorCode;
    }
}

5.4 Implementation Tasks

Task ID Description Effort
FAC-001 Create CruiseException class 0.5h
FAC-002 Implement delete methods with validation (7 entities) 3h
FAC-003 Implement status change methods (itinerary, cruise) 1h
FAC-004 Implement bulkAssignCabinTypes 1h
FAC-005 Implement initCabinChargesForCruise 1h
FAC-006 Implement regenerateCruisePoints 2h
FAC-007 Implement getItineraryTree 2h
FAC-008 Implement getCruiseAvailability 2h
FAC-009 Implement calculateCruisePrice 2h
FAC-010 Implement getItinerariesForBooking 1h
FAC-011 Add search enhancements (status filter, company filter) 2h

6. Phase 4: API Endpoints - Dimension Data

6.1 API Class Structure

File: tqapi/src/main/java/com/perun/tlinq/api/CruiseApi.java

The existing CruiseApi class needs significant expansion. Recommended approach:

  1. Keep existing endpoints for backward compatibility
  2. Add new endpoints following the /cruise/{entity}/{action} pattern
  3. Use consistent parameter naming and response structure

6.2 Company Endpoints

// ============== COMPANY ENDPOINTS ==============

/**
 * List cruise companies (NEW - supplements existing listCompanies)
 * POST /tlinq-api/cruise/company/list
 */
@POST
@Path("/company/list")
public TlinqApiResponse companyList(
        @FormParam("session") String session,
        @FormParam("companyCode") String companyCode,
        @FormParam("companyName") String companyName) {
    try {
        List<CCruiseCompany> companies = cruiseFacade.getCruiseCompanies(companyCode, companyName);
        return TlinqApiResponse.ok(companies);
    } catch (Exception e) {
        return TlinqApiResponse.error("CRU0001", e.getMessage());
    }
}

/**
 * Read single company
 * POST /tlinq-api/cruise/company/read
 */
@POST
@Path("/company/read")
public TlinqApiResponse companyRead(
        @FormParam("session") String session,
        @FormParam("companyId") Integer companyId) {
    try {
        if (companyId == null) {
            return TlinqApiResponse.error("CRU0002", "companyId is required");
        }
        CCruiseCompany company = cruiseFacade.getCruiseCompany(companyId);
        if (company == null) {
            return TlinqApiResponse.error("CRU0003", "Company not found");
        }
        return TlinqApiResponse.ok(company);
    } catch (Exception e) {
        return TlinqApiResponse.error("CRU0001", e.getMessage());
    }
}

/**
 * Create or update company
 * POST /tlinq-api/cruise/company/write
 */
@POST
@Path("/company/write")
public TlinqApiResponse companyWrite(
        @FormParam("session") String session,
        @FormParam("companyId") Integer companyId,
        @FormParam("code") String code,
        @FormParam("name") String name) {
    try {
        // Validation
        if (code == null || code.isEmpty()) {
            return TlinqApiResponse.error("CRU0002", "code is required");
        }
        if (name == null || name.isEmpty()) {
            return TlinqApiResponse.error("CRU0002", "name is required");
        }
        if (code.length() > 10) {
            return TlinqApiResponse.error("CRU0002", "code must be 10 characters or less");
        }

        CCruiseCompany company;
        if (companyId == null) {
            // Create new
            company = cruiseFacade.createNewCompany(code, name);
        } else {
            // Update existing
            company = cruiseFacade.getCruiseCompany(companyId);
            if (company == null) {
                return TlinqApiResponse.error("CRU0003", "Company not found");
            }
            company.setCompanyCode(code);
            company.setCompanyName(name);
            company = cruiseFacade.updateCompany(company);
        }
        return TlinqApiResponse.ok(company);
    } catch (Exception e) {
        if (e.getMessage().contains("unique") || e.getMessage().contains("duplicate")) {
            return TlinqApiResponse.error("CRU0004", "Company code must be unique");
        }
        return TlinqApiResponse.error("CRU0001", e.getMessage());
    }
}

/**
 * Delete company
 * POST /tlinq-api/cruise/company/delete
 */
@POST
@Path("/company/delete")
public TlinqApiResponse companyDelete(
        @FormParam("session") String session,
        @FormParam("companyId") Integer companyId) {
    try {
        if (companyId == null) {
            return TlinqApiResponse.error("CRU0002", "companyId is required");
        }
        cruiseFacade.deleteCompany(companyId);
        return TlinqApiResponse.ok("Company deleted successfully");
    } catch (CruiseException e) {
        return TlinqApiResponse.error(e.getErrorCode(), e.getMessage());
    } catch (Exception e) {
        return TlinqApiResponse.error("CRU0001", e.getMessage());
    }
}

6.3 Area, Ship, CabinType, ChargeType, Port Endpoints

Follow the same pattern as Company endpoints: - /cruise/area/list, /cruise/area/read, /cruise/area/write, /cruise/area/delete - /cruise/ship/list, /cruise/ship/read, /cruise/ship/write, /cruise/ship/delete - /cruise/cabintype/list, /cruise/cabintype/read, /cruise/cabintype/write, /cruise/cabintype/delete - /cruise/chargetype/list, /cruise/chargetype/read, /cruise/chargetype/write, /cruise/chargetype/delete - /cruise/port/list, /cruise/port/read, /cruise/port/write, /cruise/port/delete

6.4 Ship Cabin Endpoints

// ============== SHIP CABIN ENDPOINTS ==============

/**
 * List ship cabin assignments
 */
@POST
@Path("/shipcabin/list")
public TlinqApiResponse shipcabinList(
        @FormParam("session") String session,
        @FormParam("shipId") Integer shipId) {
    try {
        if (shipId == null) {
            return TlinqApiResponse.error("CRU0002", "shipId is required");
        }
        List<CShipCabin> cabins = cruiseFacade.getAllShipCabins(shipId);
        return TlinqApiResponse.ok(cabins);
    } catch (Exception e) {
        return TlinqApiResponse.error("CRU0001", e.getMessage());
    }
}

/**
 * Bulk assign cabin types to ship
 */
@POST
@Path("/shipcabin/bulkAssign")
@Consumes(MediaType.APPLICATION_JSON)
public TlinqApiResponse shipcabinBulkAssign(JsonObject params) {
    try {
        Integer shipId = params.getInt("shipId");
        JsonArray cabinTypeIdsArray = params.getJsonArray("cabinTypeIds");
        Integer defaultMaxPax = params.containsKey("defaultMaxPax") ?
            params.getInt("defaultMaxPax") : 2;

        List<Integer> cabinTypeIds = new ArrayList<>();
        for (int i = 0; i < cabinTypeIdsArray.size(); i++) {
            cabinTypeIds.add(cabinTypeIdsArray.getInt(i));
        }

        List<CShipCabin> cabins = cruiseFacade.bulkAssignCabinTypes(shipId, cabinTypeIds, defaultMaxPax);
        return TlinqApiResponse.ok(cabins);
    } catch (Exception e) {
        return TlinqApiResponse.error("CRU0001", e.getMessage());
    }
}

/**
 * Delete ship cabin with optional cascade
 */
@POST
@Path("/shipcabin/delete")
public TlinqApiResponse shipcabinDelete(
        @FormParam("session") String session,
        @FormParam("shipCabinId") Integer shipCabinId,
        @FormParam("confirmCascade") Boolean confirmCascade) {
    try {
        cruiseFacade.deleteShipCabin(shipCabinId, confirmCascade != null && confirmCascade);
        return TlinqApiResponse.ok("Ship cabin deleted successfully");
    } catch (CruiseException e) {
        return TlinqApiResponse.error(e.getErrorCode(), e.getMessage());
    } catch (Exception e) {
        return TlinqApiResponse.error("CRU0001", e.getMessage());
    }
}

6.5 Implementation Tasks

Task ID Description Effort
API-001 Implement company CRUD endpoints (4 endpoints) 2h
API-002 Implement area CRUD endpoints (4 endpoints) 1.5h
API-003 Implement ship CRUD endpoints (4 endpoints) 1.5h
API-004 Implement cabintype CRUD endpoints (4 endpoints) 1.5h
API-005 Implement chargetype CRUD endpoints (4 endpoints) 1.5h
API-006 Implement port CRUD endpoints (4 endpoints) 1.5h
API-007 Implement shipcabin endpoints including bulkAssign (5 endpoints) 2h

7. Phase 5: API Endpoints - Itinerary & Cruise Management

7.1 Itinerary Endpoints

// ============== ITINERARY ENDPOINTS ==============

/**
 * List itineraries with filters
 */
@POST
@Path("/itinerary/list")
public TlinqApiResponse itineraryList(
        @FormParam("session") String session,
        @FormParam("companyId") Integer companyId,
        @FormParam("areaId") Integer areaId,
        @FormParam("shipId") Integer shipId,
        @FormParam("includeInactive") Boolean includeInactive) {
    try {
        // If companyId provided, get ships for that company first
        Integer effectiveShipId = shipId;
        if (companyId != null && shipId == null) {
            // Get all ships for company and search itineraries for each
            // This requires enhanced search logic
        }

        List<CCruiseItinerary> itineraries = cruiseFacade.searchItinerary(
            areaId, effectiveShipId, null, null, null);

        // Filter by status if needed
        if (includeInactive == null || !includeInactive) {
            itineraries = itineraries.stream()
                .filter(i -> !"inactive".equals(i.getStatus()))
                .collect(Collectors.toList());
        }

        return TlinqApiResponse.ok(itineraries);
    } catch (Exception e) {
        return TlinqApiResponse.error("CRU0001", e.getMessage());
    }
}

/**
 * Get itinerary by ID with optional template
 */
@POST
@Path("/itinerary/read")
public TlinqApiResponse itineraryRead(
        @FormParam("session") String session,
        @FormParam("itineraryId") Integer itineraryId,
        @FormParam("withTemplate") Boolean withTemplate) {
    try {
        CCruiseItinerary itinerary = cruiseFacade.getItinerary(itineraryId);
        if (itinerary == null) {
            return TlinqApiResponse.error("CRU0003", "Itinerary not found");
        }

        // If withTemplate requested, include template entries
        if (withTemplate != null && withTemplate) {
            List<CCruiseTemplate> templates = cruiseFacade.searchCruiseTemplate(itineraryId, null);
            // Return as composite object or add to itinerary
        }

        return TlinqApiResponse.ok(itinerary);
    } catch (Exception e) {
        return TlinqApiResponse.error("CRU0001", e.getMessage());
    }
}

/**
 * Create or update itinerary
 */
@POST
@Path("/itinerary/write")
public TlinqApiResponse itineraryWrite(
        @FormParam("session") String session,
        @FormParam("itineraryId") Integer itineraryId,
        @FormParam("code") String code,
        @FormParam("name") String name,
        @FormParam("duration") Integer duration,
        @FormParam("shipId") Integer shipId,
        @FormParam("areaId") Integer areaId,
        @FormParam("imageUrl") String imageUrl,
        @FormParam("status") String status) {
    try {
        // Validation
        if (code == null || code.isEmpty()) {
            return TlinqApiResponse.error("CRU0002", "code is required");
        }
        if (duration == null || duration <= 0) {
            return TlinqApiResponse.error("CRU0008", "duration must be a positive number");
        }
        if (shipId == null) {
            return TlinqApiResponse.error("CRU0002", "shipId is required");
        }
        if (areaId == null) {
            return TlinqApiResponse.error("CRU0002", "areaId is required");
        }

        CCruiseItinerary itinerary;
        if (itineraryId == null) {
            // Create new
            itinerary = cruiseFacade.createItinerary(shipId, areaId, duration, code, imageUrl, name);
            if (status != null) {
                itinerary.setStatus(status);
                itinerary = cruiseFacade.updateItinerary(itinerary);
            }
        } else {
            // Update existing
            itinerary = cruiseFacade.getItinerary(itineraryId);
            if (itinerary == null) {
                return TlinqApiResponse.error("CRU0003", "Itinerary not found");
            }
            itinerary.setItineraryCode(code);
            itinerary.setName(name);
            itinerary.setDuration(duration);
            itinerary.setCruiseShipId(shipId);
            itinerary.setCruiseAreaId(areaId);
            itinerary.setImageUrl(imageUrl);
            if (status != null) {
                itinerary.setStatus(status);
            }
            itinerary = cruiseFacade.updateItinerary(itinerary);
        }
        return TlinqApiResponse.ok(itinerary);
    } catch (Exception e) {
        return TlinqApiResponse.error("CRU0001", e.getMessage());
    }
}

/**
 * Change itinerary status
 */
@POST
@Path("/itinerary/changeStatus")
public TlinqApiResponse itineraryChangeStatus(
        @FormParam("session") String session,
        @FormParam("itineraryId") Integer itineraryId,
        @FormParam("status") String status) {
    try {
        CCruiseItinerary itinerary = cruiseFacade.changeItineraryStatus(itineraryId, status);
        return TlinqApiResponse.ok(itinerary);
    } catch (CruiseException e) {
        return TlinqApiResponse.error(e.getErrorCode(), e.getMessage());
    } catch (Exception e) {
        return TlinqApiResponse.error("CRU0001", e.getMessage());
    }
}

/**
 * Get hierarchical tree of companies and itineraries
 */
@POST
@Path("/itinerary/tree")
public TlinqApiResponse itineraryTree(
        @FormParam("session") String session,
        @FormParam("includeInactive") Boolean includeInactive) {
    try {
        List<CItineraryTreeNode> tree = cruiseFacade.getItineraryTree(
            includeInactive != null && includeInactive);
        return TlinqApiResponse.ok(tree);
    } catch (Exception e) {
        return TlinqApiResponse.error("CRU0001", e.getMessage());
    }
}

7.2 Template Endpoints

// ============== TEMPLATE ENDPOINTS ==============

@POST
@Path("/template/list")
public TlinqApiResponse templateList(
        @FormParam("session") String session,
        @FormParam("itineraryId") Integer itineraryId) {
    try {
        if (itineraryId == null) {
            return TlinqApiResponse.error("CRU0002", "itineraryId is required");
        }
        List<CCruiseTemplate> templates = cruiseFacade.searchCruiseTemplate(itineraryId, null);
        // Sort by stopSeq
        templates.sort((a, b) -> a.getStopSeq().compareTo(b.getStopSeq()));
        return TlinqApiResponse.ok(templates);
    } catch (Exception e) {
        return TlinqApiResponse.error("CRU0001", e.getMessage());
    }
}

@POST
@Path("/template/write")
public TlinqApiResponse templateWrite(
        @FormParam("session") String session,
        @FormParam("cruiseTemplateId") Integer cruiseTemplateId,
        @FormParam("itineraryId") Integer itineraryId,
        @FormParam("stopSeq") Integer stopSeq,
        @FormParam("dayOffsetArr") Integer dayOffsetArr,
        @FormParam("dayOffsetDep") Integer dayOffsetDep,
        @FormParam("arriveTime") String arriveTime,
        @FormParam("departTime") String departTime,
        @FormParam("cruisePortId") Integer cruisePortId,
        @FormParam("description") String description) {
    try {
        // Validate time format
        if (!isValidTimeFormat(arriveTime)) {
            return TlinqApiResponse.error("CRU0007", "arriveTime must be in HH:mm format");
        }
        if (!isValidTimeFormat(departTime)) {
            return TlinqApiResponse.error("CRU0007", "departTime must be in HH:mm format");
        }

        // Validate day offsets against itinerary duration
        CCruiseItinerary itinerary = cruiseFacade.getItinerary(itineraryId);
        if (dayOffsetArr > itinerary.getDuration() || dayOffsetDep > itinerary.getDuration()) {
            return TlinqApiResponse.error("CRU0008", "Day offset cannot exceed itinerary duration");
        }

        CCruiseTemplate template;
        if (cruiseTemplateId == null) {
            template = cruiseFacade.createCruiseTemplate(itineraryId, stopSeq, cruisePortId,
                arriveTime, departTime, dayOffsetArr, dayOffsetDep);
        } else {
            template = cruiseFacade.getCruiseTemplate(cruiseTemplateId);
            // Update fields
            template = cruiseFacade.updateCruiseTemplate(template);
        }
        return TlinqApiResponse.ok(template);
    } catch (Exception e) {
        return TlinqApiResponse.error("CRU0001", e.getMessage());
    }
}

private boolean isValidTimeFormat(String time) {
    if (time == null) return false;
    return time.matches("^([01]?[0-9]|2[0-3]):[0-5][0-9]$");
}

7.3 Cruise Instance Endpoints

// ============== CRUISE INSTANCE ENDPOINTS ==============

/**
 * List cruises for itinerary
 */
@POST
@Path("/cruise/list")
public TlinqApiResponse cruiseList(
        @FormParam("session") String session,
        @FormParam("itineraryId") Integer itineraryId,
        @FormParam("includeAll") Boolean includeAll) {
    try {
        LocalDate fromDate = (includeAll != null && includeAll) ? null : LocalDate.now();
        List<CCruise> cruises = cruiseFacade.searchCruises(itineraryId, fromDate, null);
        // Sort by start date
        cruises.sort((a, b) -> a.getStartDate().compareTo(b.getStartDate()));
        return TlinqApiResponse.ok(cruises);
    } catch (Exception e) {
        return TlinqApiResponse.error("CRU0001", e.getMessage());
    }
}

/**
 * Create cruise and generate points from template
 */
@POST
@Path("/cruise/write")
public TlinqApiResponse cruiseWrite(
        @FormParam("session") String session,
        @FormParam("cruiseId") Integer cruiseId,
        @FormParam("itineraryId") Integer itineraryId,
        @FormParam("startDate") String startDateStr) {
    try {
        if (itineraryId == null) {
            return TlinqApiResponse.error("CRU0002", "itineraryId is required");
        }
        if (startDateStr == null) {
            return TlinqApiResponse.error("CRU0002", "startDate is required");
        }

        LocalDate startDate;
        try {
            startDate = LocalDate.parse(startDateStr);
        } catch (Exception e) {
            return TlinqApiResponse.error("CRU0006", "Invalid date format, expected yyyy-MM-dd");
        }

        CCruise cruise;
        if (cruiseId == null) {
            // Check template exists
            List<CCruiseTemplate> templates = cruiseFacade.searchCruiseTemplate(itineraryId, null);
            if (templates == null || templates.isEmpty()) {
                return TlinqApiResponse.error("CRU0011", "No template found for itinerary");
            }

            // Create cruise with skeleton (auto-generates points)
            cruise = cruiseFacade.createCruiseSkeleton(itineraryId, startDate);
        } else {
            // Update existing (only start date can be changed)
            cruise = cruiseFacade.getCruise(cruiseId);
            cruise.setStartDate(startDate);
            cruise = cruiseFacade.updateCruise(cruise);
        }
        return TlinqApiResponse.ok(cruise);
    } catch (Exception e) {
        return TlinqApiResponse.error("CRU0001", e.getMessage());
    }
}

/**
 * Change cruise status
 */
@POST
@Path("/cruise/changeStatus")
public TlinqApiResponse cruiseChangeStatus(
        @FormParam("session") String session,
        @FormParam("cruiseId") Integer cruiseId,
        @FormParam("status") String status) {
    try {
        CCruise cruise = cruiseFacade.changeCruiseStatus(cruiseId, status);
        return TlinqApiResponse.ok(cruise);
    } catch (CruiseException e) {
        return TlinqApiResponse.error(e.getErrorCode(), e.getMessage());
    } catch (Exception e) {
        return TlinqApiResponse.error("CRU0001", e.getMessage());
    }
}

/**
 * Delete cruise with cascade
 */
@POST
@Path("/cruise/delete")
public TlinqApiResponse cruiseDelete(
        @FormParam("session") String session,
        @FormParam("cruiseId") Integer cruiseId,
        @FormParam("confirmCascade") Boolean confirmCascade) {
    try {
        cruiseFacade.deleteCruise(cruiseId, confirmCascade != null && confirmCascade);
        return TlinqApiResponse.ok("Cruise deleted successfully");
    } catch (CruiseException e) {
        return TlinqApiResponse.error(e.getErrorCode(), e.getMessage());
    } catch (Exception e) {
        return TlinqApiResponse.error("CRU0001", e.getMessage());
    }
}

7.4 Cruise Point Endpoints

// ============== CRUISE POINT ENDPOINTS ==============

@POST
@Path("/cruisepoint/list")
public TlinqApiResponse cruisepointList(
        @FormParam("session") String session,
        @FormParam("cruiseId") Integer cruiseId) {
    try {
        List<CCruisePoint> points = cruiseFacade.listCruisePoints(cruiseId);
        // Sort chronologically
        points.sort((a, b) -> a.getArrivalTime().compareTo(b.getArrivalTime()));
        return TlinqApiResponse.ok(points);
    } catch (Exception e) {
        return TlinqApiResponse.error("CRU0001", e.getMessage());
    }
}

@POST
@Path("/cruisepoint/regenerate")
public TlinqApiResponse cruisepointRegenerate(
        @FormParam("session") String session,
        @FormParam("cruiseId") Integer cruiseId,
        @FormParam("confirmOverwrite") Boolean confirmOverwrite) {
    try {
        List<CCruisePoint> points = cruiseFacade.regenerateCruisePoints(cruiseId,
            confirmOverwrite != null && confirmOverwrite);
        return TlinqApiResponse.ok(points);
    } catch (CruiseException e) {
        return TlinqApiResponse.error(e.getErrorCode(), e.getMessage());
    } catch (Exception e) {
        return TlinqApiResponse.error("CRU0001", e.getMessage());
    }
}

7.5 Implementation Tasks

Task ID Description Effort
API-010 Implement itinerary CRUD endpoints (5 endpoints) 3h
API-011 Implement itinerary tree endpoint 1h
API-012 Implement template CRUD endpoints (4 endpoints) 2h
API-013 Implement cruise CRUD endpoints (5 endpoints) 3h
API-014 Implement cruise point endpoints (5 endpoints) 2h

8. Phase 6: API Endpoints - Pricing Management

8.1 Cabin Charge Endpoints

// ============== CABIN CHARGE ENDPOINTS ==============

@POST
@Path("/cabincharge/list")
public TlinqApiResponse cabinchargeList(
        @FormParam("session") String session,
        @FormParam("cruiseId") Integer cruiseId) {
    try {
        List<CCabinCharge> charges = cruiseFacade.searchCabinCharge(cruiseId, null, null);
        return TlinqApiResponse.ok(charges);
    } catch (Exception e) {
        return TlinqApiResponse.error("CRU0001", e.getMessage());
    }
}

@POST
@Path("/cabincharge/write")
public TlinqApiResponse cabinchargeWrite(
        @FormParam("session") String session,
        @FormParam("chargeId") Integer chargeId,
        @FormParam("cruiseId") Integer cruiseId,
        @FormParam("shipCabinId") Integer shipCabinId,
        @FormParam("amount") Double amount,
        @FormParam("currency") String currency,
        @FormParam("exrate") Double exrate,
        @FormParam("available") Boolean available) {
    try {
        // Validation
        if (amount != null && amount < 0) {
            return TlinqApiResponse.error("CRU0008", "Amount must be non-negative");
        }
        if (currency != null && currency.length() != 3) {
            return TlinqApiResponse.error("CRU0009", "Currency must be 3 characters");
        }

        CCabinCharge charge;
        if (chargeId == null) {
            // Check for duplicate
            List<CCabinCharge> existing = cruiseFacade.searchCabinCharge(cruiseId, shipCabinId, null);
            if (existing != null && !existing.isEmpty()) {
                return TlinqApiResponse.error("CRU0012",
                    "Cabin charge already exists for this cabin on this cruise");
            }
            charge = cruiseFacade.createCabinCharge(cruiseId, shipCabinId, amount,
                exrate, currency, available);
        } else {
            charge = cruiseFacade.getCabinCharge(chargeId);
            charge.setAmount(amount);
            charge.setCurrency(currency);
            charge.setExRate(exrate);
            charge.setAvailable(available);
            charge = cruiseFacade.updateCabinCharge(charge);
        }
        return TlinqApiResponse.ok(charge);
    } catch (Exception e) {
        return TlinqApiResponse.error("CRU0001", e.getMessage());
    }
}

@POST
@Path("/cabincharge/initForCruise")
public TlinqApiResponse cabinchargeInit(
        @FormParam("session") String session,
        @FormParam("cruiseId") Integer cruiseId) {
    try {
        List<CCabinCharge> charges = cruiseFacade.initCabinChargesForCruise(cruiseId);
        return TlinqApiResponse.ok(charges);
    } catch (CruiseException e) {
        return TlinqApiResponse.error(e.getErrorCode(), e.getMessage());
    } catch (Exception e) {
        return TlinqApiResponse.error("CRU0001", e.getMessage());
    }
}

8.2 Other Charge Endpoints

// ============== OTHER CHARGE ENDPOINTS ==============

@POST
@Path("/othercharge/list")
public TlinqApiResponse otherchargeList(
        @FormParam("session") String session,
        @FormParam("cruiseId") Integer cruiseId) {
    try {
        List<COtherCharge> charges = cruiseFacade.getCruiseOtherCharges(cruiseId, null);
        return TlinqApiResponse.ok(charges);
    } catch (Exception e) {
        return TlinqApiResponse.error("CRU0001", e.getMessage());
    }
}

@POST
@Path("/othercharge/write")
public TlinqApiResponse otherchargeWrite(
        @FormParam("session") String session,
        @FormParam("chargeId") Integer chargeId,
        @FormParam("cruiseId") Integer cruiseId,
        @FormParam("chargeTypeId") Integer chargeTypeId,
        @FormParam("amount") Double amount,
        @FormParam("currency") String currency,
        @FormParam("exrate") Double exrate,
        @FormParam("mandatory") Boolean mandatory) {
    try {
        // Validation
        if (amount != null && amount < 0) {
            return TlinqApiResponse.error("CRU0008", "Amount must be non-negative");
        }

        COtherCharge charge;
        if (chargeId == null) {
            // Check for duplicate charge type on same cruise
            List<COtherCharge> existing = cruiseFacade.getCruiseOtherCharges(cruiseId, null);
            for (COtherCharge c : existing) {
                if (c.getChargeTypeId().equals(chargeTypeId)) {
                    return TlinqApiResponse.error("CRU0013",
                        "Charge type already exists for this cruise");
                }
            }
            charge = cruiseFacade.createOtherCharge(chargeTypeId, cruiseId, amount,
                currency, exrate, mandatory);
        } else {
            charge = cruiseFacade.getOtherCharge(chargeId);
            charge.setChargeTypeId(chargeTypeId);
            charge.setAmount(amount);
            charge.setCurrency(currency);
            charge.setExRate(exrate);
            charge.setMandatory(mandatory);
            charge = cruiseFacade.updateOtherCharge(charge);
        }
        return TlinqApiResponse.ok(charge);
    } catch (Exception e) {
        return TlinqApiResponse.error("CRU0001", e.getMessage());
    }
}

@POST
@Path("/othercharge/delete")
public TlinqApiResponse otherchargeDelete(
        @FormParam("session") String session,
        @FormParam("chargeId") Integer chargeId) {
    try {
        cruiseFacade.deleteOtherCharge(chargeId);
        return TlinqApiResponse.ok("Other charge deleted successfully");
    } catch (Exception e) {
        return TlinqApiResponse.error("CRU0001", e.getMessage());
    }
}

8.3 Implementation Tasks

Task ID Description Effort
API-020 Implement cabincharge endpoints (5 endpoints) 2h
API-021 Implement othercharge endpoints (4 endpoints) 1.5h

9. Phase 7: API Endpoints - Search & Booking

9.1 Enhanced Search Endpoint

/**
 * Enhanced cruise search with additional filters
 * POST /tlinq-api/cruise/searchCruises (ENHANCE EXISTING)
 */
@POST
@Path("/searchCruises")
public TlinqApiResponse searchCruises(
        @FormParam("session") String session,
        @FormParam("areaId") Integer areaId,
        @FormParam("companyId") Integer companyId,
        @FormParam("shipId") Integer shipId,
        @FormParam("dateRangeStart") String dateRangeStart,
        @FormParam("dateRangeEnd") String dateRangeEnd,
        @FormParam("minDuration") Integer minDuration,
        @FormParam("maxDuration") Integer maxDuration,
        @FormParam("portId") Integer portId,
        @FormParam("status") String status) {
    try {
        LocalDate rangeStart = dateRangeStart != null ? LocalDate.parse(dateRangeStart) : null;
        LocalDate rangeEnd = dateRangeEnd != null ? LocalDate.parse(dateRangeEnd) : null;

        CCruiseSearch search = cruiseFacade.executeCruiseSearch(
            areaId, rangeStart, rangeEnd, minDuration, maxDuration);

        // Apply additional filters not supported by existing search
        List<CCruiseSearchResultItem> filtered = new ArrayList<>();
        for (CCruiseSearchResultItem item : search.getResult()) {
            // Filter by company
            if (companyId != null && !item.getCompany().getCompanyId().equals(companyId)) {
                continue;
            }
            // Filter by ship
            if (shipId != null && !item.getShip().getCruiseShipId().equals(shipId)) {
                continue;
            }
            // Filter by status
            if (status != null && !status.equals(item.getCruise().getStatus())) {
                continue;
            }
            // Filter by port (check cruise points)
            if (portId != null) {
                boolean hasPort = false;
                for (CCruisePointRecord point : item.getCruisePoints()) {
                    if (point.getPort().getCruisePortId().equals(portId)) {
                        hasPort = true;
                        break;
                    }
                }
                if (!hasPort) continue;
            }
            filtered.add(item);
        }

        search.setResult(filtered.toArray(new CCruiseSearchResultItem[0]));
        search.setResultCount(filtered.size());

        return TlinqApiResponse.ok(search);
    } catch (Exception e) {
        return TlinqApiResponse.error("CRU0001", e.getMessage());
    }
}

9.2 Booking Interface Endpoints

// ============== BOOKING INTERFACE ENDPOINTS ==============

/**
 * Get itineraries for booking by area
 */
@POST
@Path("/booking/getItineraries")
public TlinqApiResponse bookingGetItineraries(
        @FormParam("session") String session,
        @FormParam("areaId") Integer areaId) {
    try {
        if (areaId == null) {
            return TlinqApiResponse.error("CRU0002", "areaId is required");
        }
        List<CCruiseItinerary> itineraries = cruiseFacade.getItinerariesForBooking(areaId);
        return TlinqApiResponse.ok(itineraries);
    } catch (Exception e) {
        return TlinqApiResponse.error("CRU0001", e.getMessage());
    }
}

/**
 * Get available cruises for itinerary
 */
@POST
@Path("/booking/getCruises")
public TlinqApiResponse bookingGetCruises(
        @FormParam("session") String session,
        @FormParam("itineraryId") Integer itineraryId) {
    try {
        if (itineraryId == null) {
            return TlinqApiResponse.error("CRU0002", "itineraryId is required");
        }
        // Get future cruises with available status only
        LocalDate today = LocalDate.now();
        List<CCruise> cruises = cruiseFacade.searchCruises(itineraryId, today, null);
        cruises = cruises.stream()
            .filter(c -> "available".equals(c.getStatus()))
            .sorted((a, b) -> a.getStartDate().compareTo(b.getStartDate()))
            .collect(Collectors.toList());
        return TlinqApiResponse.ok(cruises);
    } catch (Exception e) {
        return TlinqApiResponse.error("CRU0001", e.getMessage());
    }
}

/**
 * Get full availability and pricing for a cruise
 */
@POST
@Path("/booking/getAvailability")
public TlinqApiResponse bookingGetAvailability(
        @FormParam("session") String session,
        @FormParam("cruiseId") Integer cruiseId) {
    try {
        if (cruiseId == null) {
            return TlinqApiResponse.error("CRU0002", "cruiseId is required");
        }
        CCruiseAvailability availability = cruiseFacade.getCruiseAvailability(cruiseId);
        return TlinqApiResponse.ok(availability);
    } catch (CruiseException e) {
        return TlinqApiResponse.error(e.getErrorCode(), e.getMessage());
    } catch (Exception e) {
        return TlinqApiResponse.error("CRU0001", e.getMessage());
    }
}

/**
 * Calculate total price for booking
 */
@POST
@Path("/booking/calculatePrice")
@Consumes(MediaType.APPLICATION_JSON)
public TlinqApiResponse bookingCalculatePrice(JsonObject params) {
    try {
        Integer cruiseId = params.getInt("cruiseId");
        Integer shipCabinId = params.getInt("shipCabinId");
        Integer adults = params.getInt("adults");
        Integer children = params.containsKey("children") ? params.getInt("children") : 0;

        List<Integer> optionalChargeIds = new ArrayList<>();
        if (params.containsKey("optionalChargeIds")) {
            JsonArray arr = params.getJsonArray("optionalChargeIds");
            for (int i = 0; i < arr.size(); i++) {
                optionalChargeIds.add(arr.getInt(i));
            }
        }

        CPriceCalculation calc = cruiseFacade.calculateCruisePrice(
            cruiseId, shipCabinId, adults, children, optionalChargeIds);
        return TlinqApiResponse.ok(calc);
    } catch (CruiseException e) {
        return TlinqApiResponse.error(e.getErrorCode(), e.getMessage());
    } catch (Exception e) {
        return TlinqApiResponse.error("CRU0001", e.getMessage());
    }
}

9.3 Implementation Tasks

Task ID Description Effort
API-030 Enhance searchCruises with additional filters 2h
API-031 Implement booking/getItineraries 1h
API-032 Implement booking/getCruises 1h
API-033 Implement booking/getAvailability 2h
API-034 Implement booking/calculatePrice 2h

10. Phase 8: Error Handling & Validation

10.1 Error Code Constants

File: tqapi/src/main/java/com/perun/tlinq/api/CruiseErrorCodes.java (NEW)

package com.perun.tlinq.api;

public final class CruiseErrorCodes {
    public static final String GENERAL_ERROR = "CRU0001";
    public static final String MISSING_PARAMETER = "CRU0002";
    public static final String NOT_FOUND = "CRU0003";
    public static final String DUPLICATE_CODE = "CRU0004";
    public static final String HAS_DEPENDENCIES = "CRU0005";
    public static final String INVALID_DATE = "CRU0006";
    public static final String INVALID_TIME = "CRU0007";
    public static final String INVALID_AMOUNT = "CRU0008";
    public static final String INVALID_CURRENCY = "CRU0009";
    public static final String CASCADE_REQUIRED = "CRU0010";
    public static final String NO_TEMPLATE = "CRU0011";
    public static final String DUPLICATE_CABIN_CHARGE = "CRU0012";
    public static final String DUPLICATE_OTHER_CHARGE = "CRU0013";
    public static final String INVALID_STATUS = "CRU0014";
    public static final String CRUISE_HAS_BOOKINGS = "CRU0015";

    private CruiseErrorCodes() {}
}

10.2 Validation Utility

File: tqapi/src/main/java/com/perun/tlinq/api/CruiseValidation.java (NEW)

package com.perun.tlinq.api;

public class CruiseValidation {

    public static void requireNotNull(Object value, String paramName) throws CruiseException {
        if (value == null) {
            throw new CruiseException(CruiseErrorCodes.MISSING_PARAMETER,
                paramName + " is required");
        }
    }

    public static void requireNotEmpty(String value, String paramName) throws CruiseException {
        if (value == null || value.trim().isEmpty()) {
            throw new CruiseException(CruiseErrorCodes.MISSING_PARAMETER,
                paramName + " is required");
        }
    }

    public static void validateMaxLength(String value, int maxLength, String paramName)
            throws CruiseException {
        if (value != null && value.length() > maxLength) {
            throw new CruiseException(CruiseErrorCodes.INVALID_AMOUNT,
                paramName + " must be " + maxLength + " characters or less");
        }
    }

    public static void validatePositive(Number value, String paramName) throws CruiseException {
        if (value != null && value.doubleValue() <= 0) {
            throw new CruiseException(CruiseErrorCodes.INVALID_AMOUNT,
                paramName + " must be positive");
        }
    }

    public static void validateNonNegative(Number value, String paramName) throws CruiseException {
        if (value != null && value.doubleValue() < 0) {
            throw new CruiseException(CruiseErrorCodes.INVALID_AMOUNT,
                paramName + " must be non-negative");
        }
    }

    public static void validateCurrency(String currency) throws CruiseException {
        if (currency != null && currency.length() != 3) {
            throw new CruiseException(CruiseErrorCodes.INVALID_CURRENCY,
                "Currency must be 3 characters (ISO 4217)");
        }
    }

    public static void validateTimeFormat(String time, String paramName) throws CruiseException {
        if (time != null && !time.matches("^([01]?[0-9]|2[0-3]):[0-5][0-9]$")) {
            throw new CruiseException(CruiseErrorCodes.INVALID_TIME,
                paramName + " must be in HH:mm format");
        }
    }

    public static LocalDate parseDate(String dateStr, String paramName) throws CruiseException {
        if (dateStr == null) return null;
        try {
            return LocalDate.parse(dateStr);
        } catch (Exception e) {
            throw new CruiseException(CruiseErrorCodes.INVALID_DATE,
                paramName + " must be in yyyy-MM-dd format");
        }
    }

    public static void validateStatus(String status, String... validValues) throws CruiseException {
        if (status == null) return;
        for (String valid : validValues) {
            if (valid.equals(status)) return;
        }
        throw new CruiseException(CruiseErrorCodes.INVALID_STATUS,
            "Invalid status: " + status + ". Valid values: " + String.join(", ", validValues));
    }
}

10.3 Implementation Tasks

Task ID Description Effort
ERR-001 Create CruiseErrorCodes constants class 0.5h
ERR-002 Create CruiseValidation utility class 1h
ERR-003 Apply validation to all API endpoints 2h
ERR-004 Handle database constraint violations gracefully 1h

11. Implementation Summary

11.1 Total Effort Estimate

Phase Description Effort
Phase 1 Database Schema Updates 3.5h
Phase 2 Entity Class Updates (incl. NTS Service Config) 14h
Phase 3 Facade Layer Enhancements 17.5h
Phase 4 API - Dimension Data 12h
Phase 5 API - Itinerary & Cruise 11h
Phase 6 API - Pricing 3.5h
Phase 7 API - Search & Booking 8h
Phase 8 Error Handling & Validation 4.5h
Phase 9 Integration Testing 8h
Total 82h

11.2 API Endpoint Summary

Category Endpoints New Status
Company 4 3 listCompanies exists
Area 4 3 getCruiseArea exists
Ship 4 4 All new
CabinType 4 4 All new
ChargeType 4 4 All new
Port 4 4 All new
ShipCabin 5 5 All new
Itinerary 6 6 All new
Template 4 4 All new
Cruise 5 5 All new
CruisePoint 5 5 All new
CabinCharge 5 5 All new
OtherCharge 4 4 All new
Search 1 0 searchCruises exists (enhance)
Booking 4 4 All new
Total 63 60

11.3 Files to Create/Modify

New Files: - config/db-changes/cruise-schema-updates.sql - tqapp/.../entity/cruise/CCruiseAvailability.java - tqapp/.../entity/cruise/CPriceCalculation.java - tqapp/.../entity/cruise/CPriceLineItem.java - tqapp/.../entity/cruise/CItineraryTreeNode.java - tqapp/.../entity/cruise/CItineraryTreeItem.java - tqapp/.../entity/cruise/CruiseException.java - tqapp/.../client/nts/entity/cruise/NTSItineraryTreeNode.java - tqapp/.../client/nts/entity/cruise/NTSItineraryTreeItem.java - tqapp/.../client/nts/entity/cruise/NTSCruiseAvailability.java - tqapp/.../client/nts/entity/cruise/NTSPriceCalculation.java - tqapp/.../client/nts/entity/cruise/NTSPriceLineItem.java - tqapi/.../api/CruiseErrorCodes.java - tqapi/.../api/CruiseValidation.java

Modified Files: - tqapp/.../client/nts/db/cruise/ItineraryEntity.java - tqapp/.../client/nts/db/cruise/CruiseEntity.java - tqapp/.../client/nts/db/cruise/ShipcabinEntity.java - tqapp/.../client/nts/db/cruise/CruisetemplateEntity.java - tqapp/.../client/nts/db/cruise/CruisepointEntity.java - tqapp/.../client/nts/db/cruise/OtherchargeEntity.java - tqapp/.../entity/cruise/CCruiseItinerary.java - tqapp/.../entity/cruise/CCruise.java - tqapp/.../entity/cruise/CShipCabin.java - tqapp/.../entity/cruise/CCruiseTemplate.java - tqapp/.../entity/cruise/CCruisePoint.java - tqapp/.../entity/cruise/COtherCharge.java - tqapp/.../entity/cruise/CruiseFacade.java (major additions) - tqapp/.../client/nts/service/cruise/NTSCruiseService.java (5 new methods) - tqapi/.../api/CruiseApi.java (major additions) - config/entities/cruise-entities.xml (new field mappings + 5 new entity configs) - config/nts-client.xml (18 new service configurations)

  1. Week 1: Phases 1-2 (Database + Entities)
  2. Week 2: Phase 3 (Facade Layer)
  3. Week 3: Phases 4-5 (API - Dimension Data, Itinerary, Cruise)
  4. Week 4: Phases 6-8 (API - Pricing, Booking, Error Handling)
  5. Week 5: Phase 9 (Integration Testing + Bug Fixes)

11.5 Dependencies

Phase 1 (DB) → Phase 2 (Entities) → Phase 3 (Facade) → Phases 4-7 (APIs)
                                              Phase 8 (Validation)
                                              Phase 9 (Testing)

12. Appendix

12.1 Existing Facade Methods Reference

The following facade methods already exist and can be leveraged:

// Company
getCruiseCompanies(code, name), createNewCompany(code, name), updateCompany(company), getCruiseCompany(id)

// Area
createCruiseArea(code, name), updateArea(area), getArea(id), searchArea(code, name)

// Ship
createShip(companyId, code, name, desc, docUrl), updateShip(ship), searchShips(companyId, name), getShip(id), getCompanyShips(companyId)

// CabinType
createCabinType(code, name), updateCabinType(type), searchCabinTypes(code, name), getCabinType(id)

// ChargeType
createChargeType(code, name), updateChargeType(type), searchChargeType(code, name), getChargeType(id)

// Port
createCruisePort(code, name), updateCruisePort(port), getCruisePort(id), searchCruisePorts(code, name)

// ShipCabin
createShipCabin(shipId, cabinTypeId, maxPax), updateShipCabin(cabin), getShipCabin(id), getAllShipCabins(shipId), searchShipCabins(shipId, numPax, name)

// Itinerary
createItinerary(shipId, areaId, duration, code, imgUrl, name), updateItinerary(itin), getItinerary(id), searchItinerary(areaId, shipId, minDur, maxDur, name)

// Template
createCruiseTemplate(itinId, seq, portId, arrTime, depTime, arrOff, depOff), updateCruiseTemplate(tmpl), getCruiseTemplate(id), searchCruiseTemplate(itinId, portId)

// Cruise
createCruise(itinId, startDate), createCruiseSkeleton(itinId, startDate), getCruise(id), searchCruises(itinId, from, to), updateCruise(cruise)

// CruisePoint
createCruisePoint(portId, cruiseId, arrival, departure, seq, desc), updateCruisePoint(point), getCruisePoint(id), listCruisePoints(cruiseId)

// CabinCharge
createCabinCharge(cruiseId, shipCabinId, amt, exRate, curr, avail), updateCabinCharge(charge), getCabinCharge(id), searchCabinCharge(cruiseId, shipCabinId, avail)

// OtherCharge
createOtherCharge(typeId, cruiseId, amt, curr, exr, mand), getOtherCharge(id), updateOtherCharge(charge), getCruiseOtherCharges(cruiseId, mand)

// Search
executeCruiseSearch(areaId, rangeStart, rangeEnd, minDur, maxDur)

12.2 Named Queries Available

// CruiseEntity
@NamedQuery(name="listCruises", query="SELECT c FROM CruiseEntity c WHERE c.itineraryid = :itineraryid AND c.startdate >= :fromdate AND c.startdate <= :todate")
@NamedQuery(name="listCruises_noitinerary", query="SELECT c FROM CruiseEntity c WHERE c.startdate >= :fromdate AND c.startdate <= :todate")
@NamedQuery(name="searchCruiseCabins", query="...")

// ShipcabinEntity
@NamedQuery(name="searchShipCabin", query="SELECT sc FROM ShipcabinEntity sc JOIN CabintypeEntity ct ON sc.cabintypeid = ct.cabinid WHERE sc.shipid = :shipid AND sc.maxpax >= :maxpax AND ct.name LIKE :name")