Tripvance

توضیحات

Tripvance brings tours, activities and travel experiences to WordPress: a Tours post type, tour pages built from real fields, and a booking request form. It is a request and confirmation system: the agency answers each request. There is no payment processing.

Tours

Each tour has a gallery, a short description, highlights, an itinerary, what is included and what is not, a meeting point, and questions and answers. The itinerary is written a card at a time — press Add a day, give it a title and a description — and comes out in two shapes, picking the right one by itself: a run of numbered days for a trip of several days, and a timeline of times and stops for a day trip. A section with no data is not rendered, so a page never shows an empty heading.

Tour layouts

Seven layouts, each built for one kind of tour, all drawing the same content, prices and booking form: Salma (booking focused — gallery beside the two-step booking panel), Sara (marketplace product page with a price card that follows the reader), Atlas (multi-day — the route and the days across the page with a day index), Sahara (photo-first, no sidebar), Medina (compact, for city tours and short activities), Qamar (premium storytelling for private and luxury journeys) and Classic (simple and theme-friendly). Atlas has two styles (Clean, Zellige) and Sahara three gallery presentations (Cinematic, Immersive, Mosaic). The site picks a default and each tour can choose its own. Legacy layouts from earlier versions remain supported for existing tours.

Tours are filed under destinations, tour types, departure points, trip lengths and categories, each with its own archive page and filters.

Two kinds of travel business

Tripvance ships two presentations of the same content, chosen under Settings Website experience. Standard is what the plugin has always been: a catalogue with prices, availability and a booking form, and it is the default on every site. Luxury Editorial redraws the same tours, destinations and activities as a travel magazine, for a bespoke travel designer who sells a journey by describing it rather than by pricing it — journey collections, editorial destination pages, a page design with no price card, and a short private enquiry in place of a checkout. Switching between them changes nothing you have written, and there is no second catalogue to keep in step.

Booking requests

The form asks for a date, a party size and how to reach the traveller. By default it behaves as an enquiry system. If you enable seat-capacity enforcement, seats are held atomically against the departure while a request is open, so two simultaneous requests cannot both take the last seat.

Requests are managed from a booking screen with a status workflow, internal notes and an operations dashboard.

Transfers

Private transfers (airport to hotel, city to city) are a post type of their own, at /transfers/{slug}/. Each transfer describes its route, pickup and drop-off, estimated duration, operating hours, minimum booking notice and practical information, and lists the Vehicle Types it offers with a price for each. Vehicle Types are reusable, customer-facing options (for example “Comfort minivan or similar”) with a class, capacity, luggage, features and a picture; they are not public pages. A round trip is priced as two journeys in the same Vehicle Type. There is no distance-based or automatic pricing.

The visitor chooses a Vehicle Type, a pickup date and time, passengers and luggage, and can add flight details. Capacity is checked in the browser and again on the server. The request becomes an ordinary booking record, with the same statuses, reservation ledger, messages, quotes and Guest Pass as a tour, and the price is stored with it when it arrives.

In Operations, the Fleet holds the physical vehicles. A fleet vehicle can be linked to a Vehicle Type, and assigning one to a transfer booking warns when it does not match the requested type and is refused when it cannot carry the passengers. Fleet is optional: transfers can be booked without it.

Privacy & Cookies

An optional consent banner (off by default) with Necessary, Analytics, Marketing and Preferences categories holds back the scripts you place in it until the visitor accepts their category. It helps collect and honour consent; it does not by itself make a site legally compliant.

Presentation

Tour cards can carry a strip of gallery thumbnails along the foot of their picture: pointing at one, or tapping it, shows that photo in the card, so a visitor looks through a tour without opening it (Settings > Tour listings).

Seven curated tour layouts, with style and gallery variants, chosen per tour or site wide, on one shared foundation (legacy layouts from earlier versions remain supported for existing tours): a sticky section navigation, a booking ticket beside the price, a route at a glance, an itinerary drawn as days or as a timeline, group rates, questions and answers, and reviews. The plugin inherits the theme’s typography and loads no fonts of its own.

Listing pages with filters, sorting and cards; a Tours block and a [wptm_tours] shortcode for a grid of tours anywhere, and [wptm_activities] for a grid of activities — each activity can carry its own price and take booking requests directly through the same request system as a tour; a homepage template assembled from your tours, destinations, activities and trust details; and a car hire template for the driving side of the business — the fleet, the services, the routes you cover and the prices you quote.

Multilingual

Tripvance uses the WordPress translation system throughout and is ready for community language packs from translate.wordpress.org. It also includes built-in French, Spanish and Arabic front-end fallbacks, with right-to-left layouts for Arabic.

Polylang is supported properly, not nominally. Tours, activities and every taxonomy the plugin owns are registered as translatable. Tours, terms and the pages the plugin generates are given their language and linked to their translations, so a language switcher lands on the right page. A generated page is written in its own language, not in the language of whoever pressed the button. Booking requests record the language the traveller was reading, and the confirmation, the voucher and the reminder are all written in it. Wording you type yourself — the homepage, the car-hire page, the default inclusions — appears under Languages > String translations so it can differ per language.

Addresses can be translated too, and are not by default. Switch on “Addresses per language” in the settings and a French page becomes /fr/circuits/ rather than /fr/tours/, with the destination, category, departure and trip length sections following it; the words come from the same translation catalogue. The addresses you already have keep working and redirect to the new ones. Leave it off and no address on the site changes, whatever language packs are installed.

A site that generated its pages before adding a language can put them right from the settings screen: each page’s title, starting wording and address go back into its own language, a page you have edited is left alone, and the previous address redirects.

An optional language switcher goes into the theme’s own menus — desktop header, mobile menu, footer, or a shortcode wherever you like — and each language links to the translation of the page the visitor is on: a French reader on an English tour lands on the French tour. Polylang decides which page that is and keeps printing the hreflang tags; Tripvance only draws the control, in the menu’s own typography, with no flags, no external assets and no second translation system.

