---
module: vault.cit-visits
title: CIT Visits
tab: Architect
order: 30
audience: dev
---

How the CIT Visits feature is built. See [platform.service-ownership] — **hiveops-vault owns CIT visit data end to end**; it holds only a read-only mirror of device metadata.

## Services involved

| Service | Role for this feature |
|---------|----------------------|
| **hiveops-vault** (backend, port 8094) | Owns CIT visits, cassette counts, removals, CSV imports. Serves the whole `/api/vault` surface. |
| **hiveops-vault** (frontend, port 5185, `vault.bcos.cloud`) | Svelte SPA. `VisitsView` (list+filters), `VisitDetailPanel` (per-cassette + removals, confirm/dispute), `ImportView` (CSV upload), `DashboardView` (stats). Axios client `frontend/src/lib/api.ts`. |
| **hiveops-devices** | Upstream owner of device metadata. Publishes `hiveops.device.events`; vault only consumes. |
| **hiveops-auth / mgmt** | Issue the JWT (claims `sub`, `email`, `role`, `institutionKey`) that vault validates. |
| **NGINX** | Edge routing + CORS authority. Maps `api.bcos.cloud/vault/` → `hiveiq-vault:8094/api/vault/`. |

Single combined repo: `hiveops-vault/backend` + `hiveops-vault/frontend`.

## Request path (auth + scoping)

1. SPA sends `Authorization: Bearer <jwt>`.
2. `JwtAuthenticationFilter` validates via shared `com.hiveops.security.JwtTokenValidator`, builds a `VaultPrincipal(userId, email, role, institutionKey)`, sets it as the authentication principal.
3. `SecurityConfig` — stateless, CSRF off, **Spring CORS disabled** (NGINX handles CORS), `anyRequest().authenticated()`. No role checks.
4. Controllers inject `@AuthenticationPrincipal VaultPrincipal` and scope every query by `institutionKey`.

See [platform.data-architecture] for the institution-scoping pattern.

## Data flow — CIT visits (owned data)

- **Manual create/update/delete** and **status transitions** (`PENDING → CONFIRMED / DISPUTED`) go straight through `CitVisitController` → `CitVisitService` → JPA repositories → `hiveiq_vault`. Synchronous; no Kafka, no executor.
- **CSV bulk import** follows an async ingest pattern:
  1. `POST /api/vault/imports` → `ImportController` saves a `cit_imports` row (status `PROCESSING`) and returns **202** immediately.
  2. `CsvImportService.processAsync(...)` runs on the `importExecutor` `ThreadPoolTaskExecutor` (core 2 / max 4 / queue 50, thread prefix `vault-import-`; `AsyncConfig`).
  3. Parser groups rows by composite key `atm_id|visit_date|vendor|reference` into one `CitVisit` with child cassette counts / removals, then sets `cit_imports.status` = `COMPLETED` (+ `recordCount`) or `FAILED` (+ `errorMessage`).
  4. Client polls `GET /api/vault/imports/{id}` for the terminal state.

This is an **executor → record-store** pattern (async work tracked by a status row) rather than executor → Kafka; vault produces **no** Kafka messages.

## Data flow — device metadata (mirror)

Vault keeps `device_summary` in sync from Kafka. See [platform.kafka].

- **Topic consumed:** `hiveops.device.events`
- **Consumer:** `DeviceEventConsumer` (`@KafkaListener`), group `hiveops-vault-device-sync`, offset reset `earliest`, container factory `deviceEventKafkaListenerContainerFactory` (`DeviceEventKafkaConfig`).
- **Payload:** `DeviceEventMessage` (JSON; trusted packages include `com.hiveops.devices.kafka`, `com.hiveops.fleet.kafka`, `com.hiveops.vault.kafka`).
- **Behavior:** `DEVICE_DELETED` → delete the `device_summary` row by `atmId`. Otherwise upsert by `atmId`. On **insert**, a missing `deviceId` is skipped with a warning; a missing `institutionKey` defaults to `"BCOS"`.
- **Produced topics:** none.

`device_summary` feeds `GET /api/vault/atms` and the MSP-admin institution-derivation on visit create. It is never authored by vault — a stale mirror is an upstream `hiveops-devices` publish gap.

## DB tables (database `hiveiq_vault`)

| Table | Written by | Read by |
|-------|-----------|---------|
| `cit_visits` | visit CRUD, CSV import | list/detail/recent/by-atm, stats |
| `cit_cassette_counts` | visit create, CSV import | visit detail |
| `cit_removals` | visit create, CSV import | visit detail |
| `cit_imports` | import upload + async job | import list/detail |
| `device_summary` | Kafka consumer only | `/atms`, create-visit institution derivation |

Schema from Flyway `V1__create_vault_schema.sql`; `device_summary` added/altered in `V3`/`V5`. Prod = PostgreSQL (`ddl-auto: validate` + Flyway); dev = H2 `create-drop`, Flyway disabled. Env vars are the split form `DB_HOST` / `DB_PORT` / `DB_NAME` (default `hiveiq_vault`) — not `DB_URL`.

## Key API endpoints

Base `/api/vault` (all authenticated, institution-scoped):

| Method | Path | Purpose |
|--------|------|---------|
| GET | `/visits` | List, paginated (default size 25, sort `visitDate` DESC), filters `atmId`,`vendor`,`status`,`from`,`to` |
| GET | `/visits/{id}` | Detail (cassette counts + removals) |
| POST | `/visits` | Create (201) |
| PUT | `/visits/{id}` | Update status/notes/vendor/reference |
| DELETE | `/visits/{id}` | Delete (204) |
| GET | `/visits/recent` | Recent for dashboard |
| GET | `/atm/{atmId}/visits` | All visits for one ATM |
| GET | `/atms` | Device list from `device_summary` mirror |
| POST | `/imports` | CSV upload (202, async) |
| GET | `/imports` · `/imports/{id}` | Import history / poll status |
| GET | `/stats` | Dashboard aggregates |
| GET | `/users/me` | Decoded JWT identity |

Cross-links: [platform.data-architecture] · [platform.kafka] · [platform.service-ownership] · [technical.vault].
