RentalTideRentalTideDocs
Dashboard

Booking financial writes

Engineering contract for posting booking, walk-up, POS, refund, fee, and balance-payment financial state safely.

Booking financial state is ledger-derived. Treat amount_paid, amount_refunded, and outstanding_balance on inventory_schedules as cache columns, not as the source of truth. New walk-up and order-page money changes must post journal entries first, then let the cache sync derive the booking summary from the ledger.

Source-of-truth modules

ResponsibilitySource
Walk-up and order financial state changespackages/RentalTide-Server/src/services/booking/financialTransaction.ts
Balance-payment endpoint/webhook postingpackages/RentalTide-Server/src/services/booking/balancePaymentPosting.ts
Canonical booking fee mathpackages/RentalTide-Server/src/services/payments/bookingFees.ts
Platform dues accrual and recoverypackages/RentalTide-Server/src/services/platformFeeService.ts
Ledger-derived booking cachepackages/RentalTide-Server/src/routes/accountingRouter/booking-cache-sync
Parent booking DAL guardpackages/RentalTide-Server/src/db/dal/bookings.ts

Golden path

Use bookingFinancialTransaction for walk-up bookings and order-page financial mutations:

TypeScript

The helper runs inside one PostgreSQL transaction. It loads the booking, applies location GL-code overrides, builds the accounting strategy payload, writes journal entries, records tender/payment rows when applicable, syncs the booking cache, and returns the derived financial state. If any step fails, the ledger and cache roll back together.

Do not update amountPaid, amountRefunded, outstandingBalance, or PaymentInfo.RemainingBalance directly for walk-up/order money movement. The parent bookings DAL throws on non-zero direct writes to those fields for walk-up bookings unless the privileged cache-sync token is present.

Event contract

Event typeUse forPosting behavior
rental_obligation_setStarting a walk-up rental with money owed laterClears prior booking entries, posts the unpaid obligation, then syncs cache.
walkup_completeCompleting an in-engine walk-up paymentClears prior entries, optionally appends a price version for the actual sale, posts payment/revenue or deferred entries, then syncs cache.
payment_receivedCollecting money from the order page or a walk-up allocationPosts a collect-remaining event and caps clearing legs to the real open booking obligation.
refund_issuedBooking/order refundsLooks up original entries when needed, posts proportional refund entries, then syncs cache.
rental_obligation_clearCancel/reschedule paths that should remove an open obligationClears prior entries and syncs cache without layering another obligation.
gas_charge_setFuel charges on a bookingClears only prior gas entries (payment_method='gas_charge'), re-posts the fuel obligation or recognized fuel revenue, then syncs cache.
walkup_settle_via_posPOS tab settles one or more walk-up rentalsPosts settlement entries without re-pricing the rental; POS owns the sale.
price_adjustmentPriced booking changes after creationAppends an immutable price_versions row and posts only the ledger delta when the booking already has a ledger position.

Some legacy flows are intentionally outside this contract while they are being migrated: online booking creation, public reschedule, and self-service kiosk paths still have direct-write behavior. Do not add new direct-write paths.

Balance payment idempotency

Balance payments are split out because the browser endpoint and Stripe webhook can both observe the same PaymentIntent.

  • postBalancePayment(rentalId, paymentIntent, { source }) handles one booking.
  • postGroupBalancePayment(orderId, paymentIntent, { source }) handles a merged order/payment link with several booking slices.
  • Both paths use a PostgreSQL advisory lock keyed by the PaymentIntent id and re-check for an existing POS transaction inside the lock. The second caller returns alreadyPosted: true.
  • Group payments allocate each booking's share and split the single Stripe application fee across slices. Do not call the internal slice function directly; it does not own the PaymentIntent-level idempotency gate.

Callers currently include:

  • routes/publicRouter.ts endpoint confirmations for balance payments.
  • routes/hooks/hooksRouter.ts Stripe payment_intent.succeeded webhook.

Fee model

Use computeBookingFees, not inline fee math.

TypeScript

Constraints:

  • Percentage fees are based on the booking grand total and prorated by paymentAmount / bookingTotal.
  • The flat interchange base is charged in full on each card transaction.
  • Platform fee is owed on every booking. If it cannot be collected from a card charge, platformFeeService accrues it to the location's outstanding dues.
  • Overdue-dues recovery is capped at 10% of the booking total when callers pass bookingTotalCents; older callers that omit it fall back to the payment amount cap.
  • Gift-card purchases are interchange-only; booking platform fee rules should not be copied into gift-card purchase code.

Troubleshooting ledger/cache drift

  1. Read journal entries for the booking_id first. The cache should be treated as a symptom, not the cause.
  2. Use BookingLedgerService.getBookingFinancialState(rentalId) to compare the ledger-derived state with inventory_schedules.amount_paid, amount_refunded, and outstanding_balance.
  3. If journal entries are correct but the cache is stale, run the sanctioned cache sync path. Do not hand-write the three cache columns.
  4. If a payment might have been seen by both the endpoint and webhook, check the POS transaction by paymentIntentId before posting anything else.
  5. For gas edits, verify prior gas entries were removed by payment_method='gas_charge' instead of deleting the booking's base entries.
  6. For price edits, verify the price_versions row and the journal transaction id agree. A version-only edit against a booking with no base ledger position is expected; a delta entry without a base position is not.

Safe repair pattern

Repair scripts should default to dry-run and follow the same order as production code:

  1. Identify the ledger defect and the exact booking population.
  2. Post or reverse journal entries through the accounting services or bookingFinancialTransaction where possible.
  3. Sync the booking cache from the ledger.
  4. Verify balanced journal entries and expected paid/refunded/outstanding values.
  5. Commit only durable scripts/tests/docs; remove temporary instrumentation.

For JSONB reads in raw SQL, remember that the PostgreSQL client returns JSONB as strings in this repo. Parse before inspecting nested values.

Was this page helpful?
Need help? Contact Support.See what’s new. Check out changelog.Questions? Book a video chat.
Ask AI
Responses are generated using AI and may contain mistakes.
Ask questions about RentalTide and get help with your integration.