Without Polylang the plugin runs unchanged.

Search engines

Tripvance prints no canonical tag, meta description or Open Graph markup, and no site-wide schema. Those belong to an SEO plugin, and two sources for one signal is worse than one.

Tripvance provides limited Product structured data for eligible Tour and Activity pages, including accurate pricing and genuine on-site review aggregates. When Rank Math is active, Tripvance integrates with its structured-data graph to avoid duplicate Product entities. General SEO and site-wide Schema remain under the control of the site’s SEO plugin.

In practice: a price is included only when it is the price the page shows and a traveller can book (one Offer at the public “From” price — the fixed price, or the lowest valid Group Rate; nothing for a tour on request), and a rating only from approved reviews of that tour, when the page shows the same average and count. Nothing is added to the homepage, archives, taxonomy pages, FAQs or private views such as booking confirmations and Guest Passes. The setting is under Tripvance > Settings > Search result product data. Structured data makes a page eligible for rich results; search engines decide whether prices or review stars are shown, and it does not affect ranking.

It also emits clean semantic markup, keeps taxonomy archives crawlable, marks empty ones as noindex, and keeps them out of the sitemap.

Import

Existing WooCommerce products can be imported as tours, with a dry run first that reports exactly what would happen and creates nothing.

Privacy

Booking requests and operational records are stored in the site’s database. These can include names, email and WhatsApp/telephone numbers, country, travel dates, party sizes, pickup details, messages, quotes and records of payments received outside the site. Transfer requests can also include a drop-off address, pickup and return times, luggage and flight details. Notifications use wp_mail() and the mail provider configured by the site owner. Tripvance sends no telemetry to its developer and loads no remote fonts.

WordPress personal-data export and erasure tools are supported for bookings matched by email, including bookings in the trash; transfer bookings are exported with their route, pickup, drop-off, return and flight details. Erasure removes identifying booking fields, campaign attribution, email queue/history and free-text history notes, clears linked reservation emails and pickup details, and revokes private links. Non-identifying booking, quote and payment records are retained. For requests without an email address, the operator must identify the relevant records manually. Backups and mail already delivered by the site’s mail provider are outside this eraser.

Optional first-party campaign attribution uses the wptm_attr cookie for up to 30 days. It stores UTM values, referral code, referring host (not the full referring URL), first tour visited and timestamp. It is read or written only when the visitor has consented to analytics; without consent it is disabled and an existing cookie is expired on the next uncached page request. Booking totals and manually entered attribution remain available. Consent is taken from the WordPress Consent API when a consent manager configures opt-in mode, otherwise from the Analytics choice in Tripvance Privacy & Cookies when that module is switched on. Other integrations can supply the visitor’s consent through the wptm_attribution_consent filter. Do not return true globally as a substitute for visitor consent.

The optional Privacy & Cookies module (off by default) stores the visitor’s choice in one first-party cookie, wptm_consent, holding the accepted categories, the consent version and a timestamp.

Tailor-made trip requests store the name, email address and, when given, telephone, country and message a traveller enters, with their travel preferences. Nothing is sent or stored while they move between the wizard’s steps; the request is saved once, when they press Send. Requests are private, excluded from search, feeds, REST and sitemaps, and are covered by the WordPress export and erasure tools (erasure keeps the reference, dates, length and party size).

Issued quotes, invoices and trip programmes are stored in the site’s database as frozen copies of what they printed (the customer’s name, the billing details an operator entered, the trip and the prices), together with the PDF file as issued. They are never written to the uploads folder, and a customer can download one only when the agency shared it, through their own Guest Pass or quote link. Erasure keeps issued invoices, which are accounting records, and removes the booking’s quote and programme documents.

Anti-abuse counters use salted hashes of the requesting IP address with expiring transients. The raw IP is not stored in those counters. Private Guest Pass, quote and voucher links act as access keys and should be shared only with the intended traveller.

Third-party libraries

  • TCPDF 6.11.4 by Nicola Asuni — https://tcpdf.org — LGPL-3.0-or-later (vendor/tcpdf/LICENSE.TXT). Bundled to produce PDF documents on the site’s own server; trimmed to the engine and two fonts.
  • DejaVu Sans fonts — Bitstream Vera and Arev fonts licence, public-domain changes (vendor/tcpdf/fonts/DEJAVU-LICENSE.txt).

External services

No external service is required for the core booking workflow. Site owners choose the optional links, media and mail service they use and should reflect them in their privacy notice.

  • WhatsApp: contact buttons open wa.me after a click and may include a prepared message. No WhatsApp API or automatic WhatsApp messaging is included. Terms: https://www.whatsapp.com/legal/terms-of-service ; Privacy: https://www.whatsapp.com/legal/privacy-policy .
  • YouTube and Vimeo: an operator-supplied video URL can be embedded through WordPress oEmbed; WordPress may request embed metadata and the visitor’s browser connects to the video host when displayed. In the Luxury Editorial experience, a YouTube address pasted into the Video field of the hero or the full-bleed story is embedded directly from www.youtube-nocookie.com and plays on its own, silently, as the background of that one section; nothing is requested from any video host on a site that has not supplied such an address, no script is loaded from a third party at any point, and the embed is never created for a visitor whose browser asks for reduced motion or reports a slow or metered connection. YouTube terms: https://www.youtube.com/t/terms ; Google privacy: https://policies.google.com/privacy ; Vimeo terms: https://vimeo.com/terms ; Vimeo privacy: https://vimeo.com/privacy .
  • Google Maps and Search: optional meeting-point and weather links open after a click and include the selected place, coordinates or weather query. Terms: https://policies.google.com/terms ; Privacy: https://policies.google.com/privacy .
  • External media: if an operator enters a remote image or direct video URL, the browser requests that host when the media is displayed. Use the WordPress media library to serve these assets from the site itself.
  • External payment links: an operator may provide a PayPal or another payment link. The link opens its provider after a click; Tripvance does not process the payment or receive card details. PayPal terms: https://www.paypal.com/legalhub/useragreement-full ; Privacy: https://www.paypal.com/legalhub/privacy-full .

