---
module: recon.cash-reconciliation
title: Cash Reconciliation
tab: Internal
order: 20
audience: internal
---

> **Internal · Ops/Support/Admin.** The Cash Reconciliation screen is the fleet grid ("ATM Cash Positions"). Served by **hiveops-recon**. Backend port **8091**, NGINX `/api/recon/`, prod external **8013**; frontend `recon.bcos.cloud`.

## What this screen actually calls

- **The grid** is filled by a single call: `GET /api/recon/fleet/summary` (component `FleetReconTable.svelte`, store `fleetSummary`). It returns **every** ATM in scope; all filtering, sorting, and paging happen **client-side** in the browser.
- **Device Group** filter → `GET /api/recon/groups` (this is the only piece of this screen that reaches out to the **incident** service, for group membership).
- Clicking a row opens the ATM detail (cycles/summary) — not this tab.

## Who can see / do what

- **All endpoints require a valid JWT** — `SecurityConfig` sets `anyRequest().authenticated()` (only `/actuator/health*` is public). A 401 on the grid means the token is missing/expired, not a recon bug.
- **Read GETs** (`/fleet/summary`, `/groups`, `/atm/{id}/summary`) are **not** method-role-gated; visibility is by institution scope instead (below).
- **All writes are admin-only:** `@PreAuthorize("hasAnyRole('MSP_ADMIN','BCOS_ADMIN')")` — replenishment create/edit/lock, cycle `sync`, cassette update/sync, device-rules, device-selection, and every `/admin/*` endpoint (backfill, reparse, rules edit/reload, ingest-replenishments).

### Institution scoping (why two users see different rows)

`InstitutionContext.resolveScope()` decides visibility from the JWT roles:

| Role | Scope | What appears in the grid |
|---|---|---|
| BCOS_ADMIN / ADMIN / QDS_ADMIN | `null` (unrestricted) | All recon ATMs **plus** every ATM in `device_summary` (merged in so the whole fleet shows even before first replenishment) |
| MSP_ADMIN | list of granted institution keys | Only ATMs that already have a recon position in those institutions |
| CUSTOMER (BANK) | single institution key | Only that institution's ATMs that already have a recon position |

Consequence: a **non-admin only sees an ATM after it has a recon position** (first replenishment recorded / first sync). Admins see un-enrolled ATMs too. If a customer says "my ATM is missing," check whether it has been reconciled yet.

## First things to check when it misbehaves

1. **Grid empty / "No ATMs tracked yet."** — `fleet/summary` returned no rows. For a scoped user this is normal until a replenishment exists. For an admin, check that `device_summary` is populated (see stale-mirror below).
2. **Grid 401/403** — JWT problem, not data. Re-login; check NGINX `/api/recon/` routing and that `JWT_SECRET` matches the other services.
3. **Device Group dropdown empty / groups missing ATMs** — `/groups` calls **incident** (`/api/atm-groups`). If incident is down or slow, the call is caught and logged (`Failed to fetch ATM groups`) and returns nothing; the grid itself still loads.
4. **⚠ staleness badge (yellow ≥60m, red ≥360m) on Projected Balance** — the ATM's **open** cycle hasn't been synced recently. `projectionStaleMins` = minutes since the cycle's `syncedAt` (or `cycleStart` if never synced). Fix by running a sync: `POST /api/recon/atm/{atmId}/sync` (admin) or recording the next replenishment. Persistent staleness usually means journal events for that ATM stopped flowing.
5. **Wrong / stale institution on an ATM** — the `device_summary` mirror is fed by Kafka `hiveops.device.events`; there is **no reconciler**, so if `hiveops-devices` stopped emitting, the mirror (and therefore admin fleet listing + scoping) goes stale. Confirm devices is publishing.
6. **Variance looks wrong** — variance is a cycle-level number surfaced here; investigate on the ATM detail / cycle, not this grid. Formula: `expected = opening + added − removed − withdrawals + deposits − reversals`; `variance = actual − expected`.

## Common failure modes → fix

| Symptom | Likely cause | Fix |
|---|---|---|
| Admin sees full fleet, customer sees few/none | Scoping working as designed; non-admins need a recon position first | Record first replenishment for the ATM |
| Projected balance behind reality (⚠) | Open cycle not synced; journal flow gap | `POST /atm/{id}/sync`; verify journal is receiving events |
| "Pending Confirmation" status filter returns nothing | Filter value mismatch (see Architect/Claude notes) — cycles never carry that string | Known discrepancy; use other statuses to triage |
| Whole fleet missing for admins | `device_summary` empty/stale (Kafka gap) | Verify `hiveops-devices` emits `hiveops.device.events` |
| Groups filter broken, grid fine | incident service call failing | Check incident health; recon logs `Failed to fetch ATM groups` |

## Admin tooling (not on this screen, but recon-owned)

- `POST /api/recon/atm/{id}/sync` — recompute the open cycle from journal.
- `POST /api/recon/admin/backfill[/{atmId}]`, `/admin/reparse/{atmId}`, `/admin/ingest-replenishments[/{atmId}]`, `/admin/repair-slots` — bulk/repair tooling, all `MSP_ADMIN`/`BCOS_ADMIN`.
- `GET/PUT /api/recon/admin/rules` (text/plain) + `POST /api/recon/admin/reload-rules` — edit `recon-rules.yml`; reload required for changes to apply.

See the **Architect** tab for data flow and the **Claude** tab for exact paths/tables.
