VRPlatformVRPlatform
Build a Product UI

Recurring Fees

Build fee definition, listing-period, calculation, and preview flows

Mental Model

Recurring fees calculate financial lines from reservation data. A reusable fee definition describes the formula and postings; a listing period activates that definition for one listing and date range.

Resources and Lifecycle

Definitions cover management, additional, booking-channel, cleaning, and merchant fees. They can be active or archived. Listing periods preserve time-based rate changes without rewriting historical configuration.

Changing either resource can refresh fee journals for multiple reservations. Historical books and statement locks still apply to affected postings.

A definition with no active listing period matches no new reservations. A definition change can still refresh reservations whose existing journal entries already reference that fee so obsolete postings are removed safely.

Definition create and update queue one durable planning effect before expanding high-cardinality reservation refreshes. A successful mutation means that work is durably queued, not that every reservation journal has already settled. Journal-relevant updates return a reservation-journal-refresh operation. Poll that operation, then re-read the fee and affected resources. Their journalStatus remains authoritative: current means every known scoped change is applied; recalculating, failed, and stale require visible UI state instead of assuming the stored journals are current.

Read Model

Render returned includedFinancials, linkedAccounts, resolved rate, conditions, taxes, recognition, fee lines, locks, and issues. Do not parse a formula to reconstruct its calculated display.

Use journalStatus for journal-derived freshness and operations to discover the work that can change it. A fee can be stale without an active operation, so do not derive freshness from operation presence.

A fee with no known journal invalidations is current, with null freshness timestamps. It does not need a refresh merely to populate a timestamp. This status describes tracked changes; it does not independently validate every historical fee amount.

Calculation Order

When one fee formula reads an account produced by another fee, the producing fee is calculated first. That dependency applies only when both fees have overlapping listing periods for the same listing. Listing periods are half-open: startAt is included and endAt is excluded, so one period ending on a date does not overlap another period starting on that date.

This ordering is shared by journal refresh and full-journal calculation. A refresh must therefore produce the same amount as Preview after all dependent fees settle. Existing stored journals are not rewritten by a code deployment; refresh affected, unlocked reservations after a calculation-order correction.

Creating, changing, or removing a recurring-fee adjustment also recalculates fees whose formulas depend on the affected posting accounts. The targeted refresh follows those dependencies transitively, so a cleaning-fee adjustment can update Management Commission without rebuilding unrelated fee history. That recalculation applies only while the affected fee group is fully editable. If any existing row for a dependent fee is statement-attached, the adjustment still posts its own balanced correction, but every historical row for that locked fee remains unchanged. Books-close and statement locks still apply to every changed posting.

An ordinary reservation adjustment with a custom posting date is narrower. It creates its own fee effect on that date without normalizing a recurring fee's historical base rows. Statement-attached fee history therefore remains unchanged even when a current rebuild would round that historical amount differently. Its posting status is independent of an inactive base reservation; ordinary reservation and fee postings inherit the reservation's GL status.

Account-total conditions are evaluated against each calculation group. A posting-dated adjustment must satisfy the fee's conditions using that adjustment's grouped entries before its formula runs. For example, a negative adjustment cannot create a fee whose source-account condition is greater than zero, even when the reservation's historical account total was positive.

Fee formulas use active journal rows. Inactive reference-account rows remain available only when their inactive reason is nonPostingAccount, because those accounts intentionally carry formula inputs. Every other inactive reason is excluded, including GL start boundaries, inactive transactions, inactive listings, inactive accounts, currency mismatches, and cancelled reservation lines. Historical-ledger rows and reservation rows without an effective ownership period are also excluded under their explicit reasons. If a row used by the formula or an account-total condition is inactive without a reason, preview fails and journal refresh leaves the existing fee postings unchanged with an integrity issue. Preview and persisted fee calculation apply the same rule. Formula variables are parsed exactly, so an account ID cannot match a different account merely because one ID contains the other.

When a percentage fee uses pro-rata recognition, its generated schedule covers the complete window from the first through the last included revenue-recognition source date. This keeps the fee postings on the same effective date range as the formula after a GL start boundary excludes source rows. Flat pro-rata fees keep the full reservation schedule.

Each generated pro-rata or deferred posting uses the ownership period effective on its own posting date. A date with no ownership period remains unassigned so ownership validation can report the configuration gap; it is never assigned to a period from another date. If one inactive leg would otherwise leave a posting group unbalanced, companion legs use the explicit group-propagation reason while the source leg retains its own reason.

Formula results use banker rounding at the cent boundary. Exact half-cent results round to the nearest even cent, including when floating-point evaluation lands immediately beside the mathematical half-cent value. Preview and persisted fee postings use the same rounding boundary.

Decision Table

Fee Definitions

