Skip to content

TQPRO Entity Management Guide

Version: 1.0 Last Updated: 2025-11-19 Author: TourLinq Development Team


Table of Contents

  1. Introduction
  2. Entity Management Concept
  3. Architecture Overview
  4. Entity Configuration System
  5. Creating a New Entity - Step-by-Step Guide
  6. Advanced Topics
  7. Best Practices
  8. Examples
  9. Troubleshooting
  10. Reference

Introduction

TQPRO (TourLinq Professional) is a platform for managing travel and tourism business operations. At its core is a flexible entity management system that provides a unified interface for working with diverse business objects such as customers, flights, hotels, cruises, and products.

This guide explains the entity management concept in TQPRO and provides detailed instructions for creating new entities within the framework.

Key Features

  • Unified Entity Framework: All business objects inherit from a common base class
  • Configuration-Driven: Entity behavior defined in XML configuration files
  • Service Factory Pattern: Pluggable backends for different data sources
  • Field Mapping System: Automatic translation between native and TQPRO entities
  • Facade Pattern: Simplified API for entity operations (CRUD)
  • Multi-Backend Support: Single entity can work with multiple service factories

Entity Management Concept

What is an Entity?

In TQPRO, an entity represents a business domain object - a customer, hotel, flight, product, etc. Entities serve as:

  1. Data Transfer Objects (DTOs): Carrying data between application layers
  2. Domain Models: Representing business concepts
  3. Integration Adapters: Bridging TQPRO and external systems (Odoo, Amadeus, Rayna, etc.)

The Three-Layer Architecture

┌─────────────────────────────────────────────────────────────┐
│                     Application Layer                       │
│                  (Controllers, Services)                    │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│                      Facade Layer                           │
│          (CustomerFacade, ProductFacade, etc.)             │
│          - High-level API for entity operations             │
│          - CRUD operations (create, read, update, delete)   │
│          - Business logic coordination                      │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│                      Entity Layer                           │
│          (CCustomer, CProduct, CFlightOffer, etc.)         │
│          - Pure data objects (POJOs)                        │
│          - Field getters and setters                        │
│          - Extends TlinqEntity                              │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│                 Service Factory Layer                       │
│          (OdooServiceFactory, AmadeusServiceFactory)       │
│          - Backend-specific implementations                 │
│          - Field mapping and translation                    │
│          - Native API integration                           │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│                   External Systems                          │
│          (Odoo, Amadeus, Rayna B2B, NTS, etc.)            │
└─────────────────────────────────────────────────────────────┘

Core Concepts

1. Entity Classes

Entity classes extend TlinqEntity and represent business objects:

public class CCustomer extends TlinqEntity implements CustomerI {
    protected Integer customerId;
    protected String custName;
    protected String email;
    // ... more fields with getters/setters
}

2. Entity Configuration

Entities are configured in XML files that define: - Entity metadata (name, class, ID field) - Service factories that can handle the entity - Service operations (search, read, create, update, delete) - Field mappings between TQPRO and native formats

3. Service Factories

Service factories provide backend-specific implementations: - OdooServiceFactory - Odoo ERP integration - AmadeusServiceFactory - Amadeus GDS integration - RaynaB2BServiceFactory - Rayna B2B API integration - NTSServiceFactory - NTS database integration

4. Facades

Facades provide a simplified API for working with entities:

CustomerFacade facade = new CustomerFacade(securityToken);
facade.loadCustomer(customerId);
CCustomer customer = facade.getCustomer();
customer.setEmail("newemail@example.com");
facade.save();

Architecture Overview

Class Hierarchy

AbstractEntity (base)
TlinqEntity (framework integration)
CCustomer, CProduct, CFlight, etc. (domain entities)

AbstractEntity

Base class providing: - Serialization support - Field reflection utilities - Entity initialization from native objects - JSON serialization (toString()) - Annotation-based field mapping

Key Methods: - initFromEntity(Object ent) - Initialize from native entity - listEntityFields() - Get all entity fields via reflection - toString() - JSON representation

TlinqEntity

Framework integration layer providing: - Factory name tracking - Lifecycle hooks (preLoad(), postLoad()) - Factory context management

Domain Entities

Specific business objects like CCustomer, CProduct, etc.

Characteristics: - Extend TlinqEntity - Plain Java objects (POJOs) with fields and getters/setters - May implement domain interfaces (e.g., CustomerI) - Optional annotations for field mapping


Entity Configuration System

Configuration Structure

Entity configurations are stored in XML files organized by domain:

