--- module: analytics.transactions title: Transactions tab: Architect order: 30 audience: bcos --- > 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 ": ├─ 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.