---
module: recon.cash-reconciliation
title: Cash Reconciliation
tab: Architect
order: 30
audience: dev
---

> **Internal · Architecture.** How the Cash Reconciliation fleet grid is built. Owned end-to-end by **hiveops-recon** (`hiveiq_recon` DB). See [platform.service-ownership], [platform.data-architecture], [platform.kafka].

## Components for this feature

| Layer | Artifact | Role |
|---|---|---|
| Frontend | `FleetReconTable.svelte`, `App.svelte`, `lib/stores.ts` (`fleetSummary`), `lib/api.ts` (`reconAPI`) | Renders the grid; does all filter/sort/page **client-side** |
| Backend controller | `SummaryController` (`@RequestMapping("/api/recon")`) | Serves `/fleet/summary`, `/groups`, `/atm/{id}/summary`, `/fleet/pending` |
| Backend services | `AtmPositionService`, `CycleService`, `DeviceSelectionService`, `IncidentFetchService` | Assemble the response |
| Security | `InstitutionContext.resolveScope()` + `JwtAuthenticationFilter` | Institution scoping from JWT |

## Data flow — loading the grid

```
Browser (recon.bcos.cloud)
  └─ GET /api/recon/fleet/summary  ──▶ SummaryController.fleetSummary()
        1. scope = InstitutionContext.resolveScope()   // null=admin, else institution keys
        2. positions = AtmPositionService.findAllScopedByInstitution(scope)   // atm_cash_positions
        3. if admin (scope==null): merge in every id from IncidentFetchService.fetchAllAtmIds()
              └─ reads LOCAL device_summary table (NO http call)
        4. apply DeviceSelectionService.getSelected(scope)   // recon_device_selection filter
        5. per ATM, load current cycle from reconciliation_cycles → status, variance, syncedAt
        6. compute projectionStaleMins for OPEN cycles (minutes since syncedAt|cycleStart)
        7. pendingCount = replenishment_sessions WHERE status=PENDING_CONFIRMATION (scoped)
  ◀─ FleetSummaryResponse { total, balanced, variance, open, pending, atms[] }
```

- The response carries **all** in-scope ATMs. The browser then filters by search/status/format/group/devices/date and paginates — **no server round-trip per filter**.
- **Device Group** filter loads separately: `GET /api/recon/groups` → `IncidentFetchService.fetchGroups()` → HTTP `GET /api/atm-groups` on **incident** (via `incidentWebClient`), then intersected with recon-owned ATMs. This is the **only** cross-service HTTP call this screen makes; failures are caught and return empty (grid still renders).

## DB tables (all in `hiveiq_recon`)

| Table | Read/Write on this screen | Purpose |
|---|---|---|
| `atm_cash_positions` | read | Per-ATM projected/last-known balance, `currentCycleId`, `lastSyncedAt`, `journalFormat`, institution |
| `reconciliation_cycles` | read | Current cycle status (OPEN/PENDING_RECONCILIATION/BALANCED/VARIANCE/UNRECONCILED), `varianceCents`, `syncedAt` |
| `replenishment_sessions` | read | Pending-confirmation count |
| `recon_device_selection` | read | Optional device allow-list filter |
| `device_summary` | read | Local device mirror; supplies full ATM list for admins + institution keys for scoping |

Writes to `atm_cash_positions` / `reconciliation_cycles` happen on the **replenishment and sync** paths (executor recomputes balances), not on grid load.

## Kafka — how `device_summary` stays current

Recon is **consumer-only** (no producers in source). See [platform.kafka].

| Topic | Consumer | Group | Effect on this screen |
|---|---|---|---|
| `hiveops.device.events` | `DeviceEventConsumer` | `hiveops-recon-device-sync` | Maintains `device_summary` → admin fleet listing + institution scoping |
| `journal.replenishments` | `ReplenishmentEventConsumer` | `recon-replenishment-group` | Feeds cycle/position updates surfaced as balances/status |
| `hiveops.journal.transactions.recorded` | `JournalTransactionConsumer` | `recon-transaction-group` | Updates cycle totals → projected balance |

There is **no reconciler** for `device_summary`; if `hiveops-devices` stops emitting `hiveops.device.events`, the mirror (and thus admin fleet listing + scoping) drifts.

## Key API endpoints (this screen)

| Method + path | Serves |
|---|---|
| GET `/api/recon/fleet/summary` | Whole grid (`FleetSummaryResponse`) |
| GET `/api/recon/groups` | Device Group filter (calls incident `/api/atm-groups`) |
| GET `/api/recon/fleet/pending` | Pending-confirmation list (used elsewhere; same data source) |
| GET `/api/recon/atm/{atmId}/summary` | Single-ATM refresh / detail entry |

## Executor → Kafka → record-store pattern

The grid is the **read side**. The write side (recording a replenishment / running a sync) is where recon consumes journal Kafka events, an executor recomputes the cycle (`expected = opening + added − removed − withdrawals + deposits − reversals`, `variance = actual − expected`), and persists to `reconciliation_cycles` + `atm_cash_positions`. The grid then simply reads those stored values. See the module Overview (customer tab) for the user-facing cycle lifecycle.

## Cross-links

- [platform.service-ownership] — recon owns the recon domain; incident owns groups/ATM registry.
- [platform.data-architecture] — `hiveiq_recon` store; `device_summary` mirror pattern shared across incident/fleet/reports/recon.
- [platform.kafka] — topic ownership and consumer-group conventions.
