930a4b968e
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>
5.7 KiB
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-reconsource, 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 ofhiveops.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 →
CashIntelligenceServicecatches, logs a warn, returnsMap.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.