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 β
| Column | Type | Constraints | Description |
|---|---|---|---|
id | UUID | Primary Key | Unique catalog item |
tenant_id | UUID | FK (operators.id), Not Null | Owning tenant |
type | VARCHAR | Not Null | Behavioral category: INSURANCE, UPGRADE, EXTRA_LUGGAGE, EXCURSION, MEAL, OTHER. |
label | VARCHAR | Not Null | Operator-defined display name shown to passengers |
description | TEXT | Nullable | Optional longer description for the booking widget product page |
cover_image_key | VARCHAR | Nullable | Optional image key for displaying the item in the booking widget. |
default_price | DECIMAL | Not Null | Default unit price in the operator's currency |
currency | VARCHAR | Not Null, Default: EUR | ISO 4217 |
is_per_passenger | BOOLEAN | Not Null, Default: true | true: price applies per passenger. false: price applies per booking. |
max_quantity | INT | Nullable | Maximum units per booking. Null = unlimited. |
tax_strategy_override | VARCHAR | Nullable | STANDARD_VAT or MARGIN_SCHEME_25. Null = inherit from the parent tour's tax strategy. |
status | VARCHAR | Not Null, Default: ACTIVE | ACTIVE or ARCHIVED. Archived items remain on existing bookings but do not appear in new checkouts. |
sort_order | INT | Not Null, Default: 0 | Display 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 β
| Column | Type | Constraints | Description |
|---|---|---|---|
id | UUID | Primary Key | Unique assignment |
tenant_id | UUID | FK (operators.id), Not Null | Owning tenant |
tour_template_id | UUID | FK (tour_templates.id), Not Null | Target template |
ancillary_catalog_item_id | UUID | FK (ancillary_catalog_items.id), Not Null | Catalog item being assigned |
price_override | DECIMAL | Nullable | Per-template price override. Null = use default_price from catalog. |
included_by_default | BOOLEAN | Not Null, Default: false | true: auto-added to every booking (e.g., travel insurance on premium tours). Passenger can opt out. |
enabled | BOOLEAN | Not Null, Default: true | Quick 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 adeparture_ancillary_overridestable 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:
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
ACTIVEcatalog 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 = trueappear 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 β
- Base Price β A single number field: "Adult price per person".
- Category List β Shows all operator-level pricing categories with the computed price based on the category discount.
- 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 β
| Feature | Horizon |
|---|---|
| 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] |