# Bitemporal модель

Каждая запись Ordinis имеет двухмерное время:

- **Бизнес-время** (`validFrom` / `validTo`) — когда запись действует в реальности.
- **Транзакционное время** (Hibernate Envers `revinfo.timestamp`) — когда
  изменение зафиксировано в БД.

Это две независимые оси: можно ввести запись задним числом
(business → past, txTime → now), либо запланировать на будущее
(business → future, txTime → now), либо retroactively изменить
прошлое (исправить ошибку — txTime → now, но business range уже past).

## Поля

```json
{
  "businessKey": "RESURS-P-3",
  "validFrom": "2025-01-01T00:00:00+03:00",
  "validTo": "9999-12-31T23:59:59+03:00",
  "version": 2,
  "data": { ... }
}
```

| Поле | Тип | Описание |
|---|---|---|
| `validFrom` | timestamp tz | Когда версия начинает действовать. |
| `validTo` | timestamp tz | До какого момента (или `9999-12-31` если бессрочно). |
| `version` | int | Optimistic lock + порядковый номер версии. Monotonic per businessKey. |

## Update создаёт **новую версию**

Update — это не overwrite. Это:

1. Старая запись: `validTo = now()` (закрыта).
2. Новая запись: `validFrom = now()`, `validTo = 9999...`, `version + 1`.

Обе версии остаются в БД. История доступна через
`GET /records/{key}/history` (см. [Read endpoints](read-endpoints.md)).

```mermaid
gantt
    dateFormat YYYY-MM-DD
    title Версии записи RESURS-P-3
    section v1
    v1 (status=in-orbit)        :2024-06-01, 2024-12-30
    section v2
    v2 (status=active)          :2024-12-30, 2026-05-07
    section v3
    v3 (name updated)           :2026-05-07, 9999-12-31
```

## Запросы на момент

`?at=2025-06-15` возвращает версию, которая была активна 15 июня 2025:

```bash
curl "https://ordinis.k8s.264.nstart.cloud/api/v1/spacecraft/records/RESURS-P-3?at=2025-06-15T00:00:00Z"
```

→ v2 (status=active), потому что `2024-12-30 ≤ 2025-06-15 < 2026-05-07`.

Без `?at` — default = `now()`.

## Future-dated entries

Можно ввести запись в будущее:

```json
PUT /api/v1/dictionaries/spacecraft/records/MISSION-2027
{
  "validFrom": "2027-01-01T00:00:00+03:00",
  "data": { "name": "Запланированный КА", "status": "planned" }
}
```

Запись существует в БД, но `GET ?at=now` её не вернёт (потому что
`now < validFrom`). Появится автоматически 1 января 2027.

## Закрытие записи (close)

```bash
POST /api/v1/dictionaries/spacecraft/records/RESURS-P-3/close
{
  "at": "2026-05-07T12:00:00+03:00",
  "reason": "Списан с орбиты"
}
```

Устанавливает `validTo = at`. После этого:
- `GET ?at=2026-05-07T11:00:00Z` → возвращает запись (ещё активна).
- `GET ?at=2026-05-07T13:00:00Z` → 404 (уже закрыта).
- `GET ?at=2026-05-07T11:30:00Z&include_closed=true` → для UI history.

Outbox emit'ит event `action: closed`.

## Bulk close

Закрытие нескольких записей одной транзакцией:

```bash
POST /api/v1/dictionaries/spacecraft/records/_bulk-close
{
  "businessKeys": ["RESURS-P-1", "RESURS-P-2", "RESURS-P-3"],
  "at": "2026-05-07T00:00:00+03:00",
  "reason": "Программа Ресурс-П завершена"
}
```

Response:

```json
{
  "closed": 3,
  "skipped": [],
  "errors": []
}
```

Каждая запись в собственной sub-transaction (Spring AOP per-record).
Если одна fail'нула с invalid_key — она в `errors`, остальные продолжают.

## Bulk export CSV

```bash
GET /api/v1/dictionaries/spacecraft/records/_export?format=csv&lang=ru-RU
```

Stream CSV (RFC 4180) с активными на now записями. Используется UI bulk
operations.

Параметры — те же что list endpoint (`lang`, `at`, фильтры).

## Использование для consumer'ов

### Snapshot at order time

Когда нужен «как было на момент заказа» — два варианта:

**Вариант A: query с `?at=`**

```bash
GET /api/v1/spacecraft/records/RESURS-P-3?at=2025-06-15T10:00:00Z
```

Pro: всегда свежие данные если возможна retroactive correction.
Con: depends on Ordinis availability + retention.

**Вариант B: snapshot в свою БД**

```sql
CREATE TABLE imaging_order (
  id              UUID PRIMARY KEY,
  spacecraft_business_key  VARCHAR(256),
  reference_snapshot       JSONB,      -- копии записей, не FK
  created_at      TIMESTAMPTZ DEFAULT NOW()
);
```

Pro: read-only к Ordinis, не зависит от availability/retention.
Con: stale если Ordinis retroactively исправил данные.

Альтум использует **Вариант B** (snapshot) для legal compliance — заказы
attestation требуют не depending on external service.

## Update с конкретным validFrom

Не всегда нужно `validFrom = now()`. Можно указать:

```bash
PUT /api/v1/dictionaries/spacecraft/records/RESURS-P-3
{
  "validFrom": "2026-05-15T00:00:00+03:00",
  "data": { "name": "Ресурс-П № 3 (новое имя с 15 мая)" }
}
```

Старая версия закрывается с `validTo = 2026-05-15`. Новая `validFrom =
2026-05-15`. До 15 мая `?at=now` возвращает старую, после — новую.

## Дальше

- [Cross-references (FK)](x-references.md) — связи между записями
- [Caching](caching.md) — учитываем bitemporal в инвалидации