config/
├── tourlinq-config.xml              # Main config with XInclude references
└── entities/
    ├── customer-entities.xml        # Customer domain entities
    ├── product-entities.xml         # Product domain entities
    ├── flight-entities.xml          # Flight domain entities
    └── ... (more domain files)

Entity Configuration Format

<Entity name="Customer"
        class="com.perun.tlinq.entity.customer.CCustomer"
        idField="customerId"
        defaultFactory="OdooServiceFactory">

    <EntityFactoryList>
        <Factory name="OdooServiceFactory"
                 nativeEntity="com.perun.tlinq.client.odoo.entity.OdooCustomer">

            <ServiceList>
                <Service name="searchCustomers" action="search"/>
                <Service name="getCustomers" action="read"/>
                <Service name="createCustomer" action="create"/>
                <Service name="updateCustomer" action="update"/>
            </ServiceList>

            <FieldMappingList>
                <FieldMapping targetField="customerId"
                             sourceField="id"
                             mapping="DirectMapping"/>
                <FieldMapping targetField="custName"
                             sourceField="name"
                             mapping="DirectMapping"/>
                <!-- More field mappings -->
            </FieldMappingList>

        </Factory>
    </EntityFactoryList>
</Entity>

Configuration Elements

Entity Element

Attribute Description Required
name Entity name (used in code) Yes
class Fully qualified Java class name Yes
idField Name of the ID field Yes
defaultFactory Default service factory to use Yes

Factory Element

Attribute Description Required
name Service factory name Yes
nativeEntity Native entity class (backend-specific) No

Service Element

Attribute Description Required
name Service method name Yes
action CRUD action (search/read/create/update/delete) or custom Yes
returnClass Return type for the service No

FieldMapping Element

Attribute Description Required
targetField TQPRO entity field name Yes
sourceField Native entity field name Yes
mapping Mapping type (DirectMapping, ArrayMapping, etc.) Yes
reverseMapping Reverse mapping type No
targetFieldEntity Target entity for nested mappings No

Mapping Types

Type Description Example Use Case
DirectMapping Simple 1:1 field mapping customerIdid
ArrayMapping Collection/array mapping contacts[]contact_ids[]
NestedMapping Complex object relationships Customer → Address mapping
IndexMapping Reference by ID/index countryIdcountry_id[0]
NoMapping Virtual/computed field (one-way) fullName (computed from first + last)

Creating a New Entity - Step-by-Step Guide

This section provides a comprehensive guide to creating a new entity in TQPRO.

Step 1: Define the Business Requirements

Before creating an entity, answer these questions:

  1. What business object does this represent? (e.g., Visa Application, Hotel Booking)
  2. What data fields are needed? (name, ID, dates, prices, etc.)
  3. Which backend system will provide the data? (Odoo, Amadeus, custom API)
  4. What operations are needed? (search, create, update, delete)
  5. How does it relate to other entities? (belongs to Customer, contains Items, etc.)

Step 2: Create the Entity Class

Create a new Java class in the appropriate package:

Package Structure:

com.perun.tlinq.entity.[domain]

Example: Creating a Visa Application Entity

package com.perun.tlinq.entity.visa;

import com.perun.tlinq.entity.TlinqEntity;
import java.time.LocalDate;

/**
 * Visa Application entity
 * Represents a visa application request
 */
public class CVisaApplication extends TlinqEntity {

    // Primary key
    protected Integer applicationId;

    // Basic information
    protected String applicationNumber;
    protected String applicantName;
    protected String passportNumber;
    protected String nationality;

    // Application details
    protected String visaType;
    protected String visaCategory;
    protected String destinationCountry;
    protected LocalDate travelDate;
    protected Integer durationDays;

    // Status
    protected String status;
    protected LocalDate submissionDate;
    protected LocalDate approvalDate;

    // Relationships
    protected Integer customerId;
    protected String customerName;

    // Cost
    protected Double applicationFee;
    protected Double processingFee;
    protected Double totalAmount;

    // Documents
    protected String[] requiredDocuments;
    protected String[] submittedDocuments;

    // Constructor
    public CVisaApplication() {
        super();
    }

    // Getters and Setters

    public Integer getApplicationId() {
        return applicationId;
    }

    public void setApplicationId(Integer applicationId) {
        this.applicationId = applicationId;
    }

    public String getApplicationNumber() {
        return applicationNumber;
    }

    public void setApplicationNumber(String applicationNumber) {
        this.applicationNumber = applicationNumber;
    }

    public String getApplicantName() {
        return applicantName;
    }

    public void setApplicantName(String applicantName) {
        this.applicantName = applicantName;
    }

    // ... continue with all getters and setters

    public String[] getRequiredDocuments() {
        return requiredDocuments;
    }

    public void setRequiredDocuments(String[] requiredDocuments) {
        this.requiredDocuments = requiredDocuments;
    }

    // Optional: Override lifecycle hooks
    @Override
    public void preLoad() {
        // Called before entity is loaded from backend
        super.preLoad();
        // Custom logic here
    }

    @Override
    public void postLoad() {
        // Called after entity is loaded from backend
        super.postLoad();
        // Custom logic here (e.g., calculate totalAmount)
        if (applicationFee != null && processingFee != null) {
            totalAmount = applicationFee + processingFee;
        }
    }
}

Key Points: - Extend TlinqEntity - Use protected fields - Follow naming convention: C[EntityName] - Provide getters and setters for all fields - Add JavaDoc comments - Use appropriate data types (Integer for IDs, LocalDate for dates, etc.)

Step 3: Create the Entity Configuration

Add the entity configuration to the appropriate domain XML file:

File: config/entities/visa-entities.xml

<Entity name="VisaApplication"
        class="com.perun.tlinq.entity.visa.CVisaApplication"
        idField="applicationId"
        defaultFactory="OdooServiceFactory">

    <EntityFactoryList>
        <Factory name="OdooServiceFactory"
                 nativeEntity="com.perun.tlinq.client.odoo.entity.OdooVisaApplication">

            <ServiceList>
                <!-- CRUD operations -->
                <Service name="searchVisaApplications" action="search"/>
                <Service name="getVisaApplication" action="read"/>
                <Service name="createVisaApplication" action="create"/>
                <Service name="updateVisaApplication" action="update"/>
                <Service name="deleteVisaApplication" action="delete"/>

                <!-- Custom operations -->
                <Service name="submitForApproval" action="submit"/>
                <Service name="approveApplication" action="approve"/>
                <Service name="rejectApplication" action="reject"/>
            </ServiceList>

            <FieldMappingList>
                <!-- ID fields -->
                <FieldMapping targetField="applicationId"
                             sourceField="id"
                             mapping="DirectMapping"/>

                <!-- Basic information -->
                <FieldMapping targetField="applicationNumber"
                             sourceField="application_number"
                             mapping="DirectMapping"/>
                <FieldMapping targetField="applicantName"
                             sourceField="applicant_name"
                             mapping="DirectMapping"/>
                <FieldMapping targetField="passportNumber"
                             sourceField="passport_number"
                             mapping="DirectMapping"/>
                <FieldMapping targetField="nationality"
                             sourceField="nationality"
                             mapping="DirectMapping"/>

                <!-- Application details -->
                <FieldMapping targetField="visaType"
                             sourceField="visa_type"
                             mapping="DirectMapping"/>
                <FieldMapping targetField="visaCategory"
                             sourceField="visa_category"
                             mapping="DirectMapping"/>
                <FieldMapping targetField="destinationCountry"
                             sourceField="destination_country"
                             mapping="DirectMapping"/>
                <FieldMapping targetField="travelDate"
                             sourceField="travel_date"
                             mapping="DirectMapping"/>
                <FieldMapping targetField="durationDays"
                             sourceField="duration_days"
                             mapping="DirectMapping"/>

                <!-- Status fields -->
                <FieldMapping targetField="status"
                             sourceField="state"
                             mapping="DirectMapping"/>
                <FieldMapping targetField="submissionDate"
                             sourceField="submission_date"
                             mapping="DirectMapping"/>
                <FieldMapping targetField="approvalDate"
                             sourceField="approval_date"
                             mapping="DirectMapping"/>

                <!-- Customer relationship -->
                <FieldMapping targetField="customerId"
                             sourceField="partner_id"
                             mapping="IndexMapping"/>
                <FieldMapping targetField="customerName"
                             sourceField="partner_name"
                             mapping="DirectMapping"
                             reverseMapping="NoMapping"/>

                <!-- Financial fields -->
                <FieldMapping targetField="applicationFee"
                             sourceField="application_fee"
                             mapping="DirectMapping"/>
                <FieldMapping targetField="processingFee"
                             sourceField="processing_fee"
                             mapping="DirectMapping"/>
                <FieldMapping targetField="totalAmount"
                             sourceField="total_amount"
                             mapping="DirectMapping"
                             reverseMapping="NoMapping"/>

                <!-- Document arrays -->
                <FieldMapping targetField="requiredDocuments"
                             sourceField="required_document_ids"
                             mapping="ArrayMapping"/>
                <FieldMapping targetField="submittedDocuments"
                             sourceField="submitted_document_ids"
                             mapping="ArrayMapping"/>
            </FieldMappingList>

        </Factory>
    </EntityFactoryList>
