Salta ai contenuti

0051. Receivables: counterparties, open items, receipts and expected OTA payouts

Questi contenuti non sono ancora disponibili nella tua lingua.

An accountant adds a company to the organization’s directory (name, billing details, tax identifiers as text, payment terms in days, a currency, an optional credit limit and contacts). On a reservation, they link the company folio’s payer to it; closing that folio moves its owed balance to an open item of the company, due after its terms, and the folio closes settled. Reopening the folio takes the balance back exactly. The company pays by bank transfer: the accountant records the receipt and allocates it, in part, to one or several items; what is left stays available as credit. A remainder is written off with a reason by a member who may write off. The aging shows what each counterparty owes by days past due, per currency, and a non-fiscal statement lists the account with CSV. An OTA-collected booking imported from Channex opens an expected payout owed by the OTA, the total less the commission when Channex supplies it; the operator allocates the payout when it arrives. A member of another workspace of the organization sees the same directory but none of these items; another organization sees nothing. Every change of an item is audited and emits a receivable.*@1 event.

The analysis (050-domain.html, “Business accounts and credit”, “Who collects payment”) asks for commercial counterparties within an organization, open items with currency, issuer and due date, and allocations for partial collections or one transfer paying several invoices; and for each OTA-collected reservation, the receivable from the OTA and the expected settlement, distinguishing known from missing values. ADR 0029 left business accounts planned and its company payer as free text. Channex states on BookingRevisions.BookingRevision who collects (payment_collect: property | ota, nullable), how (payment_type) and the OTA commission (ota_commission, an amount, nullable); its documentation says the booking-level commission is supplied for Booking.com and Airbnb.

Counterparties are organization rows, used from active workspaces

Section titled “Counterparties are organization rows, used from active workspaces”

counterparties is an organization table. Its row-level security admits the organizations in the context (app.organization_ids, set only for organization owners and administrators) or the organization of an active workspace in the context. An ordinary member, an API key, an organization token or a client session acting in workspace A therefore reads and writes A’s organization’s directory, and nothing else of the organization: its own organization row, organization tokens and organization Activity stay out of reach, and no principal’s scope gains an organization. An archived workspace reaches no counterparty. The use cases check receivables.read / receivables.manage live with can, so credential ceilings and revoked memberships apply. receivables.read, receivables.manage and receivables.write_off are workspace-scoped and refused to members limited to selected properties: receivables span the workspace’s properties. receivables.transfer, below, acts on one property’s folio.

Workspace tables cannot reference an organization table by a foreign key that does not start with the workspace (catalog rule), so a trigger (require_workspace_counterparty) proves that the counterparty an item, a receipt or a folio payer names belongs to the workspace’s organization. Counterparties are never deleted: archiving stops new items; items already open keep the payer name, terms and due date they froze.

Directory changes are audited with the organization and the workspace they were made from (an ordinary member cannot write an organization-only audit row, and the invoking workspace is the truthful place). They emit no domain event: no workspace’s webhooks announce a shared row. Contacts and billing details never enter audit or event data.

Items, entries and receipts are workspace rows

Section titled “Items, entries and receipts are workspace rows”

receivable_items, receivable_entries and receivable_receipts carry workspace_id and its tenant_isolation, the restrictive workspace_active and property policies (created in the migration), composite foreign keys starting with the workspace and ON DELETE CASCADE from their reservation or property. Another workspace of the same organization sees none of them, and the exposure a credit-limit warning shows is what the counterparty owes this workspace.

An item is immutable; what it owes is the signed sum of its append-only entries:

Entry Sign Rules
debit + (an OTA amendment may be −) Opens the item; an OTA revision amends it. A folio item has exactly one.
allocation − Applies a receipt of the same counterparty and currency, up to what the item owes and the receipt still has.
write_off − A remainder given up, with a reason and receivables.write_off. Never a receipt, a refund or a revenue change.
release − of its entry Undoes one allocation or write-off exactly, once.
cancellation − all debits Closes a folio item whose folio reopened; only while nothing is applied.

Status: cancelled with a cancellation, else open (> 0), settled (0) or overpaid (< 0, an OTA amendment below what is applied: release an allocation). A receipt is money a counterparty paid one workspace, in one currency; its available credit is its amount less live allocations; it changes once, when voided, and only while nothing of it is allocated. A counterparty’s receipt never posts on a folio.

Concurrency: use cases take advisory locks in one order (the property’s writer gate for cash, then the receipt, then items in identifier order) and re-read before writing; a trigger (validate_receivable_entry) takes the same locks and refuses, independently, an allocation beyond the item or the receipt, another counterparty’s receipt, a voided receipt, a second release, an entry on a cancelled item and a cancellation of an applied item. A real PostgreSQL test races two allocations of the last 60 of a receipt: one wins, the other answers receivable.exceeds_receipt. Requests carry a caller posting_key: the same request retries without a second entry, another request under the key answers receivable.key_conflict.