عکس‌های صفحه

بلوک‌ها

این افزونه 1 بلوک ارائه می‌دهد.

  • Tours Tour cards from your catalogue, as a grid or as the Moroccan Editorial Carousel, filtered by destination, type, category, duration or departure.

نصب

  1. Upload the plugin folder to /wp-content/plugins/, or install it through Plugins Add New.
  2. Activate it. Tripvance prepares its required database tables, options and capabilities, but creates no tours, terms, pages or demo content automatically.
  3. A one-time notice offers a starter kit (tour types, durations, sample destinations, a Destinations page, a Tour Types page and two landing pages). Create it in one click, or dismiss the notice and build your own structure. The same buttons stay under Tripvance Settings.
  4. Go to Tripvance Settings to choose the booking behaviour, contact details and tour-page design. Currency is set on each tour’s pricing fields.
  5. Add a tour under Tours Add New.

سوالات متداول

Can travellers ask for a tailor-made trip?

Yes. Tripvance Tailor-Made Requests Settings has a “Create Trip Builder Page” button, or lets you choose an existing page; nothing is created until you press it. The page shows a step-by-step wizard (dates, length, interests, destinations, travel style, accommodation, travellers, budget, contact) whose steps and answers you manage on the same screen. Requests are private records with a TMR reference, separate from bookings, and “Create Draft Tour” starts a draft tour from one. The [tripvance_trip_builder] shortcode and the Trip Builder block place the wizard anywhere. An optional “Plan your own trip” call to action on tour pages is off by default.

Is there a setup guide?

Yes. After activation a dismissible welcome offers a step-by-step guide (Tripvance Settings Setup guide): tours, homepage and its preset, tour design, booking. Every step reads your site as it is and only offers what is missing; nothing is created or changed without a button pressed for it. The guide also offers demo content — sample tours, activities and destinations, every item marked as demo — with a one-click removal that never touches your own content. The guide can be restarted from Tripvance Settings.

Is there a homepage?

Yes. Tripvance Site pages Homepage creates a page on the “Tripvance homepage” template and fills it from what the site already has. Its hero can be a classic image cover, a muted looping video with accessible playback and optional sound controls, a split featured-tour spotlight, layered floating image stories, or an animated travel-colour mesh. The same buttons, optional reassurance points and five tour-search designs adapt to every hero; search can be hidden entirely. The rest includes featured tours, destinations and tour types with their counts, a signature tour, activities, trust, “why choose us”, articles and a final call to action. Sections can be switched off and reordered; a section with nothing to show is not printed. Setting the page as the site’s front page is a separate, explicit button.

Is there a car hire page?

Yes. Tours Car hire creates a page on the “Tripvance car hire page” template, built the same way as the homepage: sections you tick and order, a preset to arrange them, a preview before anything goes live. It holds a hero, a fleet with a filter by kind of vehicle, the services a driver is hired for, a table of routes with journey times and prices, your destinations and tours, the booking steps, trust details, a gallery, testimonials and questions. Only the vehicle’s name is required; a cell left empty prints nothing and a section with nothing in it is not printed at all. Publishing the page is a separate, explicit button, and it is a normal page, so you add it to your menu yourself.

How do people ask for a quote on the car hire page?

Through the form plugin you already use. Paste its shortcode — Contact Form 7, WPForms, Forminator, whichever it is — into the Request form group on Tours Car hire, and the form is printed in its own section, styled to match the page. Tripvance does not add a second enquiry system for cars: your form plugin already holds the fields, the address the mail goes to, the spam protection and the export, and duplicating that would only give you two places to check. Leave the shortcode empty and the section is not printed at all. The section answers to #wptm-cars-request, so a hero or call-to-action button can scroll straight to it.

Does it create pages or content when I activate it?

No. Activation only registers the post types and prepares the database. A notice then offers a starter kit, and nothing is created until you press the button. The starter kit’s sample destinations are Moroccan examples; rename or delete any of them.

How do I get review stars in Google search results?

Tripvance cannot make stars appear; Google decides that. What it does is make a tour eligible: turn on reviews under Tripvance Settings and let travellers leave rated reviews, and keep “Search result product data” switched on. Once the tour has approved rated reviews, their average and count — the same figures printed on the page — are added to the tour’s Product data. With Rank Math active they go into the Product Rank Math already builds for the tour, or into one Product added to Rank Math’s graph if it has none: Tripvance does not add a second Product when Rank Math already provides one for the current Tour or Activity. Ratings can be left out separately with “Include the average rating from approved reviews”; a site that had switched the earlier rating option off keeps it off. Ratings from other websites are never included, and activity pages, which do not display reviews, carry no rating.

Is there an activities page?

Optionally. Activities are pages of their own from the start; the listing at /activities/ and a page per activity type are switched on under Tripvance Settings (activity archive). With it on, the plugin draws both — title, description, a row of type links, the activity cards with their prices, pagination — and a theme can replace either with archive-wptm_activity.php or taxonomy-wptm_activity_type.php. The [wptm_activities] shortcode and the Activities block put the same cards on any page.

My day trip shows DAY 01, DAY 02… but it is one day

On Automatic this should not happen: a tour is drawn day by day only when it says it lasts more than one day — more than one day, at least one night, a multi-day duration preset, or an overnight written on one of its days. Check those four first, because one of them is what is claiming the days. Otherwise set Itinerary format to “Single day / Timeline” on that tour and the choice is final. Fill in the Time field at the top of each stop’s card if you have one and it leads the stop on the page; a stop with no time still reads perfectly by its title.

