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

# Cruises

> The opt-in cruises vertical: a native cruise catalog, sailings, cabin categories, itinerary stops, and an adapter contract that lets sourced cruise inventory plug into the same admin and storefront surfaces as self-managed cruises.

`@voyant-travel/cruises` is the cruise vertical for OTA, tour-operator, and DMC deployments. It models cruise inventory the way the industry actually describes it (sailings, ships, decks, cabin grades, fare codes, occupancy, onboard credit) instead of bending cruises into the per-person, per-departure shape of the products module.

Cruises are an inventory and operations capability inside those scenarios, not a separate cruise-line implementation. The module ships the canonical schema, services, admin and storefront routes, and a booking extension, plus a provider-neutral adapter contract so external cruise inventory can flow through the same surfaces as cruises an operator owns.

The package is strictly opt-in. Deployments that do not sell cruises get no cruise tables, no cruise routes, and no cruise types in their TypeScript surface.

## Key concepts

These terms come from the shared [glossary](/docs/concepts/glossary). Cruises uses them natively rather than inventing synonyms.

* **Cruise catalog**. A cruise is the route template (the voyage, itinerary, or holiday a line publishes). It carries the name, ship, nights, embark and disembark ports, and merchandising copy. The cruise is canonical, module-owned truth for self-managed inventory, and a derived read model for sourced inventory.
* **Sailings**. A sailing is a dated departure of a cruise on a specific ship, with departure and return dates, a sales status (open, on request, wait list, sold out, closed), and its own pricing. It is the equivalent of a [Slot](/docs/concepts/glossary) in the products world, but cruise availability lives per price row rather than per product day.
* **Cabin categories**. A cabin category is the archetype (Veranda Suite, Oceanview) on a ship, with a canonical room type, occupancy bounds, amenities, and the grade codes that map to it. Most operators sell at the category level. Specific cabins (cabin numbers) are an optional second layer for "guarantee a forward-facing balcony" inventory.
* **Itinerary stops**. The day-by-day itinerary lives at two levels: a per-cruise template and per-sailing overrides for sailings that skip a port or reroute. Reading the effective itinerary merges the two. Ports reference shared [Places](/docs/concepts/glossary) (facility rows) plus canonical place ids for faceting.
* **Cruise extensions**. A [Cruise Extension](/docs/concepts/glossary) is a cruise-specific pre or post-cruise hotel or land program. Its offer definition can be reused across many sailings. When the extension is cruise-owned and lifecycle-dependent it travels as an [Extra](/docs/concepts/glossary) under the cruise booking; when it is independently supplied, confirmed, cancelled, taxed, or supported it splits into a sibling [Component Booking](/docs/concepts/glossary) under the same [Trip Envelope](/docs/concepts/glossary).
* **The cruise adapter contract**. External cruises reach the module through a registered `CruiseAdapter`. The adapter is swappable, not baked in. A deployment picks the adapter package at startup; the platform never imports a concrete adapter. This is how [Connect cruises](/docs/connect/connect-cruises) plug in: a thin adapter over the Connect SDK satisfies the same contract a self-managed cruise resolves against.

### Provenance: self-managed and external

Every cruise the operator's customer sees comes from one of two places, side by side in the same admin experience.

* **Self-managed** cruises live in the operator's own database under the full canonical schema, with full admin CRUD. Bookings against them never touch an upstream.
* **External** cruises live in an upstream system reached through an adapter. The local database stores nothing about the cruise itself; reads resolve live through the adapter, and bookings snapshot the upstream state into the local booking row.

The admin list interleaves both with an `External` badge. Bookings, payments, CRM, and finance plumbing is identical across the two.

## What it owns

* The canonical cruise schema: cruises, sailings, ships, decks, cabin categories, specific cabins, prices, price components, itinerary days, per-sailing day overrides, media, inclusions, voyage groups (back-to-back, grand, world, and cruise-tour composites), and an opt-in storefront search index.
* A pricing grid keyed by (sailing, cabin category, occupancy) with fare codes, onboard credit, gratuities, port charges, taxes, and NCF modeled as composable price components.
* `cruisesService` for cruise, sailing, ship, cabin, and itinerary reads and writes, and `pricingService` for quote assembly and lowest-price aggregates.
* Admin routes (`/v1/admin/cruises/*`) that are provenance-aware, and storefront routes (`/v1/public/cruises/*`) that read from the search-index projection.
* A booking extension (`booking_cruise_details`) plus a multi-cabin party-booking path that composes the bookings module's group primitives.
* The `CruiseAdapter` contract, the adapter registry, a memoizing decorator, a `MockCruiseAdapter` for tests, and `assertCruiseAdapterCompatibility` for adapter authors.