Closing a folio to its counterparty, and reopening it, are inverses

Section titled “Closing a folio to its counterparty, and reopening it, are inverses”

A company payer may be linked to a counterparty (folios.payer_counterparty_id, by receivables.manage); the payer’s name and details become the counterparty’s as a snapshot. Existing free-text company payers stay as they are until someone links them. Only folios other than the first can be linked, because only those close by a member. A link names the payer it copied: a rename keeps it, while another payer (even another company) drops it. Dropping a link decides who is billed, so a payer change that drops one needs receivables.manage like an unlink and is audited as folio.counterparty_unlinked.

Closing a linked folio that owes a positive balance (reservationFolios.close) opens an item and books one receivable_transfer movement of minus that balance on the folio, in the same transaction, then closes the folio settled. The movement is its own kind: it counts in the folio’s balance and overflow arithmetic but in none of the charge, payment or refund totals, so revenue and cash reports (which list their kinds) never see it. Example: charges 120, a real payment 20 → transfer 100, folio balance 0, item owes 100, revenue 120, cash 20. A zero balance closes without an item; a credit, or an unlinked owed balance, still answers folio.not_settled. The transfer date is today’s date in the property’s time zone; the due date is that date plus the counterparty’s terms in calendar days, frozen on the item. It is not a fiscal invoice date. The ledger row is dated on the business date, as every member posting is.

Reopening that folio cancels the item and reverses exactly the transfer that closed it, once, whatever the folio holds now. While receipts or write-offs are applied to the item, reopening answers receivable.item_applied and changes nothing: release them first (no money is erased and no write-off is silently undone). Each close cycle opens a new item with new keys. The transfer is not reversible, refundable or transferable from the ledger; triggers refuse a reversal of a transfer without its item’s cancellation, and an item that commits without its transfer.

Direct billing at check-out is a desk action (owner decision, 1 November 2026, replacing the first design, where it needed receivables.manage and blocked routine check-outs). The close that moves a positive balance and the reopen that takes it back need receivables.transfer besides folios.split: a property permission built in for administrators and the front desk (owner decision: in multi-property workspaces the desk is usually limited to its properties), so a member limited to some properties bills only at those; accounting, which manages receivables, does not bill at the desk. A zero-balance close and its reopen keep the folio grants alone. Finance decides whom the desk may bill: only a counterparty whose directory entry allows direct billing (direct_billing_allowed, set with receivables.manage) takes a transfer; any other answers 409 counterparty.direct_billing_not_allowed, shown on the folio’s payer, and a trigger refuses a folio item for it. Taking a balance back does not need the flag: turning it off stops new transfers and never strands an open item. Linking, a payer change that drops a link, the directory, receipts, allocations, voids and write-offs stay under receivables.manage. The transfer and its reversal are audited as the member who made them (folio.receivable_transferred, receivable.item_opened, folio.receivable_transfer_reversed). The last check-out never transfers on its own: it closes open folios as before, and its undo reopens only those, which never hold a transfer; per-room settlement reads a transfer-closed folio’s zero balance. A charge a channel amends after the transfer shows as the folio’s balance again and as the item’s folio_balance; the operator reopens and closes again (after releasing what is applied).

Credit limit (owner decision, 1 November 2026): a warning, never a block. A transfer that takes what the counterparty owes this workspace, in the limit’s currency, past the limit answers 409 receivable.credit_limit and writes nothing; accept_credit_limit=true confirms. A limit in another currency is not compared. The exposure is always the workspace’s whole figure, read through a narrow security-definer function (counterparty_exposure) that answers only that total, only for an active workspace of the context, because a desk member limited to some properties reads no item of the others. A member who reaches every property gets the exposure, the limit and the amount; a member limited to some is told only that the limit is exceeded, with no workspace or organization amount.

Amounts keep their own currency: an item is in its folio’s (or booking’s) currency, never the counterparty’s default; a receipt pays only items in its currency (receivable.currency_mismatch otherwise); cash joins a drawer only in the property’s currency. Aging and statements total per currency and never sum across currencies. Receivables book no conversion.

A cash receipt names the desk’s property and joins its open cash drawer under ADR 0043’s rules (cash_shift.required when the property requires an open drawer); voiding it takes the cash out of the drawer open then. The drawer report lists these receipts apart from folio movements. The database’s guard on a drawer’s expected amount counts them too: its opening float, the cash of its folio movements, plus the receipts taken while it was open, less the receipts voided then.

