Process Dec 23, 2023 11 min read

Summary of the technical journey for Italy Rome Tour's Website

Italy Rome Tour is a comprehensive tour booking platform for tourists visiting Rome and Italy.

Lushano Perera
Lushano Perera
Author

Project Overview

Italy Rome Tour is a comprehensive tour booking platform for tourists visiting Rome and Italy. The platform allows customers to browse, customize, and book guided tours with features including dynamic pricing, multiple pickup locations, participant management, and professional PDF voucher generation.


The Challenge

Building a complete tour booking system required solving several complex problems:

  • Complex Pricing Models: Tours have different prices for adults, children, and infants, with dynamic discounts based on group size
  • Multiple Pickup Scenarios: Customers can be picked up from hotels, airports, or cruise ships – each requiring different information
  • Ticket Requirements: Some tours require participant names exactly as they appear on IDs for museum entry
  • Professional Vouchers: Generating branded PDF booking confirmations with all relevant tour details
  • Content Management: Allowing tour operators to easily manage tour specifications, itineraries, and pricing without touching code

Technology Stack

CategoryTechnologyPurpose
CMSWordPress 6.xContent management and user administration
E-CommerceWooCommerceProduct catalog, cart, checkout, and order management
Booking EngineYITH WooCommerce Booking PremiumDate/time selection, availability, and booking logic
Page BuilderElementor ProVisual page design and template building
Custom FieldsAdvanced Custom Fields ProTour metadata, specifications, and itinerary management
ThemeHello Elementor + Custom ChildLightweight base with extensive customizations
PaymentsStripeSecure payment processing
HostingCloudways (DigitalOcean)Managed WordPress hosting with optimization
Version ControlGitLabCode versioning and CI/CD pipeline
GDPR ComplianceiubendaCookie consent and privacy policy management
CachingW3 Total CachePerformance optimization

Architecture Overview

Custom Child Theme Structure

wp-content/themes/hello-theme-irt/
├── functions.php # 539 lines of custom functionality
├── style.css # Theme metadata
├── acf-json/ # ACF field group configurations
│ ├── group_654df0fdcf62e.json # Tour specifications
│ ├── group_654df4ba6b08f.json # Category/tag additional data
│ ├── group_65eaee42de1c3.json # Page extras
│ └── group_664c522c0867c.json # Booking notices
└── yith-woocommerce-booking-templates/
├── booking/
│ ├── pdf/ # Custom PDF templates
│ └── booking-details.php
├── emails/ # Custom email templates
└── single-product/add-to-cart/ # Booking form customizations

Data Flow

  1. Tour Creation: Admin creates WooCommerce product with ACF fields for specifications
  2. Frontend Display: Shortcodes render tour specs, pricing from YITH booking data
  3. Booking Process: Custom form fields collect participant names and pickup details
  4. Cart Persistence: Data flows through WooCommerce hooks to order meta
  5. Confirmation: PDF voucher generated with custom branding and notices

Custom Development

1. Tour Specifications Shortcode

Displays a formatted table of tour details with custom icons, pulling data from ACF fields.

add_shortcode( 'irt_tour_specs', 'render_irt_tour_specs' );
function render_irt_tour_specs() {
$spec_fields = [
[ 'Duration', 'irt_tour_duration' ],
[ 'Start', 'irt_starting_time' ],
[ 'Pick up location', 'irt_pick_up_location' ],
[ 'Skip the line', 'irt_skip_the_line' ],
[ 'Official Tour Guide', 'irt_official_tour_guide' ],
[ 'Tickets', 'irt_tickets_included' ],
[ 'Transportation', 'irt_transportation' ],
[ 'Wheelchair users', 'irt_wheelchair_users' ],
[ 'Activity', 'irt_activity_level' ],
];

?>
<table>
<tbody>
<?php
foreach ( $spec_fields as $spec_field ) {
if ( $value = get_field( $spec_field[1] ) ) {
?>
<tr class="<?= $spec_field[1]; ?>">
<th>
<img src="/wp-content/uploads/2023/11/<?= $spec_field[1]; ?>.svg"
alt="<?= $spec_field[0]; ?> icon"
width="18" height="18"/>
<?= $spec_field[0]; ?>
</th>
<td>
<?php
if ( is_array( $value ) ) {
// Handle checkbox fields (e.g., multiple pickup locations)
if ( $spec_field[1] === 'irt_pick_up_location'
&& in_array( 'Other', $value )
) {
echo get_field( 'irt_other_pick_up_location' );
} else {
echo implode( ", ", $value );
}
} else {
if ( $spec_field[1] === 'irt_transportation' && $value === '' ) {
echo _x( 'Not included', 'hello-irt-theme' );
} else {
echo $value;
}
}
?>
</td>
</tr>
<?php
}
}
?>
</tbody>
</table>
<?php
}

