---
module: analytics.cash
title: Cash Intelligence
tab: Architect
order: 30
audience: dev
---

> **Internal · architecture.** Grounded in `hiveops-analytics` + `hiveops-recon` source, 2026-07-01. Key point: Cash Intelligence is a **synchronous fan-out**, not the usual executor→Kafka→record-store pattern. Analytics stores nothing for this feature and consumes no Kafka topic to build it.

## Services involved
| Service | Role in this feature |
|---------|----------------------|
| **hiveops-analytics** (port 8089, `analytics.bcos.cloud`) | Serves the tab. Owns the frontend + a thin transform. **No cash data of its own.** |
| **hiveops-recon** (port 8091, prod ext 8013, `recon.bcos.cloud`) | Source of truth for all cash/reconciliation data. Owns DB `hiveiq_recon`. |
| **hiveops-incident** | Called *by recon* (not analytics) to enumerate all fleet ATM IDs for admin/full-fleet views. |
| **hiveops-journal** | Upstream of recon's cash math (withdrawals/deposits/replenishments); not touched at cash-intelligence request time. |

## Data flow (request time, fully synchronous)
```
Browser (CashIntelligenceTab.svelte)
   │  GET /api/analytics/cash-intelligence   (Bearer JWT)
   ▼
hiveops-analytics · CashIntelligenceController
   │  extracts raw bearer token from Authorization header
   ▼
CashIntelligenceService.buildCashIntelligence(token)
   │  reconWebClient (baseUrl services.recon.url = http://hiveiq-recon:8091)
   │  GET /api/recon/fleet/summary   (forwards caller's "Bearer <token>")
   ▼
hiveops-recon · SummaryController.fleetSummary()
   │  InstitutionContext.resolveScope()  → institution list or null (full fleet)
   │  reads atm_cash_positions (scoped) + reconciliation_cycles (per position)
   │  admin/full-fleet only: incidentFetchService.fetchAllAtmIds()  → merge all ATMs
   │  applies recon device-selection filter (recon_device_selection)
   │  counts replenishment_sessions in PENDING_CONFIRMATION
   ▼  FleetSummaryResponse{totalAtms, balancedCycles, varianceCycles, openCycles,
   │                       pendingConfirmations, atms[AtmSummaryResponse]}
   ▲
CashIntelligenceService  (transform, in-memory, no persistence)
   │  near-empty  = atms where projectedBalanceCents < 1_000_000, asc
   │  variance    = atms where abs(varianceCents) > 0, desc by |variance|
   ▼  CashIntelligenceResponse
Browser renders 4 stat cards + 2 tables
```

## No Kafka on this path
- The **cash-intelligence request itself uses no Kafka topic and no consumer.** Do not look for an `analytics.*` cash topic — there isn't one.
- The broader analytics service *does* run Kafka (snapshot/insight pipeline: `analytics.snapshot-ready`, consumers of `hiveops.incidents.*` / `journal.file-parsed`), but **Cash Intelligence bypasses all of it** and reads recon live. See [technical.analytics] for that separate machinery. Cross-link [platform.kafka].
- Recon's own cash figures are built asynchronously (journal sync + replenishment cycles) *before* this request; by read time they are already materialised in `hiveiq_recon`.

## DB tables
Analytics reads/writes **no table** for this feature. All reads happen in recon against **`hiveiq_recon`**:

| Table | Read here for | Key columns used |
|-------|---------------|------------------|
| `atm_cash_positions` | per-ATM balances + cycle pointer | `atm_id`, `institution`, `current_cycle_id`, `projected_balance_cents`, `last_replenishment_at`, `journal_format` |
| `reconciliation_cycles` | cycle status + variance | `status` (OPEN / PENDING_RECONCILIATION / BALANCED / VARIANCE / UNRECONCILED), `variance_cents`, `synced_at`, `cycle_start` |
| `replenishment_sessions` | pending-confirmation count | `status` = `PENDING_CONFIRMATION` |

Only `BALANCED`, `VARIANCE`, `OPEN` cycle statuses contribute to the three cycle counts; other statuses fall through `default -> {}`. Cross-link [platform.data-architecture].

## Key API endpoints
| Method | Path | Service | Auth | Notes |
|--------|------|---------|------|-------|
| GET | `/api/analytics/cash-intelligence` | analytics | authenticated JWT (no role gate) | The tab's only call |
| GET | `/api/recon/fleet/summary` | recon | authenticated JWT | Upstream source; institution-scoped internally |
| GET | `/api/recon/fleet/pending` | recon | authenticated JWT | Related, not used by this tab |
| GET | `/api/recon/atm/{atmId}/summary` | recon | authenticated JWT | Per-ATM detail, not used by this tab |

## Auth & scoping model
- Analytics has **no service account**: it forwards the **caller's** bearer JWT verbatim to recon (`Authorization: Bearer <token>`). If the user's token is invalid/expired at recon, the tab empties.
- Scoping is entirely a **recon** concern via `InstitutionContext.resolveScope()`:
  - `ROLE_CUSTOMER` / `ROLE_MSP_ADMIN` → scoped to JWT credentials (their institution ATMs only, existing recon positions only).
  - anything else (e.g. `BCOS_ADMIN`) → `null` → full fleet, with all ATM IDs merged from incident.
- Cross-link [platform.service-ownership]: cash reconciliation is recon's domain; analytics is a presentation layer that must not persist or mutate cash state.

## Configuration surface
| Property / env | Default | Effect |
|----------------|---------|--------|
| `services.recon.url` (Spring; override via `SERVICES_RECON_URL`) | `http://hiveiq-recon:8091` | recon base URL. **Not wired to `RECON_SERVICE_URL`** in analytics `application.yml`. |
| WebClient `maxInMemorySize` | 16 MiB | caps the recon response the analytics WebClient will buffer |

## Failure semantics (by design)
- Recon error / timeout → `CashIntelligenceService` catches, logs a warn, returns `Map.of()` → **degrades to all-zero response** rather than surfacing an error to the UI. Freshness/health of this tab therefore depends entirely on recon being reachable and the JWT being accepted.