After the import saves an accepted revision (same transaction, before acknowledgement; stale, quarantined and unowned revisions never get here), settleBooking reads the revision’s settlement facts through a typed, optional contract (amount, payment_collect, payment_type, ota_commission); an archive without them is unknown, never zero. Facts that cannot be true are not settled: a total or commission that is not an amount in the currency or is negative, a commission above the total, or an OTA name that cannot key the directory. The import quarantines such a revision (invalid_settlement) before it saves anything, so the booking, its inventory and any payout it already has stay as they were, and the operator sees why; the database also refuses negative facts and a commission above its total. The payout is the checked difference of two integers with 0 ≤ commission ≤ total, so it can neither grow above the total nor overflow.

  • An item opens only for payment_collect: ota, against the organization’s OTA counterparty holding the revision’s ota_name, exactly as Channex sends it, as its channel: a sanitized or truncated key could merge two OTAs (A/B and A?B) into one debtor. The import adds that counterparty, as the system, when the directory lacks it (terms 0, to be completed by the operator); two imports adding it at once keep one row.
  • The expected payout is the booking total less a known commission. A null commission is unknown: the item expects the total and shows the commission as unknown. "0.00" is a known zero. Booking-level amounts are used, never added to room-level ones.
  • One item per booking and property. A later revision amends it by the difference, once per revision (its key); its facts become the item’s current facts. A cancellation is no proof that nothing is owed: its facts are followed like any revision’s; a switch to property collection brings the expectation to zero, and an applied payout then shows the item overpaid. An item whose booking is cancelled says so (reservation_cancelled), so the operator reviews the expectation instead of letting it age as an ordinary debt.
  • The terms run from the stay’s last departure. The OTA’s payout is matched by hand: there is no ingestion of Booking.com payout reads and no bank-statement reconciliation (owner decision).
  • Bookings in a currency other than the property’s are still quarantined by the projection before this step (ADR 0028); this change does not alter that.
  • Aging now: each item’s whole calendar days past its due date, counted on today’s date in its property’s time zone (never the business date): current until the due date, then 1–30, 31–60, 61–90 and over 90. Only what is owed is aged; receipts’ available credit and overpaid items show as credit apart. Historical aging as of a past date is not offered.
  • Statement of a counterparty with this workspace over [from, to): opening balance, items opened, amended, cancelled or written off and receipts by date, closing balance, per currency; then what is open now and the credit not yet allocated. Allocations only match. Voided receipts are left out. Not a fiscal document. CSV in RFC 4180.

API revision 6.8.0: counterparties.list|create|get|replace|exposure|statement|statementCsv, receivableItems.list|get|writeOff, receivableEntries.release, receivableReceipts.list|create|get|allocate|void, receivables.aging|agingCsv, reservationFolios.linkCounterparty|unlinkCounterparty; reservationFolios.close gains accept_credit_limit and the problems above; reservationFolios.reopen gains receivable.item_applied; FolioMovement.kind gains receivable_transfer with receivable_item_id; ReservationFolio.counterparty_id; CashShiftReport.receipts. Events receivable.item_opened|item_adjusted|item_cancelled|allocated|allocation_released|written_off|write_off_released@1 (object receivable_item) and receivable.receipt_recorded|receipt_voided@1 (object receivable_receipt, with the property of the desk that took cash, or none). Permissions receivables.read, receivables.manage, receivables.write_off, built in for administrators and accounting, and receivables.transfer, built in for administrators and the front desk; Counterparty.direct_billing_allowed and the problem counterparty.direct_billing_not_allowed.

  • Direct billing and OTA collections have an auditable sub-ledger without touching revenue, cash or the folio’s append-only rules.
  • A receipt cannot pay items of another workspace, even of the same organization: allocation across issuing workspaces needs an owner decision and a central workflow (reported limitation).
  • Organization-wide exposure (counterparties.exposure) is shown only to the organization’s owners and administrators in a browser, per workspace and currency; everyone else, integration credentials included, sees their workspace’s. The transfer’s warning compares this workspace’s exposure, read in the closing transaction.
  • Counterparty contacts are personal data on the directory row; the personal-data catalog entries are added when that catalog exists on main.
  • Booking the transfer as a payment (Opera’s “direct bill” method): rejected; it would count as cash received and could be refunded.
  • Reusing the two-folio transfer: rejected; its pair must net to zero across folios.
  • Mutable item balances: rejected; entries keep every change and its actor.
  • Blocking at the credit limit: rejected by the owner for v1.
  • Counterparties per workspace: rejected; the analysis puts them in the organization, and one company books several properties.