Busflow Docs

Internal documentation portal

Skip to content

Feature Spec: Ancillary Catalog & Extras (Backoffice) ​

Goal: Define how operators create and manage bookable extras (insurance, room upgrades, excursions, meals) on tour templates, how these flow to the booking widget, and how they appear in the workspace.


Domain Concepts ​

Centralized, Overridable Design ​

Operators define extras once at the operator level (the "Catalog"), then attach them to templates. Per-template and per-departure overrides follow the same cascade pattern as boarding points and deposit config.

Convention over Configuration ​

The 6 behavioral types (INSURANCE, UPGRADE, EXTRA_LUGGAGE, EXCURSION, MEAL, OTHER) are system-defined conventions. Operators pick a type and provide a label β€” they never define behavioral categories. This allows the system to determine refund rules, reporting, and invoice line grouping automatically.


Data Model ​

Tier 1: Operator-Level Catalog ​

A global library of extras that the operator maintains. Reusable across all templates.

backoffice.ancillary_catalog_items ​

ColumnTypeConstraintsDescription
idUUIDPrimary KeyUnique catalog item
tenant_idUUIDFK (operators.id), Not NullOwning tenant
typeVARCHARNot NullBehavioral category: INSURANCE, UPGRADE, EXTRA_LUGGAGE, EXCURSION, MEAL, OTHER.
labelVARCHARNot NullOperator-defined display name shown to passengers
descriptionTEXTNullableOptional longer description for the booking widget product page
cover_image_keyVARCHARNullableOptional image key for displaying the item in the booking widget.
default_priceDECIMALNot NullDefault unit price in the operator's currency
currencyVARCHARNot Null, Default: EURISO 4217
is_per_passengerBOOLEANNot Null, Default: truetrue: price applies per passenger. false: price applies per booking.
max_quantityINTNullableMaximum units per booking. Null = unlimited.
tax_strategy_overrideVARCHARNullableSTANDARD_VAT or MARGIN_SCHEME_25. Null = inherit from the parent tour's tax strategy.
statusVARCHARNot Null, Default: ACTIVEACTIVE or ARCHIVED. Archived items remain on existing bookings but do not appear in new checkouts.
sort_orderINTNot Null, Default: 0Display order in the booking widget and template configuration

NOTE

Unique constraint: (tenant_id, label) β€” prevents duplicate names within a tenant.

Tier 2: Template-Level Assignment ​

Which catalog items are offered on which tour template, with optional price overrides. (Note: Bundling of ancillaries is explicitly deferred to [future]).

backoffice.template_ancillary_assignments ​

ColumnTypeConstraintsDescription
idUUIDPrimary KeyUnique assignment
tenant_idUUIDFK (operators.id), Not NullOwning tenant
tour_template_idUUIDFK (tour_templates.id), Not NullTarget template
ancillary_catalog_item_idUUIDFK (ancillary_catalog_items.id), Not NullCatalog item being assigned
price_overrideDECIMALNullablePer-template price override. Null = use default_price from catalog.
included_by_defaultBOOLEANNot Null, Default: falsetrue: auto-added to every booking (e.g., travel insurance on premium tours). Passenger can opt out.
enabledBOOLEANNot Null, Default: trueQuick toggle to disable an extra for this template without removing the assignment.

Tier 3: Departure-Level Override (Inherited) ​

Departures inherit their template's ancillary assignments.

  • Default behavior: A departure inherits all assignments from its template. No separate table needed at [v0.1].
  • [v0.2]: Add a departure_ancillary_overrides table for per-departure price changes or disabling specific items.

Resolution Cascade ​

Effective Price = departure_override ?? template_override ?? catalog_default_price
Effective Enabled = departure_enabled ?? template_enabled ?? true (if assigned)

Commerce Projection ​

When a TourDeparture publishes, the payload includes the resolved ancillary catalog for that departure:

typescript
interface TripPublishedAncillary {
  catalog_item_id: string;
  type: AncillaryType;
  label: string;
  description: string | null;
  cover_image_key: string | null;
  price: number;
  currency: string;
  is_per_passenger: boolean;
  max_quantity: number | null;
  included_by_default: boolean;
  sort_order: number;
}

Commerce stores this as a denormalized JSONB array on tour_offerings.available_ancillaries.

IMPORTANT

New column on commerce.ancillaries: Add catalog_item_id UUID (nullable, soft FK to backoffice.ancillary_catalog_items). Null for manually added ancillaries. Enables Backoffice β†’ Commerce traceability.


Workspace UX ​

Tour Template Detail β€” Tab 5: Settings ​

Ancillaries live in the Settings tab under an "Extras & Add-ons" section.

Layout: A card-list showing all ancillary catalog items assigned to this template. Actions:

  • "Add Extra" β†’ Picker showing all ACTIVE catalog items not yet assigned to this template. Multi-select enabled.
  • "Edit Price" β†’ Inline edit the price_override.
  • "Manage Catalog" β†’ Navigates to /settings/catalog.
  • Remove (trash icon) β†’ Removes the assignment.

Global Ancillary Catalog Management ​

Path: /settings/catalogLayout: Table of all ancillary_catalog_items for the tenant.

Smart Defaults (Concierge Onboarding): The tenant provisioning flow seeds 3 starter items (e.g., ReiserΓΌcktrittsversicherung, Einzelzimmerzuschlag, ZusatzgepΓ€ck). Operators customize labels and prices immediately β€” they never face an empty catalog.


Commerce UX: Passenger Side (Booking Widget) ​

During checkout, after the passenger selects their boarding point and ticket type:

Step: "Extras & Add-ons"

  • Cards for each available ancillary, sorted by sort_order.
  • Items with included_by_default = true appear pre-selected with an "Included" badge. The passenger can remove them.
  • Each card shows: label, cover image (if present), price, description.
  • Toggle to add/remove. Quantity selector if max_quantity > 1.
  • Note: Availability of specific ancillaries based on the chosen boarding point is explicitly deferred.

V0.1 Pricing Tab Clarification ​

The V0.1 pricing model uses Centralized Pricing Categories + Base Price + Overrides.

Operator-Level: Pricing Categories ​

Path: /settings/pricing Operators define reusable pricing categories (e.g., ADULT, CHILD, INFANT) with discount_type and discount_value. Exactly one category is marked as base (is_base = true).

Template-Level: Base Price + Category Overrides ​

  1. Base Price β€” A single number field: "Adult price per person".
  2. Category List β€” Shows all operator-level pricing categories with the computed price based on the category discount.
  3. Per-template overrides β€” The operator can override the discount for a specific category on this template.

(No CostingSheet, no margin calculator, no multi-channel pricing at V0.1).


Phasing ​

FeatureHorizon
Operator-level ancillary catalog (CRUD)[v0.1]
Template β†’ ancillary assignment with price overrides[v0.1]
Commerce projection via TripPublished[v0.1]
Booking widget: extras selection step[v0.1]
Concierge onboarding seeds (3 starter items)[v0.1]
Operator-level pricing categories (CRUD)[v0.1]
Template base price + category overrides[v0.1]
Ancillary cover image support[v0.1]
Departure-level ancillary overrides[v0.2]
Per-departure pricing overrides[v0.2]
B2B reseller pricing catalogs[v0.2]
Ancillary analytics[v0.2]
Filter ancillaries by Boarding Point[future]
Ancillary Bundling[future]

Internal documentation β€” Busflow