---
module: analytics.transactions
title: Transactions
tab: Claude
order: 40
audience: dev
---

> Act-without-guessing reference. Transactions tab = analytics UI tab `id: 'journal', label: 'Transactions'` → `JournalCoverageTab.svelte` → `GET /api/analytics/journal-coverage`. Data source of truth is **hiveops-journal**, not analytics.

## Ownership

- **hiveops-journal** OWNS the data (`journal_transactions`, `journal_gaps`, DB `hiveiq_journal`).
- **hiveops-analytics** (port 8089) is a read-only aggregator/proxy; forwards caller JWT to journal.
- Frontend: `hiveops-analytics/frontend`, `analytics.bcos.cloud` (port 5174).

## Endpoints (METHOD path → service)

| Method | Path | Service | Auth |
|--------|------|---------|------|
| GET | `/api/analytics/journal-coverage` | analytics | authenticated JWT, **no role gate** |
| GET | `/api/journal/transactions/summary?dateFrom=&dateTo=` | journal | JWT (no method-level `@PreAuthorize`; filter chain enforces) |
| GET | `/api/journal/gaps?status=OPEN` | journal | `@PreAuthorize("isAuthenticated()")` |
| GET | `/api/journal/transactions/top-atms?dateFrom=&dateTo=&limit=10` | journal | `@PreAuthorize("isAuthenticated()")`, size cap 50 |
| PUT | `/api/journal/gaps/{id}/request-fetch` | journal | `isAuthenticated()` (marks gap `FETCH_REQUESTED`) |

- Analytics frontend `apiBase` default (dev): `http://localhost:8089/api/analytics`; prod from `window.__APP_CONFIG__.apiUrl`. NGINX path `/api/analytics/`.
- Analytics `journalWebClient` base = `JOURNAL_SERVICE_URL` (default `http://hiveops-journal:8087`).

## Tables (DB `hiveiq_journal`)

| Table | Use |
|-------|-----|
| `journal_transactions` | total / APPROVED / DECLINED counts, top-ATMs |
| `journal_gaps` | open gaps (status `OPEN`) |

- No table is WRITTEN to serve this tab.
- `analytics_snapshots` / `ai_insights` (DB `hiveiq_analytics`) are NOT used by this tab.

## Kafka

- **None in this request path.** `journal-coverage` is a synchronous WebClient pull.
- (Context only, other tabs) analytics consumes `journal.file-parsed`, `hiveops.incidents.{created,updated,resolved}`; produces `analytics.snapshot-ready`. Journal produces `journal.file-parsed`, consumes `journal.parse-requests.{INSTITUTION}`.

## Response shape (`JournalCoverageResponse`)

- `dailyVolume: [{date, total, successful, failed}]` — **today only, single element**.
- `parseSuccessRateToday = successful*100/total` (0 if total 0).
- `openGapsTotal`, `atmsWithGaps: [{atmId, gapCount}]` (≤20, UI shows 10).
- `topAtmsByVolume: [{atmId, transactionCount}]` (≤10).

## Field semantics (critical)

- journal `successful` = `COUNT(host_result='APPROVED')`; `failed` = `COUNT(host_result='DECLINED')`; `total` = all rows.
- Therefore "Parse Success Rate" and the "Successful/Failed" bars = transaction **approval** stats, NOT journal-parse health.
- `transactions/summary` is `@Cacheable("txSummary")` key `institutions:dateFrom:dateTo` → today's numbers can be stale.
- All scoped by `InstitutionContext.resolveScope()` in journal — results are per caller's institution.

## Gotchas

- Downstream failures are caught and return empty → tab shows **0 / "No data"** with no user error. Grep analytics logs: `JournalCoverage: failed to fetch`.
- Everything except open-gaps is `LocalDate.now()` (today) — early-day emptiness is expected.
- Gap rows only created by journal's daily 06:00 UTC gap-detection job.

## Do NOT

- Do NOT claim analytics stores transaction/gap data — it does not; query journal.
- Do NOT treat "Parse Success Rate" as an ingest/parse metric — it is approval rate.
- Do NOT expect multi-day history on this tab — `dailyVolume` is today-only.
- Do NOT assume `MSP_ADMIN`/`BCOS_ADMIN` is required — `journal-coverage` is any-authenticated.
- Do NOT look for Kafka in the read path — it is a synchronous pull.
- Do NOT add write endpoints or resolve gaps from analytics — gap fetch is `PUT /api/journal/gaps/{id}/request-fetch` on journal.