</Entity>

Configuration Guidelines: - Use descriptive service names - Map all entity fields - Use appropriate mapping types - Set reverseMapping="NoMapping" for read-only/computed fields - Include custom actions beyond CRUD if needed

Step 4: Create the Native Entity (Backend-Specific)

Create the native entity class that represents the backend format:

File: com/perun/tlinq/client/odoo/entity/OdooVisaApplication.java

package com.perun.tlinq.client.odoo.entity;

import com.perun.tlinq.client.odoo.OdooEntity;

/**
 * Native Odoo entity for visa applications
 */
public class OdooVisaApplication extends OdooEntity {

    public Integer id;
    public String application_number;
    public String applicant_name;
    public String passport_number;
    public String nationality;

    public String visa_type;
    public String visa_category;
    public String destination_country;
    public String travel_date;
    public Integer duration_days;

    public String state;
    public String submission_date;
    public String approval_date;

    public Integer[] partner_id;  // Odoo uses ID arrays for many2one
    public String partner_name;

    public Double application_fee;
    public Double processing_fee;
    public Double total_amount;

    public Integer[] required_document_ids;
    public Integer[] submitted_document_ids;

    // Constructor
    public OdooVisaApplication() {
        super();
    }
}

Native Entity Notes: - Match backend field names exactly - Use backend data types (Odoo uses Integer[] for references) - Extends framework-specific base class (OdooEntity, AmadeusEntity, etc.)

Step 5: Implement Service Methods

Add service methods to the appropriate service factory:

File: com/perun/tlinq/client/odoo/service/OdooServiceFactory.java

/**
 * Search for visa applications
 */
public List<CVisaApplication> searchVisaApplications(
        String token,
        SelectCriteriaList criteria) throws TlinqClientException {

    // Call Odoo API
    OdooVisaApplication[] results = odooClient.searchRead(
        "visa.application",
        criteria.toOdooDomain(),
        OdooVisaApplication.class
    );

    // Map to TQPRO entities
    List<CVisaApplication> applications = new ArrayList<>();
    for (OdooVisaApplication native : results) {
        CVisaApplication app = new CVisaApplication();
        entityMapper.map(native, app);
        applications.add(app);
    }

    return applications;
}

/**
 * Get visa application by ID
 */
public CVisaApplication getVisaApplication(
        String token,
        Integer id) throws TlinqClientException {

    OdooVisaApplication native = odooClient.read(
        "visa.application",
        id,
        OdooVisaApplication.class
    );

    CVisaApplication app = new CVisaApplication();
    entityMapper.map(native, app);
    return app;
}

/**
 * Create visa application
 */
public CVisaApplication createVisaApplication(
        String token,
        CVisaApplication application) throws TlinqClientException {

    OdooVisaApplication native = new OdooVisaApplication();
    entityMapper.mapReverse(application, native);

    Integer id = odooClient.create("visa.application", native);

    return getVisaApplication(token, id);
}

/**
 * Update visa application
 */
public CVisaApplication updateVisaApplication(
        String token,
        CVisaApplication application) throws TlinqClientException {

    OdooVisaApplication native = new OdooVisaApplication();
    entityMapper.mapReverse(application, native);

    odooClient.update(
        "visa.application",
        application.getApplicationId(),
        native
    );

    return getVisaApplication(token, application.getApplicationId());
}

/**
 * Custom action: Submit application for approval
 */
public CVisaApplication submitForApproval(
        String token,
        Integer id) throws TlinqClientException {

    odooClient.callMethod(
        "visa.application",
        "action_submit",
        new Object[]{id}
    );

    return getVisaApplication(token, id);
}

Create a facade to simplify entity operations:

File: com/perun/tlinq/entity/visa/VisaApplicationFacade.java

package com.perun.tlinq.entity.visa;

import com.perun.tlinq.config.SelectCriteriaList;
import com.perun.tlinq.entity.EntityFacade;
import com.perun.tlinq.util.TlinqClientException;

import java.util.List;

/**
 * Facade for VisaApplication entity operations
 */
public class VisaApplicationFacade extends EntityFacade {

    private CVisaApplication application;
    private static final String ENTITY_NAME = "VisaApplication";

    // Constructors

    public VisaApplicationFacade(String token) {
        super(token);
    }