What it does **not** own: cruise-line connectors (those live in Connect or an operator's own adapter), sync scheduling and polling, provider authentication, and real-time hold or live-book orchestration beyond the adapter commit boundary.

## Working with it

### Create and price a self-managed cruise

```ts theme={null}
import { cruisesService } from "@voyant-travel/cruises"
import { pricingService } from "@voyant-travel/cruises/service"

const cruise = await cruisesService.createCruise(db, {
  slug: "danube-symphony",
  name: "Danube Symphony",
  cruiseType: "river",
  nights: 7,
})

const sailing = await cruisesService.upsertSailing(db, {
  cruiseId: cruise.id,
  shipId,
  departureDate: "2026-05-12",
  returnDate: "2026-05-19",
  salesStatus: "open",
})

// Bulk-replace the pricing grid for this sailing in one transaction.
await cruisesService.upsertPrices(db, sailing.id, [
  { cabinCategoryId, occupancy: 2, fareCode: "EARLY_BIRD", currency: "EUR", pricePerPerson: "2495.00" },
  { cabinCategoryId, occupancy: 1, fareCode: "EARLY_BIRD", currency: "EUR", pricePerPerson: "3245.00" },
])
```

### Assemble a quote

`assembleQuote` is the single place quote math happens. The booking extension, the storefront, and the admin UI all call it, so there is one source of truth. Quotes return the native currency of the underlying price rows; the module never converts at ingest time.

```ts theme={null}
const quote = await pricingService.assembleQuote(db, {
  sailingId: sailing.id,
  cabinCategoryId,
  occupancy: 2,
  guestCount: 2,
})
// quote: base price, applied price components, total per person,
// total per cabin, and the native currency that was quoted.
```

### Read the effective itinerary

```ts theme={null}
const itinerary = await cruisesService.getEffectiveItinerary(db, sailing.id)
// Merges cruise_days (template) with cruise_sailing_days (per-sailing overrides),
// so skipped ports and reroutes for this departure are reflected.
```

### Book a cabin

A cruise booking is a booking. The extension wraps the bookings module so the quote, the booking row, and the cruise detail snapshot all land in one transaction. The `mode` flag chooses between lead-gen inquiry and a payment-capable reserve, per tenant and per line.

```ts theme={null}
import { bookingCruiseDetailsService } from "@voyant-travel/cruises/booking-extension"

const booking = await bookingCruiseDetailsService.createCruiseBooking(db, {
  sailingId: sailing.id,
  cabinCategoryId,
  occupancy: 2,
  fareCode: "EARLY_BIRD",
  passengers: [
    { firstName: "Ada", lastName: "Byron", travelerCategory: "adult", isPrimary: true },
    { firstName: "George", lastName: "Byron", travelerCategory: "adult" },
  ],
  contact: { personId },
  mode: "reserve",
})
```

For a family booking several cabins on the same sailing as one logical purchase, `createCruisePartyBooking` composes the bookings group primitives: one shared confirmation number, atomic cancellation across cabins, a single deposit and invoice, with each cabin keeping its own travelers and quote snapshot.

### Register an external adapter

Standard modules are not registered by hand. `@voyant-travel/cruises` 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).

Once registered, admin list and detail routes interleave local cruises (read from the database) with external cruises (read live through the adapter). Booking creation detects provenance from the sailing key, commits upstream through `adapter.createBooking()` first, then snapshots the result. The caller does not branch; the helper does.

## Links to other modules and Connect

* **bookings**. The `booking_cruise_details` extension and the `booking_group_cruise_details` party extension plug into the bookings state machine, PII handling, and financial linkage.
* **pricing**. Dated promotions and onboard-credit overlays reuse `pricing.price_catalogs` and `pricing.price_schedules` via soft foreign keys on cruise price rows. There is no cruise-local promotions table.
* **finance**. Bookings become invoices through the finance module; cruise line items live in the booking quote snapshot, not in finance.
* **suppliers** and **Places**. Cruise lines are supplier rows linked from `cruises.lineSupplierId`; ports are facility (Place) rows referenced by soft foreign key.
* **products**. Pre and post-cruise extensions that are reusable sellable products link through a template-level link rather than a cross-package foreign key.
* **Connect**. [Connect cruises](/docs/connect/connect-cruises) is the default source of external cruise inventory. A thin adapter over the [Connect SDK](/docs/connect/connect-sdk) implements the `CruiseAdapter` contract; from the module's point of view it is just another registered adapter.

## React package

`@voyant-travel/cruises-react` is the cruises client tier: headless data hooks and a Zod-validated fetch client at the root and `./hooks`, `./client`, and `./query-keys` subpaths, plus styled UI under `./ui` and `./components/*`. The headless subpaths pull no styling peers; the styled subpaths add `@voyant-travel/ui`.

```tsx theme={null}
import { VoyantCruisesProvider } from "@voyant-travel/cruises-react/provider"
import { useStorefrontCruises } from "@voyant-travel/cruises-react"

function CruiseGrid() {
  const { data, isLoading } = useStorefrontCruises({ cruiseType: "expedition" })
  if (isLoading) return <p>Loading</p>
  return (
    <ul>
      {data?.data.map((cruise) => (
        <li key={cruise.id}>{cruise.name}</li>
      ))}
    </ul>
  )
}

function App() {
  return (
    <VoyantCruisesProvider baseUrl="/api">
      <CruiseGrid />
    </VoyantCruisesProvider>
  )
}
```

Components render English by default. Wrap them in `CruisesUiMessagesProvider` and import only the locales you support (for example `./i18n/en` and `./i18n/ro`) to localize.

## Next steps

<CardGroup cols={2}>
  <Card title="Connect cruises" icon="ship" href="/docs/connect/connect-cruises">
    How sourced cruise inventory reaches the adapter contract.
  </Card>

  <Card title="Connect adapter" icon="plug" href="/docs/connect/connect-adapter">
    The contract external inventory implements to plug in.
  </Card>

  <Card title="Charters" icon="anchor" href="/docs/platform/modules/charters">
    The sister module for per-suite and whole-yacht voyages.
  </Card>

  <Card title="Glossary" icon="book-open" href="/docs/concepts/glossary">
    The cruise vocabulary in the shared domain language.
  </Card>
</CardGroup>
