---
module: analytics.cash
title: Cash Intelligence
tab: Claude
order: 40
audience: dev
---

> Terse agent reference. Cash Intelligence = **read-only pass-through**: analytics re-shapes one recon call. Analytics owns NO cash data.

## Ownership
- **UI + endpoint owner:** `hiveops-analytics` (port 8089, `analytics.bcos.cloud`).
- **Data owner (source of truth):** `hiveops-recon` (port 8091, prod ext 8013, DB `hiveiq_recon`).
- Analytics persists nothing and consumes no Kafka for this feature.

## Endpoints (METHOD + path)
| Method | Path | Service | Auth |
|--------|------|---------|------|
| GET | `/api/analytics/cash-intelligence` | analytics | JWT authenticated, **no role gate** |
| GET | `/api/recon/fleet/summary` | recon (upstream) | JWT authenticated; institution-scoped internally |

- Frontend base: `window.__APP_CONFIG__.apiUrl` else `http://localhost:8089/api/analytics`.
- Analytics → recon base URL: property `services.recon.url`, default `http://hiveiq-recon:8091`. Caller JWT forwarded verbatim.

## Response shape
- Analytics `CashIntelligenceResponse`: `totalAtms, balancedCount, varianceCount, openCount, pendingConfirmationsCount, atmsNearEmpty[], atmsWithVariance[]`.
- `atmsNearEmpty` item: `{atmId, projectedBalanceCents, lastReplenishmentAt}`.
- `atmsWithVariance` item: `{atmId, varianceCents, cycleStatus}`.
- Recon `FleetSummaryResponse`: `totalAtms, balancedCycles, varianceCycles, openCycles, pendingConfirmations, atms[]`.

## Transform rules (in analytics)
- Near-empty filter: `projectedBalanceCents < 1_000_000` (< $10,000), sort asc.
- Variance filter: `abs(varianceCents) > 0`, sort desc by `|variance|`.
- `cycleStatus` maps from recon field `currentCycleStatus`.
- Frontend red-row highlight (visual only): `< 300_000` cents (< $3,000).

## Tables (all in `hiveiq_recon`, read by recon only)
| Table | Purpose |
|-------|---------|
| `atm_cash_positions` | balances, `current_cycle_id`, `institution`, `projected_balance_cents`, `last_replenishment_at` |
| `reconciliation_cycles` | `status`, `variance_cents`, `synced_at` |
| `replenishment_sessions` | pending count (`status = PENDING_CONFIRMATION`) |

- Cycle `Status` enum: `OPEN, PENDING_RECONCILIATION, BALANCED, VARIANCE, UNRECONCILED`. Only `BALANCED/VARIANCE/OPEN` counted.

## Topics
- **None** for cash-intelligence. (Analytics' snapshot/insight Kafka is unrelated — do not attribute it to this feature.)

## Scoping (in recon `InstitutionContext.resolveScope()`)
- `ROLE_CUSTOMER` / `ROLE_MSP_ADMIN` → scoped to JWT institutions; only ATMs with existing recon positions.
- other roles (e.g. `BCOS_ADMIN`) → null → full fleet; recon merges all ATM IDs from hiveops-incident.
- recon device-selection filter (`recon_device_selection`) can further narrow results.

## Gotchas
- Recon fetch failure → analytics catches, logs `CashIntelligence: failed to fetch fleet summary from recon:`, returns empty map → **all-zero tab, no error surfaced**. Empty tab == recon problem, not analytics.
- recon URL override is `SERVICES_RECON_URL` (Spring relaxed binding), **NOT** `RECON_SERVICE_URL` (that env is unmapped in analytics `application.yml`).
- Null `projectedBalanceCents` coerced to `0` → ATM appears in Near Empty at `$0.00` (admin full-fleet view includes position-less ATMs).
- Cents are integers; frontend divides by 100. Negative variance = short cash.

## Do NOT
- Do NOT add cash endpoints/tables to analytics — cash domain belongs to recon ([platform.service-ownership]).
- Do NOT look for a Kafka topic feeding this tab; it's a synchronous recon call.
- Do NOT assume a role gate on `/api/analytics/cash-intelligence` — any authenticated JWT reaches it (scoping happens downstream in recon).
- Do NOT set `RECON_SERVICE_URL` expecting it to change the recon target; use `SERVICES_RECON_URL`.
- Do NOT expect data for ATMs never onboarded in Recon (scoped users won't see them).
