Cash drawer shifts
Open Cash drawer for a property, enter the opening float in its currency, then record cash payments/refunds in the usual reservation folio. The server links each new cash posting to the open shift. The drawer shows the expected count and linked cash activity. Enter the physical count to close it; a difference above the property’s threshold requires a reason. Refresh and recount after a stale-version conflict. Reload retains both open and closed shifts. History opens a report with the float, expected/declared/difference, actors, times and movements. Use Older shifts to page beyond the newest 20 records and Latest shifts to return.
Expected cash is opening float plus payments minus refunds and cash corrections. A positive report movement enters the drawer; a negative one leaves it. Difference is declared minus expected. Minor units and currency precision remain exact, including currencies without decimals. One drawer may be open per property. Negative expected cash is visible, never silently clamped. A refund or reversal belongs to the drawer open when it is recorded; an earlier closed report never changes. Idempotent retries keep their original attribution.
Administrators configure Require an open shift for cash postings and the nonnegative
threshold. Enforcement is off by default: cash recorded without an open drawer then has a
null shift reference and is absent from shift reports. With enforcement enabled, open a
drawer before retrying cash_shift.required. Card-terminal, bank-transfer and other methods
are outside the drawer. No online money transfer or fiscal document is produced.
API, SDK and permissions
Section titled “API, SDK and permissions”All paths below are under /properties/{property_id} and respect workspace and property RLS.
API keys and device tokens use the live member permissions as browser requests do.
| Operation | Permission | Version |
|---|---|---|
GET /cash_shift_context |
Any cash shift grant | Currency/time zone only |
GET /cash_shift_settings |
cash_shifts.read or cash_shifts.configure |
Returns settings ETag |
PUT /cash_shift_settings |
cash_shifts.configure |
Settings If-Match |
GET /cash_shifts/current |
cash_shifts.read |
Returns open shift ETag or null |
GET /cash_shifts?limit=20&cursor=… |
cash_shifts.read |
Newest first, opaque cursor |
POST /cash_shifts |
cash_shifts.open |
New drawer, no If-Match |
GET /cash_shifts/{id} |
cash_shifts.read |
Report and shift ETag |
POST /cash_shifts/{id}/close |
cash_shifts.close |
Shift If-Match |
Settings input is { "require_open_shift": true, "discrepancy_threshold": "5.00" }.
Opening input is { "opening_float": "100.00" }; closure is
{ "declared": "150.00", "reason": null }. Amounts are decimal strings in the property’s
currency; response monetary objects carry amount and currency. Cash postings change the shift
version, so clients read it immediately before closing. Open/close return the shift only to
cash readers, otherwise CashShiftAcknowledgement. The server selects the nullable posting
reference; it is never accepted as client input.
The SDK exposes cashShiftSettings.get/update and cashShifts.current/list/create/get/close,
with generated React query factories. cashShifts.context returns only currency/time zone
for module-only custom roles without requiring properties.read. Built-in admin/front desk/accounting may open and close;
admin/front desk/accounting/revenue manager/viewer may read; only admin configures. Custom roles
can grant these separately within the member’s own live permissions and property selection.
cash_shift.already_open recovers by using the current drawer; cash_shift.closed by opening a
new one; cash_shift.version_mismatch by refreshing/recounting. cash_shift.reason_required
requires an explanation. Field-linked cash_shift.invalid rejects invalid amounts;
cash_shift.amount_exceeded rejects arithmetic outside the exact supported range. No write
survives a denied permission, invalid input or failed transaction.
Cash history returns { data, next_cursor, has_more }; pass the returned cursor unchanged.
Invalid cursors return cursor.invalid. Reports resolve opening/closing actor names live;
unavailable members have a readable fallback.