I set Itinerary format by hand. Will the plugin overrule me?

No. Automatic only decides for tours that have not chosen, and a tour that has chosen keeps that choice whatever its duration says.

The stylesheets are minified. How do I read the source?

The readable files sit beside the minified ones (assets/css/tour.css next to tour.min.css). Define SCRIPT_DEBUG as true in wp-config.php and the readable files are served instead, as WordPress does for its own. The minified copies are built with esbuild 0.25.9. To rebuild a file, run npx --yes esbuild@0.25.9 assets/css/tour.css --minify --outfile=assets/css/tour.min.css (use the corresponding paths for other CSS or JavaScript files); until then the edited file is served as it is, because a minified copy older than its source is never used.

Does this take payments?

No. It records booking requests and the agency confirms them. No payment gateway is included and none is required.

Can I send a quote, an invoice or a trip programme as a PDF?

Yes. Open a booking, set its price under Price & services if needed, and use the Documents panel: Preview gives a draft without a number, Issue gives the numbered document and keeps an unchangeable copy. PDFs are produced on your own server. Tripvance prints the details you give it; check that your invoices carry what the rules where you trade require.

Is Advanced Custom Fields required?

No. The plugin has its own fields and works without it. If ACF is installed the same fields are shown through ACF instead. The free version is enough; no premium field type is used.

Will it conflict with my SEO plugin?

It should not. The plugin outputs no canonical, meta description or Open Graph markup and no site-wide schema, leaving those to Rank Math, Yoast or whichever you use. Its only structured data is one Product on eligible Tour and Activity pages; with Rank Math it is merged into Rank Math’s own graph, and with Rank Math the page builder no longer writes Rank Math’s title, description or focus keyword. If another SEO plugin already publishes a Product for your tours, switch off “Search result product data” or use the wptm_product_schema_print filter.

Does it work with Polylang?

Yes, with the free version. Tours, activities, taxonomy terms and generated pages are assigned a language and linked to their translations, and each generated page is written in its own language. The homepage and the car hire page are created in every language at once and linked, so the language switcher and the hreflang tags are right from the start. The homepage’s own wording gets a language tab per language in Tripvance > Site pages > Homepage: the same form, the source text above every box, and a box left empty falls back to the source language rather than leaving a hole. Booking mails, vouchers and reminders answer in the language the traveller booked in. Your other wording is still exposed in Languages > String translations. A language switcher can be switched on in Settings and printed in the theme’s menus or through [wptm_language_switcher]; each language links to the real translation of the current page and never to a guessed address. Without Polylang nothing changes.

Can two people book the last seat at once?

If seat-capacity enforcement is enabled, no. Tripvance obtains a departure lock and performs the capacity test as part of the reservation write, so only one request can take the final seat. Capacity enforcement is off by default because many agencies use the form as an enquiry system.

نقد و بررسی‌ها

29 سپتامبر 2026
I have been using Tripvance for my travel website, and it has exceeded all my expectations. It provides absolutely everything needed to manage travel packages, tours, and activities seamlessly.What I love most about it: Pure Performance & Design: It inherits the theme's typography effortlessly without loading unnecessary external fonts or heavy scripts, keeping the site ultra-fast. Flexible Display Options: The itinerary builder (whether for multi-day trips or single-day timelines) is intuitive and clean. Plus, the Luxury Editorial mode gives high-end travel agencies a stunning, bespoke magazine look! Smart Booking System: The booking request workflow and seat-capacity management are super reliable and keep client inquiries organized.If you are building a travel agency or tour operator site on WordPress, Tripvance is a game-changer. Kudos to the developer for such an exceptionally well-crafted plugin!"
خواندن تمامی 1 نقد و بررسی‌

توسعه دهندگان و همکاران

“Tripvance” نرم افزار متن باز است. افراد زیر در این افزونه مشارکت کرده‌اند.

مشارکت کنندگان

“Tripvance” به 1 زبان ترجمه شده است. با تشکر از مترجمین برای همکاری و کمک‌هایشان.

ترجمه “Tripvance” به زبان شما.

علاقه‌ مند به توسعه هستید؟

کد را مرور کنید، مخزن SVN را بررسی کنید، یا از طریق RSS در گزارش توسعه مشترک شوید.

گزارش تغییرات

2.8.7

Settings screen reorganised, and stability fixes found by a full pass over the admin screens, the front end in every tour layout, both experiences and right-to-left. No setting, option, table, price or booking behaviour is changed.

  • Settings screen reorganised: one section navigation instead of two — a column beside the settings on a wide screen, grouped under Core, Booking, Content & pages, Public & localization, Setup & integration and Advanced, with “Find a setting” at its top; on a tablet or phone the same list opens from the section button in the toolbar. The sections follow that order on the page, and the content uses the width the navigation used to leave empty. One Save button, in a slim toolbar that stays in reach without covering the section headings it scrolls to. Nothing about the settings themselves changed: the same fields, names, values, defaults and save.
  • Fix: Settings printed a PHP warning (“Undefined array key 0”) in Section pages when a hierarchical section had terms without tours. The example link now uses the first term returned, whatever its key.
  • Fix: System status was laid out as one row, because its wrapper shared the class of the status badge; the diagnostic report was squeezed beside the cards and the page scrolled sideways. The report now sits below the cards at full width.
  • Phones: the Site pages list and the Section pages table in Settings scroll inside their own region instead of widening the screen, and the Site pages actions wrap.
  • Code: the WPML hooks used for translation are marked as WPML’s own for Plugin Check, a file-scope loop variable carries the plugin prefix, and the rate-table cache key is built with wp_json_encode() instead of serialize(). The booking screen’s stylesheet and script now ship with minified copies like every other asset.
  • readme: the 2.8.3 upgrade notice is shortened to fit the 300-character limit.