Key Features:

  • Dynamic field mapping with labels and ACF field names
  • SVG icon support for each specification type
  • Conditional logic for “Other” pickup location text
  • Handles both single values and arrays (checkboxes)

2. Custom Booking Form Fields

Collects additional customer data required for tour operations – participant names for ticketed attractions and pickup location details.

function yith_wc_custom_input_data() {
global $product;

$product_id = $product ? $product->get_id() : 0;
$tickets_included = get_field( 'irt_tickets_included', $product_id );
$pickup_location = get_field( 'irt_pick_up_location', $product_id );

// Participant Names Field - Only shown for tours with tickets
if ( $product_id && $tickets_included ) {
$label = _x( 'Participants', 'hello-irt-theme' );
printf(
'<div class="yith_wc_input_text__wrapper yith-wcbk-form-section participants"
style="display: none;">
<label>%s</label>
<p>Please provide the first and last name of each traveler exactly
as they appear on your official travel documents. This additional
data is collected for ticket booking purposes only.
<strong>Failure to do so will result in denied entry and loss of tour</strong>.</p>
<div class="participants_fields"></div>
<input type="hidden" id="irt_booking_names" name="irt_booking_names" value="" />
</div>',
esc_html( $label )
);
}

// Pickup Point Field - Context-aware instructions
if ( $product_id && ! empty( $pickup_location ) ) {
// Determine pickup type based on product tags
if ( has_term( 1384, 'product_tag', $product_id ) ) {
// From Rome City Center
$pickup_instructions = 'Please enter the pickup location in Rome city center.';
} elseif ( has_term( 1383, 'product_tag', $product_id ) ) {
// From Port/Cruise
$pickup_instructions = _x(
'Please enter the pickup location. In the case of coming from a cruise,
please indicate the name of the ship and the arrival port.',
'hello-irt-theme'
);
} elseif ( has_term( 1382, 'product_tag', $product_id ) ) {
// From Airport
$pickup_instructions = _x(
'Please enter the pickup location. In the case of coming from an airport,
please indicate the code of the flight and the airport and the estimated
arrival time.',
'hello-irt-theme'
);
} else {
// Generic fallback
$pickup_instructions = _x(
'Please enter the pickup location. In the case of coming from a cruise,
please indicate the name of the ship and the arrival port.',
'hello-irt-theme'
);
}

$label = _x( 'Pick up point', 'hello-irt-theme' );
printf(
'<div class="yith_wc_input_text__wrapper yith-wcbk-form-section pick-up-point">
<label>%s</label>
<p>' . $pickup_instructions . '</p>
<textarea id="irt_pickup_point" name="irt_pickup_point"
value="" required></textarea>
</div>',
esc_html( $label )
);
}
}
add_action( 'woocommerce_before_add_to_cart_button', 'yith_wc_custom_input_data' );

Cart Data Persistence:

function yith_wc_add_cart_item_data( $cart_item_data, $product_id, $variation_id, $quantity ) {
if ( ! empty( $_POST['irt_booking_names'] ) ) {
$cart_item_data['irt_booking_names'] = sanitize_text_field(
wp_unslash( $_POST['irt_booking_names'] )
);
}

if ( ! empty( $_POST['irt_pickup_point'] ) ) {
$cart_item_data['irt_pickup_point'] = sanitize_text_field(
wp_unslash( $_POST['irt_pickup_point'] )
);
}

return $cart_item_data;
}
add_filter( 'woocommerce_add_cart_item_data', 'yith_wc_add_cart_item_data', 10, 4 );

