---
module: analytics.transactions
title: Transactions
tab: Architect
order: 30
audience: dev
---

> How the **Transactions** tab is built. It is a **synchronous pull-through aggregation**, not an event-driven or snapshot-backed view. Cross-link [platform.service-ownership], [platform.data-architecture], [platform.kafka].

## Services involved

| Service | Role for this feature |
|---------|----------------------|
| `hiveops-analytics-frontend` | Serves the dashboard SPA (`analytics.bcos.cloud`, port 5174). The Transactions tab = `JournalCoverageTab.svelte`. |
| `hiveops-analytics` (port 8089) | Aggregator. Exposes `GET /api/analytics/journal-coverage`, forwards the caller JWT to journal, reshapes 3 journal responses into one `JournalCoverageResponse`. **Owns no data for this tab.** |
| `hiveops-journal` (port 8087) | **Source of truth.** Owns `journal_transactions` and `journal_gaps` in DB `hiveiq_journal`. |

Ownership boundary: transaction + gap data belongs exclusively to **hiveops-journal**; analytics is a read-only consumer. See [platform.service-ownership].

## Data flow (request path)

```
Browser (Transactions tab)
  → GET /api/analytics/journal-coverage        [hiveops-analytics : JournalCoverageController]
      JournalCoverageService.buildJournalCoverage(callerToken)
        via journalWebClient (JOURNAL_SERVICE_URL), forwarding "Bearer <callerToken>":
        ├─ GET /api/journal/transactions/summary?dateFrom=today&dateTo=today   → {total, successful, failed}
        ├─ GET /api/journal/gaps?status=OPEN                                    → [gaps...]
        └─ GET /api/journal/transactions/top-atms?dateFrom=today&dateTo=today&limit=10 → [{atmId, transactionCount}]
  ← JournalCoverageResponse { dailyVolume[today], openGapsTotal, atmsWithGaps[≤20], parseSuccessRateToday, topAtmsByVolume }
```

Notes on the aggregation logic (`JournalCoverageService`):
- `dailyVolume` is built with **today only** (single call; the multi-bar chart therefore renders one bar).
- `parseSuccessRateToday = successful * 100.0 / total` (0 when `total == 0`).
- `atmsWithGaps` = open gaps grouped by `atmId`, counted, sorted desc, capped at 20 (UI slices to 10).
- `topAtmsByVolume` = journal's top-ATMs list (limit 10; journal hard-caps page size at 50).

## Kafka — NOT in this request path

This tab is a live synchronous pull; **no Kafka topic is read or written to serve `journal-coverage`.** For context on the surrounding platform (relevant to *other* analytics tabs, not this one):

- hiveops-analytics consumes `journal.file-parsed` (`JournalFileParsedConsumer`), `hiveops.incidents.created/updated/resolved` (`IncidentEventConsumer`) to drive **snapshots** (`analytics_snapshots`) and produces `analytics.snapshot-ready`.
- hiveops-journal uses `journal.parse-requests.{INSTITUTION}` (worker parse) and produces `journal.file-parsed`.
- The Transactions tab does **not** read snapshots (`analytics_snapshots`) — it queries journal live every load. See [platform.kafka].

## DB tables

Read (in `hiveiq_journal`, owned by hiveops-journal):

| Table | Used for | Via |
|-------|----------|-----|
| `journal_transactions` | today's total / APPROVED / DECLINED counts; top ATMs by tx count | `JournalTransactionRepository.countByDateRange...`, `findTopAtmsByTransactionCount` |
| `journal_gaps` | open gap count + gaps-per-ATM | `/api/journal/gaps?status=OPEN` |

Written: **none by this feature.** `journal_gaps` rows are created by journal's own scheduled **gap detection** job (daily 06:00 UTC): for each ATM, any date between its first journal and yesterday with no `journal_files` row becomes an OPEN gap row. `analytics_snapshots` / `ai_insights` in `hiveiq_analytics` are unrelated to this tab.

See [platform.data-architecture] for the hot/cold split (`journal_transactions` vs `journal_transactions_archive`, 90-day boundary) — the today-only queries here always hit the hot table.

## Key endpoints

| Method | Path | Service | Auth |
|--------|------|---------|------|
| GET | `/api/analytics/journal-coverage` | analytics | authenticated JWT (no role gate) |
| GET | `/api/journal/transactions/summary?dateFrom=&dateTo=` | journal | JWT; institution-scoped |
| GET | `/api/journal/gaps?status=OPEN` | journal | `isAuthenticated()`; institution-scoped |
| GET | `/api/journal/transactions/top-atms?dateFrom=&dateTo=&limit=` | journal | `isAuthenticated()`; page size capped 50 |
| PUT | `/api/journal/gaps/{id}/request-fetch` | journal | `isAuthenticated()` — marks a gap `FETCH_REQUESTED` (agent re-upload) |

## Design characteristics

- **No executor→Kafka→record-store pattern here.** That pattern applies to snapshot/insight generation, not to this pull-through tab.
- **Auth is caller-forwarded**, not a service account: analytics extracts the `Authorization: Bearer` header and re-sends it to journal (`WebClientConfig` / `journalWebClient`). Downstream scope = caller scope.
- **Fault tolerance is silent degradation**: each downstream call is wrapped in try/catch that logs a warning and returns empty on failure, so the tab renders zeros rather than an error.
- **Caching**: journal's summary is `@Cacheable("txSummary")` keyed `institutions:dateFrom:dateTo`; gaps and top-ATMs are uncached.
