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¶
4.2.4 CCruiseTemplate.java¶
4.2.5 CCruisePoint.java¶
4.2.6 COtherCharge.java¶
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:
- Keep existing endpoints for backward compatibility
- Add new endpoints following the
/cruise/{entity}/{action}pattern - 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)
11.4 Recommended Implementation Order¶
- Week 1: Phases 1-2 (Database + Entities)
- Week 2: Phase 3 (Facade Layer)
- Week 3: Phases 4-5 (API - Dimension Data, Itinerary, Cruise)
- Week 4: Phases 6-8 (API - Pricing, Booking, Error Handling)
- 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")