Skip to main content
The trips module (@voyant-travel/trips) owns the composition layer that turns several independent commitments into one customer experience. A real itinerary is rarely a single booking: it is a tour plus a hotel stay plus a flight plus a transfer, each supplied, confirmed, taxed, and cancelled on its own terms. Trips groups those parts into a single Trip Envelope so the customer sees one itinerary, one price, one checkout, one set of documents, and one cancellation preview, without erasing the lifecycle boundaries underneath. It is a deterministic composer, not a new commitment primitive. It aggregates pricing, runs a reserve workflow across components, hands off to checkout, and previews and executes per-component cancellations. Checkout and cancellation are dependency-injected, so the app and runtime keep owning the payment provider, bank transfer, storefront URL, supplier, and staff-remediation policy. The trip itself does not become a Booking; it references the component bookings that are.

Key concepts

Trip Envelope
customer-facing aggregate
A customer-facing aggregate that groups one or more component bookings (and provider or source order refs) into one itinerary, checkout, support, document, and cancellation-preview experience. It is not necessarily one Booking, and it does not erase the lifecycle boundaries of its components. See Trip Envelope.
Component Booking
independently committed part
One independently committed part of a Trip Envelope, with its own supplier or provider reference, cancellation rules, tax treatment, fulfillment state, and operational owner. A component is independent: cancelling one does not implicitly cancel its siblings. See Component Booking.
Composed FIT Trip
individually assembled itinerary
An individually composed Trip Envelope assembled from independent commitments such as a product, stay, flight, transfer, cruise, or staff-confirmed placeholder. This is the bespoke, build-it-yourself itinerary, distinct from a single packaged product. See Composed FIT Trip.
Extra
dependent child line
A child line that modifies or extends a component booking and shares that component’s lifecycle closely enough to be cancelled, fulfilled, taxed, and supported with it. Product-internal bundles and dependent extras stay inside their component booking; they do not become siblings. See Extra.
Cruise Extension
vertical-specific extension
A cruise-specific pre or post-cruise hotel or land program. Its offer definition can be shared across cruises and sailings; a selected extension is an Extra when cruise-owned and lifecycle-dependent, or a sibling component booking when independently supplied and confirmed. The module ships ./cruise-extension helpers for the link and the selection representation. See Cruise Extension.
Trip snapshot and reservation plan
frozen composition state
A trip snapshot freezes the composed itinerary for reserve and provenance; a trip reservation plan captures what reserve secured (and what failed) before the component bookings are created. A booking’s Booking Origin can point back at the trip snapshot.

How independent commitments group into one itinerary

The load-bearing rule is the lifecycle boundary. What groups into one envelope, and what stays nested inside a component, is decided by whether the part has independent supplier, cancellation, tax, and fulfillment state.
  • Independent commitments become sibling component bookings. A customer-composed addition with its own supplier, its own cancellation rules, and its own fulfillment state is a sibling Component Booking under the same Trip Envelope. Cancelling it previews and executes against that component alone.
  • Dependent parts stay nested. Product-internal bundles and dependent Extras stay inside their component booking. They share that component’s lifecycle and are cancelled, fulfilled, taxed, and supported with it.
  • A Cruise Extension is decided by the same rule. Its catalog definition may be reused across cruises, but the selected extension is an Extra when cruise-owned and lifecycle-dependent, or a sibling component when independently supplied and confirmed.
The envelope gives the customer one surface (itinerary, price, checkout, documents, cancellation preview) while each component keeps its own truth. Grouping is a customer-experience concern, not a commitment merge.

What it owns

Trips landed as a composition layer with a deliberately narrow, deterministic surface.
  • The durable schema (./schema): trip envelopes, trip components, component events, trip snapshots, and trip reservation plans.
  • The deterministic trip service (./service): create, get, list, add and update and reorder components, and update component refs.
  • Price aggregation (priceTrip) that rolls component prices into one envelope total, and applyQuoteToComponent.
  • The reserve workflow (reserveTrip) that runs across components and produces a reservation plan.
  • Checkout handoff (startCheckout, completeTripCheckout), dependency-injected so the runtime owns payment, bank transfer, and storefront URL policy.
  • Component-level cancellation (previewCancellation, cancelComponents), so support can preview and cancel one component without touching siblings.
  • Cruise Extension helpers (./cruise-extension) and a catalog component adapter (./catalog-component-adapter) that maps catalog entities into trip components.
  • MCP tools (./mcp-tools): AI-safe trip planning, revision, pricing, and reserve operations.
It does not own the component bookings’ commitment truth, supplier confirmation, or money. Those stay in bookings and the finance module. The envelope references; it does not absorb.

Working with it

Mount the module routes and compose a trip from components.
import { createApp } from "@voyant-travel/hono";
import { tripsModule } from "@voyant-travel/trips";

const app = createApp({
  modules: [tripsModule],
});
The service functions take a Drizzle handle and return plain trip records.
import { createTrip, addComponent, priceTrip, reserveTrip } from "@voyant-travel/trips";

const trip = await createTrip(db, {
  personId: "pers_01h...",
});

// Each component is an independent commitment under the one envelope.
await addComponent(db, trip.id, {
  kind: "stay",
  // catalog reference and dates
});
await addComponent(db, trip.id, {
  kind: "flight",
});

// Aggregate the component prices into one envelope total.
const priced = await priceTrip(db, trip.id);

// Reserve runs across components and produces a reservation plan.
const plan = await reserveTrip(db, trip.id /* ...injected policy */);
Cancellation is per component. A preview shows what cancelling one part costs before anything is executed.
import { previewCancellation, cancelComponents } from "@voyant-travel/trips";

const preview = await previewCancellation(db, trip.id, {
  componentIds: ["tcmp_01h..."],
});

// Execute only the previewed components; siblings are untouched.
await cancelComponents(db, trip.id, {
  componentIds: ["tcmp_01h..."],
});
Checkout and cancellation are dependency-injected. The trips package stays deterministic and side-effect-free at its core; the app supplies the payment provider, bank-transfer, storefront-URL, supplier, and staff-remediation policy when it wires the routes.
  • bookings owns each Component Booking’s commitment truth. A booking’s origin can point at the trip snapshot the envelope froze.
  • quotes produces quote versions whose Trip Envelope snapshot mirrors a composed itinerary; reserve crosses from accepted version into component reservation.
  • catalog is the source the catalog component adapter maps into trip components, so owned and sourced inventory compose into one envelope.
  • The cruises vertical pairs with the Cruise Extension helpers to decide whether an extension nests as an Extra or splits into a sibling component.
  • The finance module collects against the envelope’s checkout while each component keeps its own tax treatment.

React package

@voyant-travel/trips-react provides admin and public API clients, validation-aware operations, TanStack Query keys and options, cache writers, provider wiring (VoyantTripsProvider under ./provider), and hooks (./hooks) for Trip Envelope composition. It supports trip creation, component add, pricing, reserve, checkout handoff, and support-facing component cancellation preview and cancel. Draft is a trip lifecycle status, not a separate object name. Fetcher and error utilities live under ./client, and stable query keys under ./query-keys.

Next steps

Bookings

The component bookings each envelope references, with their own lifecycles.

Quotes

The proposal whose Trip Envelope snapshot a composed itinerary mirrors.

Cruises

The vertical behind Cruise Extensions that nest as Extras or split as siblings.

Glossary

The shared vocabulary: Trip Envelope, Component Booking, Composed FIT Trip, Extra.