2.8.6

Visual polish of the booking screens, the Guest Pass and the PDF documents. No change to how bookings, prices, seats, notifications, documents or the Guest Pass work; no data, option or table is changed.

  • Add booking: the agreed total and its currency sit together in one highlighted panel, the amount larger and the currency a narrow code; status and “Send the usual emails” are each a tile, the ticked one marked; more space between rows and their descriptions; the button fills the width on a phone. The error list is indented on the correct side in right-to-left admin.
  • Booking screen on a phone: the page no longer scrolls sideways. The Guest Pass map-pin pair and the Trip content image and itinerary fieldsets now shrink to the screen.
  • Money fields: “Total to offer” and “Deposit asked for” (Quote) and “Amount” (Payments) are wide enough to show the full figure, with the currency beside them; the balance in Payments is larger; the invoice card shows “Not issued yet” / “Issued as …” as a small label.
  • Right to left: no letter-spacing on Arabic small-capital headings in the booking screens, or on the Guest Pass reference label, status pill and payment terms.
  • Trip programme PDF: the “Itinerary” heading is kept on the same page as the first day instead of being left alone at the foot of a page.
  • “On this page” bar (Bookings dashboard, Operations, Settings): it now sticks directly under the WordPress toolbar at the toolbar’s real height (32px, 46px on narrow screens) instead of a fixed 46px that left a strip of content showing above it, and it stays one row that scrolls sideways instead of wrapping into a two- or three-row bar (up to 99px) over the content. Headings it links to land below it. On phones it is not sticky.
  • Edit booking, shorter: for anyone who has not arranged the screen themselves, the panels follow the work — request, trip, reception, crew, price, payments, quote, documents, trip content, messages, notes — and the less used ones (Guest Pass details, crew, quote, trip content, messages, WhatsApp, referral, voucher, history) start closed. A closed panel shows a one-line summary of what it holds in its heading, marked when something was typed in it before closing. Fields in a closed panel are still saved; the jump links open the panel they point to. Opening or closing any panel keeps that person’s own choice, as WordPress always does. The header adds the received amount and the balance beside the agreed total; on a phone its document buttons sit two to a row.
  • Booking screen: a screen-reader label inside the documents table no longer widens the page on a phone; the internal notes panel is tinted and edged so it reads apart from what the guest sees.

2.8.5