Display in Cart:

function yith_wc_get_item_data( $cart_data, $cart_item ) {
if ( ! empty( $cart_item['irt_booking_names'] ) ) {
$cart_data[] = [
'name' => esc_html__( 'Participants', 'hello-irt-theme' ),
'display' => $cart_item['irt_booking_names'],
];
}

if ( ! empty( $cart_item['irt_pickup_point'] ) ) {
$cart_data[] = [
'name' => esc_html__( 'Pick up point', 'hello-irt-theme' ),
'display' => $cart_item['irt_pickup_point'],
];
}

return $cart_data;
}
add_filter( 'woocommerce_get_item_data', 'yith_wc_get_item_data', 25, 2 );

Save to Order Meta:

function yith_wc_order_line_item( $item, $cart_item_key, $values, $order ) {
foreach ( $item as $cart_item_key => $data ) {
if ( isset( $data['irt_booking_names'] ) ) {
$item->add_meta_data( 'irt_booking_names', $data['irt_booking_names'], TRUE );
}

if ( isset( $data['irt_pickup_point'] ) ) {
$item->add_meta_data( 'irt_pickup_point', $data['irt_pickup_point'], TRUE );
}
}
}
add_action( 'woocommerce_checkout_create_order_line_item', 'yith_wc_order_line_item', 10, 4 );

Key Features:

  • Product tag-based conditional instructions
  • Complete WooCommerce data flow (form → cart → order)
  • Proper sanitization and security
  • Translation-ready strings

3. Weather API Integration

Custom REST API endpoint providing real-time Rome weather data using OpenWeatherMap.

// Fetch and cache weather data
function fetch_openweathermap_data() {
$api_url = 'https://api.openweathermap.org/data/2.5/weather?q=Rome,IT&appid=YOUR_API_KEY&units=metric';

$response = wp_remote_get( $api_url );

if ( is_wp_error( $response ) ) {
return;
}

$body = wp_remote_retrieve_body( $response );
$data = json_decode( $body, TRUE );

if ( json_last_error() !== JSON_ERROR_NONE ) {
return;
}

// Store in transient cache for 1 hour
set_transient( 'openweathermap_rome_data', $data, HOUR_IN_SECONDS );
}

// Schedule hourly updates
if ( ! wp_next_scheduled( 'fetch_openweathermap_data_hook' ) ) {
wp_schedule_event( time(), 'hourly', 'fetch_openweathermap_data_hook' );
}
add_action( 'fetch_openweathermap_data_hook', 'fetch_openweathermap_data' );

// REST API callback
function get_openweathermap_data() {
$data = get_transient( 'openweathermap_rome_data' );

if ( $data === FALSE ) {
fetch_openweathermap_data();
return new WP_Error( 'no_data', 'No weather data available', [ 'status' => 404 ] );
}

return rest_ensure_response( $data );
}

// Register REST route
function register_openweathermap_route() {
register_rest_route( 'custom/v1', '/weather', [
'methods' => 'GET',
'callback' => 'get_openweathermap_data',
] );
}
add_action( 'rest_api_init', 'register_openweathermap_route' );

// Cleanup on deactivation
function my_plugin_deactivation() {
$timestamp = wp_next_scheduled( 'fetch_openweathermap_data_hook' );
wp_unschedule_event( $timestamp, 'fetch_openweathermap_data_hook' );
}
register_deactivation_hook( __FILE__, 'my_plugin_deactivation' );

Key Features:

  • WordPress Transient API for caching (1-hour TTL)
  • WP Cron for scheduled updates
  • Custom REST API endpoint at /wp-json/custom/v1/weather
  • Proper error handling and cleanup

4. Dynamic Price Calculations

Parses YITH Booking’s serialized price data to display intelligent pricing information.