    public VisaApplicationFacade(String token, boolean initializeNew) {
        super(token);
        if (initializeNew) {
            application = new CVisaApplication();
        }
    }

    public VisaApplicationFacade(String token, Integer applicationId)
            throws TlinqClientException {
        super(token);
        application = findById(applicationId);
    }

    public VisaApplicationFacade(String token, CVisaApplication app) {
        super(token);
        this.application = app;
    }

    // CRUD Operations

    /**
     * Save the current application (create or update)
     */
    public void save() throws TlinqClientException {
        if (application == null) {
            throw new TlinqClientException("Application not initialized");
        }

        if (application.getApplicationId() != null) {
            application = update(application);
        } else {
            application = create(application);
        }
    }

    /**
     * Load application by ID
     */
    public void load(Integer id) throws TlinqClientException {
        application = findById(id);
    }

    /**
     * Delete current application
     */
    public void delete() throws TlinqClientException {
        if (application == null || application.getApplicationId() == null) {
            throw new TlinqClientException("Cannot delete unsaved application");
        }
        deleteById(application.getApplicationId());
        application = null;
    }

    /**
     * Search for applications
     */
    public List<CVisaApplication> search(SelectCriteriaList criteria)
            throws TlinqClientException {
        return (List<CVisaApplication>) searchEntities(ENTITY_NAME, criteria);
    }

    // Helper Methods

    private CVisaApplication findById(Integer id) throws TlinqClientException {
        return (CVisaApplication) getEntityFactory()
            .readEntity(getToken(), ENTITY_NAME, id);
    }

    private CVisaApplication create(CVisaApplication app)
            throws TlinqClientException {
        return (CVisaApplication) getEntityFactory()
            .createEntity(getToken(), ENTITY_NAME, app);
    }

    private CVisaApplication update(CVisaApplication app)
            throws TlinqClientException {
        return (CVisaApplication) getEntityFactory()
            .updateEntity(getToken(), ENTITY_NAME, app);
    }

    private void deleteById(Integer id) throws TlinqClientException {
        getEntityFactory().deleteEntity(getToken(), ENTITY_NAME, id);
    }

    // Business Methods

    /**
     * Submit application for approval
     */
    public void submitForApproval() throws TlinqClientException {
        if (application == null || application.getApplicationId() == null) {
            throw new TlinqClientException("Cannot submit unsaved application");
        }

        // Call custom service action
        application = (CVisaApplication) getEntityFactory()
            .executeCustomAction(
                getToken(),
                ENTITY_NAME,
                "submit",
                application.getApplicationId()
            );
    }

    /**
     * Check if application is approved
     */
    public boolean isApproved() {
        return application != null && "approved".equals(application.getStatus());
    }

    /**
     * Calculate total cost
     */
    public Double calculateTotalCost() {
        if (application == null) return 0.0;

        Double total = 0.0;
        if (application.getApplicationFee() != null) {
            total += application.getApplicationFee();
        }
        if (application.getProcessingFee() != null) {
            total += application.getProcessingFee();
        }
        return total;
    }

    // Getters and Setters

    public CVisaApplication getApplication() {
        return application;
    }

    public void setApplication(CVisaApplication application) {
        this.application = application;
    }
}

Step 7: Update Configuration Index

If you created a new domain file, update the main configuration:

File: config/tourlinq-config.xml

<Entities>
    <!-- ... existing includes ... -->

    <!-- Visa entities -->
    <xi:include href="entities/visa-entities.xml" xpointer="xpointer(/Entities/*)"/>

    <!-- ... more includes ... -->
</Entities>

Step 8: Test the Entity

Create a test class to verify the entity works correctly:

File: test/.../visa/VisaApplicationTest.java

package com.perun.tlinq.entity.visa;

import com.perun.tlinq.util.TlinqClientException;
import org.junit.Test;
import static org.junit.Assert.*;

public class VisaApplicationTest {

    @Test
    public void testCreateVisaApplication() throws TlinqClientException {
        String token = getTestToken();

        // Create new application
        VisaApplicationFacade facade = new VisaApplicationFacade(token, true);
        CVisaApplication app = facade.getApplication();

        app.setApplicantName("John Doe");
        app.setPassportNumber("AB123456");
        app.setNationality("US");
        app.setVisaType("Tourist");
        app.setDestinationCountry("FR");
        app.setDurationDays(30);
        app.setApplicationFee(50.0);
        app.setProcessingFee(25.0);

        // Save
        facade.save();

        // Verify
        assertNotNull(app.getApplicationId());
        assertEquals("John Doe", app.getApplicantName());
        assertEquals(75.0, facade.calculateTotalCost(), 0.01);
    }

