added docs and AGENTS.md to Balance for ios,macos,watchos
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
# AGENTS.md
|
||||
|
||||
## Назначение проекта
|
||||
|
||||
**Balance** — нативное мультиплатформенное приложение для учёта личных финансов:
|
||||
- iOS 17+
|
||||
- macOS 14+
|
||||
- watchOS 10+
|
||||
|
||||
Основной UI написан на SwiftUI, локальное хранилище — SwiftData. Проект поддерживает два взаимоисключающих способа синхронизации данных: CloudKit или собственный HTTP(S)-сервер.
|
||||
|
||||
## Структура репозитория
|
||||
|
||||
```text
|
||||
Balance/
|
||||
├── Balance/ # Общий код iOS/macOS/watchOS
|
||||
│ ├── App/ # Точка входа iOS и корневой ContentView
|
||||
│ ├── Models/ # SwiftData-модели и доменные типы
|
||||
│ ├── Shared/ # Общее состояние, контейнер SwiftData, server sync
|
||||
│ ├── Utilities/ # Чистые функции расчётов и форматирование
|
||||
│ └── Views/ # Общие SwiftUI-экраны
|
||||
├── BalanceMac/ # macOS entry point и macOS-specific UI
|
||||
├── BalanceWatch/ # watchOS entry point и компактный UI
|
||||
├── BalanceTests/ # XCTest
|
||||
├── Config/ # Entitlements
|
||||
├── Balance.xcodeproj/
|
||||
└── docs/ # Архитектура, модель данных, sync и dev guide
|
||||
```
|
||||
|
||||
## Правила разработки
|
||||
|
||||
### 1. Сначала определяй слой изменения
|
||||
|
||||
- **Модель/данные:** `Balance/Models/`
|
||||
- **Расчёты:** `Balance/Utilities/FinanceCalculations.swift`
|
||||
- **Общий сервис:** `Balance/Shared/`
|
||||
- **Общий UI:** `Balance/Views/`
|
||||
- **Только Mac:** `BalanceMac/`
|
||||
- **Только Watch:** `BalanceWatch/`
|
||||
- **Тесты:** `BalanceTests/`
|
||||
|
||||
Не дублируй доменную логику в iOS/macOS/watchOS-экранах, если её можно выразить общей функцией или сервисом.
|
||||
|
||||
### 2. SwiftData
|
||||
|
||||
Текущая схема зарегистрирована в `BalanceModelContainer.schema`:
|
||||
|
||||
- `FinanceTransaction`
|
||||
- `MonthlyBudget`
|
||||
- `CustomCategory`
|
||||
- `SyncTombstone`
|
||||
|
||||
При изменении `@Model` учитывай миграции существующих локальных баз. Не удаляй и не переименовывай persisted-поля без явной стратегии миграции.
|
||||
|
||||
### 3. Изменения финансовых записей
|
||||
|
||||
Любая редактируемая сервером сущность должна обновлять `syncUpdatedAt`.
|
||||
|
||||
Для удаления используй:
|
||||
|
||||
```swift
|
||||
modelContext.deleteForSync(...)
|
||||
```
|
||||
|
||||
а не прямой `modelContext.delete(...)`, если сущность должна удалиться и на других устройствах. Это создаёт tombstone в `SyncDeletionStore`.
|
||||
|
||||
### 4. Баланс и аналитика
|
||||
|
||||
`FinanceCalculations` содержит центральные правила:
|
||||
|
||||
- общий баланс = доходы − расходы;
|
||||
- `isBalanceAdjustment == true` учитывается в общем балансе;
|
||||
- корректировки не учитываются в периодической аналитике;
|
||||
- расходы группируются по `categoryName`.
|
||||
|
||||
Не меняй эти правила только в одном UI. При изменении поведения обновляй `BalanceTests/FinanceCalculationsTests.swift`.
|
||||
|
||||
### 5. Категории
|
||||
|
||||
Системные категории находятся в `FinanceCategory`.
|
||||
|
||||
Пользовательские категории — `CustomCategory`. Их имя является важной частью текущей модели: операции и бюджеты хранят snapshot имени/иконки/emoji/цвета. При переименовании пользовательской категории текущий код обновляет связанные операции и бюджеты — это поведение нужно сохранять.
|
||||
|
||||
### 6. Синхронизация
|
||||
|
||||
`ServerAccountStore` — единая точка серверной авторизации и синхронизации.
|
||||
|
||||
Не хранить пароль в `UserDefaults`, SwiftData или файлах. Сессия хранится в Keychain.
|
||||
|
||||
При изменении URL сервера:
|
||||
1. текущая сессия очищается;
|
||||
2. cursor/push state удаляется;
|
||||
3. пользователь должен авторизоваться заново.
|
||||
|
||||
Не включай одновременно CloudKit и собственный сервер как два источника синхронизации одной и той же базы.
|
||||
|
||||
### 7. Безопасность
|
||||
|
||||
- Для удалённого сервера использовать HTTPS.
|
||||
- HTTP разрешён клиентом только для `localhost`, `127.0.0.1` и `::1`.
|
||||
- Access/refresh tokens — только Keychain.
|
||||
- Не логируй токены, пароли или полные Authorization headers.
|
||||
- Не добавляй секреты, production URLs или credentials в репозиторий.
|
||||
|
||||
### 8. UI
|
||||
|
||||
Используй SwiftUI и системные компоненты Apple.
|
||||
|
||||
- Общие компоненты размещай в `Balance/Views/Components/`.
|
||||
- Не тащи macOS-only API в общий код без `#if os(macOS)`.
|
||||
- Не тащи watchOS-only API в общий код без условной компиляции.
|
||||
- На Watch предпочитай короткие списки, `Form`, `List`, `NavigationStack` и компактные действия.
|
||||
|
||||
### 9. Тестирование
|
||||
|
||||
Минимум перед PR:
|
||||
|
||||
```bash
|
||||
xcodebuild -project Balance.xcodeproj -scheme Balance -destination 'platform=iOS Simulator,name=<available iPhone>' test
|
||||
```
|
||||
|
||||
Также полезно проверить Mac и Watch schemes:
|
||||
|
||||
```bash
|
||||
xcodebuild -project Balance.xcodeproj -scheme BalanceMac -showBuildSettings
|
||||
xcodebuild -project Balance.xcodeproj -scheme BalanceWatch -showBuildSettings
|
||||
```
|
||||
|
||||
Если имя симулятора отличается, сначала получи список:
|
||||
|
||||
```bash
|
||||
xcrun simctl list devices available
|
||||
```
|
||||
|
||||
Не добавляй тесты, которые зависят от текущей даты или локали без явной фиксации `Calendar`, `Date` и formatter.
|
||||
|
||||
## Commit/PR checklist
|
||||
|
||||
- [ ] Изменение находится в правильном target/layer.
|
||||
- [ ] Нет дублирования финансовой логики.
|
||||
- [ ] Все серверные изменения сущностей обновляют `syncUpdatedAt`.
|
||||
- [ ] Удаления синхронизируемых сущностей используют tombstone.
|
||||
- [ ] Изменения расчётов покрыты XCTest.
|
||||
- [ ] Проверены iOS/macOS/watchOS-условия компиляции.
|
||||
- [ ] Не добавлены секреты и токены.
|
||||
- [ ] Документация обновлена, если изменились данные, sync API или setup.
|
||||
@@ -94,3 +94,15 @@ CloudKit является опциональным режимом. Для нег
|
||||
## Устранение проблем сборки
|
||||
|
||||
После перехода с версии 2.0–2.0.2 выполните **Product → Clean Build Folder**. Если Xcode продолжает использовать старую конфигурацию watchOS target, закройте Xcode и удалите DerivedData для проекта `Balance`, затем снова откройте проект.
|
||||
|
||||
## Документация проекта
|
||||
|
||||
- [`AGENTS.md`](AGENTS.md) — правила для AI-агентов и разработчиков, структура проекта, invariants и checklist.
|
||||
- [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) — архитектура приложения и ответственность слоёв.
|
||||
- [`docs/DATA_MODEL.md`](docs/DATA_MODEL.md) — SwiftData-модели и финансовые инварианты.
|
||||
- [`docs/SYNC.md`](docs/SYNC.md) — протокол авторизации и синхронизации с собственным сервером.
|
||||
- [`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md) — локальная разработка, сборка, тесты и signing.
|
||||
- [`docs/PRODUCT.md`](docs/PRODUCT.md) — функциональная документация.
|
||||
|
||||
> Примечание: текущий архив содержит Apple-клиент Balance. Внешний Go-сервер, упомянутый в старой версии README, в этом архиве отсутствует; `docs/SYNC.md` документирует клиентский контракт по коду `ServerSync.swift`.
|
||||
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
# Архитектура Balance
|
||||
|
||||
## Обзор
|
||||
|
||||
Balance использует простую feature-oriented структуру поверх SwiftUI + SwiftData:
|
||||
|
||||
```text
|
||||
SwiftUI Views
|
||||
│
|
||||
├── FinanceCalculations
|
||||
│
|
||||
├── SwiftData ModelContext / @Query
|
||||
│
|
||||
└── ServerAccountStore
|
||||
│
|
||||
├── Keychain (session)
|
||||
├── UserDefaults (server URL, cursor, sync metadata)
|
||||
└── HTTP(S) API /v1/*
|
||||
```
|
||||
|
||||
Общий код находится в `Balance/Balance/`. Платформенные оболочки находятся в `BalanceMac/` и `BalanceWatch/`.
|
||||
|
||||
## Точки входа
|
||||
|
||||
| Platform | Entry point | Основной root |
|
||||
|---|---|---|
|
||||
| iOS | `Balance/App/BalanceApp.swift` | `ContentView` |
|
||||
| macOS | `BalanceMac/BalanceMacApp.swift` | `MacContentView` |
|
||||
| watchOS | `BalanceWatch/BalanceWatchApp.swift` | `WatchRootView` |
|
||||
|
||||
Каждое приложение создаёт `BalanceModelContainer.make()` и передаёт его через `.modelContainer(...)`.
|
||||
|
||||
## Хранение данных
|
||||
|
||||
`BalanceModelContainer` выбирает конфигурацию:
|
||||
|
||||
1. если `BalanceCloudKitEnabled == true` и CloudKit container создаётся — SwiftData + CloudKit;
|
||||
2. иначе — локальная SwiftData база;
|
||||
3. если локальная база не создаётся — используется in-memory recovery container.
|
||||
|
||||
Recovery container предназначен для предотвращения падения приложения при невозможности открыть persistent store; он не является способом восстановления данных.
|
||||
|
||||
## Модели
|
||||
|
||||
### FinanceTransaction
|
||||
|
||||
Основная финансовая операция:
|
||||
|
||||
- UUID;
|
||||
- сумма `Double`;
|
||||
- дата;
|
||||
- заметка;
|
||||
- snapshot категории;
|
||||
- тип `income` / `expense`;
|
||||
- флаг корректировки баланса;
|
||||
- `syncUpdatedAt`.
|
||||
|
||||
`TransactionKind` сериализуется через `kindRawValue`, а наружу предоставляется computed property `kind`.
|
||||
|
||||
### CustomCategory
|
||||
|
||||
Пользовательская категория:
|
||||
|
||||
- имя;
|
||||
- SF Symbol или emoji;
|
||||
- цвет;
|
||||
- тип операции;
|
||||
- `createdAt`;
|
||||
- `syncUpdatedAt`.
|
||||
|
||||
### MonthlyBudget
|
||||
|
||||
Месячный лимит для категории:
|
||||
|
||||
- snapshot категории;
|
||||
- `limit`;
|
||||
- `monthStart`;
|
||||
- `syncUpdatedAt`.
|
||||
|
||||
### SyncTombstone
|
||||
|
||||
SwiftData-модель для совместимости схемы, но клиентский журнал удалений фактически ведётся через `SyncDeletionStore` в `UserDefaults`. Это сделано специально, чтобы удаление не зависело от миграции основной SwiftData-схемы.
|
||||
|
||||
## Расчёты
|
||||
|
||||
`FinanceCalculations` — чистый доменный слой:
|
||||
|
||||
- `totalBalance` считает весь баланс;
|
||||
- `monthInterval` возвращает календарный месяц;
|
||||
- `summary` считает доходы/расходы периода;
|
||||
- `spendingByCategory` агрегирует расходы.
|
||||
|
||||
Корректировки (`isBalanceAdjustment`) влияют на `totalBalance`, но исключаются из `summary` и `spendingByCategory`.
|
||||
|
||||
## UI/features
|
||||
|
||||
Общие features:
|
||||
|
||||
- Dashboard
|
||||
- Transactions
|
||||
- Budgets
|
||||
- Analytics
|
||||
- Categories
|
||||
- Settings
|
||||
- Balance Adjustment
|
||||
- CSV export
|
||||
- Server account/sync
|
||||
|
||||
macOS добавляет полноценную desktop-навигацию и расширенный аналитический экран.
|
||||
|
||||
watchOS использует отдельные экраны с теми же моделями и расчётами, но адаптированным UI.
|
||||
|
||||
## Состояние приложения
|
||||
|
||||
`@Query` используется как основной способ наблюдения за SwiftData. Feature views не должны создавать собственные параллельные кеши финансовых сущностей без необходимости.
|
||||
|
||||
Глобальное состояние серверного аккаунта сосредоточено в `ServerAccountStore.shared`.
|
||||
|
||||
Настройки пользователя хранятся через `@AppStorage`, в частности:
|
||||
|
||||
- `currencyCode`;
|
||||
- `appTheme`;
|
||||
- server URL;
|
||||
- sync cursor;
|
||||
- last push/sync timestamps.
|
||||
|
||||
## Принцип изменения
|
||||
|
||||
Предпочтительный поток:
|
||||
|
||||
```text
|
||||
UI action
|
||||
→ update/insert/delete SwiftData
|
||||
→ update syncUpdatedAt / tombstone
|
||||
→ UI обновляется через @Query
|
||||
→ ServerAccountStore синхронизирует изменения
|
||||
```
|
||||
|
||||
Не следует делать HTTP-запросы непосредственно из feature view.
|
||||
@@ -0,0 +1,97 @@
|
||||
# Модель данных
|
||||
|
||||
## Persisted schema
|
||||
|
||||
```text
|
||||
FinanceTransaction
|
||||
CustomCategory
|
||||
MonthlyBudget
|
||||
SyncTombstone
|
||||
```
|
||||
|
||||
## FinanceTransaction
|
||||
|
||||
| Поле | Тип | Назначение |
|
||||
|---|---|---|
|
||||
| `id` | UUID | стабильный идентификатор |
|
||||
| `amount` | Double | положительная сумма |
|
||||
| `date` | Date | дата операции |
|
||||
| `note` | String | заметка |
|
||||
| `categoryName` | String | snapshot имени категории |
|
||||
| `categoryIcon` | String | SF Symbol |
|
||||
| `categoryEmoji` | String | emoji, если используется |
|
||||
| `categoryColorName` | String | имя цвета |
|
||||
| `isBalanceAdjustment` | Bool | специальная корректировка |
|
||||
| `kindRawValue` | String | `income` / `expense` |
|
||||
| `syncUpdatedAt` | Date | версия изменения |
|
||||
|
||||
## CustomCategory
|
||||
|
||||
| Поле | Тип |
|
||||
|---|---|
|
||||
| `id` | UUID |
|
||||
| `name` | String |
|
||||
| `icon` | String |
|
||||
| `emoji` | String |
|
||||
| `colorName` | String |
|
||||
| `kindRawValue` | String |
|
||||
| `createdAt` | Date |
|
||||
| `syncUpdatedAt` | Date |
|
||||
|
||||
Имя пользовательской категории уникально без учёта регистра/диакритики относительно системных и других пользовательских категорий.
|
||||
|
||||
При редактировании категории текущая реализация распространяет новое имя и presentation metadata на связанные транзакции и бюджеты.
|
||||
|
||||
## MonthlyBudget
|
||||
|
||||
| Поле | Тип |
|
||||
|---|---|
|
||||
| `id` | UUID |
|
||||
| `categoryName` | String |
|
||||
| `categoryIcon` | String |
|
||||
| `categoryEmoji` | String |
|
||||
| `limit` | Double |
|
||||
| `monthStart` | Date |
|
||||
| `syncUpdatedAt` | Date |
|
||||
|
||||
Бюджет идентифицируется UUID, но связывается с категорией по сохранённому `categoryName`.
|
||||
|
||||
## SyncTombstone
|
||||
|
||||
| Поле | Тип |
|
||||
|---|---|
|
||||
| `id` | UUID |
|
||||
| `entity` | String |
|
||||
| `recordID` | String |
|
||||
| `updatedAt` | Date |
|
||||
|
||||
В текущем клиентском sync-flow tombstone удалений хранится в `SyncDeletionStore` (`UserDefaults`) и отправляется как `deleted: true`.
|
||||
|
||||
Поддерживаемые entity:
|
||||
|
||||
```text
|
||||
transaction
|
||||
category
|
||||
budget
|
||||
```
|
||||
|
||||
## Финансовые инварианты
|
||||
|
||||
1. `amount` не должен быть отрицательным через пользовательский UI.
|
||||
2. Тип операции хранится в `kindRawValue`.
|
||||
3. Баланс:
|
||||
`sum(income.amount) - sum(expense.amount)`.
|
||||
4. Корректировка баланса является обычной транзакцией для общего баланса, но не участвует в периодической аналитике.
|
||||
5. `syncUpdatedAt` должен меняться при каждом изменении синхронизируемой сущности.
|
||||
6. Удаляемая синхронизируемая сущность должна создавать tombstone.
|
||||
|
||||
## Почему категория хранится snapshot'ом
|
||||
|
||||
Операция хранит `categoryName`, `categoryIcon`, `categoryEmoji` и `categoryColorName`, а не relationship на `CustomCategory`.
|
||||
|
||||
Это позволяет:
|
||||
- не терять отображение старой операции при удалении категории;
|
||||
- синхронизировать запись без SwiftData relationship;
|
||||
- поддерживать системные категории без persisted object.
|
||||
|
||||
Из-за этого переименование пользовательской категории должно явно обновлять связанные записи — именно так работает текущая реализация.
|
||||
@@ -0,0 +1,180 @@
|
||||
# Разработка
|
||||
|
||||
## Требования
|
||||
|
||||
- Xcode 16+
|
||||
- Swift 5
|
||||
- iOS 17+
|
||||
- macOS 14+
|
||||
- watchOS 10+
|
||||
|
||||
В проекте нет Swift Package Manager-зависимостей.
|
||||
|
||||
## Открытие
|
||||
|
||||
```bash
|
||||
open Balance.xcodeproj
|
||||
```
|
||||
|
||||
Доступные shared schemes:
|
||||
|
||||
```text
|
||||
Balance
|
||||
BalanceMac
|
||||
BalanceWatch
|
||||
```
|
||||
|
||||
## Сборка
|
||||
|
||||
Сначала можно посмотреть схемы:
|
||||
|
||||
```bash
|
||||
xcodebuild -project Balance.xcodeproj -list
|
||||
```
|
||||
|
||||
Для CI используйте явный `-scheme` и подходящий `-destination`.
|
||||
|
||||
Пример проверки компиляции:
|
||||
|
||||
```bash
|
||||
xcodebuild \
|
||||
-project Balance.xcodeproj \
|
||||
-scheme Balance \
|
||||
-destination 'generic/platform=iOS' \
|
||||
build
|
||||
```
|
||||
|
||||
Для Mac:
|
||||
|
||||
```bash
|
||||
xcodebuild \
|
||||
-project Balance.xcodeproj \
|
||||
-scheme BalanceMac \
|
||||
-destination 'generic/platform=macOS' \
|
||||
build
|
||||
```
|
||||
|
||||
Для Watch:
|
||||
|
||||
```bash
|
||||
xcodebuild \
|
||||
-project Balance.xcodeproj \
|
||||
-scheme BalanceWatch \
|
||||
-destination 'generic/platform=watchOS' \
|
||||
build
|
||||
```
|
||||
|
||||
## Тесты
|
||||
|
||||
Основной тестовый target:
|
||||
|
||||
```text
|
||||
BalanceTests
|
||||
```
|
||||
|
||||
Запуск:
|
||||
|
||||
```bash
|
||||
xcodebuild \
|
||||
-project Balance.xcodeproj \
|
||||
-scheme Balance \
|
||||
-destination 'platform=iOS Simulator,name=<available iPhone>' \
|
||||
test
|
||||
```
|
||||
|
||||
Тесты сосредоточены на финансовых расчётах и sync deletion journal.
|
||||
|
||||
## Signing
|
||||
|
||||
Проект рассчитан на запуск с собственной Team.
|
||||
|
||||
Для обычной локальной разработки:
|
||||
- выберите свою Team в Signing & Capabilities;
|
||||
- используйте локальное SwiftData;
|
||||
- CloudKit можно не включать.
|
||||
|
||||
Bundle identifiers текущего проекта:
|
||||
|
||||
```text
|
||||
com.tolamironcenko.Balance
|
||||
com.tolamironcenko.Balance.mac
|
||||
com.tolamironcenko.Balance.watchkitapp
|
||||
```
|
||||
|
||||
При публикации их следует заменить на собственные identifiers.
|
||||
|
||||
## Local mode
|
||||
|
||||
Без `BalanceCloudKitEnabled=YES` приложение использует локальное SwiftData-хранилище.
|
||||
|
||||
Это рекомендуемый режим для:
|
||||
- UI-разработки;
|
||||
- unit tests;
|
||||
- Personal Team;
|
||||
- отладки без Apple Developer Program.
|
||||
|
||||
## CloudKit mode
|
||||
|
||||
CloudKit является опциональным.
|
||||
|
||||
Нужно:
|
||||
1. иметь платную Apple Developer Team;
|
||||
2. создать собственный CloudKit container;
|
||||
3. заменить placeholder container ID в entitlements;
|
||||
4. включить iCloud/CloudKit capabilities;
|
||||
5. выставить `BalanceCloudKitEnabled=YES`.
|
||||
|
||||
Не смешивайте CloudKit и собственный сервер как два активных sync backends для одной пользовательской базы.
|
||||
|
||||
## Server mode
|
||||
|
||||
В Settings укажите URL сервера, затем зарегистрируйтесь или войдите.
|
||||
|
||||
Для физического устройства удалённый адрес должен быть HTTPS.
|
||||
|
||||
Для локального сервера допустим:
|
||||
|
||||
```text
|
||||
http://localhost:...
|
||||
http://127.0.0.1:...
|
||||
```
|
||||
|
||||
На физическом iPhone `localhost` означает сам iPhone, а не Mac-разработчика.
|
||||
|
||||
## Экспорт CSV
|
||||
|
||||
CSV export реализован в:
|
||||
|
||||
```text
|
||||
Balance/Views/Settings/CSVDocument.swift
|
||||
```
|
||||
|
||||
Экспорт доступен на iOS и macOS.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Xcode использует старые build artifacts
|
||||
|
||||
```text
|
||||
Product → Clean Build Folder
|
||||
```
|
||||
|
||||
Если проблема сохраняется, удалите DerivedData проекта и заново откройте Xcode.
|
||||
|
||||
### SwiftData schema error
|
||||
|
||||
Проверьте изменения `@Model` и не меняйте persisted fields без migration strategy.
|
||||
|
||||
### Sync не работает
|
||||
|
||||
Проверьте по порядку:
|
||||
1. server URL;
|
||||
2. HTTPS/localhost rule;
|
||||
3. авторизацию;
|
||||
4. дату последней синхронизации;
|
||||
5. cursor;
|
||||
6. совместимость версии Balance Server.
|
||||
|
||||
### Данные не удаляются на другом устройстве
|
||||
|
||||
Проверьте, что удаление выполнено через `deleteForSync(...)`, а не прямой `delete(...)`.
|
||||
@@ -0,0 +1,101 @@
|
||||
# Функциональная документация
|
||||
|
||||
## Dashboard
|
||||
|
||||
Показывает:
|
||||
- общий баланс;
|
||||
- доходы текущего месяца;
|
||||
- расходы текущего месяца;
|
||||
- последние операции;
|
||||
- быстрый переход к аналитике, операциям, бюджетам, категориям, корректировке и настройкам.
|
||||
|
||||
## Операции
|
||||
|
||||
Поддерживаются:
|
||||
- доходы;
|
||||
- расходы;
|
||||
- категория;
|
||||
- сумма;
|
||||
- дата;
|
||||
- заметка;
|
||||
- редактирование;
|
||||
- удаление;
|
||||
- поиск;
|
||||
- фильтрация по типу.
|
||||
|
||||
## Корректировка баланса
|
||||
|
||||
Пользователь задаёт фактический баланс.
|
||||
|
||||
Разница между фактическим и текущим балансом сохраняется как специальная операция с `isBalanceAdjustment=true`.
|
||||
|
||||
Такая запись:
|
||||
- влияет на общий баланс;
|
||||
- не влияет на месячные income/expenses;
|
||||
- не попадает в spending-by-category.
|
||||
|
||||
Это позволяет исправить расхождение между приложением и реальным счётом, не искажая историю расходов.
|
||||
|
||||
## Аналитика
|
||||
|
||||
Доступны периоды:
|
||||
- месяц;
|
||||
- 3 месяца;
|
||||
- 12 месяцев.
|
||||
|
||||
Используются:
|
||||
- доходы;
|
||||
- расходы;
|
||||
- результат;
|
||||
- savings rate;
|
||||
- структура расходов по категориям;
|
||||
- динамика доходов и расходов.
|
||||
|
||||
## Бюджеты
|
||||
|
||||
Для категории можно задать месячный лимит.
|
||||
|
||||
Budget хранится отдельно от транзакций и синхронизируется через собственную entity `budget`.
|
||||
|
||||
## Категории
|
||||
|
||||
Системные категории разделены на income/expense.
|
||||
|
||||
Пользователь может создать свою категорию:
|
||||
- название;
|
||||
- тип;
|
||||
- SF Symbol или emoji;
|
||||
- цвет.
|
||||
|
||||
Имя категории не должно дублировать существующее имя.
|
||||
|
||||
## Настройки
|
||||
|
||||
- валюта: RUB, EUR, USD, SEK;
|
||||
- тема: system/light/dark;
|
||||
- сервер и синхронизация;
|
||||
- CSV export на iOS/macOS.
|
||||
|
||||
## watchOS
|
||||
|
||||
Watch-приложение предоставляет компактные версии:
|
||||
- Dashboard;
|
||||
- Add/Edit transaction;
|
||||
- All transactions;
|
||||
- Analytics;
|
||||
- Budgets;
|
||||
- Categories;
|
||||
- Balance adjustment;
|
||||
- Settings.
|
||||
|
||||
## macOS
|
||||
|
||||
Mac-приложение предоставляет:
|
||||
- dashboard;
|
||||
- полную аналитику;
|
||||
- операции;
|
||||
- бюджеты;
|
||||
- категории;
|
||||
- корректировку;
|
||||
- настройки;
|
||||
- CSV export.
|
||||
@@ -0,0 +1,232 @@
|
||||
# Серверная синхронизация
|
||||
|
||||
## Назначение
|
||||
|
||||
`Balance/Shared/ServerSync.swift` реализует авторизацию и двустороннюю синхронизацию с собственным сервером.
|
||||
|
||||
Сервер не входит в этот Xcode-проект; клиент ожидает API под базовым URL сервера.
|
||||
|
||||
## Хранилище сессии
|
||||
|
||||
Сессия содержит:
|
||||
|
||||
- access token;
|
||||
- refresh token;
|
||||
- expiry;
|
||||
- user id/email.
|
||||
|
||||
Сессия хранится в Keychain с service `BalanceServer`.
|
||||
|
||||
В `UserDefaults` хранятся только несекретные sync-настройки:
|
||||
|
||||
- server URL;
|
||||
- device ID;
|
||||
- cursor;
|
||||
- last push;
|
||||
- last sync.
|
||||
|
||||
Пароль клиента не сохраняется.
|
||||
|
||||
## Endpoints
|
||||
|
||||
Клиент использует POST JSON:
|
||||
|
||||
```text
|
||||
POST /v1/auth/register
|
||||
POST /v1/auth/login
|
||||
POST /v1/auth/logout
|
||||
POST /v1/auth/refresh
|
||||
POST /v1/sync
|
||||
```
|
||||
|
||||
Для авторизованных запросов используется:
|
||||
|
||||
```http
|
||||
Authorization: Bearer <access-token>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
При HTTP 401 клиент один раз обновляет access token через refresh endpoint и повторяет запрос.
|
||||
|
||||
## Валидация server URL
|
||||
|
||||
Разрешены:
|
||||
|
||||
- `https://...`
|
||||
- `http://localhost...`
|
||||
- `http://127.0.0.1...`
|
||||
- `http://[::1]...`
|
||||
|
||||
Удалённый HTTP запрещён.
|
||||
|
||||
При смене адреса сервера клиент:
|
||||
- удаляет сессию;
|
||||
- удаляет sync cursor/push state;
|
||||
- требует новую авторизацию.
|
||||
|
||||
## Sync request
|
||||
|
||||
Логическая форма:
|
||||
|
||||
```json
|
||||
{
|
||||
"cursor": 123,
|
||||
"deviceId": "UUID",
|
||||
"changes": [],
|
||||
"pull": true
|
||||
}
|
||||
```
|
||||
|
||||
Для push `changes` заполнен, `pull=false`.
|
||||
|
||||
## Sync change
|
||||
|
||||
```json
|
||||
{
|
||||
"entity": "transaction",
|
||||
"id": "UUID",
|
||||
"deleted": false,
|
||||
"updatedAt": "ISO-8601",
|
||||
"payload": {}
|
||||
}
|
||||
```
|
||||
|
||||
Также сервер может возвращать `version` и `sequence`.
|
||||
|
||||
Поддерживаемые entity:
|
||||
|
||||
```text
|
||||
transaction
|
||||
category
|
||||
budget
|
||||
```
|
||||
|
||||
## Payloads
|
||||
|
||||
### transaction
|
||||
|
||||
```json
|
||||
{
|
||||
"amount": 1000,
|
||||
"date": "2026-09-08T10:00:00.000Z",
|
||||
"note": "Кофе",
|
||||
"categoryName": "Кофе",
|
||||
"categoryIcon": "cup.and.saucer.fill",
|
||||
"categoryEmoji": "☕️",
|
||||
"categoryColorName": "brown",
|
||||
"isBalanceAdjustment": false,
|
||||
"kindRawValue": "expense"
|
||||
}
|
||||
```
|
||||
|
||||
### category
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Кофе",
|
||||
"icon": "cup.and.saucer.fill",
|
||||
"emoji": "☕️",
|
||||
"colorName": "brown",
|
||||
"kindRawValue": "expense",
|
||||
"createdAt": "2026-09-01T10:00:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
### budget
|
||||
|
||||
```json
|
||||
{
|
||||
"categoryName": "Кофе",
|
||||
"categoryIcon": "cup.and.saucer.fill",
|
||||
"categoryEmoji": "☕️",
|
||||
"limit": 5000,
|
||||
"monthStart": "2026-09-01T00:00:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
Даты кодируются как ISO-8601; клиент принимает варианты с fractional seconds и без них.
|
||||
|
||||
## Push
|
||||
|
||||
Клиент отправляет:
|
||||
1. изменённые transactions;
|
||||
2. изменённые categories;
|
||||
3. изменённые budgets;
|
||||
4. tombstones.
|
||||
|
||||
Размер batch — до 100 записей.
|
||||
|
||||
Если тело запроса превышает 1.5 MB, batch делится рекурсивно. Один payload больше 256 KB считается слишком большим.
|
||||
|
||||
## Pull
|
||||
|
||||
Клиент отправляет `pull=true` с cursor и применяет изменения по `sequence`.
|
||||
|
||||
После успешного применения сохраняется новый cursor.
|
||||
|
||||
Если сервер сообщает `hasMore=true`, клиент продолжает pagination до завершения.
|
||||
|
||||
Клиент защищается от:
|
||||
- уменьшения cursor;
|
||||
- `hasMore=true` без продвижения cursor.
|
||||
|
||||
## Conflict resolution
|
||||
|
||||
При применении входящего изменения запись обновляется только если:
|
||||
|
||||
```text
|
||||
local.syncUpdatedAt < remote.updatedAt
|
||||
```
|
||||
|
||||
Для удаления используется аналогичная проверка:
|
||||
|
||||
```text
|
||||
local.syncUpdatedAt <= remote.updatedAt
|
||||
```
|
||||
|
||||
Это простая last-write-wins стратегия на уровне timestamp.
|
||||
|
||||
## Deletions
|
||||
|
||||
Удаление локально:
|
||||
|
||||
```swift
|
||||
modelContext.deleteForSync(transaction)
|
||||
```
|
||||
|
||||
создаёт запись в `SyncDeletionStore`.
|
||||
|
||||
При sync она отправляется как:
|
||||
|
||||
```json
|
||||
{
|
||||
"entity": "transaction",
|
||||
"id": "...",
|
||||
"deleted": true,
|
||||
"updatedAt": "..."
|
||||
}
|
||||
```
|
||||
|
||||
После получения соответствующего server change tombstone удаляется из локального журнала.
|
||||
|
||||
## Полная пересинхронизация
|
||||
|
||||
`resetSynchronization()` очищает cursor и last-push timestamp текущего server/user context, после чего выполняет обычный sync.
|
||||
|
||||
Это не удаляет локальные финансовые записи автоматически.
|
||||
|
||||
## Автосинхронизация
|
||||
|
||||
Apple clients запускают sync:
|
||||
- после авторизации;
|
||||
- при запуске/появлении root view;
|
||||
- затем примерно каждые 120 секунд, пока цикл жив;
|
||||
- вручную из Settings.
|
||||
|
||||
Фактическая фоновая работа всё равно зависит от lifecycle ОС.
|
||||
|
||||
## Совместимость
|
||||
|
||||
Клиент специально распознаёт `invalid_json` на `/v1/sync` и сообщает, что Balance Server нужно обновить до версии, совместимой с текущим архивом.
|
||||
|
||||
Клиент и сервер желательно обновлять согласованно.
|
||||
Reference in New Issue
Block a user