Skip to content

Products

The Products navigation group in Arkana turns tour content into sellable, priced packages and tracks them all the way to a customer booking. It spans four stages: the master template, its market-specific localization, the concrete offers, and the resulting bookings.

Item Manages In navigation
Product Templates Master (source-locale) tour content Hidden — see below
Products by Market A template localized to one market + locale Yes
Offers Concrete sellable packages (date + airport + pricing) Yes
Last Seats The “Últimas plazas” discount showcase Yes — admins only
Bookings Customer bookings of an offer, with the full workflow Yes

The Product Template is the reusable master record: title, descriptions, highlights, media, itinerary, logistics and the structured “what’s included / not included” content in the source locale.

Its resource is deliberately hidden from the navigation (shouldRegisterNavigation() returns false). Templates are no longer edited as a standalone menu item — they are created and managed through Tours (SupplierTour) under the Suppliers group. The template resource remains reachable by direct URL for admin debugging only.

Once a template has at least one market product it becomes content-locked: text and images can still be edited, but the itinerary structure is frozen. See Product Templates for the content model and Products overview for how templates, tours and market products relate.

A template’s itinerary endpoints carry an integer overnight flight days count (flight_days, 0 to a small cap) — the nights a traveller sleeps aloft on the way to the destination and on the way back. Each night adds a dedicated flight-day narrative (and its own image) before or after the itinerary, so a long-haul route with a red-eye renders correctly instead of collapsing into a single day. This generalizes the legacy has_overnight_flight boolean. The full resolution rules and image fallbacks are documented in Product Templates → Overnight Flight Days.

A Product by Market ties a template to one market and locale — the actual translated, sellable product. It owns the flight search configuration (departure airports, route legs, search window derived from the tour’s travel windows) and the localized content, and it links back to its Tour.

Products have a status of draft, active or inactive; product_template_id, market_id and locale are immutable once created. Notable operator actions from the view page:

  • Auto-Generate Offers — queues the offer generator for the product’s airports and dates (1–5 offers per departure date). The run happens in the background; the created/skipped breakdown arrives in your notification bell, with a link back to the product’s offers.
  • Diagnose missing offers — for chosen airport × date slots, runs each through the same gate sequence the auto-generator uses and reports why offers are or aren’t produced (capped at 100 slots per run). This is the operator’s first stop when a product “won’t generate offers”.
  • Translate from Tour — full AI translation of all fields from the tour content.
  • Share with client — a long-lived signed magic link so ops can send a client a private view-and-book link without publishing.
  • Duplicate, View Cached Flights, plus Preview (normal / with flight diagnostic).

A product cannot be deleted while it has offers. See Products by Market.

The new day-by-day itinerary on the product page lists, per day, what the traveller does, grouped by Morning / Afternoon / Full Day / Night. Content curates that list per market product in the Day Highlights & Activities section of the edit page, right after Itinerary Info. The module names each day by the day label written in Itinerary Info (tidied for display, not the day title), so those labels should read as place names; overnight flight days become “Vuelo a ” / “Vuelo de regreso” on their own. It is purely editorial: it does not create supplier activities and never affects pricing, contracts or allotments, which keep running on the tour’s included activities.

  • Each day is a fixed row; add as many items as needed and drag to reorder.
  • An item is free text (max 80 characters) with an optional time slot.
  • Optionally link the item to one of that day’s included activities. The link only makes the item clickable on the product page, opening the activity’s detail sheet (its existing name, description and photos); an item stays plain text when its linked activity has neither a description nor a photo and it has no Description or Photo of its own. Picking an activity fills an empty title with its name, and an empty slot with the activity’s slot when it has one of the four; both stay editable.
  • Optionally give the item its own Description (max 500 characters) and Photo. Either one makes the item clickable even without a linked activity (arrivals, transfers, sailing, free time), and on a linked item they replace the activity’s description and photo on the sheet. Without a photo of its own or from the activity, the sheet shows the day photo.
  • Warnings at the top of the section flag items that drifted after a tour edit: the day no longer exists, the day’s location changed, or a linked activity is no longer included. Days that disappeared are kept as “Day N (no longer in the itinerary)” so nothing is lost; re-enter those items on the right day or delete them.
  • “Translate from Tour” and tour itinerary changes never overwrite these items.
  • A supplier’s day-by-day PDF can fill in a day’s text and highlights after review — see Importing a supplier’s day-by-day PDF.

Customers see the new itinerary only on products where Show the new day-by-day itinerary is on, in the Product & Market section of the edit page; with it off they get the current itinerary. Content turns it on product by product, best-selling destinations first, and only after checking each day against the supplier’s real programme. Turning it off takes effect on the next page load, with no deploy. Staff can preview the module on any product, switch on or off, with Preview on Frontend → With the day-by-day itinerary module (see Feature Toggles).

See Products by Market admin → Itinerary Sections and the Itinerary Items API.

An Offer is a concrete sellable package: a market product + departure airport + departure date + fare group, with combined flight, land and insurance pricing. Offers are listed under Offers and also embedded on each product’s view page.

