Skip to main content
The legal module owns the operator’s legal and compliance surface in a single package: contracts and policies. Contracts are signed document instances rendered from reusable templates with variable substitution, versioning, number series, and a signing workflow. Policies are scoped, versioned rule sets (cancellation, payment, terms and conditions, guarantee, commission) with structured rule evaluation, scope-based assignment, and acceptance tracking. It ships as @voyant-travel/legal, a headless module with Drizzle schema, validation, services, and Hono routes for both admin and public surfaces.
pnpm add @voyant-travel/legal

Key concepts

These terms come from the glossary. Legal owns the documents and rule sets behind them.
  • Contract. A signed legal document instance bound to a Booking, Quote Version, Program, Product, supplier or channel relationship, or explicit provider reference.
  • Contract Template. A reusable contract form with variable placeholders, rendered per instance.
  • Signature. A record of a Contract being signed (signer, method, IP, timestamp).
  • Policy. A scoped rule set (cancellation, payment, terms and conditions, guarantee, commission); versioned.
  • Policy Version. An immutable snapshot of a Policy’s rules, either published or retired.
  • Policy Acceptance. A recorded confirmation that a Person or Booking accepted a specific Policy Version.
  • PII. Personally Identifiable Information. Reads and writes to PII fields are audit-logged.
Accept, Issue, and Sign are distinct. A customer accepts a Policy Version or a Quote Version; you issue a Contract to produce the artifact; a signer adds a Signature. Accepting required terms is not the same as a fully executed contract.

What it owns

Legal owns two barrels, contracts and policies, each with its own schema, service, validation, and routes.

Contracts

The contract entities (with TypeID prefixes) are contracts (cont), contract templates (ctpl) with variable schemas and optional channel scope, contract template versions (ctpv, immutable snapshots), contract signatures (ctsi), contract number series (ctns) with auto-increment, and contract attachments (ctat) for rendered PDFs and appendices. The contract lifecycle is enforced by the contract service:
draft -> issued -> sent -> signed -> executed
A contract may be voided from any non-void stage. Each transition appends to stageHistory and, when an event bus is configured, emits a deliberately minimal domain event (contract.issued, contract.sent, contract.signed, contract.executed, or contract.voided). Event payloads carry contract ids, relationship ids, stage names, and timestamps only. Rendered bodies, variables, metadata, and signature details stay out of the event payload.

Default storefront templates

A contract template can be marked isDefault: true. At most one default may exist for a given (scope, channelId, language) selector. A default with channelId: null is the global fallback for that scope and language; a channel-specific default wins when callers pass a channelId. Storefronts resolve the active customer-safe template through the default-template routes, which check requested and fallback languages in order, prefer channel-specific defaults, ignore inactive templates, and only fall back to the newest active matching template when no explicit default exists.

Document operations

The contract routes expose stable operations for storefront previews and stored document handling: render-preview routes (by id or by slug) that accept { variables } and return only the rendered text, an attach-document route that uploads a multipart file through the configured documentStorage and persists a contract attachment, and a regenerate-pdf route that replaces the canonical generated document artifact through the configured generator. Public preview routes require the template to be active.

Policies

The policy entities are policies (pol) by kind, policy versions (plvr, immutable snapshots with a publish and retire lifecycle), policy rules (plrl, structured rules per version such as cancellation windows and percentages), policy assignments (plas, scope-based assignment to products, channels, or markets), and policy acceptances (plac, acceptance records per booking, order, or person). A policy is assigned by scope and evaluated structurally, so a Cancellation Policy is an ordered rule set defining refund percentages by cutoff window, not free text. A Policy Acceptance binds a specific, immutable Policy Version to a Person or Booking, which is what makes consent auditable later.

Working with it

Register the module:
import { legalHonoModule } from "@voyant-travel/legal"
import { createApp } from "@voyant-travel/hono"

const app = createApp({
  modules: [legalHonoModule],
})
Render a template preview without persisting anything:
import { contractService } from "@voyant-travel/legal/contracts/service"

const preview = await contractService.renderTemplate(db, {
  templateId: "ctpl_…",
  variables: { customerName: "Henderson Family", tripTitle: "Egypt, 14 days" },
})
// preview.text is the rendered body only.
Issue a contract and advance it through the lifecycle, emitting minimal events:
const contract = await contractService.issue(db, {
  templateId: "ctpl_…",
  bookingId: "bkng_…",
  variables: { totalDue: "€12,400" },
}, { eventBus })

await contractService.markSent(db, { contractId: contract.id }, { eventBus })
// stageHistory records draft -> issued -> sent; contract.sent is emitted.
Publish a policy version and record acceptance against a booking:
import { policyService } from "@voyant-travel/legal/policies/service"

const version = await policyService.publishVersion(db, {
  policyId: "pol_…",
  rules: [
    { cutoffDaysBefore: 30, refundPercent: 100 },
    { cutoffDaysBefore: 14, refundPercent: 50 },
    { cutoffDaysBefore: 0, refundPercent: 0 },
  ],
})

await policyService.recordAcceptance(db, {
  policyVersionId: version.id,
  bookingId: "bkng_…",
  personId: "pers_…",
})
  • Bookings and Quotes. A Contract binds to a Booking, Quote Version, or Program, and a Policy Acceptance binds to a Booking or Person. Legal stays decoupled and references these through links and ids.
  • Distribution. Templates and policies can be scoped to a channel, so storefront default-template resolution and policy assignment respect channel context.
  • Finance. A cancellation Policy’s refund windows inform what finance refunds or credits; legal owns the rule set, finance owns the money. See Finance.
  • Notifications. The booking document bundle sends the latest customer-facing contract attachment alongside invoice and proforma renditions. See Notifications.

React package

@voyant-travel/legal-react provides hooks, a client, query keys, reusable UI, and admin surfaces for contracts and policies.
import { LegalProvider } from "@voyant-travel/legal-react/provider"
It exposes the standard family of subpaths (./hooks, ./client, ./query-keys, ./ui, ./admin, and ./components/*) so an app can drop in the contract editor, signing surfaces, and the policy rule editor without reimplementing the data layer.

Next steps

Finance

Invoices, credit notes, and the refund math behind cancellation policies.

Notifications

Deliver contracts and policy documents over multiple channels.

Modules

The full catalog of domain modules and how they link.

Glossary

The legal and compliance vocabulary, including PII handling.