Skip to main content
The commerce module (@voyant-travel/commerce) owns the narrow commercial decision: given a Catalog Item or vertical item, a date, a party, a market, a channel, a buyer, and a currency, can it be bought, and at what price? It is the layer that turns catalog (what exists) into something buyable (what this customer can purchase right now). It owns the quote-time markets, pricing, promotions, and sellability runtime sources behind a single root call. Commerce is deliberately thin and adapter-driven. It does not depend on Product or inventory schemas; operated inventory and every sourced vertical enter commerce the same way, through an item reference resolved by a registered price-availability adapter. The decision is the resolved answer to Sellability: it combines availability, pricing, allotments, and policies into one verdict, and it does so without side effects.

Key concepts

Commercial decision
root output
The result of evaluateCommercialDecision. It carries status and buyable (buyable, unbuyable, or error), a canonical reason code, full pricing, fx, promotions, availability, sellability, rule traces, adapter/source handles, and validFrom / validUntil bounds.
Cost
our outflow
The amount the operator pays a Supplier: the input to margin. Distinct from Rate and Price even when the numbers are equal. See Cost.
Rate
supplier tariff
A Supplier’s per-unit tariff: per_person, per_night, per_vehicle, or flat. The pricing input, not the customer-facing number. See Rate.
Price
customer sell amount
The customer-facing sell amount. Commerce returns it in minor units with components, tax and fee facts, and source price facts. See Price.
Market
region anchor
A geographic and economic region (for example “EU” or “US East Coast”) that anchors currency and pricing. The decision input carries the requested market and the requested sell currency separately; FX resolves one to the other. See Market.
FX rate set
exchange snapshot
A timestamped snapshot of exchange rates used to resolve prices in a non-native currency. The decision’s fx block records the requested currency, source currency, rate, rate set, and provider handle when a conversion was applied. See FX Rate Set.
Promotion
discount rule
A discount applied at decision time. The promotions block reports applied and rejected promotions, the requested codes, the total discount, and the stacking facts that explain why each promotion did or did not apply.
Price-availability adapter
pluggable source
The unit of extensibility. An adapter declares id, kind, supports(input), and evaluate(input, context). Inventory is just one adapter kind; cruise, accommodation, and sourced verticals each register their own. The evaluator selects the adapter that supports the item and delegates pricing and availability to it.

What it owns

  • The commercial decision Interface: createCommercialDecisionEvaluator, evaluateCommercialDecision, and the decision, trace, pricing, promotion, FX, and snapshot types, all exposed from the package root.
  • Markets and FX: the market definitions and rate sets that anchor currency and resolve cross-currency pricing.
  • Pricing: the rule engine that assembles components, taxes, fees, and surcharges into a sell price from the selected adapter’s source facts.
  • Promotions: the promotion catalog and the stacking and eligibility logic, with a dedicated admin surface.
  • Sellability: the runtime that folds availability and policy facts from the adapter into the buyable verdict.
  • The adapter registry: createCommerceAdapterRegistry and the boot-time registration of price-availability adapters.
  • Snapshot recording: recordCommercialSnapshot, the explicit persistence command for decision evidence.
Commerce does not own catalog content, inventory truth, bookings, or finance documents. Crucially, evaluateCommercialDecision is side-effect-free from commerce’s perspective: it may call adapters that read operational or source state, but it must never create offers, orders, reservations, holds, snapshots, ledgers, or audit records. Persisting evidence is a separate, explicit step.

Working with it

You build an evaluator at boot, register the adapters your deployment uses, and call the root decision for each priced surface.
import { createCommercialDecisionEvaluator } from "@voyant-travel/commerce";

const evaluator = createCommercialDecisionEvaluator();

// Inventory is just another adapter kind; commerce never imports its schema.
evaluator.registerPriceAvailabilityAdapter(operatedInventoryAdapter);
evaluator.registerPriceAvailabilityAdapter(cruiseSourceAdapter);
A decision answers the buyable question for one item, date, and party. Pass an idempotencyKey to replay the same decision deterministically.
const decision = await evaluator.evaluateCommercialDecision({
  item: { module: "products", id: "prod_aswan_felucca" },
  date: "2026-09-14",
  party: { adults: 2, children: 1 },
  market: "EU",
  currency: "RON",
  channel: "direct",
  buyer: { personId: "pers_henderson" },
  promotionCodes: ["EARLYBIRD"],
  idempotencyKey: "quote:abc123",
});

if (decision.buyable === "buyable") {
  // decision.pricing carries minor-unit totals, components, tax/fee facts.
  // decision.fx records the RON conversion and the rate set used.
  // decision.traces explains every applied, skipped, and blocked rule.
}
Because evaluation never writes, persisting the decision as evidence is a separate command that the snapshot repository owns, including its own idempotency enforcement.
import { recordCommercialSnapshot } from "@voyant-travel/commerce";

await recordCommercialSnapshot(decision, { kind: "quote", id: "quot_123" }, snapshotRepository);
Templates declare one commerce runtime entry and expand it; the route prefixes stay stable.
import { createCommerceHonoModules } from "@voyant-travel/commerce";

const modules = createCommerceHonoModules();
// Mounts /v1/pricing, /v1/public/pricing, /v1/markets, /v1/sellability, /v1/admin/promotions
The decision distinguishes error modes precisely. An item with no registered adapter returns an unbuyable decision with unsupported_item; duplicate adapter ids throw at registration; an adapter that fails at evaluation returns an error decision with adapter_failed and an error trace; a buyable decision that lacks pricing facts is rejected as adapter_invalid_result. A buyable decision must always carry real pricing.
  • catalog supplies the item reference. Commerce reads the catalog plane to know what is being priced; for sourced items, the catalog source adapter’s liveResolve feeds the price-availability adapter.
  • inventory registers the operated price-availability adapter, reporting Product pricing and Slot availability into the decision.
  • distribution supplies channel and commission context. A decision is scoped to a Channel, and what the channel earns (its commission rule) is reconciled and settled in distribution.
  • bookings is where a decision becomes a commitment. The booking captures the commercial snapshot evidence alongside its own per-line transactional state.
  • finance consumes the priced components, taxes, and fees when it issues invoices.

React package

The @voyant-travel/commerce-react family owns the reusable commercial UI under owner-path subpaths: @voyant-travel/commerce-react/markets, @voyant-travel/commerce-react/pricing, @voyant-travel/commerce-react/promotions (with an /admin surface), and @voyant-travel/commerce-react/sellability. Prefer these for any commerce-owned admin or storefront wiring.

Next steps

Catalog

The sellable plane commerce prices, owned and sourced alike.

Inventory

The operated availability and pricing the commercial decision reads.

Distribution

Channels and commission rules that scope the commercial decision.

Glossary

Cost, Rate, Price, Market, and Sellability defined precisely.