    @Test
    public void testLoadVisaApplication() throws TlinqClientException {
        String token = getTestToken();
        Integer testId = 1;

        // Load existing application
        VisaApplicationFacade facade = new VisaApplicationFacade(token, testId);
        CVisaApplication app = facade.getApplication();

        // Verify
        assertNotNull(app);
        assertEquals(testId, app.getApplicationId());
        assertNotNull(app.getApplicantName());
    }

    @Test
    public void testSearchVisaApplications() throws TlinqClientException {
        String token = getTestToken();

        VisaApplicationFacade facade = new VisaApplicationFacade(token);

        SelectCriteriaList criteria = new SelectCriteriaList();
        criteria.add("status", "=", "pending");

        List<CVisaApplication> results = facade.search(criteria);

        assertNotNull(results);
        assertTrue(results.size() > 0);
    }

    private String getTestToken() {
        // Get test security token
        return "test-token";
    }
}

Step 9: Document the Entity

Add documentation to the entity mapping docs:

File: config/entities/docs/visa-mapping.md

Update or create the documentation file with your entity details.

Step 10: Build and Deploy

# Build the project
./gradlew build

# Run tests
./gradlew test

# Deploy
./gradlew deploy

Advanced Topics

Multi-Factory Entities

Some entities can work with multiple service factories:

<Entity name="Product" class="com.perun.tlinq.entity.product.CProduct">
    <EntityFactoryList>
        <!-- Odoo backend -->
        <Factory name="OdooServiceFactory" nativeEntity="...">
            <!-- Odoo-specific services and mappings -->
        </Factory>

        <!-- Rayna B2B backend -->
        <Factory name="RaynaB2BServiceFactory" nativeEntity="...">
            <!-- Rayna-specific services and mappings -->
        </Factory>
    </EntityFactoryList>
</Entity>

Usage:

// Use default factory (Odoo)
ProductFacade facade = new ProductFacade(token);
facade.load(productId);

// Use specific factory
facade.setFactoryName("RaynaB2BServiceFactory");
facade.load(productId);

Custom Service Actions

Beyond CRUD, you can define custom actions:

<Service name="cancelBooking" action="cancel">
    <NamedParams>
        <Param name="bookingId" source="input"/>
        <Param name="reason" source="input"/>
    </NamedParams>
</Service>

Implementation:

public CBooking cancelBooking(String token, Integer bookingId, String reason) {
    // Custom cancellation logic
}

Nested Entity Mappings

For complex relationships:

<FieldMapping targetField="customer"
             targetFieldEntity="Customer"
             sourceField="partner_id"
             mapping="NestedMapping">
    <ModelLookupMapping>
        <ParentEntity name="Customer" idField="id"/>
    </ModelLookupMapping>
</FieldMapping>

Method Reference Mappings

Use method calls for field transformation:

<FieldMapping targetField="status"
             sourceField="#getStatusString"
             mapping="DirectMapping"/>

This calls getStatusString() method on the native entity.

Computed Fields

Fields calculated on the fly:

@Override
public void postLoad() {
    super.postLoad();
    // Compute total from line items
    this.total = calculateTotal();
}

private Double calculateTotal() {
    return items.stream()
        .mapToDouble(Item::getAmount)
        .sum();
}

Mark as read-only in config:

<FieldMapping targetField="total"
             sourceField="amount_total"
             mapping="DirectMapping"
             reverseMapping="NoMapping"/>

Best Practices

Entity Design

  1. Single Responsibility: Each entity represents one business concept
  2. Immutable IDs: Never change ID fields after creation
  3. Null Safety: Check for nulls, especially in relationships
  4. Data Types: Use appropriate types (Integer for IDs, LocalDate for dates, Double for money)
  5. Naming: Follow naming conventions (C prefix, camelCase)

Field Naming

  1. Descriptive Names: customerId not cid
  2. Consistent Suffixes: Id for IDs, Name for names, Date for dates
  3. Boolean Prefix: isActive, hasPermission, canEdit
  4. Arrays/Lists: Plural names items, documents

Configuration

  1. Organize by Domain: Keep related entities in the same XML file
  2. Complete Mappings: Map all fields, even if NoMapping
  3. Document Unusual Mappings: Add XML comments for complex mappings
  4. Test Configuration: Validate XML after changes

Service Implementation

  1. Error Handling: Always throw TlinqClientException with descriptive messages
  2. Null Checks: Validate inputs before processing
  3. Logging: Log important operations and errors
  4. Transaction Safety: Ensure operations are atomic when possible

