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

# Dynamic packaging

> Assemble an itinerary from live, independently-sourced supply: the source-adapter fan-out, the cross-vertical availability candidate, and the trip requirements that compose those candidates into one bookable envelope.

Dynamic packaging answers a hard question: how do you build one itinerary out of supply you do not own, cannot pre-load, and that reprices on every request? A customer wants a hotel for these dates, a flight from this airport, and a transfer to match. None of it is a fixed dated departure sitting in your database. Each part lives upstream, behind a different provider, with its own price that is only true for the next few minutes.

The answer is a live search and a composition layer. You fan a search out across every provider that can serve a need, merge what comes back into one ranked list, let an operator or an agent pick from it, and pin each pick into a trip. The composition stays resumable: candidates are persisted, re-validated, and only committed at reserve time.

This page is the conceptual map. It names the primitives precisely, because the words "offer" and "package" are overloaded across the codebase, and explains what is wired today versus what a deployment still has to inject.

## Offers are not one thing

The word "offer" is reused for several different shapes. They are genuinely different primitives, and treating them as one is the most common mistake in this area. Keep them apart.

| Concept                 | What it is                                                                                                                                                                                                             | Lives in                                                                                                        |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `CatalogSlot`           | A dated inventory unit of scheduled supply: a seat in a fixed departure drawn from a finite allotment. It carries `startsAt`, `status`, and `remainingPax`. Departures-first, not search-composed.                     | `@voyant-travel/catalog-react/schemas-catalog-offers`                                                           |
| `PackageOffer`          | One live, sourced-package offer (for example a TUI sun package). The card and summary are cached in the catalog index, but the bookable dated offers are compiled **live per request** by fanning out to the provider. | `@voyant-travel/catalog/offers/operator-routes`, typed in `@voyant-travel/catalog-react/schemas-catalog-offers` |
| `AvailabilityCandidate` | The normalized, cross-vertical result of a live search. The unit dynamic packaging ranks and attaches to a trip.                                                                                                       | `@voyant-travel/catalog-contracts/adapter/availability-search`                                                  |
| `QuoteResponseV1`       | A live, per-draft quote from the booking engine for an already-selected entity.                                                                                                                                        | `@voyant-travel/catalog-contracts/booking-engine/engine-contracts`                                              |
| `TripSnapshotProposal`  | The frozen, immutable priced itinerary a Quote freezes for acceptance and provenance.                                                                                                                                  | `@voyant-travel/trips`                                                                                          |

The word "package" is overloaded too, and it means one of two completely different primitives:

* A **sourced package** is a single product from a single provider. A TUI sun package is one accommodation plus its flights, priced and booked as one unit upstream. The `PackageOffer` routes serve this.
* A **composed FIT trip** is a [Trip Envelope](/docs/platform/modules/trips) made of independently-sourced components: a hotel from one provider, a flight from another, a transfer from a third, each committed on its own terms.

These are not the same thing. A sourced package is one provider's offer. A composed FIT trip is many providers' offers assembled by Voyant. Dynamic packaging is mostly about the second, but it leans on the same live-search machinery the first uses.

<Note>
  `AvailabilityCandidate.candidateRef` is stable **within one search response** only. It is not replay-safe for booking. The composer persists candidates as resumable trip state and re-resolves the candidate's `selection` against the provider before reserve. Treat `candidateRef` as a correlation handle, never a booking token.
</Note>

## Providers and the live fan-out

A provider here is a `SourceAdapter` (`@voyant-travel/catalog-contracts/adapter/source-adapter`), one instance per upstream connection. No implementer is privileged; Voyant Connect is one adapter among many, and a wholesaler, cruise line, or operator can build their own against the same contract.

Each adapter declares an `AdapterCapabilities` record. Two fields gate live search: `verticals` (the verticals this adapter feeds, for example `["accommodations"]`) and the optional `supportsAvailabilitySearch` flag that gates the `searchAvailability` method. Adapters are keyed by `connection_id` in the `SourceAdapterRegistry` (`createSourceAdapterRegistry`, `@voyant-travel/catalog/booking-engine/registry`), so two connections of the same kind get two distinct entries with their own credentials.

`fanOutAvailabilitySearch` (`@voyant-travel/catalog/search/availability-fan-out`) is the cross-vertical search primitive. Given one `AvailabilitySearchRequest` scoped to a single vertical, it:

* **Gates by vertical.** An adapter whose `verticals` do not include the requested vertical is skipped, not queried with criteria it does not own.
* **Gates by capability.** An adapter without `supportsAvailabilitySearch` (or without the method) is skipped and flagged `capability_missing`.
* **Enforces a per-connection hard timeout** (default 5000ms). A slow source is reported `timeout`; the rest still return on time.
* **Merges and ranks by price** across every responding source into one `AvailabilityCandidate` list.
* **Never lets partial failure tank the search.** Each source's outcome lands in `AvailabilityConnectionResult[]`, with a status of `ok`, `partial`, `empty`, `unsupported`, `timeout`, `error`, `capability_missing`, or `vertical_skipped`.

Owned inventory is a first-class search source, not a faked external provider. An `OwnedAvailabilitySearchHandler` registered in the `OwnedAvailabilitySearchHandlerRegistry` (`@voyant-travel/catalog/search/owned-search-handler`) claims exactly one vertical (its `entityModule`) and is treated by the fan-out exactly like a sourced adapter. Owned and sourced supply land in the **same** ranked list. Being registered is an owned handler's capability declaration; unlike a sourced adapter, it is always search-capable.

