---
module: recon.cash-reconciliation
title: Cash Reconciliation
tab: Claude
order: 40
audience: dev
---

> Act-without-guessing reference for the Cash Reconciliation fleet grid ("ATM Cash Positions").

## Ownership

- **Service:** `hiveops-recon` (owns everything for this screen).
- **DB:** `hiveiq_recon` (Postgres prod; H2 in dev).
- **Ports:** backend `8091`, NGINX `/api/recon/`, prod external `8013`; frontend `recon.bcos.cloud` (5175).
- **Frontend:** `FleetReconTable.svelte` + store `fleetSummary` + `reconAPI` in `frontend/src/lib/api.ts`.

## Endpoints (this screen)

| METHOD | Path | Notes |
|---|---|---|
| GET | `/api/recon/fleet/summary` | Fills entire grid; returns ALL in-scope ATMs; client filters/sorts/pages |
| GET | `/api/recon/groups` | Device Group filter; internally HTTP→incident `/api/atm-groups` |
| GET | `/api/recon/atm/{atmId}/summary` | Single ATM |
| GET | `/api/recon/fleet/pending` | Pending-confirmation ATMs |

Admin/write (not on grid load, `@PreAuthorize hasAnyRole('MSP_ADMIN','BCOS_ADMIN')`): `POST /api/recon/atm/{atmId}/sync`, replenishment create/edit/lock, `/api/recon/admin/*` (backfill, reparse, ingest-replenishments, repair-slots, rules), device-rules, device-selection.

## Tables (`hiveiq_recon`)

- `atm_cash_positions` — balances, `currentCycleId`, `lastSyncedAt`, `journalFormat`, institution.
- `reconciliation_cycles` — status, `varianceCents`, `syncedAt`. Status enum: `OPEN, PENDING_RECONCILIATION, BALANCED, VARIANCE, UNRECONCILED`.
- `replenishment_sessions` — status enum: `PENDING_CONFIRMATION, CONFIRMED, LOCKED`.
- `recon_device_selection` — optional allow-list filter.
- `device_summary` — local device mirror (admin fleet list + institution scoping).

## Kafka (consumer-only; recon publishes nothing)

| Topic | Consumer / group |
|---|---|
| `hiveops.device.events` | `DeviceEventConsumer` / `hiveops-recon-device-sync` → feeds `device_summary` |
| `journal.replenishments` | `ReplenishmentEventConsumer` / `recon-replenishment-group` |
| `hiveops.journal.transactions.recorded` | `JournalTransactionConsumer` / `recon-transaction-group` |

## Scoping (JWT)

- `InstitutionContext.resolveScope()`: `null` for BCOS_ADMIN/ADMIN/QDS_ADMIN (all ATMs); institution-key list for MSP_ADMIN; single key for CUSTOMER.
- Admin (`null`) grid = recon positions **merged with all `device_summary` ids**. Scoped users = only ATMs that already have a recon position.
- All endpoints need a valid JWT (`anyRequest().authenticated()`; only `/actuator/health*` public).

## Gotchas

- `fleet/summary` returns the full in-scope set; **filters are client-side** — do not expect query params to narrow it.
- `IncidentFetchService.fetchAllAtmIds()` reads **`device_summary` locally, NO http**. Only `/groups` (and cassette config) actually calls incident.
- `device_summary` has **no reconciler** → stale if `hiveops-devices` stops emitting `hiveops.device.events`.
- Staleness badge is computed for **OPEN cycles only**: `projectionStaleMins` = minutes since `syncedAt` (or `cycleStart`); frontend thresholds yellow ≥60, red ≥360.
- **Frontend status-filter mismatch:** dropdown offers value `PENDING_CONFIRMATION`, but cycle `currentCycleStatus` uses `PENDING_RECONCILIATION`. That filter option matches no cycle rows. (See uncertainties.)
- Money is in **cents** (`*Cents` fields).
- Prod DB config = `DB_HOST`/`DB_PORT`/`DB_NAME` (default `hiveiq_recon`) — NOT a single `DB_URL`.

## Do NOT

- Do NOT add recon endpoints/tables to another service — recon owns this domain.
- Do NOT assume `fleet/summary` is paginated/filtered server-side.
- Do NOT expect a Kafka producer in recon — it is consumer-only.
- Do NOT rely on incident being up for the grid to load (only the group filter needs it).
- Do NOT treat a scoped user's "missing ATM" as a bug — it needs a recon position first.
- Do NOT hit prod Postgres on the services VM; it is on `10.10.10.188` via ProxyJump.
- Do NOT expose PANs — this is journal-derived data; keep sanitization intact.