Three stored statuses (OfferStatus):

  • Draft — editable internal state, not published.
  • Approved by supplier — auto-set when the supplier signs the contract covering the offer’s tour rate. Still not published; Volāre must explicitly activate.
  • Active — published on the web.

Two states are derived, not stored: Stop sale (when a tour stop-sale covers the offer’s date) and Active · release window (an active offer kept live only because a committed booking exists while inside the release window).

Margin is editable in every status, and editing an active offer reprices it live — existing bookings keep their snapshotted price. Each offer records a margin basis (MarginBasis):

  • Sale (default for new offers) — price = cost ÷ (1 − margin/100). A 20% margin is a real 20% profit on the selling price. Capped below 100% (max 90).
  • Cost (legacy, backfilled onto older offers) — price = cost × (1 + margin/100). A 20% markup is a 16.67% profit on sale.

The basis is frozen into the checkout session and booking snapshot at purchase. See the Financial Model for the margin waterfall.

An offer’s bookability is gated by release windows — the travel-window date ranges on the supplier tour rate, plus each rate’s release_days booking cutoff and any tour stop-sale. Only active offers outside their lead-time floor, outside any covering release window, and outside a stop-sale are bookable on the web; the strictest covering rule applies. Editing and diagnostics stay available in any status. Full pricing and generation detail lives in Offers.

Bulk actions include Activate selected and Recalculate selected (re-runs flight pricing on a queue); the delete action guards offers that already have bookings so historical sales survive.

Flight Price is the flight component of an offer’s Base (Base = flight + land) — a cost, not a sale price — surfaced so a batch of generated offers can be compared without opening each one. Unlike its neighbours Base, Margin and Per Pax, it is visible by default, and it is sortable. The number is the sum of the offer’s stored flight legs, falling back to the flight_base_price column only when an offer has no legs at all, so it always matches the “Total Flight Price” on the offer detail rather than disagreeing on every multi-leg offer the way the raw column would; a leg that legitimately costs 0.00 stays 0.00 instead of resurrecting the stored base price.

Flight price age answers “when did we last ask Aerticket for the price this offer is selling?”. It is the age of the oldest current flight leg — the total is only as fresh as its stalest component — badged green (Fresh, under 14 days), amber (Ageing, 14–45 days) or red (Stale, over 45 days), with the exact timestamp on hover. The “Flight set by …” sub-line names what put the offer on this flight (generator, live_search_upgrade, arkana_search_upgrade, …), not who last checked its price.

The Flight price older than [N] days filter keeps offers with any leg searched more than N days ago; its record count is how Operations sizes the stale catalogue before feeding the worst offenders to Recalculate selected. The older Last manual recalc column is hidden by default: it only records manual recalcs and maintenance runs, so it says nothing about freshness. Nothing refreshes prices on a schedule — the column reports age, it does not fix it.

Publishing is not a free status flip. Both doors onto Active — Activate selected and the status toggle on the offer’s edit form — re-price the offer first and refuse it, one offer at a time, when its land cost cannot be fully computed or its selling price no longer covers what the trip costs. The bulk action reports what it activated and what it declined, with the reason per offer: a supplier closure on any day of the trip, not just the departure (naming the service, the rate and the closed range), a room type with no price or a price of 0 on a named rate, a package whose rates ran out, an alternative group covering no member for the date, or a missing exchange rate — that last one flagged as an FX gap rather than something to fix in the product data. The usual remedy is Recalculate selected, then activate again; the guard never moves the customer-facing price behind a status change.

This gates only the move into Active. Offers already Active stay editable exactly as before, so live margin edits are unaffected.

Export to Excel, above the offers list, downloads the offers currently on screen: filter by product, market, departure airport and departure-date range first and the file carries exactly that scope. The airport filter takes several airports at once, so comparing Madrid against Barcelona is one file rather than two exports stitched together by hand. Export selected in the toolbar does the same for the rows you tick. Both offer CSV or XLSX.

The sheet itemises what the list only totals — Land price, Flight price and Insurance price as their own columns, followed by the same three per traveller, next to Base price, Margin %, Final price and Price per pax, with SKU, product, market, departure airport, departure date, return date, room type, pax, currency and CUG for context. This is the breakdown a commercial conversation with a distributor needs. Money is written as plain numbers so the columns add up in Excel, and the currency has a column of its own. The list itself also carries a Land column beside the other prices, so the largest component is visible without exporting.

Offers are priced for a party of two, so the per-pax columns are the ones to compare one product against another with — no dividing by hand. A component an offer simply doesn’t have is left blank rather than shown as 0.

The return date is the day the traveller lands back home on the flights the offer is actually built on — the same date the customer’s booking email gives them — not the departure date plus the tour length. On long-haul trips the two differ by a day or two, because an overnight outbound and a return that lands the morning after both fall outside the nights counted at destination.

Exports run in the background: the finished file arrives as a download link in your notification bell rather than as an immediate download, and only the person who started an export can download it.

Last Seats (/admin/last-seats) is the whole “Últimas plazas” section on one screen: the cover image and headline discount, the switch and schedule that decide whether it exists on the web at all, and the products in the order the page renders them. Only admins can open it — it moves real selling prices.