Booking documents: a price set for one booking (revisions with a reason, never over the original), the booking’s own trip content, and real PDF quotes, invoices and trip programmes — numbered at issue, frozen, shareable on the Guest Pass. Manual bookings for tours, activities, transfers and custom trips. Schema 6 adds two tables; nothing existing is migrated or repriced.

  • Price & services (booking screen): a line editor — service, quantity, unit price, line total — with a discount (amount or percentage), an optional tax line and the currency. Totals are worked out on the server in integer minor units; the browser’s running total is a preview. A price of zero and a price on request are kept apart. Each save is a revision stored with the booking (_wptm_price_revision, one meta row each, append-only) carrying who, when, the reason (required), the previous figure, its source and the difference; the snapshot taken when the request arrived is never touched, and the product, its rates and its group prices are never written. Payments already recorded fix the currency: a revision in another currency is refused. A revision records no payment, moves no status and sends nothing.
  • One money authority: Payments\expected() now reads the latest agreement — a revision, or a quote the customer accepted after it — before the snapshot, so the list, the header, the payments panel, the Guest Pass, quotes and invoices print the same figure. A total below what was paid shows “paid in excess” instead of hiding it; nothing is refunded automatically. A quote drafted after a revision starts from the revised lines (wptm_quote_pricing_context, priority 5).
  • Imported prices never double: an engine line is read as quantity × unit only where that reproduces its own total. A group price (“Private tour, 4 travellers, 660”) imports as 1 × 660. The basis (per person, per group, per vehicle) is shown.
  • Exact parsing: Payments\to_minor() converts the decimal on its digits (Payments\decimal_to_minor()), not through a float. What it accepts is unchanged.
  • Trip content for this booking: an explicit “Make an editable copy” stores title, description, chosen images, itinerary (keeping days, timeline or route), included / not included, meeting point, time and instructions on the booking (_wptm_trip_copy). Opening a booking never copies anything; editing the copy never writes to the product; editing the product never reaches the copy. The Guest Pass prints the copy’s title and itinerary when there is one.
  • Documents: quotes (from Quotes versions), invoices and trip programmes, previewed as DRAFT without a number and issued with one. New table wptm_documents (schema 6): the issued content is frozen as JSON — company, customer, lines, totals and the words in the booking’s language — with its SHA-256; an altered row is never served. Numbers are allocated at issue only, from the rows (MAX + 1 per type and series), under a UNIQUE (type, series, number, revision) key that refuses a duplicate under concurrent requests and retries. A correction is a revision with the same number; the previous one is kept, marked superseded. Issuing confirms nothing and records no payment. The PDF is rendered once before a number is taken: a layout that cannot be produced issues nothing and uses no number.
  • PDF: real PDF files from the bundled TCPDF 6.11.4 (LGPL-3.0-or-later, vendor/tcpdf, trimmed to the engine and DejaVu Sans; loaded only when a PDF is produced; no Composer, no external service). Arabic is shaped and laid out right to left; references, amounts, telephone numbers and Latin-only lines keep their direction. Images come only from the media library, checked to be inside the uploads folder; WebP and other formats are converted to a temporary JPEG through WordPress’s image editor. Page numbers, a running header, the company footer, and headings kept with what follows them. Every text is escaped before layout.
  • Access: admin downloads need the booking capability and a nonce. A customer can download a document only when it is issued, current, intact and shared, with their Guest Pass key (revoked or regenerated keys stop working at once) or, for a quote, that quote’s own key. The Guest Pass lists shared documents; the quote page offers its shared PDF. No file is written to the uploads folder and no address works without a key.
  • Company details (Settings): trading and legal name, logo, address, contacts, free identifier rows (ICE, RC, IF, VAT…), footer and short terms per language, numbering (prefixes, year, digits, starting numbers), defaults for payment instructions and programme prices, and an optional tax line (off by default; rate in basis points). Empty fields fall back to the values the site already has (Guest Pass name and logo, support telephone and e-mail, WhatsApp, site address). Payment details stay on the Payments screen and are printed only when chosen.
  • Add booking: a tour or activity, a transfer (vehicle and trip type, priced per vehicle) or a custom trip with no product (holds no seats). The product’s price for the party is stored as the booking’s snapshot, as a request from the site stores it; an optional agreed total is recorded as a revision. Source “Other” needs a description. Values are kept when a field is refused.
  • Fix: a booking entered by hand never held its seats. Its ledger key was 43 characters for a 40-character column, so $wpdb refused the row; it is now 39. Capacity is respected again for manual bookings with a date.
  • Fix: the Guest Pass showed only the old typed payment fields to guests: the ledger summary was registered on wp-admin requests only. It now loads with the pass, and “paid” is what is held (received less refunded).
  • Bookings list: Trip, Source and “Total · paid · due” columns (Guests and Submitted are under Screen Options), filters by source and travel-date range, a custom trip’s own title, and an empty state that says what to do.
  • Booking screen: the header carries the Quote, Invoice and Programme PDF buttons (the current issued file, or the Documents panel), the jump links follow the order a booking is handled, and the default panel order matches it until the user rearranges. The payments form is one readable column.
  • Front-end request form: a summary line at the top when a field below was refused.
  • Debug notice: the Luxury section registry is attached on init instead of at load, so WordPress 6.7+ no longer reports a translation read too early.
  • Erasure keeps issued invoices (accounting records) and removes quote and programme documents of the erased booking.
  • Issued PDFs are kept. After the number is allocated the document is rendered with it and the file stored in wptm_document_files (database only, base64 with a SHA-256 checked on every read; nothing in uploads or the media library). The row stays “pending” — not current, not shared, not shown to the customer — until the file is read back intact; only then is it issued and the revision it replaces marked superseded. Every download, admin or customer (same capability, nonce and key checks), is that file, so later changes to images, logo, company details or the product do not reach it. A render or storage failure leaves the numbered row “Not issued — PDF not stored” with “Finish issuing”, which reuses the row and the number; issuing again meanwhile is refused. Deleting a booking or erasing it removes the files with the documents. A document issued by an earlier 2.8.5 build has no stored file: it is drawn again from its frozen text with the pictures as they are now (said so in the list) and its row is not changed.
  • Fix (Add booking): an agreed total that could not be saved no longer lets the booking be confirmed or announced. The booking is kept, stays pending, queues nothing, and quotes and invoices are refused until the price is saved; the booking screen shows the amount entered and “Save the agreed price and finish”. The retry runs on the same booking under a lock and reads the table first, so a revision whose write reported a failure but held is not written twice. 0 is a price. A booking finished later by recovery follows the same rule. The confirmation is checked in the table, not taken from set_status()‘s answer: if it did not hold, nothing is queued, what is left stays on the booking and the retry resumes there; a booking already confirmed is not transitioned again, and one a person moved elsewhere (cancelled, declined…) keeps that decision. In recovery, a booking that is saved but not finished is not marked done: its entry is kept and the next run resumes after the effects, without binning it or releasing its seats.
  • Fix (Add booking): “Notify” now covers the status change too. Unchecked, confirming queues nothing (Mail\held_back(): one booking, this request only, released in finally; every other status listener still runs). Checked, the messages are queued once, after the booking and its price are saved. Later changes made on the booking behave as before.
  • Fix (Add booking): with seat limits enforced, any failure to hold the seats refuses the booking, not only “full”, as the website form does. Nothing is written and what was typed is kept. Without enforcement, transfers and custom trips are unchanged.
  • Fix (recovery): settle() compares the saved values with those submitted; a missing row is incomplete as before, a different value is unknown (kept for the administrator notice, not binned). A record that stopped mid-save with nothing to compare against is unknown instead of complete. Seats are checked on the ledger row. The settling lock (wptm_booking_settling_{id}) lapses after five minutes and only its owner releases it; the 2.8.3 claim that could stall a booking for ever is ignored. Progress after the save is recorded (_wptm_settle_stage), so a stop after “complete” still queues the messages; a stop inside the wptm_booking_created listeners does not run them again (logged). Waiting bookings are one option row each (wptm_booking_unverified_{id}), so concurrent writers lose none; the 2.8.3 list is moved on first read.
  • Fix (prices): Pricing\snapshot_state() no longer accepts an existing snapshot on presence. It must be one readable row with its total and currency agreeing with the expected snapshot; otherwise the new answer invalid (unknown, never complete or binned). Missing or wrong report copies (_wptm_price_total_minor, _wptm_price_currency) are written again from the stored snapshot, never from current rates; the snapshot is never replaced.

2.8.3