Field or contextRule
TypemanagementFee, additionalFee, bookingChannelFee, cleaningFee, or merchantFee
Flat ratedefaultRate is integer cents
Percentage ratedefaultRate is basis points; 100000 is 100%
Partial definition updateOmitted fields preserve their stored values
PostingActive debit/credit accounts plus `owners
TaxTax rate plus included/excluded behavior
Management-fee taxPayable account splits tax; missing debit override uses fee debit
RecognitionOptional fee-specific override
ConditionsBooking channel, reservation status, and account totals
Period rateOptional override; a value equal to the definition default remains inherited
Period overlapChoose one documented conflict strategy

Account, category, party, fee type, formula, and tax validity must be evaluated as one configuration. An active fee requires both direct posting accounts, and all direct, tax, and formula-input accounts must be active and owned by the same team. Every account-total condition must also reference an account owned by the same team. A missing account reference returns 400 BAD_REQUEST before the fee is written. An Account used anywhere by an active fee cannot be deleted or archived. An archived fee can retain historical account references, but those accounts must be active before the definition is reactivated. Every posting-affecting change to a tax rate used by an active fee revalidates its effective accounts, even when the account IDs do not change.

For Management Commission, the payable tax account controls whether tax is a separate posting. This applies to both included and excluded tax behavior. If the tax rate has no debit-side override, the tax debit uses the fee definition's debit account. Other recurring-fee types continue to require both tax account overrides for a separate tax posting; otherwise their tax remains embedded.

Listing Periods

Listing-period strategies are error, updateExisting, adjustInsertingItem, and closeExistingPeriods. Explain that a strategy can change stored neighboring periods, not just the visible row.

Listing-period reads return the effective rate. When a create or update sends that same value as the current definition default, the API stores inheritance instead of a redundant override. A later definition-default change carries those inherited periods forward. A period rate different from the definition default remains an explicit override and does not change with the default.

startAt and endAt must be parseable date strings. The API rejects invalid values with a 400 validation response before reading or writing fee periods. Two overlapping open-ended periods cannot both omit boundary dates because no strategy can determine where one period ends and the other begins. The API returns a 400 user error instead of inventing a boundary.

Editability

Locked periods cannot be deleted normally. onLocked=archive closes the open portion while preserving locked journal history. Definition changes can also be rejected when affected reservation fee entries are in closed or statement-attached history.

A fee definition can be hard-deleted only before it has generated journal or payment history. Once related records exist, the default delete returns a controlled response with suggestedOnLocked=archive. Retry with onLocked=archive to deactivate the definition and persist one planning effect that expands into lock-aware reservation refreshes. The API response does not wait for every affected reservation journal to settle.

Books closing protects every journal entry a listing-period delete would remove, including inactive history. Making an entry inactive does not make a closed posting removable.

Preview and Preflight

Use fee preview to answer what one proposed definition would calculate for one reservation. It returns projected lines with account, party, amount, and fee metadata; projection IDs are not persisted.

Use mutation dry run to validate whether a definition or listing period can be created, changed, or removed. A successful calculation preview does not prove that persistence is currently writable.

Re-run preview after rate, formula, account, party, tax, recognition, filter, listing-period, or reservation changes.

Mutation Recipe

  1. Load eligible accounts, tax rates, channels, and existing periods.
  2. Select fee type and rate model.
  3. Resolve formula inputs and both posting sides.
  4. Add conditions and recognition only when needed.
  5. Preview against a representative reservation.
  6. Configure the listing period and explain its conflict strategy.
  7. Dry-run the complete definition or period mutation.
  8. Confirm calculated and persistence effects separately.
  9. Apply and re-read affected fee and period resources.

Failure and Recovery

stale means known work remains unapplied without an active recalculation. recalculating means work is queued or running. Re-read the fee after its operation finishes. failed includes exhausted recalculations and changes that could not apply because journals were locked; it does not by itself mean the fee formula is wrong.

Inspect the linked operation and affected reservations before retrying. Fix reported mapping or fee-configuration errors, then request a full journal refresh for the affected editable reservations. Books-close and statement locks still apply. Do not unlock published history merely to clear a fee badge; review whether the intended change needs an adjustment in an open period. A successful full refresh covers earlier changes for that reservation. A partial fee refresh does not establish that every older change has been applied.

Keep formula and account errors beside the posting configuration. When periods overlap, preserve the proposed range while the user chooses a strategy; a lock should surface the affected history and the supported archive outcome. If a posting account is archived, repoint or deactivate the fee before archiving the account. Deactivating a fee cannot inactivate one of its generated accounts while another active fee still uses that account. Repoint or deactivate the dependent fee first, then retry and re-preview.

Common Recipes

Percentage management fee

Choose managementFee, percentage basis points, the included financials, owner/manager posting sides, tax and recognition behavior, then preview it against a reservation before saving.

This example calculates a 15% fee from one linked revenue account:

POST /recurring-fees/preview
Content-Type: application/json

{
  "name": "Management fee",
  "type": "managementFee",
  "rateType": "percentage",
  "defaultRate": 15000,
  "formula": "\"22222222-2222-4222-8222-222222222222\" * \"rate\"",
  "creditAccountId": "33333333-3333-4333-8333-333333333333",
  "creditParty": "manager",
  "debitAccountId": "44444444-4444-4444-8444-444444444444",
  "debitParty": "owners",
  "reservationId": "88888888-8888-4888-8888-888888888888"
}

Change a fee rate next month

Keep the definition stable and create a new listing period with the new rate and non-overlapping boundary. Dry-run the selected conflict strategy.

POST /recurring-fees/listing-periods?dryRun=true
Content-Type: application/json

{
  "recurringFeeId": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
  "listingId": "11111111-1111-4111-8111-111111111111",
  "startAt": "2026-08-01",
  "endAt": null,
  "rate": 18000,
  "onConflict": "closeExistingPeriods"
}

API Reference

On this page