---
module: vault.cit-visits
title: CIT Visits
tab: Claude
order: 40
audience: dev
---

Terse act-without-guessing reference. All facts grounded in `hiveops-vault` source.

## Ownership
- **Owner service:** `hiveops-vault` (backend port 8094, frontend port 5185, `vault.bcos.cloud`).
- **Owns:** CIT visits, cassette counts, removals, CSV imports (full CRUD).
- **Mirrors (read-only):** `device_summary` via Kafka from `hiveops-devices`. Vault never authors device data.
- **DB:** `hiveiq_vault` (prod PostgreSQL; dev H2 in-memory). Env: `DB_HOST`/`DB_PORT`/`DB_NAME` (not `DB_URL`).

## Endpoints (base `/api/vault`, edge `api.bcos.cloud/vault/...`)
| METHOD | Path |
|--------|------|
| GET | `/api/vault/visits` (params: `atmId`,`vendor`,`status`,`from`,`to`,`page`,`size`) |
| GET | `/api/vault/visits/{id}` (id = UUID) |
| POST | `/api/vault/visits` → 201 |
| PUT | `/api/vault/visits/{id}` (`status`/`notes`/`vendorName`/`referenceNumber`) |
| DELETE | `/api/vault/visits/{id}` → 204 |
| GET | `/api/vault/visits/recent` |
| GET | `/api/vault/atm/{atmId}/visits` |
| GET | `/api/vault/atms` |
| POST | `/api/vault/imports` (multipart `file`) → 202 async |
| GET | `/api/vault/imports` · `/api/vault/imports/{id}` |
| GET | `/api/vault/stats` |
| GET | `/api/vault/users/me` |

## Tables (`hiveiq_vault`)
- `cit_visits` (status `PENDING`/`CONFIRMED`/`DISPUTED`; `institution_key`)
- `cit_cassette_counts` (`cassette_type` DISPENSER/RECYCLER/RETRACT/MIX)
- `cit_removals` (`removal_type` CHECK/DEPOSIT/REJECTED_NOTE)
- `cit_imports` (status `PROCESSING`/`COMPLETED`/`FAILED`)
- `device_summary` (Kafka-fed mirror)

## Kafka
- **Consumes:** `hiveops.device.events` → `DeviceEventConsumer`, group `hiveops-vault-device-sync`, offset `earliest`, payload `DeviceEventMessage`. `DEVICE_DELETED` deletes; else upsert by `atmId`.
- **Produces:** NONE.

## Auth / roles
- JWT Bearer only (`Authorization: Bearer`). Validated by `JwtTokenValidator` → `VaultPrincipal(userId,email,role,institutionKey)`.
- **No role gating.** `SecurityConfig` = `anyRequest().authenticated()`. There is NO `hasAnyRole(...)` in vault. Access = institution scoping via `institutionKey` claim.

## Gotchas
- Every read filters by `principal.institutionKey()`. Wrong/missing claim → empty page, **no error**.
- MSP admin = null `institutionKey`: only **create-visit** (derives institution from ATM's `device_summary`) and **`/atms`** (returns `findAll()`) handle null. `/imports`, `/stats`, visit list/recent pass null through → typically empty.
- CSV upload returns **202 immediately**; work runs on `importExecutor` (`vault-import-*`, core2/max4/queue50). **Must poll** `/imports/{id}` for `COMPLETED`/`FAILED`.
- Upload content-type must contain `csv` or `text`, non-empty, else 400.
- CSV parser groups rows by `atm_id|visit_date|vendor|reference`; `REMOVAL` in slot col = removal row.
- On device event: missing `deviceId` skips insert; missing `institutionKey` defaults to `"BCOS"`.
- Spring CORS **disabled**; NGINX is CORS authority + routes `/vault/` → `hiveiq-vault:8094/api/vault/`.
- `visitDate`/`from`/`to` are `Instant` (ISO date-time), not date-only.

## Do NOT
- Do NOT expect role checks or add `hasAnyRole` claims to test access — it's institution-scoped only.
- Do NOT write `device_summary` from vault or treat a missing device as a vault bug — fix upstream `hiveops-devices` publish.
- Do NOT treat a 202 import as done — poll `/imports/{id}`.
- Do NOT look for vault-produced Kafka topics — there are none.
- Do NOT use `DB_URL` env for vault — it uses `DB_HOST`/`DB_PORT`/`DB_NAME`.
- Do NOT add CIT-visit endpoints to another service — vault owns this domain ([platform.service-ownership]).
