Files
hiveops-guide/backend/src/main/resources/content/analytics/cash/architect.md
T
johannes 930a4b968e feat(guide): dev audience tier + GUIDE_SERVED_AUDIENCES env cap (prod = customer,internal)
4-tier ladder customer<internal<dev<bcos. Re-tagged Architect/Testing/Claude +
platform/technical from internal->dev; Processes stays bcos. Backend visible()
enforces a per-instance served-audiences cap at the API. bcos.dev/DLX serve all;
production sets GUIDE_SERVED_AUDIENCES=customer,internal so dev/bcos never leave
dev. Verified: capped instance returns only Customer+Internal, 404s dev/bcos.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 17:11:00 -04:00

5.7 KiB

module, title, tab, order, audience
module title tab order audience
analytics.cash Cash Intelligence Architect 30 dev

Internal · architecture. Grounded in hiveops-analytics + hiveops-recon source, 2026-07-01. Key point: Cash Intelligence is a synchronous fan-out, not the usual executor→Kafka→record-store pattern. Analytics stores nothing for this feature and consumes no Kafka topic to build it.

Services involved

Service Role in this feature
hiveops-analytics (port 8089, analytics.bcos.cloud) Serves the tab. Owns the frontend + a thin transform. No cash data of its own.
hiveops-recon (port 8091, prod ext 8013, recon.bcos.cloud) Source of truth for all cash/reconciliation data. Owns DB hiveiq_recon.
hiveops-incident Called by recon (not analytics) to enumerate all fleet ATM IDs for admin/full-fleet views.
hiveops-journal Upstream of recon's cash math (withdrawals/deposits/replenishments); not touched at cash-intelligence request time.

Data flow (request time, fully synchronous)

Browser (CashIntelligenceTab.svelte)
   │  GET /api/analytics/cash-intelligence   (Bearer JWT)
   ▼
hiveops-analytics · CashIntelligenceController
   │  extracts raw bearer token from Authorization header
   ▼
CashIntelligenceService.buildCashIntelligence(token)
   │  reconWebClient (baseUrl services.recon.url = http://hiveiq-recon:8091)
   │  GET /api/recon/fleet/summary   (forwards caller's "Bearer <token>")
   ▼
hiveops-recon · SummaryController.fleetSummary()
   │  InstitutionContext.resolveScope()  → institution list or null (full fleet)
   │  reads atm_cash_positions (scoped) + reconciliation_cycles (per position)
   │  admin/full-fleet only: incidentFetchService.fetchAllAtmIds()  → merge all ATMs
   │  applies recon device-selection filter (recon_device_selection)
   │  counts replenishment_sessions in PENDING_CONFIRMATION
   ▼  FleetSummaryResponse{totalAtms, balancedCycles, varianceCycles, openCycles,
   │                       pendingConfirmations, atms[AtmSummaryResponse]}
   ▲
CashIntelligenceService  (transform, in-memory, no persistence)
   │  near-empty  = atms where projectedBalanceCents < 1_000_000, asc
   │  variance    = atms where abs(varianceCents) > 0, desc by |variance|
   ▼  CashIntelligenceResponse
Browser renders 4 stat cards + 2 tables

No Kafka on this path

  • The cash-intelligence request itself uses no Kafka topic and no consumer. Do not look for an analytics.* cash topic — there isn't one.
  • The broader analytics service does run Kafka (snapshot/insight pipeline: analytics.snapshot-ready, consumers of hiveops.incidents.* / journal.file-parsed), but Cash Intelligence bypasses all of it and reads recon live. See [technical.analytics] for that separate machinery. Cross-link [platform.kafka].
  • Recon's own cash figures are built asynchronously (journal sync + replenishment cycles) before this request; by read time they are already materialised in hiveiq_recon.

DB tables

Analytics reads/writes no table for this feature. All reads happen in recon against hiveiq_recon:

Table Read here for Key columns used
atm_cash_positions per-ATM balances + cycle pointer atm_id, institution, current_cycle_id, projected_balance_cents, last_replenishment_at, journal_format
reconciliation_cycles cycle status + variance status (OPEN / PENDING_RECONCILIATION / BALANCED / VARIANCE / UNRECONCILED), variance_cents, synced_at, cycle_start
replenishment_sessions pending-confirmation count status = PENDING_CONFIRMATION

Only BALANCED, VARIANCE, OPEN cycle statuses contribute to the three cycle counts; other statuses fall through default -> {}. Cross-link [platform.data-architecture].

Key API endpoints

Method Path Service Auth Notes
GET /api/analytics/cash-intelligence analytics authenticated JWT (no role gate) The tab's only call
GET /api/recon/fleet/summary recon authenticated JWT Upstream source; institution-scoped internally
GET /api/recon/fleet/pending recon authenticated JWT Related, not used by this tab
GET /api/recon/atm/{atmId}/summary recon authenticated JWT Per-ATM detail, not used by this tab

Auth & scoping model

  • Analytics has no service account: it forwards the caller's bearer JWT verbatim to recon (Authorization: Bearer <token>). If the user's token is invalid/expired at recon, the tab empties.
  • Scoping is entirely a recon concern via InstitutionContext.resolveScope():
    • ROLE_CUSTOMER / ROLE_MSP_ADMIN → scoped to JWT credentials (their institution ATMs only, existing recon positions only).
    • anything else (e.g. BCOS_ADMIN) → null → full fleet, with all ATM IDs merged from incident.
  • Cross-link [platform.service-ownership]: cash reconciliation is recon's domain; analytics is a presentation layer that must not persist or mutate cash state.

Configuration surface

Property / env Default Effect
services.recon.url (Spring; override via SERVICES_RECON_URL) http://hiveiq-recon:8091 recon base URL. Not wired to RECON_SERVICE_URL in analytics application.yml.
WebClient maxInMemorySize 16 MiB caps the recon response the analytics WebClient will buffer

Failure semantics (by design)

  • Recon error / timeout → CashIntelligenceService catches, logs a warn, returns Map.of()degrades to all-zero response rather than surfacing an error to the UI. Freshness/health of this tab therefore depends entirely on recon being reachable and the JWT being accepted.