@voyant-travel/finance, a headless module with Drizzle schema, validation, services, and Hono routes, plus a checkout collection runtime under the ./checkout subpaths.
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:- A caller-supplied
invoiceNumberalways wins. The server stores it as-is and does not advance a sequence. - If
invoiceNumberis omitted butseriesIdis supplied, the server uses that series. It must be active and must match the requested scope (invoicefor invoices,proformafor proformas). - If both are omitted, the server resolves the active default series for the scope, falling back to the most recently updated active series.
- If no active series exists for the scope, the route returns
409witherror: "no_active_series_for_scope".
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
costAmountCentssnapshots 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.
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 enrichinvoice.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:Links to other modules
- Bookings. An invoice targets a Booking (among other targets), a payment schedule belongs to a Booking, and
booking_payment_schedule.paidlets 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, anduseProductProfitability. @voyant-travel/finance-react/checkout-uiexports<PaymentStep>(a single capability-gated payment selector that every vertical reuses) and<PaymentLinkLandingPage>(the one universal/pay/:sessionIdpage).
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.