---
module: recon.cash-forecast
title: Cash Forecast
tab: Claude
order: 40
audience: dev
---

> Terse agent reference. Every path/table/topic below is confirmed in `hiveops-recon` source.

## Owner
- Service: **`hiveops-recon`** (owns it fully; no cross-service call for this feature).
- Backend port **8091**, NGINX `/api/recon/`, prod external **8013**. DB **`hiveiq_recon`**.
- Frontend: `ForecastView.svelte` @ `recon.bcos.cloud`; `apiBase()` = `window.__APP_CONFIG__.apiUrl` || `http://localhost:8091/api/recon`.

## Endpoints
| METHOD | Path | Auth | Returns |
|---|---|---|---|
| GET | `/api/recon/fleet/forecast` | authed, institution-scoped | `CashForecastResponse[]` |
| GET | `/api/recon/atm/{atmId}/forecast` | authed + `verifyAtmAccess` (403 if out of scope) | `CashForecastResponse[]` |
| POST | `/api/recon/admin/forecast/recompute` | `hasAnyRole('MSP_ADMIN','BCOS_ADMIN')` | `{ "atmsProcessed": N }` |

## Tables (`hiveiq_recon`)
- Write+read: `cash_forecasts` (unique `(atm_id, slot)` = `uq_forecast_atm_slot`).
- Read only: `replenishment_sessions`, `replenishment_cassette_entries`, `recon_device_selection` (enrollment), `atm_cash_positions` (access check).
- Status enum values: `OK`, `INSUFFICIENT_HISTORY`, `NO_BURN`.

## Kafka (consumer-only; upstream of this feature)
- `journal.replenishments` → `ReplenishmentEventConsumer` (group `recon-replenishment-group`) → writes `replenishment_sessions`.
- `hiveops.device.events` → `DeviceEventConsumer` (group `hiveops-recon-device-sync`) → `device_summary` scoping.
- Cash forecast itself consumes/produces **no** topic — it's a batch read over `replenishment_sessions`.

## Recompute triggers
- Nightly cron `0 30 3 * * *` (`recon.forecast.cron`) → `ReconSyncScheduler.recomputeForecasts` → `CashForecastService.recomputeAll()`.
- Admin POST above → same `recomputeAll()`.
- `recomputeAll()` loops only enrolled ATMs (`recon_device_selection`); per-ATM = delete-by-atm + saveAll.

## Config (`recon.forecast.*`)
- `lookback-sessions`=6 · `safety-buffer-days`=2 · `max-gap-days`=45 · `min-history-days`=7 · `cron`=`0 30 3 * * *`.

## Gotchas
- Input is **replenishment history, not live journal** — needs ≥2 `CONFIRMED`/`LOCKED` sessions AND ≥7 days span for `OK`, else `INSUFFICIENT_HISTORY`.
- Device must be enrolled in `recon_device_selection` or it's never computed (empty result, no error).
- `RETRACT` cassettes and `denominationCents == 0` slots are excluded; slots absent from the latest session are skipped.
- Current level source order: `physicalCountCents` → `journalClosingCents` → `opening − used + added`.
- `RECYCLER`/`MIX` use net burn (`used − impliedDeposits`, clamped ≥0).
- All money fields are **cents** (Long). `notesPerDay` = burnPerDay / denominationCents.
- Fleet GET has no role annotation — scoping is institution-based inside the service (admin scope = null = all).
- Empty UI message references the 03:30 nightly job.

## Do NOT
- Do NOT look for forecast logic in `hiveops-incident` / `hiveops-journal` / `hiveops-devices` — it's all in `hiveops-recon`.
- Do NOT POST recompute as a non-admin (403).
- Do NOT expect a Kafka topic named for forecasts — none exists; recon is consumer-only.
- Do NOT assume a device with no replenishment sessions or not enrolled will ever appear — it won't.
- Do NOT use `DB_URL` for recon; prod uses split `DB_HOST`/`DB_PORT`/`DB_NAME` (default `hiveiq_recon`).
- Do NOT confuse the admin recompute path — it is `/api/recon/admin/forecast/recompute`, not `/admin/recompute`.