Flights pre-date this generic fan-out and keep their own connector contract and search. `fanOutFlightSearch` (`@voyant-travel/flights`) produces `MergedFlightOffer` results, and `mergedFlightOfferToCandidate` (`@voyant-travel/flights/orchestration/availability-bridge`) bridges those into the same `AvailabilityCandidate` shape so flights rank alongside everything else. This is a result-shape mapping, not proof that flights satisfy the generic source-adapter contract.

The `AvailabilityCandidate` carries a public-safe `price` for ranking and display, an `entity_module` / `entity_id` pair, a vertical-shaped `selection` (what is needed to re-resolve and pin the exact candidate at reserve), and an optional `source` (`{ kind: "sourced", connectionId }` or `{ kind: "owned", module }`) so a pick routes back to the right origin. Internal economics (net cost, margin, supplier ref) live under `providerData`, an opaque round-trip that is never serialized into a public DTO.

The design is captured in ADR-0006 and the dynamic-packaging RFC.

## Composing a package

A composed FIT trip is built in the [trips](/docs/platform/modules/trips) module, which adds three primitives on top of the deterministic envelope:

* `tripRequirements` is an **unresolved need**: a `vertical` plus `criteria` plus a `criteriaVersion`. Its status walks `open` to `sourcing` to `candidates_ready` to `selected`, or to `no_availability` when a search returns nothing.
* `tripCandidates` are persisted, ranked `AvailabilityCandidate` rows under a requirement, each with a TTL. Status is `ranked`, `selected`, `expired`, or `discarded`.
* `tripComponents` are the independently-committed parts of the envelope. A pinned component carries its `entityModule`, `sourceConnectionId`, and the candidate's `selection` and `candidateRef` in metadata.

The flow lives in `service-requirements.ts`:

1. `addRequirement` records the unresolved need on the envelope.
2. `sourceRequirementCandidates(db, input, deps)` runs the fan-out and persists the ranked candidates. The fan-out is **injected** through `deps.search`. Trips never imports a catalog adapter; the deployment wires the search and hands it in, keeping trips deterministic and provider-agnostic.
3. `selectCandidate` pins the chosen candidate into a draft `catalog_booking` component. It enforces selected-uniqueness: picking one demotes any previously-selected candidate for that requirement.
4. `reshopRequirement` and `reshopTrip` clear a prior selection and re-source candidates when prices or availability go stale.
5. `expireStaleTripCandidates` is a TTL reaper, intended to be driven by a deployment cron, that marks elapsed `ranked` candidates `expired`.

The draft component a candidate pins into is intentionally unpriced. `priceTrip` re-resolves the selection (because `candidateRef` is not replay-safe), and `reserveTrip` re-validates before any supplier dispatch. Reserve calls `assertEnvelopeRequirementsSatisfied` as a gate: every required requirement must be `selected` before the envelope can reserve.

The admin routes are in `@voyant-travel/trips/routes` (relative paths under the trips mount):

* `POST /:envelopeId/requirements` and `GET /:envelopeId/requirements`
* `POST /requirements/:requirementId/candidates` (source candidates)
* `POST /requirements/:requirementId/select`
* `POST /requirements/:requirementId/reshop`
* `POST /:envelopeId/reshop`

## What is wired today

Be honest about the seam between the contract and a running deployment.

* **The generic fan-out is not wired into the operator application.** The trip requirement-sourcing routes (`candidates`, both `reshop` paths) resolve `sourceCandidatesDeps`, and when no deployment has injected the fan-out search dependency they return **501**. There is no turnkey operator surface for generic cross-vertical dynamic packaging yet. The primitives, the contract, and the service are in place; the wiring is the deployment's job.
* **The live `PackageOffer` routes are wired.** The operator application mounts `createCatalogOffersAdminRoutes` (`createCatalogOffersAdminRoutesForOperator`), so the sourced-package surface (`package-offers`, `package-search`, `package-detail`, `cruise-price`, and friends under `/v1/admin/catalog`) is live. That surface is the working, single-provider half of "package": cards cached in the index, bookable offers compiled live by fanning out to Voyant Connect.
* **Today's live callers of the generic candidate flow are higher-level AI tooling.** The trips module ships MCP tools for AI-safe trip planning, revision, pricing, and reserve. The end-to-end customer-facing generic packaging surface is not a packaged operator screen yet.
* **`candidateRef` is not replay-safe.** Callers persist candidates and re-resolve `selection` before reserve. Do not book against a `candidateRef`.
* **`providerData` is internal.** Net cost and margin live there and are never serialized into a public DTO.

## Next steps

<CardGroup cols={2}>
  <Card title="Catalog" icon="layer-group" href="/docs/platform/modules/catalog">
    The source-adapter contract, the live availability fan-out, and the sourced-package offer routes.
  </Card>

  <Card title="Trips" icon="route" href="/docs/platform/modules/trips">
    Requirements, candidates, and pinned components: how live supply composes into one envelope.
  </Card>

  <Card title="Proposals" icon="filter" href="/docs/platform/modules/proposals">
    Freezing a composed itinerary into an immutable priced proposal a customer accepts.
  </Card>

  <Card title="Flights" icon="plane" href="/docs/platform/modules/flights">
    The dedicated flight fan-out bridged into the same ranked candidate list.
  </Card>
</CardGroup>