function irt_get_best_base_price() {
$post_id = get_the_ID();

// Retrieve serialized YITH booking costs
$serialized_data = get_post_meta($post_id, '_yith_booking_costs_range', TRUE);

if (empty($serialized_data)) {
return NULL;
}

$data = maybe_unserialize($serialized_data);

$lowest_price = PHP_FLOAT_MAX;
$lowest_name = '';

foreach ($data as $availability) {
// Skip entries without required data
if (!isset($availability['base_price']) ||
empty($availability['conditions']) ||
!isset($availability['conditions'][1]['type'])) {
continue;
}

// Skip Kids (person-type-24) and Infants (person-type-25)
// to show best adult/group price
$type = $availability['conditions'][1]['type'];
if ($type === 'person-type-24' || $type === 'person-type-25') {
continue;
}

$base_price = floatval($availability['base_price']);
$name = $availability['name'];

if ($base_price < $lowest_price && $base_price > 0) {
$lowest_price = $base_price;
$lowest_name = $name;
}
}

if ($lowest_price === PHP_FLOAT_MAX) {
return NULL;
}

// Return formatted price with tier name
return '<p class="price-range">Best price p.p. at ' .
'<span class="woocommerce-Price-amount amount"><bdi>' .
number_format($lowest_price, 2, ',', '.') .
' <span class="woocommerce-Price-currencySymbol">€</span></bdi></span> (' .
$lowest_name . ')</p>';
}
add_shortcode('irt_get_best_base_price', 'irt_get_best_base_price');

Key Features:

  • Parses YITH’s complex serialized pricing structure
  • Filters out kids/infant prices for meaningful “best price” display
  • European number formatting (comma decimals, dot thousands)
  • Displays the pricing tier name for transparency

5. PDF Booking Voucher Customization

Custom PDF template with branded header and configurable notice sections.

Custom Header (header.php):

<?php
defined( 'YITH_WCBK' ) || exit;

$logo = apply_filters( 'yith_wcbk_booking_pdf_logo_url', '' );
$booking_link = $is_admin
? get_edit_post_link( $booking->get_id() )
: $booking->get_view_booking_url();
?>
<div class="logo">
<!-- Embedded base64 logo for reliable PDF rendering -->
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJYAAACWCAMAAAAL34HQ..."
alt="Italy Rome Tour" width="100" height="100"/>
</div>
<div class="clear"></div>
<div class="booking-title">
<h2 style="color: #026D61;">
<a href="<?php echo esc_url( $booking_link ); ?>" style="color: #026D61;">
<?php echo esc_html( $booking->get_name() ); ?>
</a>
</h2>
</div>

Custom Booking Template (booking.php) – Notice Section:

<div class="notice">
<?php if ($notice_1 = get_post_meta($booking->get_id(), 'irt_booking_notice_1', true)) { ?>
<h3 style="color: #026D61;">Please note</h3>
<p><?= $notice_1; ?></p>
<?php } ?>

<?php if ($notice_2 = get_post_meta($booking->get_id(), 'irt_booking_notice_2', true)) { ?>
<p><?= $notice_2; ?></p>
<?php } ?>

<?php if ($notice_3 = get_post_meta($booking->get_id(), 'irt_booking_notice_3', true)) { ?>
<p><?= $notice_3; ?></p>
<?php } ?>
</div>

<div class="sign">
<p>Your Local Operator</p>
<p>Max Parini<br/>
<p>Official Tour Guide</p>
<p><b>Phone:</b> +393939901484</p>
</div>

PDF Styling:

body {
color: #2f3742;
font-family: DejaVu Sans, sans-serif;
}

.logo img {
width: 100px;
height: auto;
}

.booking-table th, .booking-table td {
padding: 11px 20px;
text-align: left;
border-bottom: 1px solid #676f76;
}

.notice p {
font-size: 11px;
font-style: italic;
}

.footer {
position: fixed;
width: 100%;
bottom: 0;
text-align: center;
font-size: 70%;
}

