> ## Documentation Index
> Fetch the complete documentation index at: https://voyant.travel/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Trips

> The deterministic composition layer: Trip Envelopes that group independent component bookings into one customer-facing itinerary, pricing, reserve, checkout, and support experience, including composed FIT trips and cruise extensions.

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](/docs/platform/modules/bookings); it references the component bookings that are.

## Key concepts

<ResponseField name="Trip Envelope" type="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](/docs/concepts/glossary).
</ResponseField>

<ResponseField name="Component Booking" type="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](/docs/concepts/glossary).
</ResponseField>

<ResponseField name="Composed FIT Trip" type="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](/docs/concepts/glossary).
</ResponseField>

<ResponseField name="Extra" type="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](/docs/concepts/glossary).
</ResponseField>

<ResponseField name="Cruise Extension" type="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](/docs/concepts/glossary).
</ResponseField>

<ResponseField name="Trip snapshot and reservation plan" type="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](/docs/platform/modules/bookings) can point back at the trip snapshot.
</ResponseField>

## 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.

## The envelope lifecycle

The envelope hardens through an explicit status as pricing, reservation, and checkout complete. Each component carries its own parallel status, so one component can be `unavailable` or `failed` while siblings advance.

```mermaid theme={null}
stateDiagram-v2
    [*] --> draft
    draft --> priced
    priced --> reserve_in_progress
    reserve_in_progress --> reserved
    reserve_in_progress --> failed
    reserved --> checkout_started
    checkout_started --> booked
    checkout_started --> failed
    booked --> [*]
    draft --> cancelled
    priced --> cancelled
    reserved --> cancelled
    failed --> [*]
    cancelled --> [*]
```

## 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](/docs/platform/modules/bookings) and the finance module. The envelope references; it does not absorb.

## Dynamic assembly: requirements and candidates

Everything above assumes the components are already pinned. Trips also supports **dynamic assembly**, where a component starts as an unresolved need and is filled from live, independently-sourced supply. This is the trips half of [dynamic packaging](/docs/platform/modules/dynamic-packaging).

* A **Trip Requirement** (`tripRequirements`) is an unresolved need: a `vertical` plus `criteria`. Its status walks `open` to `sourcing` to `candidates_ready` to `selected`, or `no_availability`.
* **Trip Candidates** (`tripCandidates`) are persisted, ranked `AvailabilityCandidate` rows under a requirement, each with a TTL; status is `ranked`, `selected`, `expired`, or `discarded`.
* `sourceRequirementCandidates` runs the [catalog](/docs/platform/modules/catalog) availability fan-out (**injected** through `deps.search`, so trips never imports a catalog adapter) and persists the ranked candidates. `selectCandidate` pins one into a draft component, enforcing selected-uniqueness. `reshopRequirement` and `reshopTrip` re-source when supply goes stale, and `expireStaleTripCandidates` is a TTL reaper for a deployment cron.
* The candidate's `candidateRef` is not replay-safe: `priceTrip` re-resolves the selection and `reserveTrip` re-validates before any supplier dispatch, gated by `assertEnvelopeRequirementsSatisfied`.

<Note>
  The requirement-sourcing routes (`POST /:envelopeId/requirements`, `/requirements/:id/candidates`, `/requirements/:id/select`, `/requirements/:id/reshop`, `/:envelopeId/reshop`) need the fan-out search dependency injected. Until a deployment wires it, the sourcing routes return **501**. See [dynamic packaging](/docs/platform/modules/dynamic-packaging) for what is wired today.
</Note>

## Working with it

Standard modules are not registered by hand. `@voyant-travel/trips` is part of the standard product graph, so its routes, services, subscribers, and jobs are resolved into the application at build time. See [Configuration](/docs/platform/fundamentals/configuration).

The service functions take a Drizzle handle and return plain trip records.

```ts theme={null}
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.

```ts theme={null}
import { previewCancellation, cancelComponents } from "@voyant-travel/trips";

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

// Execute only the previewed components; siblings are untouched.
await cancelComponents(db, trip.id, {
  componentIds: ["trcp_01h..."],
});
```

<Note>
  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.
</Note>

## Links to other modules

* **[bookings](/docs/platform/modules/bookings)** owns each Component Booking's commitment truth. A booking's origin can point at the trip snapshot the envelope froze.
* **[proposals](/docs/platform/modules/proposals)** produces proposal versions whose Trip Envelope snapshot mirrors a composed itinerary; reserve crosses from accepted version into component reservation.
* **[catalog](/docs/platform/modules/catalog)** is the source the catalog component adapter maps into trip components, so owned and sourced inventory compose into one envelope.
* The [cruises](/docs/platform/modules/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

<CardGroup cols={2}>
  <Card title="Bookings" icon="ticket" href="/docs/platform/modules/bookings">
    The component bookings each envelope references, with their own lifecycles.
  </Card>

  <Card title="Proposals" icon="filter" href="/docs/platform/modules/proposals">
    The proposal whose Trip Envelope snapshot a composed itinerary mirrors.
  </Card>

  <Card title="Cruises" icon="ship" href="/docs/platform/modules/cruises">
    The vertical behind Cruise Extensions that nest as Extras or split as siblings.
  </Card>

  <Card title="Glossary" icon="book-open" href="/docs/concepts/glossary">
    The shared vocabulary: Trip Envelope, Component Booking, Composed FIT Trip, Extra.
  </Card>
</CardGroup>
