Skip to main content
The finance module owns money. Every payment session, capture, refund, and bank-transfer instruction flows through it for persistence, and it stays authoritative on what was actually paid and reconciled. It is the durable record behind the universal payments stack: the payments runtime holds sessions and links, finance holds the payments, invoices, credit notes, and supplier payments that document them. It ships as @voyant-travel/finance, a headless module with Drizzle schema, validation, services, and Hono routes, plus a checkout collection runtime under the ./checkout subpaths.
pnpm add @voyant-travel/finance

Key concepts

These terms come from the glossary. Finance is where they live.
  • Invoice. A billing document issued to a payer; lifecycle draft → sent → partially_paid / paid / overdue / void.
  • Invoice Number Series. A configured numbering sequence (per legal entity, year, and type) that invoices draw from.
  • Credit Note. A reversal or adjustment document referencing an Invoice.
  • Payment. A recorded inbound transfer of money (bank transfer, card, cash, voucher, direct bill).
  • Supplier Payment. A recorded outbound transfer to a Supplier.
  • Payment Schedule. An installment plan attached to a Booking (deposit, installment, balance, hold) with due dates.
  • Guarantee. A security hold (deposit, pre-auth, card-on-file, agency letter, voucher) ensuring eventual payment.
  • Payment Session. An active payment attempt against a target: a Booking, Invoice, Schedule line, Guarantee, Program, or explicit provider reference.
  • Collection Plan. A preview of what will be collected from the customer and when.
Cancel, Void, and Close are different verbs. Cancel is an operational reversal of a Booking. Void is a financial reversal of an Invoice or Payment. If a customer paid and then cancels, you do not void a paid invoice; you issue a Credit Note.

What it owns

Finance owns the documents and the runtime that produce and reconcile them.

Documents and ledger

The core entities (with TypeID prefixes) are invoices and invoice lines (inv, inli), payments (pay), credit notes and credit-note lines (crn, cnli), supplier payments (spay), finance notes (fnot), invoice number series (invs), invoice templates (invt), invoice renditions (invr), tax regimes (txrg), and invoice external refs (iner).

Invoice numbering

Routes that issue invoice-like documents share one precedence rule for document numbers:
  1. A caller-supplied invoiceNumber always wins. The server stores it as-is and does not advance a sequence.
  2. If invoiceNumber is omitted but seriesId is supplied, the server uses that series. It must be active and must match the requested scope (invoice for invoices, proforma for proformas).
  3. If both are omitted, the server resolves the active default series for the scope, falling back to the most recently updated active series.
  4. If no active series exists for the scope, the route returns 409 with error: "no_active_series_for_scope".
Local allocation uses financeService.allocateInvoiceNumber(...), which row-locks the series while advancing currentSequence. Only one active default series is allowed per scope. Series with an externalProvider set do not advance local sequences: they create the local invoice with a PENDING-… placeholder and status: "pending_external_allocation", emit the issued event with externalAllocationRequired: true, and a provider adapter later patches in the provider-owned number via financeService.applyExternalInvoiceAllocation(...).

Checkout collection

Finance owns the checkout collection runtime under ./checkout, ./checkout-routes, and ./checkout-validation. Provider startup is injected through payment starters, bank-transfer details resolve through host wiring, and notification delivery stays behind a dispatcher rather than a direct package dependency. Mounted routes include the collection plan, the initiate-collection flow, a collections bootstrap, and reminder-run listing. When a session linked to a booking payment schedule completes, finance emits booking_payment_schedule.paid after the transaction commits, so booking lifecycles can promote a deposit-first booking from on_hold to confirmed without provider-specific webhook glue.

Supplier-invoice profitability

Voyant historically modeled money owed to the operator (AR) but nothing owed by the operator. The supplier-invoice design closes that gap inside finance as a sibling of the customer-facing invoices model, not by overloading them with a direction column. The conceptual model keeps three layers distinct:
  • Revenue (AR) already exists as customer invoices.
  • Planned cost already exists as the costAmountCents snapshots captured on booking items at quote or booking time.
  • Actual cost is new: supplier invoices the operator records, whose lines are allocated to a departure, product, booking, or (derived) traveler.
From those, the read model computes profit (revenue minus actual cost) and variance (planned minus actual). The unit of profitability is the departure (an availability slot, the departureLinkable); a product’s P&L is the sum of its departures. Per-traveler cost is derived by splitting a departure-level allocation, not stored as a first-class allocation. New services such as getDepartureProfitability and getProductProfitability sit beside getFinanceAggregates, built on explicit service SQL over allocation ids rather than graph traversal. Supplier-invoice currency may differ from the operator base currency, so allocations store a base-currency amount resolved through the existing baseCurrency and fxRateSetId plumbing, with reverse-charge tax handled by the shared tax_regimes.