Stabilisation: bookings, prices and notifications are confirmed as stored before success is announced; Guest Pass map priority. No schema, URL, token, QR, permission or pricing-formula change; nothing is migrated or repriced.

  • Bookings: Booking\create() reads the required values back from the table (uncached) and compares them with what was written — reference, customer name, e-mail and phone, product, date and start time, departure, party, status, the price snapshot when one was worked out, and the reservation link — instead of trusting wp_insert_post() or update_post_meta()‘s return. Three outcomes are kept apart. Complete: the booking carries _wptm_save_state = complete (itself verified), the ledger is joined and wptm_booking_created fires. Confirmed incomplete: the post is moved to the bin and the move is confirmed in the table before create() returns wptm_booking_incomplete; only then do tour, activity, transfer and manual callers release the seats; if the move cannot be confirmed the outcome is unknown. Unknown (the verification read failed): create() returns wptm_booking_unverified; nothing is binned, deleted or released, the ledger row and the reservation link stay with the request so a resend waits for this booking instead of creating another, the visitor sees the translated “still being recorded” message with their answers kept (the manual form says to check the Bookings list first), and the booking is recorded in wptm_booking_unverified. The queue runner (loop back, cron, hourly sweep, admin fallback) settles it with Booking\recover_unverified(): complete the save state, ledger join, history, wptm_booking_created and the notifications, once (an atomic claim row guards them); confirmed incomplete binned, the move confirmed, then the seats released; still unreadable kept, and after ten minutes administrators see a count with a “Check now” button. A request that stopped mid-save (state still “pending” after ten minutes) is settled the same way. Age alone never decides an outcome. Luxury’s transaction is unchanged: the same checks run inside it, and any failure rolls it back.
  • Repeated requests: a repeated copy of a request (await_booking(), resolve_duplicate(), and the fingerprint shortcut for tours, activities and transfers) is answered with a booking only once Booking\completion_state() says it is complete. A ledger row naming a booking, or a booking naming its reservation, is no longer enough; a failed read is never complete. Until then the copy waits up to the existing limit and gets the “still being recorded” message. Bookings made before 2.8.3 carry no save state and count as complete when they have a reference and a status. Reconcile no longer joins a booking whose save is still pending. Uninstall removes the two new operational options.
  • Prices: Pricing\store_snapshot() keeps its signature and returns true only when the snapshot (and, for a priced booking, its total and currency) read back intact; Pricing\snapshot_state() gives the full answer (stored / exists / failed / unknown). An existing snapshot is never replaced, checked in the table. Tour, activity and transfer requests pass the server-side snapshot to create(), which stores it before the booking is announced. A zero total is stored as a price; a price on request stores the snapshot without a total; no quote stores nothing, as before. The calculation is unchanged and uses the same inputs, just before the post rather than after it; listeners of wptm_booking_created (the payment state) now see the snapshot. Recovery stores the snapshot worked out at request time if it is missing; nothing is repriced.
  • Notifications: Mail\queue_state() tells queued / exists / skipped / failed apart (queue() still returns the row id). Customer mail switched off, a missing address or a refused duplicate is not a failure. A required request or confirmation message that could not be added to the outbox flags the booking with the existing _wptm_mail_queued key; the queue runner adds what is still owed — dedupe keys make repeated or concurrent runs write one row — and removes the flag only when nothing owed is missing. What is owed follows the booking’s current status (Mail\still_owed()): “request received” only while the request is open (pending, contacted), the confirmation only while confirmed, the agency’s new-request message not once cancelled, declined or completed. The same test runs again when a row is composed for sending; a message that no longer fits is cancelled as “No longer applicable”, not failed. Retries are spaced by a one-minute gate; a retry that still fails shows administrators a count and “Try again now” (capability and nonce checked). No personal data in notices or the log. A failed notification never cancels or recreates a booking; “sending”/”uncertain” rows are untouched and never resent automatically.
  • Guest Pass map: the booking’s own map link, then the booking’s own coordinates (a complete, in-range pair; 0 is valid), then the tour’s meeting map link, then a search for the place and address. Before, the tour’s link outranked a pin set for the booking.
  • Luxury: the private-enquiry storage check is shown under Settings “How journeys are sold” when the luxury experience is on, so a non-InnoDB or unreadable-engine database is visible before the first enquiry is refused. Nothing is converted and nothing is saved unsafely.
  • Changelog: 2.8.2 said sites with non-InnoDB tables “keep the previous behaviour and log a warning”; in fact the enquiry is refused, the reason logged and an administrator notice shown. Corrected below.

2.8.2

Luxury enquiry concurrency fix and Guest Pass visual polish. No schema, URL, token, QR or permission change.

  • Luxury enquiries: “sent” is answered only when the enquiry is stored. The booking carries its submission key (_wptm_enquiry_once, written with the record), so a retry after a crash between saving and finishing resolves to it instead of filing a duplicate. A copy that arrives while the first is still writing waits up to 6 s (wptm_luxury_once_wait) and then gets a translated, recoverable message with its answers kept. A failed save releases the claim at once. Claims are taken with INSERT IGNORE, expired claims (120 s) are taken over with a compare-and-swap UPDATE, and releases delete only the releasing request’s own token, so a stale worker can neither overwrite nor release a newer claim. The hourly sweep deletes only the exact expired values it read.
  • Luxury enquiries, storage: the booking, every meta row (the submission key included), the enquiry fields, the outbox rows and the end of the claim are one InnoDB transaction (store_enquiry()). It opens by locking the claim row (SELECT … FOR UPDATE) and checking the owner token, so a worker whose lease was taken over writes nothing and queues no mail, and no takeover can happen while the owner is inside it. A request that dies before COMMIT leaves nothing (the server rolls back on disconnect); a silent reconnect mid-transaction is detected by connection id and its fragments removed; a failed COMMIT is resolved by reading. Notifications are queued inside the transaction and only started after COMMIT (queue_mail(), notify() unchanged for callers); wptm_luxury_enquiry_created fires after COMMIT. Lock waits are bounded to 3 s for this connection. Sites whose posts, postmeta, options or outbox tables are not InnoDB (or whose engines cannot be read) store no enquiry: the visitor gets the translated retry message, the reason is logged and an administrator notice explains it (see Safeguards).
  • Luxury enquiries, safeguards: no enquiry is stored without a transaction. If posts, postmeta, options or the outbox table is not InnoDB (‘unsupported’) or the engines cannot be read (‘check_failed’), the visitor gets the translated retry message with their answers kept, only this request’s claim is released, the reason is logged and an administrator notice explains it (clearing itself once the check passes). Before COMMIT every required value (customer, tour, party, notes, status, enquiry fields, reference, request key) is read back and compared with the validated submission, and the owed outbox rows are checked against the booking; queue_mail() now reports queued / exists / skipped / failed, so switched-off customer mail or a missing address never blocks an enquiry while a failed insert rolls it back. A failed COMMIT, or one answered on a replacement connection, is never taken at its word: the transaction is ended with ROLLBACK and the durable state is read afresh (committed_state()): complete is success, absent is a retryable failure, anything else is “confirmation pending” with nothing recreated or deleted. The lock-wait setting is restored at shutdown.
  • Guest Pass: header reads agency, pass title with reference and status, trip, then the facts; the facts grid has no empty cells; a reset rule that outranked every spacing rule now carries no weight (:where()), restoring the intended spacing; long names and addresses wrap inside their cards; pickup details and guest actions are grouped under hairlines; plain buttons get the primary style instead of the browser’s; the QR sits on plain white with quiet space; the phone bar colours WhatsApp by destination; print shows black text (the header’s white text vanished on paper), hides controls and WhatsApp links, keeps the QR block and key groups whole.

