Contract v2: clarify ids, rates, recurring, ledger and attachment details
CI / Rust (fmt / clippy / test / build) (push) Successful in 1m4s
CI / HTTP/JSON reference + SQLite persistence (push) Successful in 1m36s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
wtandClaude Opus 5.5 committed 2026-10-31 23:51:38 +07:00
1 parent dfb4fe4e1a
commit 7fbe86a943
1 file changed
+41 -12
+41 -12
View File
@@ -24,7 +24,10 @@ tombstone и last-write-wins не меняются. Клиент v1 продол
внутри клиентов — целые сотые. Курс валют — **строка** десятичного числа.
5. Даты — ISO 8601 UTC. Интервалы — `[начало, конец)`.
6. Коды валют — ISO 4217 в верхнем регистре (`RUB`, `EUR`, `USD`, `SEK`, …).
7. Идентификаторы записей — UUID в нижнем регистре, кроме `setting` (ключ).
7. Новые записи получают UUID в нижнем регистре (кроме `setting`, где `id` —
ключ). Сервер сравнивает `id` как есть, с учётом регистра, поэтому клиент
хранит `id` полученной записи в точности как пришёл и отправляет его так же
(старые клиенты могли создавать UUID в верхнем регистре).
### Возможности сервера
@@ -70,7 +73,8 @@ tombstone и last-write-wins не меняются. Клиент v1 продол
```
`kindRawValue`: `cash` | `card` | `deposit` | `other` (незнакомое → `other`).
`currency` по умолчанию — базовая.
Клиент v2 записывает `currency` явно при создании счёта. Отсутствие поля
(старая запись) = текущая базовая валюта.
### transfer
@@ -146,9 +150,10 @@ tombstone и last-write-wins не меняются. Клиент v1 продол
- Валюта транзакции: `currency`, иначе валюта её счёта, иначе базовая.
- Перевод суммы `x` из `C` в базовую `B`: при `C == B` без изменений; иначе
берётся последняя (по `date`, затем по `updatedAt`) запись `rate` с
`from=C,to=B` или обратная `from=B,to=C` (тогда делим). Если подходящих
записей несколько с одной датой, прямая пара важнее обратной.
берётся запись `rate` с `from=C,to=B` или обратная `from=B,to=C` (тогда
делим). Выбор: самая поздняя `date` не позже даты расчёта (курсы с будущей
датой не используются); при равной дате прямая пара важнее обратной; затем
более поздний `updatedAt`.
- Результат округляется до сотых половиной вверх (от нуля). Считать через
целые сотые и десятичную строку курса, не через `double`-накопление.
- Нет курса → сумма **не входит** в общие итоги, клиент показывает
@@ -177,7 +182,8 @@ tombstone и last-write-wins не меняются. Клиент v1 продол
и время суток): `daily` — каждые `interval` дней, `weekly` — каждые
`7·interval` дней, `monthly` — каждые `interval` месяцев в тот же день
(если дня нет — последний день месяца), `yearly` — то же с годами
(29 февраля → 28 февраля). Повторение после `endDate` не создаётся.
(29 февраля → 28 февраля). `endDate` сравнивается по UTC-дате и включается:
повторение в день `endDate` создаётся, после — нет.
- `occurrence` = UTC-дата `YYYY-MM-DD`; дата транзакции = эта дата + время
суток `startDate`.
- ID транзакции **детерминирован**: UUID v5 от пространства имён
@@ -190,8 +196,11 @@ tombstone и last-write-wins не меняются. Клиент v1 продол
повторение не воскрешается, т.к. `generatedThrough` уже позади. Не более
366 повторений за раз.
- `remind`: клиент показывает повторения из того же окна как «ожидают»;
«Записать» создаёт транзакцию с тем же детерминированным ID, «Пропустить»
только двигает `generatedThrough`.
«Записать» создаёт транзакцию с тем же детерминированным ID и двигает
`generatedThrough` только через подряд обработанные повторения (если
записать более позднее раньше, более раннее остаётся ожидать).
«Пропустить» двигает `generatedThrough` до этого повторения включительно —
более ранние ожидающие тоже считаются пропущенными.
- `isPaused` — ничего не создаётся и не напоминается.
- Часы регулярные операции не создают (их создают телефон и десктоп).
@@ -222,12 +231,23 @@ tombstone и last-write-wins не меняются. Клиент v1 продол
- Пространство выбирается заголовком `X-Balance-Ledger: <id>` на
`/v1/sync` и `/v1/attachments/*`. Без заголовка — личное пространство.
Не участник → `403 forbidden`, несуществующее → `404 not_found`.
- Владелец не может выйти, пока есть участники (только удалить пространство).
- Владелец не может выйти (`409 owner_cannot_leave`), даже из пустого
пространства — только удалить его.
- Ответы: создание пространства и приглашения — `201`; вход по коду и
переименование — `200`; удаление пространства и участника — `204`.
Повторный вход участника возвращает пространство и не тратит код.
`/v1/ledgers/join` ограничен по частоте вместе со входом (30 запросов в
минуту с IP).
- Клиент, получивший `403`/`404` для выбранного пространства, может сам
вернуться в личное (Flutter) или показать ошибку и предложить переключиться
(часы).
- Все участники равны в правах на записи (last-write-wins как в v1).
- Клиент хранит данные пространства отдельно: scope = сервер + пользователь
+ пространство. Переключение пространства не переносит записи. Курсор и
очередь изменений — свои у каждого пространства.
- Ошибки: `invalid_code` (400), `forbidden` (403), `not_found` (404),
- Ошибки: `invalid_code` (400), `invalid_name` (400: пустое после `trim`,
длиннее 100 символов или с управляющими символами), `invalid_json` (400),
`forbidden` (403), `not_found` (404, в том числе удаление не-участника),
`owner_cannot_leave` (409). Коды приглашений хранятся на сервере только
как хеш.
@@ -241,11 +261,20 @@ tombstone и last-write-wins не меняются. Клиент v1 продол
- Порядок клиента: сначала PUT байтов, потом запись `attachment` через sync.
- Файлы хранятся в пространстве; один и тот же файл в двух пространствах
хранится дважды.
- Ответ PUT: `{"sha256","size","contentType"}`. GET отдаёт
`Content-Disposition: attachment`.
- Когда в пространстве не осталось живых записей `attachment` с этим
`sha256`, сервер может удалить файл.
`sha256`, сервер может удалить файл. Файл, на который ещё не ссылалась ни
одна запись, хранится не меньше 24 часов после загрузки (клиент загружает
байты раньше записи).
- Лимит тела 10 МиБ действует только для загрузки вложения; остальные
запросы — 2 МиБ, как в v1.
- Ошибки: `payload_too_large` (413), `unsupported_media_type` (415),
`hash_mismatch` (400).
`hash_mismatch` (400, в том числе неверный формат хеша в пути).
- Клиент скачивает файл лениво при просмотре и кеширует локально.
- Сообщение ошибки для незнакомой сущности на сервере осталось как в v1
(«entity must be transaction, category, or budget») ради совместимости с
записанными ответами; код `invalid_change` верный.
## 9. Совместимость с клиентами v1