Files
family_budget/docs/backlog/db.md
2026-02-27 19:08:55 +03:00

148 lines
8.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Модель БД (PostgreSQL)
## Общие принципы
- Используется PostgreSQL, развёрнутый локально (например, на Synology).
- Основные сущности:
- `accounts` — банковские счета пользователя;
- `transactions` — движения средств по счетам;
- `category_rules` — правила автоматической категоризации транзакций (подготовка к SPA-редактору правил).
- Все суммы хранятся в минорных единицах (копейки) как `BIGINT`.
- Время хранится в `TIMESTAMPTZ` (временная зона сохраняется).
## Таблица `accounts`
Предназначение: хранение информации о счетах, по которым загружаются выписки.
Структура:
- `id BIGSERIAL PRIMARY KEY` — внутренний идентификатор счёта в системе.
- `bank TEXT NOT NULL` — код/имя банка (например, `"VTB"`).
- `account_number TEXT NOT NULL` — номер счёта в банке (как в выписке).
- `currency TEXT NOT NULL` — код валюты счёта (например, `"RUB"`).
Ограничения и индексы:
- Уникальность комбинации `(bank, account_number)` — один и тот же счёт в банке не должен дублироваться.
Рекомендуемый DDL:
```sql
CREATE TABLE accounts (
id BIGSERIAL PRIMARY KEY,
bank TEXT NOT NULL,
account_number TEXT NOT NULL,
currency TEXT NOT NULL
);
CREATE UNIQUE INDEX ux_accounts_bank_number
ON accounts(bank, account_number);
```
## Таблица `transactions`
Предназначение: хранение всех операций по счетам с привязкой к `accounts`.
Структура:
- `id BIGSERIAL PRIMARY KEY` — внутренний идентификатор транзакции.
- `account_id BIGINT NOT NULL` — внешний ключ (FK) на `accounts(id)`;
каждая транзакция жёстко привязана к одному счёту.
- `operation_at TIMESTAMPTZ NOT NULL` — дата и время операции.
- `amount_signed BIGINT NOT NULL` — сумма операции в копейках; знак отражает тип движения (приход/расход).
- `commission BIGINT NOT NULL` — комиссия по операции в копейках.
- `description TEXT NOT NULL` — описание операции из выписки.
- `direction TEXT NOT NULL` — направление движения:
- `"income"` — приход;
- `"expense"` — расход;
- `"transfer"` — перевод между своими счетами / на другие свои счета;
- `"internal"` — служебные/внутренние движения (опционально, по мере необходимости).
- `fingerprint TEXT NOT NULL` — вычисляемый хэш для обеспечения идемпотентности импорта.
- `category_id BIGINT` — ссылка на таблицу категорий (будет определена позже).
- `created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()` — время создания записи в БД.
- `updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()` — время последнего обновления записи.
Ограничения и индексы:
- Внешний ключ `account_id` ссылается на `accounts(id)` и обеспечивает целостность (нельзя создать транзакцию для несуществующего счёта).
- Уникальный индекс `(account_id, fingerprint)` обеспечивает идемпотентность: одна и та же операция не может быть загружена дважды.
- Опционально — CHECK-ограничение на поле `direction`.
Рекомендуемый DDL:
```sql
CREATE TABLE transactions (
id BIGSERIAL PRIMARY KEY,
account_id BIGINT NOT NULL REFERENCES accounts(id),
operation_at TIMESTAMPTZ NOT NULL,
amount_signed BIGINT NOT NULL,
commission BIGINT NOT NULL,
description TEXT NOT NULL,
direction TEXT NOT NULL,
fingerprint TEXT NOT NULL,
category_id BIGINT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE UNIQUE INDEX ux_transactions_account_fingerprint
ON transactions(account_id, fingerprint);
ALTER TABLE transactions
ADD CONSTRAINT chk_transactions_direction
CHECK (direction IN ('income', 'expense', 'transfer', 'internal'));
```
## Таблица `category_rules`
Предназначение: хранение правил автоматической категоризации транзакций на основе текста описания.
Структура:
- `id BIGSERIAL PRIMARY KEY` — идентификатор правила.
- `pattern TEXT NOT NULL` — строка-шаблон, вводимая пользователем через SPA (в простом виде).
- `match_type TEXT NOT NULL` — тип сопоставления:
- `"contains"` — простое вхождение подстроки;
- `"starts_with"` — строка начинается с шаблона;
- `"regex"` — регулярное выражение (формируется и/или проверяется в коде на основе пользовательского ввода).
- `category_id BIGINT NOT NULL` — ссылка на категорию (таблица категорий будет описана отдельно).
- `priority INT NOT NULL DEFAULT 0` — приоритет правила; чем выше число, тем раньше правило применяется при конфликте.
- `is_active BOOLEAN NOT NULL DEFAULT TRUE` — активно ли правило.
- `created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()` — время создания правила.
Логика приоритета:
- При авто-категоризации для транзакции ищутся все правила, которые ей соответствуют.
- Если совпало несколько правил, выбирается правило с максимальным `priority`.
- Это позволяет задавать общие правила с низким приоритетом и более точные (например, по конкретным мерчантам) с высоким приоритетом.
Рекомендуемый DDL:
```sql
CREATE TABLE category_rules (
id BIGSERIAL PRIMARY KEY,
pattern TEXT NOT NULL,
match_type TEXT NOT NULL,
category_id BIGINT NOT NULL,
priority INT NOT NULL DEFAULT 0,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
```
## Взаимосвязь JSON → БД при импорте
1. По полям `bank` и `statement.accountNumber` ищется или создаётся запись в `accounts`.
2. Для каждой транзакции из `transactions`:
- преобразуются суммы в копейки (`amountSigned`, `commission``BIGINT`);
- вычисляется `fingerprint` на основе комбинации полей (например,
`accountNumber + operationAt + amountSigned + commission + normalizedDescription`);
- определяется `direction` по знаку суммы и/или шаблонам текста (приход/расход/перевод);
- выполняется попытка вставки в `transactions`;
- при срабатывании уникального ограничения `(account_id, fingerprint)` запись считается дубликатом и пропускается.
- При импорте новых транзакций is_category_confirmed всегда = FALSE.
- При автокатегоризации обычными правилами, если правило “жёсткое” (не маркетплейс) — TRUE, для маркетплейсов — всегда FALSE.
3. Категория (`category_id`) пока может оставаться `NULL` и заполняться на следующих этапах
(правила, ИИ-агент, ручное редактирование в SPA).