Key Features:

  • Base64-embedded logo for reliable rendering in PDF
  • Brand color (#026D61 – teal green) consistency
  • Three configurable notice sections via ACF
  • Professional operator signature block
  • DejaVu Sans font for PDF compatibility

6. ACF Field Configuration

Tour specifications managed through Advanced Custom Fields with JSON sync.

{
"key": "group_654df0fdcf62e",
"title": "Tour additional information",
"fields": [
{
"key": "field_654e141684d5e",
"label": "Tour specifications",
"type": "tab",
"placement": "top"
},
{
"key": "field_654df0fe0295d",
"label": "Tour Duration (verbose)",
"name": "irt_tour_duration",
"type": "text"
},
{
"key": "field_654e1287583d1",
"label": "Starting time",
"name": "irt_starting_time",
"type": "text"
},
{
"key": "field_654e12b8583d2",
"label": "Pick up Location",
"name": "irt_pick_up_location",
"type": "checkbox",
"choices": {
"Hotel": "Hotel",
"Bed and Breakfast": "Bed and Breakfast",
"Cruise Ship": "Cruise Ship",
"Other": "Other"
}
},
{
"key": "field_65afc9d44e806",
"label": "Other Pickup Location",
"name": "irt_other_pick_up_location",
"type": "text",
"conditional_logic": [
[{
"field": "field_654e12b8583d2",
"operator": "==",
"value": "Other"
}]
]
},
{
"key": "field_654e1302583d3",
"label": "Skip the line",
"name": "irt_skip_the_line",
"type": "radio",
"choices": {
"Included": "Included",
"Not included": "Not included"
},
"default_value": "Included"
},
{
"key": "field_654e133a583d4",
"label": "Transportation",
"name": "irt_transportation",
"type": "text",
"default_value": "Luxury Mercedes minivan and driver guide"
},
{
"key": "field_654e15b47f7f8",
"label": "Tour Itinerary Steps",
"name": "irt_tour_itinerary_steps",
"type": "repeater",
"layout": "table",
"sub_fields": [
{
"key": "field_654e15ed7f7f9",
"label": "Tour Itinerary Step",
"name": "irt_tour_itinerary_step",
"type": "text"
}
]
}
],
"location": [[{
"param": "post_type",
"operator": "==",
"value": "product"
}]]
}

Field Groups Summary:

  • Tour Specifications: 10 fields covering duration, tickets, transportation, accessibility
  • Tour Itinerary: Repeater field for unlimited itinerary steps
  • Category/Tag Data: Hero images and introductions for archive pages
  • Booking Notices: Three customizable notices for PDF and email templates

The 2024 Migration

From Custom Plugin to YITH WooCommerce Booking

Original System (2019-2024):

  • Custom tours-booking plugin
  • Separate tours and shared-tours custom post types
  • External API for cart management (panel.italyrometour.com)
  • Direct Stripe integration with Checkout.js

Migration (May 2024):

  • Consolidated to WooCommerce products
  • YITH WooCommerce Booking Premium for availability/pricing
  • Native WooCommerce cart and checkout
  • Stripe through WooCommerce payment gateway

Benefits:

  • Better long-term maintainability
  • WooCommerce ecosystem compatibility
  • Improved security with managed payment processing
  • Easier content management for tour operators
  • Access to YITH’s continuous development and support

Key Features Summary

FeatureImplementation
Tour SpecificationsACF fields + custom shortcode with icons
Dynamic PricingYITH Booking + custom price calculation shortcodes
Multiple Pickup TypesProduct tag-based conditional form fields
Participant ManagementCustom WooCommerce form fields persisted to orders
PDF VouchersYITH template overrides with custom branding
Weather WidgetCustom REST API with OpenWeatherMap + transient caching
GDPR Complianceiubenda integration with Elementor form hooks
Blog PaginationElementor query hooks for custom layouts
Price PrefixWooCommerce filter adding “from” on archive pages

Technical Highlights

  • 539 lines of custom PHP in theme functions.php
  • 74+ template overrides for YITH Booking
  • 4 ACF field groups with JSON sync enabled
  • Custom REST API endpoint for weather data
  • Complete WooCommerce integration for custom booking fields
  • Schema.org structured data for price markup
  • Multi-language ready with translation functions

File Reference

FilePurpose
functions.phpCore customizations (shortcodes, hooks, API)
acf-json/*.jsonField group configurations
yith-woocommerce-booking-templates/booking/pdf/PDF voucher templates
yith-woocommerce-booking-templates/emails/Email notification templates

Written by Lushano Perera

Digital craftsman exploring the intersection of design, technology, and human experience.