2.8.1

Stabilisation pass before 3.0. No schema change, no new dependency, no change to pricing formulas, URLs, hooks or stored data.

  • Bookings: the idempotency key is now the form’s request identifier plus the normalised request (who, and every fact that distinguishes the booking) and the nonce. Two visitors served the same cached page no longer share a booking or see each other’s confirmation; a retried or concurrent copy of one submission still resolves to one booking. A copy that arrives while the first is still being written waits for it instead of creating a second booking on the same reservation. The request identifier is also regenerated per page view in the browser.
  • Bookings: duplicate detection includes start time, departure, extras, pick-up and, for transfers, vehicle, trip type, pick-up time, drop-off and return. Phone numbers are no longer cut to their last nine digits.
  • Bookings: an expired form returns to the tour, activity, transfer or journey with a translated message and the answers put back (server-side, never in the address, page uncacheable). Nothing is booked from it; the nonce check is unchanged.
  • Luxury enquiries: the same cache-safe dedupe, claimed atomically; the answer page is kept out of page caches.
  • Outbox: a send interrupted mid-way is marked “Delivery unconfirmed” after 15 minutes and never retried automatically. The booking’s Messages panel offers “Mark as delivered” or “Send again anyway”; both are capability- and nonce-checked and version-guarded, so a double click acts once. Composition errors no longer leave a row stuck; the first retry waits the intended two minutes.
  • Pricing: an extra with a non-Latin name (e.g. Arabic) could be ticked but was silently dropped from the price and the booking; it is now kept.
  • Editor: saving an untouched tour no longer changes it. A pre-1.6 per-group tour kept “Per group”, a tour with departures kept “Scheduled”, a tour without a currency kept the site currency, and a price written “45,5” or “1 250,50” is no longer emptied by the browser.
  • Layouts: “Route at a glance” uses the design’s heading face; its end label no longer widens the page on phones; Atlas’s ruled route row spans the section. The closing booking form (Qamar) is held to its intended width. The phone booking bar no longer covers the end of the page.

2.8.0

A curated set of tour layouts. Seven layouts with distinct purposes replace the long list of overlapping ones; the older choices stay supported. Nothing is migrated and no tour needs re-saving.

  • Layout families: Salma (booking focused), Sara (marketplace), Atlas (multi-day), Sahara (photo-first), Medina (short experiences), Qamar (premium storytelling), Classic (simple).
  • Variants: Atlas styles Clean and Zellige; Sahara galleries Cinematic, Immersive and Mosaic. A variant changes presentation only.
  • Legacy layouts (Immersive, Editorial, Mosaic, Adventure, ZelligeDesign) keep their templates and stylesheets and render as before; they are no longer offered for new choices.
  • Layout picker in Settings, the tour editor and the setup wizard: each layout with its purpose and when to use it.
  • Structural changes to Sara, Atlas, Sahara, Medina and Qamar (see changelog.txt). Prices, Group Rates and booking are the shared components, unchanged.
  • Final polish: Sara’s booking column no longer hides the help card’s WhatsApp button while scrolling; a proper WhatsApp mark on every WhatsApp button; a cleaner Atlas route summary; Medina places its questions and reviews without empty half pages; a finishing pass on Qamar and on the “Need help?” card.

2.7.0

Tour display stabilization and consistency. Every layout keeps its look; the layouts now share the same components and data. Nothing is migrated and no tour needs re-saving.

  • One tour card data layer and renderer for standard, archive and related cards; prices always from the tour’s own pricing (Group Rates included).
  • Trust section on SalmaDesign and QamarDesign; section navigation follows each layout’s real order.
  • One booking confirmation per page; no booking dialog or empty link when booking is off; no duplicate ids when the form is drawn twice.
  • The two-step booking form is a shared component: its styles moved to booking-steps.css, and salma.css (same handle) holds SalmaDesign’s layout only.
  • Highlights: one shared item style on every layout — fixed 34px icon aligned to the first line, two columns where the section is wide enough, one on phones, lighter type.
  • Empty states: no empty bands, cards, asides or headings when optional content (facts, highlights, related tours, booking, trust, meeting point, help) is missing.
  • Phone booking bar no longer collapses with long translated labels; itinerary titles no longer repeat the day number; bind-once scripts; documented z-index scale.

2.5.1

  • Homepage FAQ section: equal card heights and no layout shifting when answers open.
  • Safe manual upgrade option for legacy tours to use the new tour editor, keeping existing data and legacy metadata.

2.5.0

Admin editor workflow and polish: stable layout, status-aware Save/Publish bar, clearer validation, and a finishing pass on tour and activity pages. Pasted itineraries and FAQs no longer collapse.

The full release history is in changelog.txt, which ships with the plugin.