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