Night audit and business date
A property’s desk date persists across midnight. A member with night_audits.run (built-in:
administrators and front desk) opens Night audit, resolves arrivals and departures through their reservation links,
acknowledges other warnings, then completes the audit. Completion is allowed after the
next midnight in the property’s time zone. Catch up one day at a time after downtime.
Every property screen displays a persistent lag banner, with an audit link for authorized
operators. Normal check-out and its balance preview return night_audit.required if departure
is after the business date but on or before the local date. Catch up first; this retains every
stayed night’s charge. Shortening an in-house stay cannot release a night before the local date.
Undo check-in after local midnight returns reservation.invalid_state; its correction window
is closed permanently, including after catch-up. Early departure (after the local date)
releases inventory and room charges from the later of the business and local dates, so every
stayed night stays charged. Balance preview uses that release date; the check-out date remains
the business date, preserving same-day undo. Physical block defaults use local dates.
Today displays the business date. Stay eligibility, same-day undo, explicit no-shows and folio
posting date bounds use it. Reports default to its month but still accept explicit future
stay-date ranges: ADR 0031 measures booked revenue, not closed-ledger revenue.
Administrators configure automatic audits on this screen through the separate versioned
/properties/{property_id}/night_audit_settings resource. Defaults are disabled, 03:00 local
time and no automatic no-shows. Enabling no-shows applies the existing frozen policy engine;
unresolved departures still block. Tentative direct reservations accept an optional local
option_expires_on: auditing that date releases them fee-free and lists their identifiers in
released_options. Applied no-show room identifiers appear in no_show_rooms, including on
pending runs after a later blocker. Confirming clears the option date.
The resident Docker worker dispatches every minute. On Vercel ordinary traffic probes for due audits after the response (at most once per warm instance per minute) and wakes the existing bounded pump when due. The daily 03:00 UTC cron is a safety net. With no traffic, execution waits for that trigger. No paid plan is required. Times before noon close the previous business date the next morning; noon or later closes that date in the evening. Settings show the next computed run instant. DST uses compatible disambiguation: missing minutes move forward by the gap and repeated minutes choose the earlier occurrence. Jobs catch up oldest first, at most seven dates per invocation. Failure retries wait fifteen minutes; health writes are data-only and audit/events record first failure and recovery, not repeated attempts.
Automatic configuration acknowledges nonblocking financial/OTA warnings. The health card shows their counts and a link to the last automatic run. The business-date banner area keeps the same notice, even after catch-up and reload, until an operator with audit rights opens that run. Reviewing an older run never dismisses newer warnings. Departures still block. Cash drawer shifts are counted explicitly; allotment release remains planned.
Room obligations retain ADR 0029’s check-in posting rule: all frozen stay nights post at check-in. Audit reconciles missing/changed obligations without duplicating them or undoing human waivers. It does not switch accounts to nightly accrual. An installed TaxCalculator is passed through the same composition. The completed summary reports additional ledger movements, acknowledgements and the next date. It is immutable and reloadable; the last 100 runs are listed. Booked performance reports remain restatable and need no frozen snapshot.
Public contract
Section titled “Public contract”Audit execution operations require night_audits.run on the selected property. This permission includes
reading the financial checklist; grant it only to operators trusted to close the desk.
Built-in administrators/front desk receive it, viewers do not. Workspace and selected-property
RLS apply independently. The resource identity is property plus ISO business date.
GET /properties/{property_id}/night_audits/precheck: current business/local dates and live checklist.GET /properties/{property_id}/night_audits: newest 100 pending/completed runs.POST /properties/{property_id}/night_auditswith{ "date": "2026-10-25" }: create (201) or retrieve (200) that day’s run.GET /properties/{property_id}/night_audits/{date}: durable run/result and completion version tag.POST /properties/{property_id}/night_audits/{date}/complete,If-Match: "1", with{ "acknowledged": ["opaque warning key"] }: atomically close and advance once.
The generated SDK exposes sdk.nightAudits.context/precheck/list/create/get/complete/settings/updateSettings/warnings/reviewWarnings and the generated
query factories. A successful close invalidates the caller’s property/front-desk/reservation
cache. Warning keys are opaque: send the keys of the live nonblocking warnings the operator
acknowledged; changed balances have changed keys and must be reviewed again.
night_audit.blocked requires stay resolution. night_audit.acknowledgement_required requires
a refreshed review. night_audit.too_early requires waiting until the next local date.
night_audit.stale requires opening the current date. A missing run returns
night_audit.not_found. Run versions persist: pending 1, completed 2. Automatic reservation results accumulate on the pending run; its completion advances the version. Read the current ETag before completing. Missing or malformed
If-Match returns 400 request.invalid. A stale version returns
night_audit.version_mismatch (412) with the current ETag. After a lost response,
GET the run: a completed result can be replayed with its current ETag. Ordinary public
properties require business_date; clients cannot edit it.
Persistence and operational limits
Section titled “Persistence and operational limits”A pending run commits first. Completion locks the property row used by lifecycle writers, rechecks live data, plans all financial changes before writes, then commits ledger movements, result, audit/event and date advancement together. A crash rolls the whole completion back; reopen the same pending date. PostgreSQL uniqueness prevents multiple runs for one date and a trigger protects completed results. No report, payment, guest/contact or upstream body is copied into audit logs. Immutable summaries store identifiers, warning keys, categories and balance amounts/currencies. Names are hydrated live in bulk for authorized readers. Room labels name an arrival’s first room and a departure’s last room, so a stay split across rooms (ADR 0042) shows where the guest arrives and where they leave. Balance warnings include in-house stays and stays released/checked out on the audited date, with arrival on or before that date; future deposits and unrelated older departed accounts are excluded.
Authentication expiry, contractual instant deadlines, worker leases/retries, OTA/ARI publication and physical maintenance windows retain wall time. A lagging desk date must not delay them. Today’s deposit projection follows the selected desk date; its frozen contractual due instant does not change. Corrections remain append-only financial reversals after a day is closed.
Migration 0021_night_audit.sql is contract: the date semantics require the new runtime,
and older runtime writes after promotion are unsupported. Drain old writers before
applying it; deploying the new runtime afterward cannot repair a write made with old
semantics. It initializes each property from its current local date; creation supplies
its injected clock. Before the first completion,
resolve the entire historical expected-arrival/overdue-departure backlog in the precheck:
its links include records older than Today’s seven-day overdue window. Configured option/no-show processing does not invent charges for historical tentative bookings.
Automation settings
Section titled “Automation settings”GET /properties/{property_id}/night_audit_settings requires night_audits.run or
night_audits.configure; PUT requires configure and current If-Match. Body:
{ "enabled": true, "local_time": "03:00", "auto_no_show": false }. It returns the
configuration version and health with last attempt, success, failure instants and safe failure
code. Only administrators receive configure by default. Disabled automation preserves manual
execution. Options and configured no-shows commit atomically per reservation with durable
pending-run result IDs; day reconciliation and advancement then commit atomically together.
Migration 0033_audit_automation.sql promotes worker access and nullable cash attribution; drain
old writers before applying it. An older runtime refuses startup after contract promotion.
Recovery is roll-forward only, with no Vercel Instant Rollback to an older runtime.
No-show result responses may include live no_show_details (room ID, parent reservation ID,
code, guest name and room label). These labels are hydrated in bulk within the authorized
property/workspace scope; only room IDs are persisted. The UI links the parent reservation,
with plain room-ID fallback for unavailable historical labels.
Module-only custom roles do not require the general properties.read grant. The read-only
GET /properties/{property_id}/night_audit_context exposes only time_zone, under audit
run/configure permission. GET /properties/{property_id}/cash_shift_context exposes only
currency and time zone under any cash grant. These scoped metadata resources let the web
format the module correctly without reading unrelated property settings. They return no
configuration or financial facts and do not change settings versions.
The live precheck marks an eligible expired-option arrival with
resolves_at_audit: "option_expiry". Its raw arrival blocker stays recorded until the
reservation is released. Operators may complete that audit even when automatic no-shows
are disabled; unrelated unresolved arrivals and departures still require resolution.
GET /properties/{property_id}/night_audit_warnings reads the last completed automatic run’s
counts and review state. The unreviewed=true query selects the latest warned run newer
than the review date, so later clean runs do not erase a notice. POST /properties/{property_id}/night_audits/{date}/review records
opening that completed automatic run. Both require night_audits.run; review is idempotent.
Settings ETags and pending-run ETags guard configuration/completion writes. They are not
representation validators: health, next-run instants, live labels and pending results may
change independently. night_audit.in_progress means another batch remains; retry continues
saved progress, rather than requiring an operator to resolve a stay.
Round 3 operator recovery and scheduling
Section titled “Round 3 operator recovery and scheduling”An automatic no-show exposes additive live policy_decision.undo_available on authorized
reservation reads and writes. The server computes it using the same domain predicate as undo,
under the existing property read snapshot or mutation lock. It is never stored in policy JSON.
The reservation screen offers undo during that window; capacity checks and fee reversals still
run atomically when submitted.
Automation continues reservation batches within the current job’s cancellation budget without
starting failure back-off for night_audit.in_progress. Manual completion says “Complete the
audit again to continue.” Disabled automation has no next run. An enabled next-run instant is
the earliest next attempt, including catch-up and the fifteen-minute failure back-off.
Warning review requires a browser member session with audit rights. Opening a rendered run
posts review only when that date has unreviewed warnings, then invalidates only warning queries.
Manual runs return night_audit.review_unavailable with an explanation that they need no review.
Clean runs have no warning notice. Warning counts use ICU plurals.
The settings help states both consequences of the noon pivot: with automatic no-shows on, a 12:00–17:00 run marks that day’s still-expected arrivals as no-shows; after an evening close, a late walk-in cannot be booked on the closed date.