Configuration is per market; Spain is the only one set up today. The screen always loads the Spanish section (Market::firstWhere('code', 'ES'), falling back to the first active market) — the Market select governs which market a save writes to, not which one is displayed, so it is not yet a working market switcher.

Inside a product the campaign is expressed as discount periods. A period is a date range, the offers ticked inside it, and the three terms they all share:

Field Meaning
Discount on land (%) Taken off the supplier’s land price only — 1 to 50, in half points. Flights, insurance and the extra-night supplement are unchanged
Campaign seats per offer The quota each ticked offer gets, counted in travellers. When one runs out, that departure returns to its normal price while the others stay on campaign
Title What the supplier sees on the booking, in Arkana and in the settlement

A product needs a second period whenever any of those three differ. A departure date can belong to only one period of a product — dates already used elsewhere are not listed, and changing a period’s range unticks the offers that fall outside it.

The offer list per period is the product’s genuinely sellable departures in that range, each with the price it goes for today. Selected offers underneath shows, per date, how many of its campaign seats are already sold.

Nothing about the price is entered by hand except the comparison price:

  • From (auto) — the lowest discounted per-person price among the ticked offers that still have seats, across every departure airport, for a double room. Re-quoted from supplier rates, so a percentage you have changed but not saved is already reflected.
  • Discount shown on card — computed from From (auto) against the comparison price, floored.
  • Web status — why the card is or is not on the web, in the order the problems would be fixed: no period with offers, no offers with seats left, no comparison price, comparison price not higher.

The Maximum discount field (the “-40%” in the navbar and “hasta 40% de descuento” on the page) is typed by hand and warns — without refusing — when it promises more than any card actually delivers.

  • Intro text — the paragraphs above the cards. Leave a blank line between paragraphs. A section that never had its own copy opens with the default text the page shipped with; the field is required.
  • Show countdown (under Visibility) — shows “Las ofertas terminan en” with the days, hours, minutes and seconds left until Ends at, above the intro text. It needs an end date: with the switch on and no end date the field warns and the page shows no countdown. When the time runs out the countdown hides itself, and the section stops being visible at the same moment.

Both are reflected by Preview before saving.

Visibility and pricing are the same switch

Section titled “Visibility and pricing are the same switch”

Turning the section off, or letting its schedule lapse, does not merely hide it: every campaign departure goes back to its normal price. The start/end times are entered and displayed in your own admin timezone (Spain by default). With no visible card the section behaves as switched off entirely — no navbar link, no home band, and the page 404s.

The discount only reaches customers once the persisted offer prices are rewritten, so saving queues a re-price for every departure date the product held before or after the save — including dates that were removed, which must lose the discount straight away. The notification says the affected offers are being re-priced; the prices on the web follow when the queue drains. A scheduled sweep every 15 minutes catches the changes nobody triggered: a schedule that opens or closes, and a quota that runs out.

Preview opens the public page with whatever is in the form, saved or not, bypassing the switch and the schedule. The link carries an encrypted token valid for 30 minutes, is never indexed and is never cached.

Full mechanics in Last Seats.

A Booking is a customer’s purchase of an offer — client, passengers, extras, payments, flight bookings and a full status history. The client’s total_amount is fixed at purchase; downstream fare drift is absorbed by margin, not passed to the customer.

Bookings move through a forward-only state machine (BookingStatus, 17 states): Draft / Checkout → quotation (agent-handled bookings only; self-service checkout goes straight to payment at any party size) → payment → flight booking → land confirmation → Confirmed → balance / fully paid → Completed, with Cancelled and Expired as terminal states. Any non-forward move is a manual override requiring a reason, and every transition is audited. See Booking Lifecycle for the state machine and Bookings for the data model.

The booking view shows a full cost breakdown at the actual passenger count — P&L summary, customer payment schedule, how the price was built at purchase, direct supplier costs, land-cost composition, FX position and flight-price variation, with a reconciliation footer. When a deposit exists, a payment schedule (deposit, balance due date, total paid, remaining) and the payments table are shown. The whole cost breakdown is hidden from supplier managers.

The booking view is the operational engine. Key actions, gated by status:

  • Book Flight / Book All Flights — dispatch flight-booking jobs for each leg (at PendingFlightBooking).
  • Retry Flight Booking — after a failed attempt.
  • Issue Flights / Issue Flight — queue the Aerticket ticket-issue job for each eligible flight booking (the ticketing → issued promotion).
  • Confirm Land Services / Confirm Services — advance land confirmation and payment stages.
  • Confirm Availability / Send Checkout Link — for bookings an agent placed in a quotation status.
  • Generate Payment Link — deposit, full or balance links via the payment service.
  • Share Trip with Pax — emails the selected passengers their trip-access link, and (once per booking) schedules the visa information email for the next day, to all eligible passengers rather than only the selected ones.
  • Log Interaction, Override Attribution, Change Status.

The Diagnose missing offers action lives on Products by Market, not on Bookings; offer-level flight analysis is the per-offer “Recalculate offer” action under Offers.