---
module: vault.import
title: Import
tab: Claude
order: 40
audience: dev
---

Terse agent reference for CIT CSV import. **Owner: `hiveops-vault`** (port 8094, image `hiveiq-vault`, DB `hiveiq_vault`). Frontend: `hiveops-vault/frontend` `components/Import/ImportView.svelte`.

## Endpoints (JWT Bearer; scoped to `principal.institutionKey()`)

| METHOD | Path (internal) | Prod URL prefix | Notes |
|--------|-----------------|-----------------|-------|
| POST | `/api/vault/imports` | `api.bcos.cloud/vault/imports` | multipart field name **`file`**; returns **202** + PROCESSING record |
| GET | `/api/vault/imports?page=&size=` | same | history, `importedAt` DESC, default size 25 |
| GET | `/api/vault/imports/{id}` | same | poll for status; 404 if wrong institution |

Controller: `controller/ImportController.java`. Async worker: `service/CsvImportService.java` (`processAsync`, `@Async("importExecutor")`, `@Transactional`).

## Tables (DB `hiveiq_vault`, all have `institution_key`)

- `cit_imports` — audit; status `PROCESSING → COMPLETED | FAILED`; `record_count`, `error_message`, `imported_by`(=JWT email), `format`=`CSV`
- `cit_visits` — one per `atm_id|visit_date|vendor|reference`; FK `cit_import`
- `cit_cassette_counts` — per cassette row; `cassette_type` DISPENSER/RECYCLER/RETRACT/MIX
- `cit_removals` — per REMOVAL row; `removal_type` CHECK/DEPOSIT/REJECTED_NOTE
- `device_summary` — read-only mirror, **not** written by import

## Kafka

- **Produced by import: none.**
- Vault consumes `hiveops.device.events` (group `hiveops-vault-device-sync`) only to maintain `device_summary` — unrelated to import.

## Roles

- **No role checks.** `SecurityConfig` = `anyRequest().authenticated()`. No `hasAnyRole(...)`. Authorization = institution-scoping via `VaultPrincipal.institutionKey()`.

## CSV shape

- Header row required. Group key = `atm_id + visit_date(ISO-8601, `Z` appended if missing) + vendor + reference`.
- Cassette row = 11 cols: `atm_id,visit_date,vendor,reference,slot,denomination(cents),cassette_type,opening,added,removed,closing`
- Removal row = 8 cols, slot literal `REMOVAL`: `atm_id,visit_date,vendor,reference,REMOVAL,removal_type,count,amount(cents)`
- `record_count` = **#visits** (groups), not #rows.

## Gotchas

- Upload validation: rejects empty file; rejects content-type only if it contains neither `csv` nor `text`. **Null content-type passes** the check.
- Whole file parses in one `@Transactional` — one bad `visit_date` (`Instant.parse` throws) → status FAILED, **zero** visits saved.
- Malformed values silently coerced (NOT failed): bad int/long → `0`; unknown `cassette_type` → `DISPENSER`; unknown `removal_type` → `REJECTED_NOTE`.
- Short rows silently skipped: `<7` cols dropped; cassette needs 11, removal needs 8.
- No dedup/idempotency — re-uploading the same file creates duplicate visits.
- MSP admin with null `institutionKey`: import path uses the principal key directly (does NOT derive institution from ATM like manual visit-create does) → visits tagged `institution_key = null`.
- Async: POST returns 202 before processing; must poll `/imports/{id}` for terminal state. UI polls every 2s.
- Uploads dir `UPLOADS_DIR` default `/tmp/vault-uploads`; multipart max 20MB.

## Do NOT

- Do NOT expect a synchronous result from POST — it's 202 + async; read status from `GET /imports/{id}`.
- Do NOT add role gating assumptions — there are none in vault.
- Do NOT look for a Kafka topic/consumer for import — the import path is pure DB.
- Do NOT treat `record_count` as CSV line count.
- Do NOT assume a COMPLETED import is clean — bad cells become 0/default silently.
- Do NOT write `device_summary` from anything but the `hiveops.device.events` consumer.
- Do NOT re-upload to "retry" without deleting prior visits — no idempotency.
