Tiqets Product API - Complete Implementation Plan¶
Overview¶
Implement complete response handling for the Tiqets "Search and Filter Products" API, updating database structure, persistence entities, and expanding the canonical model (CProduct) with additional fields. Nested objects require their own canonical classes, database tables, and proper XML mappings.
Architecture Summary¶
API Response (JSON)
↓
TiqetsProduct.java (Remote DTO with nested classes)
↓
ProductEntity.java + Nested Entity Tables (JPA/DB)
↓
TqProduct.java + TqNested*.java (Native entities)
↓ (XML Field Mappings)
CProduct.java + CNested*.java (Canonical entities)
↓
REST API Response
Part 1: Simple Fields (Direct to CProduct)¶
Fields to Add to CProduct¶
Pricing (6 fields):
- retailPrice - BigDecimal - Retail price in requested currency
- priceSupplierCurrency - BigDecimal - Price in supplier's currency
- supplierCurrencyCode - String - Supplier currency code
- prediscountPrice - BigDecimal - Original price before discount
- discountPercentage - BigDecimal - Discount percentage
- bookingFee - BigDecimal - Booking fee amount
Content (4 fields):
- contentLanguage - String - Content language code
- tagline - String - Short tagline
- promoLabel - String - Promotional label
- summary - String - Summary description
Feature Flags (7 fields):
- supportsCancellationInsurance - Boolean
- instantTicketDelivery - Boolean
- wheelchairAccess - Boolean
- smartphoneTicket - Boolean
- isPackage - Boolean
- inPackageIds - String - Comma-separated package IDs
- packageProductIds - String - Comma-separated product IDs in package
Language Support (4 fields):
- supportedLanguages - String - Comma-separated language codes
- liveGuideLanguages - String - Comma-separated
- audioGuideLanguages - String - Comma-separated
- languageSelectionOptions - String - Comma-separated
Time/Duration (5 fields):
- startingTime - String - HH:MM:SS format
- productTimezone - String - Timezone identifier
- durationFormatted - String - HH:MM:SS format
- advanceArrivalTime - String - HH:MM:SS
- lastAdmissionWindow - String - HH:MM:SS
Ratings (2 fields):
- ratingAverage - BigDecimal - 0-5 rating
- ratingCount - Integer - Number of reviews
Sale Status (3 fields):
- saleStatus - String - "available" or "unavailable"
- saleStatusReason - String - Reason for unavailability
- saleStatusExpectedReopen - Date - Expected reopen date
URLs (2 fields):
- productUrl - String - URL to product on tiqets.com
- productCheckoutUrl - String - URL to checkout on tiqets.com
Content Extended (3 fields):
- productSlug - String - URL slug
- whatsIncluded - String - Localized list of inclusions
- whatsExcluded - String - Localized list of exclusions
Tags (1 field):
- tagIds - String - Comma-separated tag IDs
Part 2: Nested Objects (Require Separate Entities)¶
2.1 Cancellation Policy¶
Canonical Class: CProductCancellation
package com.perun.tlinq.entity.product;
public class CProductCancellation extends TlinqEntity {
private String productCode; // FK to CProduct.prodCode
private Integer windowHours; // Hours before cutoff
private String policyType; // before_timeslot, before_date, never
}
Database Table: tiqets.product_cancellation
CREATE TABLE IF NOT EXISTS tiqets.product_cancellation (
id INTEGER PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
supplier_product_id VARCHAR(30) NOT NULL REFERENCES tiqets.product(supplier_product_id),
window_hours INTEGER,
policy_type VARCHAR(20), -- before_timeslot, before_date, never
CONSTRAINT uk_prod_cancellation UNIQUE (supplier_product_id)
);
Native Entity: TqProductCancellation
JPA Entity: ProductCancellationEntity
2.2 Starting Point¶
Canonical Class: CProductStartingPoint
package com.perun.tlinq.entity.product;
public class CProductStartingPoint extends TlinqEntity {
private String productCode; // FK to CProduct.prodCode
private BigDecimal latitude;
private BigDecimal longitude;
private String address;
private String cityId;
private String label;
}
Database Table: tiqets.product_starting_point
CREATE TABLE IF NOT EXISTS tiqets.product_starting_point (
id INTEGER PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
supplier_product_id VARCHAR(30) NOT NULL REFERENCES tiqets.product(supplier_product_id),
latitude DECIMAL(10,7),
longitude DECIMAL(10,7),
address VARCHAR(500),
city_id VARCHAR(30),
label VARCHAR(255),
CONSTRAINT uk_prod_starting_point UNIQUE (supplier_product_id)
);
Native Entity: TqProductStartingPoint
JPA Entity: ProductStartingPointEntity
2.3 Supplier Info¶
Canonical Class: CProductSupplier
package com.perun.tlinq.entity.product;
public class CProductSupplier extends TlinqEntity {
private String productCode; // FK to CProduct.prodCode
private String supplierName;
private String supplierAddress;
private String supplierCity;
private String supplierPostalcode;
private String supplierCountrycode;
}
Database Table: tiqets.product_supplier
CREATE TABLE IF NOT EXISTS tiqets.product_supplier (
id INTEGER PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
supplier_product_id VARCHAR(30) NOT NULL REFERENCES tiqets.product(supplier_product_id),
supplier_name VARCHAR(255),
supplier_address VARCHAR(500),
supplier_city VARCHAR(255),
supplier_postalcode VARCHAR(50),
supplier_countrycode VARCHAR(10),
CONSTRAINT uk_prod_supplier UNIQUE (supplier_product_id)
);
Native Entity: TqProductSupplier
JPA Entity: ProductSupplierEntity
2.4 Exhibitions (One-to-Many)¶
Canonical Class: CProductExhibition
package com.perun.tlinq.entity.product;
public class CProductExhibition extends TlinqEntity {
private String productCode; // FK to CProduct.prodCode
private String exhibitionId; // Tiqets exhibition ID
private String title;
private String description;
private Date fromDate;
private Date toDate;
private String exhibitionUrl;
}
Database Table: tiqets.product_exhibition
CREATE TABLE IF NOT EXISTS tiqets.product_exhibition (
id INTEGER PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
supplier_product_id VARCHAR(30) NOT NULL REFERENCES tiqets.product(supplier_product_id),
supplier_exhibition_id VARCHAR(30) NOT NULL,
title VARCHAR(500),
description TEXT,
from_date DATE,
to_date DATE,
exhibition_url VARCHAR(500),
CONSTRAINT uk_prod_exhibition UNIQUE (supplier_product_id, supplier_exhibition_id)
);
CREATE INDEX IF NOT EXISTS idx_prod_exhibition_product ON tiqets.product_exhibition(supplier_product_id);
Native Entity: TqProductExhibition
JPA Entity: ProductExhibitionEntity
2.5 Product Images (One-to-Many)¶
Canonical Class: CProductImage
package com.perun.tlinq.entity.product;
public class CProductImage extends TlinqEntity {
private String productCode; // FK to CProduct.prodCode
private Integer imageOrder; // Display order
private String smallUrl; // Small image URL
private String mediumUrl; // Medium image URL
private String largeUrl; // Large image URL
private String extraLargeUrl; // Extra large image URL
private String altText; // Alt text for accessibility
private String credit; // Image credit information
}
Database Table: tiqets.product_image
CREATE TABLE IF NOT EXISTS tiqets.product_image (
id INTEGER PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
supplier_product_id VARCHAR(30) NOT NULL REFERENCES tiqets.product(supplier_product_id),
image_order INTEGER NOT NULL DEFAULT 0,
small_url VARCHAR(500),
medium_url VARCHAR(500),
large_url VARCHAR(500),
extra_large_url VARCHAR(500),
alt_text VARCHAR(500),
credit VARCHAR(500),
CONSTRAINT uk_prod_image UNIQUE (supplier_product_id, image_order)
);
CREATE INDEX IF NOT EXISTS idx_prod_image_product ON tiqets.product_image(supplier_product_id);
Native Entity: TqProductImage
JPA Entity: ProductImageEntity
2.6 Product Groups (One-to-Many with nested products)¶
Canonical Class: CProductGroup
package com.perun.tlinq.entity.product;
public class CProductGroup extends TlinqEntity {
private String productCode; // FK to CProduct.prodCode
private String groupId; // Group ID (main product ID)
private String groupType; // booking_sources, upsell, comparison
private String groupProductIds; // Comma-separated product IDs in group
}
Database Table: tiqets.product_group
CREATE TABLE IF NOT EXISTS tiqets.product_group (
id INTEGER PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
supplier_product_id VARCHAR(30) NOT NULL REFERENCES tiqets.product(supplier_product_id),
group_id VARCHAR(30) NOT NULL,
group_type VARCHAR(30), -- booking_sources, upsell, comparison
group_product_ids TEXT, -- Comma-separated IDs
CONSTRAINT uk_prod_group UNIQUE (supplier_product_id, group_id)
);
CREATE INDEX IF NOT EXISTS idx_prod_group_product ON tiqets.product_group(supplier_product_id);
Native Entity: TqProductGroup
JPA Entity: ProductGroupEntity
Part 3: Files to Create/Modify¶
New Files to Create¶
| File | Location | Purpose |
|---|---|---|
CProductCancellation.java |
tqapp/.../entity/product/ |
Canonical cancellation entity |
CProductStartingPoint.java |
tqapp/.../entity/product/ |
Canonical starting point entity |
CProductSupplier.java |
tqapp/.../entity/product/ |
Canonical supplier entity |
CProductExhibition.java |
tqapp/.../entity/product/ |
Canonical exhibition entity |
CProductGroup.java |
tqapp/.../entity/product/ |
Canonical product group entity |
CProductImage.java |
tqapp/.../entity/product/ |
Canonical image entity |
ProductCancellationEntity.java |
tqtiqets/.../db/ |
JPA entity |
ProductStartingPointEntity.java |
tqtiqets/.../db/ |
JPA entity |
ProductSupplierEntity.java |
tqtiqets/.../db/ |
JPA entity |
ProductExhibitionEntity.java |
tqtiqets/.../db/ |
JPA entity |
ProductGroupEntity.java |
tqtiqets/.../db/ |
JPA entity |
ProductImageEntity.java |
tqtiqets/.../db/ |
JPA entity |
TqProductCancellation.java |
tqtiqets/.../entity/ |
Native entity |
TqProductStartingPoint.java |
tqtiqets/.../entity/ |
Native entity |
TqProductSupplier.java |
tqtiqets/.../entity/ |
Native entity |
TqProductExhibition.java |
tqtiqets/.../entity/ |
Native entity |
TqProductGroup.java |
tqtiqets/.../entity/ |
Native entity |
TqProductImage.java |
tqtiqets/.../entity/ |
Native entity |
Files to Modify¶
| File | Changes |
|---|---|
tqapp/.../entity/product/CProduct.java |
Add ~37 new simple fields |
tqtiqets/.../remote/entity/TiqetsProduct.java |
Add new fields + nested DTO classes |
tqtiqets/.../db/ProductEntity.java |
Add simple fields, update initFromRemote() |
tqtiqets/.../entity/TqProduct.java |
Add new fields with @TlinqEntityField |
config/db/tiqets-schema.sql |
Add columns + new tables |
config/entities/product-entities.xml |
Add field mappings for all new fields |
tqtiqets/.../service/StaticDataRefresher.java |
Handle nested entity refresh |
tqtiqets/.../util/TiqetsCacheManager.java |
Cache nested entities |
Part 4: XML Configuration Updates¶
product-entities.xml - New Entity Definitions¶
<!-- CProductCancellation Entity -->
<Entity name="CProductCancellation"
class="com.perun.tlinq.entity.product.CProductCancellation"
idField="productCode" defaultFactory="TiqetsServiceFactory">
<EntityFactoryList>
<Factory name="TiqetsServiceFactory"
nativeEntity="com.perun.tlinq.client.tiqets.entity.TqProductCancellation">
<FieldMappingList>
<FieldMapping targetField="productCode" sourceField="productId" mapping="DirectMapping"/>
<FieldMapping targetField="windowHours" sourceField="windowHours" mapping="DirectMapping"/>
<FieldMapping targetField="policyType" sourceField="policyType" mapping="DirectMapping"/>
</FieldMappingList>
</Factory>
</EntityFactoryList>
</Entity>
<!-- CProductStartingPoint Entity -->
<Entity name="CProductStartingPoint"
class="com.perun.tlinq.entity.product.CProductStartingPoint"
idField="productCode" defaultFactory="TiqetsServiceFactory">
<EntityFactoryList>
<Factory name="TiqetsServiceFactory"
nativeEntity="com.perun.tlinq.client.tiqets.entity.TqProductStartingPoint">
<FieldMappingList>
<FieldMapping targetField="productCode" sourceField="productId" mapping="DirectMapping"/>
<FieldMapping targetField="latitude" sourceField="latitude" mapping="DirectMapping"/>
<FieldMapping targetField="longitude" sourceField="longitude" mapping="DirectMapping"/>
<FieldMapping targetField="address" sourceField="address" mapping="DirectMapping"/>
<FieldMapping targetField="cityId" sourceField="cityId" mapping="DirectMapping"/>
<FieldMapping targetField="label" sourceField="label" mapping="DirectMapping"/>
</FieldMappingList>
</Factory>
</EntityFactoryList>
</Entity>
<!-- Similar for CProductSupplier, CProductExhibition, CProductGroup -->
CProduct Field Mapping Updates¶
<!-- Add to existing CProduct TiqetsServiceFactory mapping -->
<!-- New simple field mappings -->
<FieldMapping targetField="retailPrice" sourceField="retailPrice" mapping="DirectMapping"/>
<FieldMapping targetField="priceSupplierCurrency" sourceField="priceSupplierCurrency" mapping="DirectMapping"/>
<FieldMapping targetField="supplierCurrencyCode" sourceField="supplierCurrency" mapping="DirectMapping"/>
<FieldMapping targetField="prediscountPrice" sourceField="prediscountPrice" mapping="DirectMapping"/>
<FieldMapping targetField="discountPercentage" sourceField="discountPercentage" mapping="DirectMapping"/>
<FieldMapping targetField="bookingFee" sourceField="bookingFee" mapping="DirectMapping"/>
<FieldMapping targetField="contentLanguage" sourceField="contentLanguage" mapping="DirectMapping"/>
<FieldMapping targetField="tagline" sourceField="tagline" mapping="DirectMapping"/>
<FieldMapping targetField="promoLabel" sourceField="promoLabel" mapping="DirectMapping"/>
<FieldMapping targetField="summary" sourceField="summary" mapping="DirectMapping"/>
<FieldMapping targetField="supportsCancellationInsurance" sourceField="supportsCancellationInsurance" mapping="DirectMapping"/>
<FieldMapping targetField="instantTicketDelivery" sourceField="instantTicketDelivery" mapping="DirectMapping"/>
<FieldMapping targetField="wheelchairAccess" sourceField="wheelchairAccess" mapping="DirectMapping"/>
<FieldMapping targetField="smartphoneTicket" sourceField="smartphoneTicket" mapping="DirectMapping"/>
<FieldMapping targetField="isPackage" sourceField="isPackage" mapping="DirectMapping"/>
<FieldMapping targetField="ratingAverage" sourceField="ratingAverage" mapping="DirectMapping"/>
<FieldMapping targetField="ratingCount" sourceField="ratingCount" mapping="DirectMapping"/>
<FieldMapping targetField="saleStatus" sourceField="saleStatus" mapping="DirectMapping"/>
<FieldMapping targetField="saleStatusReason" sourceField="saleStatusReason" mapping="DirectMapping"/>
<FieldMapping targetField="productUrl" sourceField="productUrl" mapping="DirectMapping"/>
<FieldMapping targetField="productCheckoutUrl" sourceField="productCheckoutUrl" mapping="DirectMapping"/>
<FieldMapping targetField="whatsIncluded" sourceField="whatsIncluded" mapping="DirectMapping"/>
<FieldMapping targetField="whatsExcluded" sourceField="whatsExcluded" mapping="DirectMapping"/>
<FieldMapping targetField="tagIds" sourceField="tagIds" mapping="DirectMapping"/>
<!-- Nested entity mappings (NestedMapping type) -->
<FieldMapping targetField="cancellation" sourceField="cancellation" mapping="NestedMapping">
<NestedMapping entity="CProductCancellation" loadAction="read"/>
</FieldMapping>
<FieldMapping targetField="startingPoint" sourceField="startingPoint" mapping="NestedMapping">
<NestedMapping entity="CProductStartingPoint" loadAction="read"/>
</FieldMapping>
<FieldMapping targetField="supplier" sourceField="supplier" mapping="NestedMapping">
<NestedMapping entity="CProductSupplier" loadAction="read"/>
</FieldMapping>
<FieldMapping targetField="exhibitions" sourceField="exhibitions" mapping="ArrayMapping">
<ArrayMapping entity="CProductExhibition" loadAction="search"/>
</FieldMapping>
<FieldMapping targetField="productGroups" sourceField="productGroups" mapping="ArrayMapping">
<ArrayMapping entity="CProductGroup" loadAction="search"/>
</FieldMapping>
<FieldMapping targetField="images" sourceField="images" mapping="ArrayMapping">
<ArrayMapping entity="CProductImage" loadAction="search"/>
</FieldMapping>
Part 5: Refresh Process Updates¶
StaticDataRefresher.java Changes¶
// In saveProduct() method, after saving main product:
private void saveProduct(TiqetsProduct remote, String supplierExperienceId) {
// ... existing product save logic ...
// Save nested entities
saveCancellation(remote, productEntity.getSupplierProductId());
saveStartingPoint(remote, productEntity.getSupplierProductId());
saveSupplier(remote, productEntity.getSupplierProductId());
saveExhibitions(remote, productEntity.getSupplierProductId());
saveProductGroups(remote, productEntity.getSupplierProductId());
saveImages(remote, productEntity.getSupplierProductId());
}
private void saveCancellation(TiqetsProduct remote, String supplierProductId) {
if (remote.getCancellation() == null) return;
try (Session session = TiqetsDBSession.getSession()) {
Transaction tx = session.beginTransaction();
ProductCancellationEntity entity = session
.createNamedQuery("ProductCancellationEntity.findByProductId", ProductCancellationEntity.class)
.setParameter("productId", supplierProductId)
.uniqueResultOptional()
.orElse(new ProductCancellationEntity());
entity.setSupplierProductId(supplierProductId);
entity.setWindowHours(remote.getCancellation().getWindow());
entity.setPolicyType(remote.getCancellation().getPolicy());
session.merge(entity);
tx.commit();
}
}
private void saveExhibitions(TiqetsProduct remote, String supplierProductId) {
if (remote.getExhibitions() == null || remote.getExhibitions().isEmpty()) return;
try (Session session = TiqetsDBSession.getSession()) {
Transaction tx = session.beginTransaction();
// Delete existing exhibitions for this product
session.createQuery("DELETE FROM ProductExhibitionEntity WHERE supplierProductId = :productId")
.setParameter("productId", supplierProductId)
.executeUpdate();
// Insert new exhibitions
for (TiqetsExhibition exh : remote.getExhibitions()) {
ProductExhibitionEntity entity = new ProductExhibitionEntity();
entity.setSupplierProductId(supplierProductId);
entity.setSupplierExhibitionId(String.valueOf(exh.getId()));
entity.setTitle(exh.getTitle());
entity.setDescription(exh.getDescription());
entity.setFromDate(parseDate(exh.getFromDate()));
entity.setToDate(parseDate(exh.getToDate()));
entity.setExhibitionUrl(exh.getUrl());
session.persist(entity);
}
tx.commit();
}
}
private void saveImages(TiqetsProduct remote, String supplierProductId) {
if (remote.getImages() == null || remote.getImages().isEmpty()) return;
try (Session session = TiqetsDBSession.getSession()) {
Transaction tx = session.beginTransaction();
// Delete existing images for this product
session.createQuery("DELETE FROM ProductImageEntity WHERE supplierProductId = :productId")
.setParameter("productId", supplierProductId)
.executeUpdate();
// Insert new images with order
int order = 0;
for (TiqetsImage img : remote.getImages()) {
ProductImageEntity entity = new ProductImageEntity();
entity.setSupplierProductId(supplierProductId);
entity.setImageOrder(order++);
entity.setSmallUrl(img.getSmall());
entity.setMediumUrl(img.getMedium());
entity.setLargeUrl(img.getLarge());
entity.setExtraLargeUrl(img.getExtraLarge());
entity.setAltText(img.getAltText());
entity.setCredit(img.getCredit());
session.persist(entity);
}
tx.commit();
}
}
// Similar methods for saveStartingPoint(), saveSupplier(), saveProductGroups()
Part 6: Implementation Order¶
Phase 1: Database Schema¶
- Add new columns to
tiqets.producttable for simple fields - Create new tables for nested entities (cancellation, starting_point, supplier, exhibition, product_group)
- Run schema migration
Phase 2: Remote DTO¶
- Add nested DTO classes to TiqetsProduct.java
- Add new simple fields to TiqetsProduct.java
- Test JSON deserialization
Phase 3: JPA Entities¶
- Create JPA entities for nested tables
- Add fields to ProductEntity.java
- Update initFromRemote() for simple fields
Phase 4: Refresh Process¶
- Add save methods for nested entities in StaticDataRefresher
- Update saveProduct() to call nested save methods
- Test refresh with API data
Phase 5: Native Entities¶
- Add fields to TqProduct.java
- Create TqProductCancellation, TqProductStartingPoint, etc.
- Add @TlinqEntityField annotations
Phase 6: Canonical Entities¶
- Add fields to CProduct.java
- Create CProductCancellation, CProductStartingPoint, etc.
Phase 7: XML Mappings¶
- Add simple field mappings to product-entities.xml
- Add nested entity definitions
- Add NestedMapping and ArrayMapping configurations
Phase 8: Testing¶
- Run full refresh
- Verify database population
- Test API responses with new fields
- Verify other plugins (Odoo, Rayna) unaffected
Verification Steps¶
-
Schema Migration:
-
Build:
-
Refresh Data:
-
Verify Nested Tables:
SELECT p.supplier_product_id, p.title, c.window_hours, c.policy_type, s.supplier_name, (SELECT COUNT(*) FROM tiqets.product_image i WHERE i.supplier_product_id = p.supplier_product_id) as image_count FROM tiqets.product p LEFT JOIN tiqets.product_cancellation c ON p.supplier_product_id = c.supplier_product_id LEFT JOIN tiqets.product_supplier s ON p.supplier_product_id = s.supplier_product_id WHERE p.active = true LIMIT 5; -
Test API:
- Call
tiqets/productsendpoint - Verify nested objects in response
- Test with Odoo product endpoint - should be unaffected
Risk Considerations¶
-
Performance: Nested entity loading may add latency. Consider eager vs lazy loading strategy.
-
Null Handling: Many nested objects may be null in API response. All save methods must handle null gracefully.
-
One-to-Many Refresh: For exhibitions and product_groups, need to delete existing records before inserting new ones to handle removals.
-
Cache Invalidation: TiqetsCacheManager may need updates to cache nested entities or load them on demand.
-
Backward Compatibility: Existing API consumers should still work with additional fields (additive change).
Part 7: Experience API Enhancements¶
The Experience API is already well-implemented. Most fields from the Tiqets Experience API are captured. This section documents the missing fields to add.
7.1 Missing Simple Fields for CExperience¶
Content (1 field):
- contentLanguage - String - Language code of content (ISO 639-1)
Location Names (2 fields):
- cityName - String - Localized city name
- countryName - String - Localized country name
Image (1 field):
- imageCredit - String - Image credit information (currently missing)
7.2 Database Schema Updates¶
Add columns to tiqets.experience table:
ALTER TABLE tiqets.experience ADD COLUMN IF NOT EXISTS content_language VARCHAR(10);
ALTER TABLE tiqets.experience ADD COLUMN IF NOT EXISTS city_name VARCHAR(255);
ALTER TABLE tiqets.experience ADD COLUMN IF NOT EXISTS country_name VARCHAR(255);
ALTER TABLE tiqets.experience ADD COLUMN IF NOT EXISTS image_credit VARCHAR(500);
7.3 Files to Modify for Experience¶
| File | Changes |
|---|---|
tqapp/.../entity/product/CExperience.java |
Add 4 new fields + images array |
tqtiqets/.../remote/entity/TiqetsExperience.java |
Add missing fields to DTO |
tqtiqets/.../db/ExperienceEntity.java |
Add 4 columns, update initFromRemote() |
tqtiqets/.../entity/TqExperience.java |
Add 4 fields with @TlinqEntityField |
tqtiqets/.../service/StaticDataRefresher.java |
Add saveExperienceImages() method |
config/db/tiqets-schema.sql |
Add columns + experience_image table |
config/entities/product-entities.xml |
Add field mappings + CExperienceImage entity |
7.4 Experience Images (Nested Entity)¶
Current State: Only the first image is stored (imageSmall, imageMedium, imageLarge, imageXlarge, imageAltText).
API Reality: The API returns an array of images.
Solution: Create tiqets.experience_image table similar to tiqets.product_image.
Canonical Class: CExperienceImage¶
package com.perun.tlinq.entity.product;
public class CExperienceImage extends TlinqEntity {
private String experienceId; // FK to CExperience.experienceId
private Integer imageOrder; // Display order
private String smallUrl; // Small image URL
private String mediumUrl; // Medium image URL
private String largeUrl; // Large image URL
private String extraLargeUrl; // Extra large image URL
private String altText; // Alt text for accessibility
private String credit; // Image credit information
}
Database Table: tiqets.experience_image¶
CREATE TABLE IF NOT EXISTS tiqets.experience_image (
id INTEGER PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
supplier_experience_id VARCHAR(30) NOT NULL REFERENCES tiqets.experience(supplier_experience_id),
image_order INTEGER NOT NULL DEFAULT 0,
small_url VARCHAR(500),
medium_url VARCHAR(500),
large_url VARCHAR(500),
extra_large_url VARCHAR(500),
alt_text VARCHAR(500),
credit VARCHAR(500),
CONSTRAINT uk_exp_image UNIQUE (supplier_experience_id, image_order)
);
CREATE INDEX IF NOT EXISTS idx_exp_image_experience ON tiqets.experience_image(supplier_experience_id);
Native Entity: TqExperienceImage¶
package com.perun.tlinq.client.tiqets.entity;
@TlinqClientEntity
public class TqExperienceImage extends TqEntity {
@TlinqEntityField(sourceName = "ExperienceImageEntity.supplierExperienceId")
String experienceId;
@TlinqEntityField(sourceName = "ExperienceImageEntity.imageOrder")
Integer imageOrder;
@TlinqEntityField(sourceName = "ExperienceImageEntity.smallUrl")
String smallUrl;
@TlinqEntityField(sourceName = "ExperienceImageEntity.mediumUrl")
String mediumUrl;
@TlinqEntityField(sourceName = "ExperienceImageEntity.largeUrl")
String largeUrl;
@TlinqEntityField(sourceName = "ExperienceImageEntity.extraLargeUrl")
String extraLargeUrl;
@TlinqEntityField(sourceName = "ExperienceImageEntity.altText")
String altText;
@TlinqEntityField(sourceName = "ExperienceImageEntity.credit")
String credit;
// Getters and setters...
}
JPA Entity: ExperienceImageEntity¶
package com.perun.tlinq.client.tiqets.db;
@Entity
@Table(name = "experience_image", schema = "tiqets")
public class ExperienceImageEntity implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(name = "supplier_experience_id", nullable = false)
private String supplierExperienceId;
@Column(name = "image_order", nullable = false)
private Integer imageOrder;
@Column(name = "small_url")
private String smallUrl;
@Column(name = "medium_url")
private String mediumUrl;
@Column(name = "large_url")
private String largeUrl;
@Column(name = "extra_large_url")
private String extraLargeUrl;
@Column(name = "alt_text")
private String altText;
@Column(name = "credit")
private String credit;
// Getters and setters...
}
XML Mapping for CExperienceImage¶
<!-- CExperienceImage Entity -->
<Entity name="CExperienceImage"
class="com.perun.tlinq.entity.product.CExperienceImage"
idField="experienceId" defaultFactory="TiqetsServiceFactory">
<EntityFactoryList>
<Factory name="TiqetsServiceFactory"
nativeEntity="com.perun.tlinq.client.tiqets.entity.TqExperienceImage">
<FieldMappingList>
<FieldMapping targetField="experienceId" sourceField="experienceId" mapping="DirectMapping"/>
<FieldMapping targetField="imageOrder" sourceField="imageOrder" mapping="DirectMapping"/>
<FieldMapping targetField="smallUrl" sourceField="smallUrl" mapping="DirectMapping"/>
<FieldMapping targetField="mediumUrl" sourceField="mediumUrl" mapping="DirectMapping"/>
<FieldMapping targetField="largeUrl" sourceField="largeUrl" mapping="DirectMapping"/>
<FieldMapping targetField="extraLargeUrl" sourceField="extraLargeUrl" mapping="DirectMapping"/>
<FieldMapping targetField="altText" sourceField="altText" mapping="DirectMapping"/>
<FieldMapping targetField="credit" sourceField="credit" mapping="DirectMapping"/>
</FieldMappingList>
</Factory>
</EntityFactoryList>
</Entity>
Add to CExperience mapping:¶
<FieldMapping targetField="images" sourceField="images" mapping="ArrayMapping">
<ArrayMapping entity="CExperienceImage" loadAction="search"/>
</FieldMapping>
StaticDataRefresher Update for Experience Images¶
private void saveExperienceImages(TiqetsExperience remote, String supplierExperienceId) {
if (remote.getImages() == null || remote.getImages().isEmpty()) return;
try (Session session = TiqetsDBSession.getSession()) {
Transaction tx = session.beginTransaction();
// Delete existing images for this experience
session.createQuery("DELETE FROM ExperienceImageEntity WHERE supplierExperienceId = :experienceId")
.setParameter("experienceId", supplierExperienceId)
.executeUpdate();
// Insert new images with order
int order = 0;
for (TiqetsImage img : remote.getImages()) {
ExperienceImageEntity entity = new ExperienceImageEntity();
entity.setSupplierExperienceId(supplierExperienceId);
entity.setImageOrder(order++);
entity.setSmallUrl(img.getSmall());
entity.setMediumUrl(img.getMedium());
entity.setLargeUrl(img.getLarge());
entity.setExtraLargeUrl(img.getExtraLarge());
entity.setAltText(img.getAltText());
entity.setCredit(img.getCredit());
session.persist(entity);
}
tx.commit();
}
}
Files to Create for Experience Images¶
| File | Location | Purpose |
|---|---|---|
CExperienceImage.java |
tqapp/.../entity/product/ |
Canonical image entity |
ExperienceImageEntity.java |
tqtiqets/.../db/ |
JPA entity |
TqExperienceImage.java |
tqtiqets/.../entity/ |
Native entity |
Migration Note¶
After implementing experience_image table, the existing single-image fields (imageSmall, imageMedium, etc.) in ExperienceEntity can be: - Option A: Kept for backward compatibility and quick access to primary image - Option B: Deprecated and removed in a future release
Recommendation: Keep them for now as they provide quick access to the primary image without joining.
7.5 Experience API Field Comparison¶
| API Field | JPA Entity | Status |
|---|---|---|
| id | supplierExperienceId | ✓ |
| type | experienceType | ✓ |
| language | contentLanguage | ADD |
| title | title | ✓ |
| images[] | experience_image table | ADD (nested) |
| address.street | streetAddress | ✓ |
| address.postal_code | postalCode | ✓ |
| address.city_id | supplierCityId | ✓ |
| address.city_name | cityName | ADD |
| address.country_id | supplierCountryId | ✓ |
| address.country_name | countryName | ADD |
| address.lat | latitude | ✓ |
| address.lng | longitude | ✓ |
| address.google_place_id | googlePlaceId | ✓ |
| tag_ids | tagIds | ✓ |
| product_ids | productIds | ✓ |
| experience_url | experienceUrl | ✓ |
| ratings.average | ratingAverage | ✓ |
| ratings.total | ratingCount | ✓ |
| currency | currency | ✓ |
| from_price | fromPrice | ✓ |
| tagline | tagline | ✓ |
| description | description | ✓ |
Note: Existing single-image fields (imageSmall, imageMedium, etc.) will be kept for backward compatibility and quick access to primary image.
Part 8: Refactoring¶
8.1 Rename StaticDataRefresher to TiqetsDataRefresher¶
Rationale: The name StaticDataRefresher is generic and doesn't indicate which plugin it belongs to. Renaming to TiqetsDataRefresher follows the naming convention used by other plugin-specific classes (e.g., TiqetsApiClient, TiqetsCacheManager).
Changes Required:
| Action | Details |
|---|---|
| Rename class | StaticDataRefresher.java → TiqetsDataRefresher.java |
| Update package | Keep in com.perun.tlinq.client.tiqets.service |
| Update references | All files that import/use StaticDataRefresher |
Files to Update:
tqtiqets/src/main/java/com/perun/tlinq/client/tiqets/service/
├── StaticDataRefresher.java → TiqetsDataRefresher.java
# Update references in:
- TiqetsSupplierIntegrationTest.java
- Any other test files using StaticDataRefresher
- TiqetsCacheManager.java (if referenced)
- TiqetsPlugin.java (if referenced)
Search for references: