From 7fbe86a943dcf35d4107ce7e8d96fd679aec974a Mon Sep 17 00:00:00 2001 From: TolaMironcenko Date: Sat, 31 Oct 2026 23:51:38 +0700 Subject: [PATCH] Contract v2: clarify ids, rates, recurring, ledger and attachment details Co-Authored-By: Claude Opus 5.5 --- docs/CONTRACT_V2.md | 53 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 41 insertions(+), 12 deletions(-) diff --git a/docs/CONTRACT_V2.md b/docs/CONTRACT_V2.md index a899188..968da03 100644 --- a/docs/CONTRACT_V2.md +++ b/docs/CONTRACT_V2.md @@ -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: ` на `/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