---
module: analytics.cash
title: Cash Intelligence
tab: Internal
order: 20
audience: internal
---

> **Internal · ops/support.** Written 2026-07-01, grounded in `hiveops-analytics` + `hiveops-recon` source. The Cash Intelligence tab is a **read-only pass-through**: analytics owns no cash data — it re-shapes one call to **hiveops-recon**. When it looks wrong, the fix is almost always in recon or the analytics→recon link, not in analytics itself.

## What it actually is
- Frontend `CashIntelligenceTab.svelte` (in `hiveops-analytics/frontend`) calls **`GET /api/analytics/cash-intelligence`**.
- `CashIntelligenceService` forwards the caller's JWT and calls **`GET /api/recon/fleet/summary`** on **hiveops-recon**.
- The four counts (Balanced / Variance / Open Cycle / Pending Conf.) and both tables (Near Empty, With Variance) are derived entirely from recon's `FleetSummaryResponse`. No analytics DB, no Kafka on this path.

## Roles required
- **Cash Intelligence endpoint (`/api/analytics/cash-intelligence`): authenticated only — no role gate.** `SecurityConfig` permits `/actuator/health` + `/api/public/**`, everything else just needs a valid JWT.
- **Recon `/api/recon/fleet/summary`: authenticated only** as well.
- **Institution scoping is enforced in recon, by role:**
  - `CUSTOMER` and `MSP_ADMIN` → **scoped** to the institutions in their JWT credentials. They see only ATMs that already have a recon cash position in their institution(s).
  - Non-scoped roles (e.g. `BCOS_ADMIN`) → **full fleet**: recon merges the complete ATM list from hiveops-incident so every ATM appears, even those with no recon position yet.
- There is **no admin-only mutating action** on this tab — it is view-only. Cash data is created/edited in the **Recon** app (custodian replenishments), not here.

## First things to check when it misbehaves

**Tab is all zeros / both tables empty**
1. This is the signature of a **recon fetch failure**. `CashIntelligenceService.fetchFleetSummary()` swallows any error and returns an empty map → every count is 0 and both lists empty.
2. Check analytics logs for: `CashIntelligence: failed to fetch fleet summary from recon:` (Loki, service `hiveiq-analytics`). The message tail is the real cause.
3. Common causes: recon container down, wrong recon URL (see below), or the caller's JWT rejected by recon.
4. Confirm recon directly: `GET /api/recon/fleet/summary` with the same JWT should return `{totalAtms, balancedCycles, varianceCycles, openCycles, pendingConfirmations, atms[]}`.

**Wrong recon hostname (silent empty tab)**
- Analytics resolves recon via property `services.recon.url`, default **`http://hiveiq-recon:8091`**.
- The analytics `application.yml` `services:` block only wires `incident`, `journal`, `mgmt` — **recon is NOT mapped to an env var there.** So a `RECON_SERVICE_URL` env var does nothing; only Spring relaxed binding **`SERVICES_RECON_URL`** overrides it.
- If recon runs under a different container name than `hiveiq-recon:8091`, the tab silently empties. Verify container name and this property first.

**A customer/MSP admin sees fewer ATMs than expected**
- Expected: scoped users only see ATMs with an existing recon position in their institution. An ATM that has never had a replenishment recorded in Recon will not appear for them.
- A **device selection filter** in recon further narrows the list: if a non-empty saved selection exists for that scope, only selected ATMs are returned.

**An ATM shows in "Near Empty" at $0.00 that shouldn't**
- For full-fleet (admin) views, recon includes ATMs with **no cash position** (`null`). Analytics coerces a null `projectedBalanceCents` to `0`, so such ATMs land in Near Empty at `$0.00`. This is expected given the current code, not a data-loss bug — it means "no recon cycle has ever run for this ATM." (See uncertainties.)

**Counts don't match the tables**
- Counts come straight from recon (`balancedCycles`, etc.); the tables are re-filtered/re-sorted in analytics. Thresholds differ intentionally:
  - **Near Empty** list = ATMs with projected balance **< $10,000** (`< 1,000,000` cents), lowest first.
  - Frontend **red-row** highlight = **< $3,000** (`< 300,000` cents) — a subset, purely visual.
  - **With Variance** list = any ATM with a non-zero variance, largest absolute first.

## Common failure modes → fix
| Symptom | Likely cause | Fix |
|--------|--------------|-----|
| All counts 0, both tables empty | recon down / unreachable / auth-rejected | Check analytics log warn line; restart/verify `hiveiq-recon`; test recon endpoint with the JWT |
| Empty tab only in prod, fine in dev | recon URL not resolving to the prod container | Set `SERVICES_RECON_URL` (not `RECON_SERVICE_URL`) or fix container name to `hiveiq-recon:8091` |
| Scoped user sees too few ATMs | no recon position for their ATMs, or device selection filter active | Confirm replenishments exist in Recon; clear the recon device selection |
| Ghost `$0.00` ATMs in Near Empty | fleet ATMs without a recon cycle (admin view) | Expected; onboard those ATMs into Recon or ignore |
| Variance/Balanced numbers stale | recon cycle not synced | Trigger recon `POST /api/recon/atm/{atmId}/sync`; cash positions update on replenishment/sync only |

## Cross-links
- Recon service internals: see `hiveops-recon/CLAUDE.md`.
- Analytics service overview: [technical.analytics].
- [platform.service-ownership] — cash reconciliation domain is owned by **recon**, surfaced by **analytics**.