Facade Pattern

  1. One Facade per Entity: Don't mix entity types in one facade
  2. Stateful: Facades hold current entity state
  3. Business Logic: Put business rules in facade, not entity
  4. Simplified API: Hide complexity from calling code

Testing

  1. Unit Tests: Test entity creation, getters, setters
  2. Integration Tests: Test full CRUD lifecycle
  3. Mock Backends: Use mocks for external services
  4. Test Data: Use realistic test data
  5. Edge Cases: Test nulls, empty collections, boundary values

Examples

Example 1: Simple Entity (Read-Only Reference Data)

Use Case: Currency reference data from Odoo

// Entity
public class CCurrency extends TlinqEntity {
    protected Integer id;
    protected String name;
    protected String symbol;
    protected Double rate;

    // Getters and setters...
}
<!-- Configuration -->
<Entity name="Currency"
        class="com.perun.tlinq.entity.common.CCurrency"
        idField="id"
        defaultFactory="OdooServiceFactory">
    <EntityFactoryList>
        <Factory name="OdooServiceFactory" nativeEntity="...">
            <ServiceList>
                <Service name="getCurrencies" action="search"/>
            </ServiceList>
            <FieldMappingList>
                <FieldMapping targetField="id" sourceField="id" mapping="DirectMapping"/>
                <FieldMapping targetField="name" sourceField="name" mapping="DirectMapping"/>
                <FieldMapping targetField="symbol" sourceField="symbol" mapping="DirectMapping"/>
                <FieldMapping targetField="rate" sourceField="rate" mapping="DirectMapping"/>
            </FieldMappingList>
        </Factory>
    </EntityFactoryList>
</Entity>

Example 2: Entity with Relationships

Use Case: Quote with line items

// Parent entity
public class CQuote extends TlinqEntity {
    protected Integer quoteId;
    protected String quoteName;
    protected Integer customerId;
    protected String customerName;
    protected List<CQuoteItem> items;
    protected Double total;

    // Getters and setters...
}

// Child entity
public class CQuoteItem extends TlinqEntity {
    protected Integer itemId;
    protected Integer quoteId;
    protected String description;
    protected Integer quantity;
    protected Double unitPrice;
    protected Double amount;

    // Getters and setters...
}

Example 3: Entity with Complex Mappings

Use Case: Flight offer with nested pricing

public class CFlightOffer extends TlinqEntity {
    protected String offerId;
    protected List<CItinerary> itineraries;
    protected List<CTravelerPricing> travelerPricing;
    protected CPrice price;

    // Complex nested structure
}
<FieldMapping targetField="itineraries"
             sourceField="itineraries"
             mapping="ArrayMapping"/>
<FieldMapping targetField="price.total"
             sourceField="price.grandTotal"
             mapping="NestedMapping"/>

Example 4: Usage in Controller

@RestController
@RequestMapping("/api/visa")
public class VisaController {

    @PostMapping("/applications")
    public ResponseEntity<CVisaApplication> createApplication(
            @RequestHeader("Authorization") String token,
            @RequestBody VisaApplicationRequest request) {

        try {
            // Create facade
            VisaApplicationFacade facade =
                new VisaApplicationFacade(token, true);

            // Populate from request
            CVisaApplication app = facade.getApplication();
            app.setApplicantName(request.getName());
            app.setPassportNumber(request.getPassport());
            app.setVisaType(request.getVisaType());
            // ... more fields

            // Save
            facade.save();

            return ResponseEntity.ok(app);

        } catch (TlinqClientException e) {
            return ResponseEntity.badRequest().build();
        }
    }

    @GetMapping("/applications/{id}")
    public ResponseEntity<CVisaApplication> getApplication(
            @RequestHeader("Authorization") String token,
            @PathVariable Integer id) {

        try {
            VisaApplicationFacade facade =
                new VisaApplicationFacade(token, id);
            return ResponseEntity.ok(facade.getApplication());
        } catch (TlinqClientException e) {
            return ResponseEntity.notFound().build();
        }
    }

    @PostMapping("/applications/{id}/submit")
    public ResponseEntity<CVisaApplication> submitApplication(
            @RequestHeader("Authorization") String token,
            @PathVariable Integer id) {

        try {
            VisaApplicationFacade facade =
                new VisaApplicationFacade(token, id);
            facade.submitForApproval();
            return ResponseEntity.ok(facade.getApplication());
        } catch (TlinqClientException e) {
            return ResponseEntity.badRequest().build();
        }
    }
}