Invoice FX and renditions

Invoice issuing can enrich invoice.issued events with the operator accounting currency, FX rate, FX commission, and effective provider rate when you configure invoiceFxSettings and an exchange-rate resolver. The default resolver calls the Voyant Data FX pair route through @voyant-travel/data-sdk. Rendered invoice artifacts are bound to an invoice with financeService.bindInvoiceRendition(...), which writes a ready rendition inside a transaction and emits invoice.rendered (metadata only, never document bodies or signed URLs) after commit.

Working with it

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

const app = createApp({
  modules: [financeModule],
})
Configure invoice FX so cross-currency invoices carry provenance:
import {
  createFinanceHonoModule,
  createVoyantDataFxExchangeRateResolver,
} from "@voyant-travel/finance"

createFinanceHonoModule({
  invoiceFxSettings: {
    baseCurrency: "RON",
    fxCommissionBps: 200,
    fxCommissionInvoiceMention: "2% comision curs risc valutar",
  },
  resolveInvoiceExchangeRate: createVoyantDataFxExchangeRateResolver({
    apiKey: process.env.VOYANT_DATA_API_KEY!,
  }),
})
Show the same booking tax line in the create dialog that finalization will persist, by mounting the booking-tax extension:
import { createBookingTaxHonoExtension } from "@voyant-travel/finance/booking-tax"

createApp({
  extensions: [
    createBookingTaxHonoExtension({
      resolveBookingTaxSettings: async (db) => {
        const settings = await getTaxSettings(db)
        return {
          taxPriceMode: settings?.taxPriceMode ?? "inclusive",
          taxPolicyProfileId: settings?.taxPolicyProfileId ?? null,
        }
      },
    }),
  ],
})
If you use the booking-create dialog without mounting the booking-tax route, the client treats a missing preview as “no tax to show” and silently drops tax rows from the dialog summary. Mount the route, or compute tax server-side with computeBookingItemTaxLine from @voyant-travel/finance/booking-tax.
Bind an already-stored invoice artifact as the ready rendition:
await financeService.bindInvoiceRendition(db, invoiceId, artifact, {
  eventBus,
  replaceExisting: true,
})
// Emits invoice.rendered (metadata only) after the transaction commits.
  • Bookings. An invoice targets a Booking (among other targets), a payment schedule belongs to a Booking, and booking_payment_schedule.paid lets booking lifecycles react to collected installments.
  • Operations. Supplier cost allocations reference a departure through the operations departureLinkable. See Operations.
  • Distribution. Supplier identity for AP comes from distribution suppliers; finance references them as indexed text ids with service-layer validation, not cross-module foreign keys.
  • Notifications. Payment-session and invoice sends, plus booking document bundles, are orchestration wrappers in Notifications over the shared notification service, dispatched through finance’s notification seam.
  • Legal. A fully-paid booking can trigger contract and invoice document generation through the notifications document-bundle lifecycle.

React package

@voyant-travel/finance-react provides the data hooks and admin UI, and @voyant-travel/finance-react/checkout-ui ships the universal checkout components.
  • Hooks include usePublicPaymentSession, usePublicBookingPaymentOptions, useInvoices, useInvoiceNumberSeries, useBookingPaymentSchedules, useBookingGuarantees, useDepartureProfitability, and useProductProfitability.
  • @voyant-travel/finance-react/checkout-ui exports <PaymentStep> (a single capability-gated payment selector that every vertical reuses) and <PaymentLinkLandingPage> (the one universal /pay/:sessionId page).
import { PaymentStep } from "@voyant-travel/finance-react/checkout-ui"

<PaymentStep
  request={paymentRequest}
  capabilities={{ chargeSavedCard: false, sendLink: true, bankTransfer: true }}
/>
There is no separate @voyant-travel/payments contract package. The canonical payment contract is finance’s schemas plus the checkout paymentStarters seam. Verticals talk to <PaymentStep> and finance-react hooks; processor plugins (such as @voyant-travel/plugin-netopia) register through paymentStarters and stay invisible to the vertical.

Next steps

Notifications

Send payment links, invoices, and booking documents over email and SMS.

Operations

The departure model that supplier-cost profitability reports against.

Legal

Contracts and policies that travel alongside invoices at checkout.

Glossary

The Cost, Rate, and Price distinction and the money vocabulary.