Troubleshooting

Common Issues

1. Entity Not Found

Error: Entity 'MyEntity' not found in configuration

Solutions: - Verify entity name in XML matches code - Check XInclude path in main config - Rebuild project after config changes - Check XML file is in entities/ directory

2. Field Mapping Errors

Error: Cannot map field 'fieldName'

Solutions: - Verify field exists in both TQPRO and native entity - Check field name spelling in XML - Verify mapping type is appropriate - Check data type compatibility

3. Service Not Found

Error: Service 'serviceName' not found for entity 'EntityName'

Solutions: - Add service to entity configuration - Check service name matches method name - Verify action type is correct - Implement service method in factory

4. Factory Not Available

Error: Factory 'FactoryName' not found

Solutions: - Check factory is registered in main config - Verify factory name spelling - Check factory is enabled - Verify factory implementation exists

5. Null Pointer Exceptions

Common Causes: - Accessing nested objects without null checks - Missing field mappings - Backend returning null values - Uninitialized collections

Solutions: - Add null checks before accessing objects - Initialize collections in constructor - Use Optional for nullable fields - Add defensive programming

Debug Tips

  1. Enable Logging:

    Logger.getLogger("com.perun.tlinq").setLevel(Level.FINE);
    

  2. Inspect Configuration:

    ClientConfig config = ClientConfig.instance();
    EntityConfig ec = config.getEntityConfig("MyEntity");
    System.out.println("Entity class: " + ec.getEntityClass());
    System.out.println("ID field: " + ec.getIdField());
    

  3. Test XML Configuration:

    xmllint --xinclude --noout config/tourlinq-config.xml
    

  4. Verify Field Mapping:

    FieldMappingConfig mapping = entityConfig.getFieldMapping(
        factoryName,
        "fieldName"
    );
    System.out.println("Source: " + mapping.getSourceField());
    System.out.println("Type: " + mapping.getMappingType());
    


Reference

Package Structure

com.perun.tlinq
├── entity/                    # Entity classes
│   ├── AbstractEntity.java
│   ├── TlinqEntity.java
│   ├── customer/             # Customer domain
│   ├── product/              # Product domain
│   ├── flight/               # Flight domain
│   └── ...
├── config/                    # Configuration classes
│   ├── EntityConfig.java
│   ├── EntityList.java
│   ├── FieldMappingConfig.java
│   └── ...
├── util/                      # Utilities
│   ├── ClientConfig.java
│   ├── TlinqClientException.java
│   └── ...
└── client/                    # Backend integrations
    ├── odoo/
    ├── amadeus/
    └── ...

Key Classes

Class Purpose
AbstractEntity Base entity with reflection utilities
TlinqEntity Framework integration layer
EntityFacade Base facade for entity operations
ClientConfig Configuration loader and accessor
EntityConfig Entity metadata and mappings
TlinqEntityFactory Entity factory for CRUD operations
ServiceFactoryConfig Service factory configuration

Configuration Files

File Purpose
config/tourlinq-config.xml Main configuration with XInclude references
config/entities/*.xml Domain-specific entity configurations
config/entities/docs/*.md Entity mapping documentation

Annotations

Annotation Purpose Usage
@TlinqEntityField Mark entity fields @TlinqEntityField(sourceName="native_field")
@Transient Exclude from persistence @Transient private String temp;

Configuration Reference

See the entity mapping documentation in config/entities/docs/ for detailed field mappings and service configurations for each domain.


Conclusion

This guide provides a comprehensive overview of entity management in TQPRO. By following these guidelines and best practices, you can successfully create, configure, and maintain entities within the framework.

Quick Checklist for New Entity

  • [ ] Define business requirements
  • [ ] Create entity class extending TlinqEntity
  • [ ] Add to appropriate domain package
  • [ ] Create entity configuration XML
  • [ ] Add field mappings
  • [ ] Define services (CRUD + custom)
  • [ ] Create native entity class
  • [ ] Implement service methods in factory
  • [ ] Create facade class (recommended)
  • [ ] Write tests
  • [ ] Document in mapping docs
  • [ ] Build and deploy

Further Reading

  • Entity Mapping Documentation: config/entities/docs/README.md
  • Service Factory Guide: docs/SERVICE_FACTORY_GUIDE.md
  • API Documentation: docs/API_REFERENCE.md
  • TQPRO Architecture: docs/ARCHITECTURE.md

Document Version: 1.0 Last Updated: 2025-11-19 Maintained by: TourLinq Development Team

For questions or issues, please contact the development team or file an issue in the project repository.