42 Commits

Author SHA1 Message Date
vakabunga
6dd8c01f5e chore: adds versions and copyright 2026-03-17 06:43:26 +03:00
495c1e89bb Merge pull request 'refactor: center main content layout' (#15) from feat/center-layout into main
Reviewed-on: #15
2026-03-16 14:56:05 +00:00
Anton
032a9f4e3b feat: center main content layout 2026-03-16 17:54:35 +03:00
d5f49fd86f Merge pull request 'feat(ui): simplify clear history confirmation modal: confirmation now relies on two checkboxes only.' (#14) from feat/simplify-clear-modal into main
Reviewed-on: #14
2026-03-16 14:52:22 +00:00
Anton
ab88a0553d feat(ui): simplify clear history confirmation modal: confirmation now relies on two checkboxes only. 2026-03-16 17:51:47 +03:00
0589da5005 Merge pull request 'feat(imports): import history and delete by import' (#13) from feat/import-history into main
Reviewed-on: #13
2026-03-16 14:48:47 +00:00
c50e48d564 Merge pull request 'chore(frontend): adds favicon' (#12) from feat/add-favicon into main
Reviewed-on: #12
2026-03-16 14:48:37 +00:00
Anton
01b1f26553 feat(imports): import history and delete by import
Track imports in DB, show history in Data section, allow deleting
transactions of a specific import instead of clearing all.
2026-03-16 17:46:15 +03:00
Anton
ba3105bbe5 chore(frontend): adds favicon 2026-03-16 17:44:04 +03:00
f32a21f87a Merge pull request 'Revert SSE streaming for PDF import, use synchronous flow' (#11) from revert/remove-sse-streaming into main
Reviewed-on: #11
2026-03-14 17:13:00 +00:00
vakabunga
8b57dd987e Revert SSE streaming for PDF import, use synchronous flow
SSE streaming added unnecessary complexity and latency due to
buffering issues across Node.js event loop, Nginx proxy, and
Docker layers. Reverted to a simple synchronous request/response
for PDF conversion. Kept extractLlmErrorMessage for user-friendly
LLM errors, lazy-loaded pdf-parse, and extended Nginx timeout.
2026-03-14 20:12:27 +03:00
ea234ea007 Merge pull request 'fix: yield to event loop after each SSE write to flush socket' (#10) from fix/sse-event-loop-flush into main
Reviewed-on: #10
2026-03-14 17:00:51 +00:00
vakabunga
db4d5e4d00 fix: yield to event loop after each SSE write to flush socket
The for-await loop over OpenAI stream chunks runs synchronously when
data is buffered, causing res.write() calls to queue without flushing.
Add setImmediate yield after each progress event so the event loop
reaches its I/O phase and pushes data to the network immediately.
2026-03-14 19:59:22 +03:00
358fcaeff5 Merge pull request 'fix: disable gzip and pad SSE events to prevent proxy buffering' (#9) from fix/sse-gzip-buffering into main
Reviewed-on: #9
2026-03-14 16:46:07 +00:00
vakabunga
67fed57118 fix: disable gzip and pad SSE events to prevent proxy buffering
Add gzip off to Nginx import location — the global gzip on was
buffering text/event-stream responses. Pad each SSE event to 4 KB
with comment lines to push past any remaining proxy buffer threshold.
2026-03-14 19:45:33 +03:00
45a6f3d374 Merge pull request 'fix: eliminate SSE buffering through Nginx proxy' (#8) from fix/sse-proxy-buffering into main
Reviewed-on: #8
2026-03-14 14:31:16 +00:00
vakabunga
aaf8cacf75 fix: eliminate SSE buffering through Nginx proxy
Add 2 KB padding comment after headers to push past proxy buffer
threshold, enable TCP_NODELAY on the socket, and remove erroneous
chunked_transfer_encoding off from Nginx that caused full response
buffering.
2026-03-14 17:30:52 +03:00
vakabunga
e28d0f46d0 fix: reopen result modal from progress bar, faster progress, handle LLM context error
- Move import modal visibility into ImportContext so the Layout
  progress pill can reopen the result dialog after the modal was closed.
- Raise LLM progress cap from 85% to 98% and drop the intermediate
  -import 88%- SSE event to eliminate the visual stall after LLM finishes.
- Detect LLM context-length errors (n_keep >= n_ctx) and surface a
  clear message instead of generic -Временная ошибка конвертации-.
2026-03-14 17:05:55 +03:00
22be09c101 Merge pull request 'fix: adaptive LLM progress estimation and emit 85% on stream end' (#6) from fix/adaptive-llm-progress into main
Reviewed-on: #6
2026-03-14 13:43:27 +00:00
vakabunga
78c4730196 fix: adaptive LLM progress estimation and emit 85% on stream end
Hardcoded EXPECTED_CHARS (15k) caused progress to stall at ~20-25% for
short statements. Now expected size is derived from input text length.
Also emit an explicit 85% event when the LLM stream finishes, and
throttle SSE events to 300ms to reduce browser overhead.
2026-03-14 16:41:12 +03:00
f2d0c91488 Merge pull request 'feat: stream PDF import progress via SSE with global progress bar' (#5) from feature/pdf-import-sse-streaming into main
Reviewed-on: #5
2026-03-14 13:18:57 +00:00
vakabunga
1d7fbea9ef feat: stream PDF import progress via SSE with global progress bar
Replace the synchronous PDF import with Server-Sent Events streaming
between the backend (LLM) and the browser. The user can now close the
import modal and continue working while the conversion runs — a fixed
progress bar in the Layout shows real-time stage and percentage.
2026-03-14 16:18:31 +03:00
vakabunga
627706228b fix: remove response_format incompatible with LM Studio API 2026-03-14 15:23:33 +03:00
vakabunga
a5f2294440 fix: adds copy of node modules from backend folder 2026-03-14 15:14:04 +03:00
25ddd6b7ed Merge pull request 'fix: lazy-load pdf-parse to prevent startup crash if module is missing' (#4) from fix-lazyload-pdfparse-crash into main
Reviewed-on: #4
2026-03-14 11:18:55 +00:00
vakabunga
3e481d5a55 fix: lazy-load pdf-parse to prevent startup crash if module is missing 2026-03-14 14:18:25 +03:00
cf24d5dc26 Merge pull request 'fix: downgrade pdf-parse to 1.1.1 to eliminate native dependency' (#3) from fix-pdfparse-crash into main
Reviewed-on: #3
2026-03-14 11:03:48 +00:00
vakabunga
723df494ca fix: downgrade pdf-parse to 1.1.1 to eliminate native dependency
pdf-parse@2.4.5 depends on @napi-rs/canvas (native Skia binary compiled
with AVX instructions) which crashes with SIGILL on the Synology NAS CPU.
Version 1.1.1 is pure JavaScript and works on any architecture.
2026-03-14 14:03:14 +03:00
5fa6b921d8 Merge pull request 'fix-pdf-parser-crash' (#2) from fix-pdf-parser-crash into main
Reviewed-on: #2
2026-03-14 10:43:21 +00:00
vakabunga
feb756cfe2 fix(docker): prevent backend crash loop caused by pdf-parse native deps
pdf-parse@2.4.5 pulls in @napi-rs/canvas (native Skia binary) which
crashes on import in Alpine containers. Moved to lazy require() so the
app starts normally and pdf-parse loads only when PDF conversion is
actually requested.
- Lazy-load pdf-parse in pdfToStatement to avoid startup crash
- Add libc6-compat, fontconfig, freetype to Alpine runner stage
- Increase npm fetch timeouts in both Dockerfiles for slow networks
- Add connectionTimeoutMillis to pg Pool for faster failure detection
2026-03-14 13:37:34 +03:00
vakabunga
b598216d24 feat(backend): настраиваемая LLM для конвертации PDF в JSON
- Добавлена переменная LLM_MODEL в конфиг
- Увеличен max_tokens до 32768 для крупных выписок
- Включен response_format: json_object
- Добавлен скрипт test:llm для проверки подключения к LLM-серверу
2026-03-13 23:12:52 +03:00
0638885812 Merge pull request 'feat: adds PDF import with conversion to JSON 1.0' (#1) from pdf2json-feat into main
Reviewed-on: #1
2026-03-13 10:40:03 +00:00
Anton
975f2c4fd2 feat: adds PDF import with conversion to JSON 1.0
- Accept only PDF and JSON files in import modal and API
- Convert PDF statements to JSON 1.0 via LLM (OpenAI-compatible)
- Use multipart/form-data for file upload (multer, 15 MB limit)
- Add LLM_API_KEY and LLM_API_BASE_URL for configurable LLM endpoint
- Update ImportModal to validate type and send FormData
- Add postFormData to API client for file upload
2026-03-13 13:38:02 +03:00
vakabunga
50154f304c chore: adds copy of migration file to prod 2026-03-12 22:25:52 +03:00
vakabunga
8625f7f6cf chore: adds .gitattributes for consistent line endings across machines 2026-03-12 22:11:21 +03:00
Anton
d1536b8872 feat: расширенные правила категоризации (миграция 004) 2026-03-12 14:50:02 +03:00
Anton
20d2a2b497 chore: добавить dist-node в .gitignore, убрать из отслеживания
Made-with: Cursor
2026-03-10 11:54:31 +03:00
Anton
56b5c81ec5 feat(frontend): адаптация под мобильные устройства
- Мобильная навигация: hamburger-меню и drawer вместо фиксированного sidebar
- Модальные окна на весь экран при ширине < 480px
- Адаптивные заголовки страниц и фильтры (touch-friendly)
- Card view для таблицы операций при ширине < 600px
- Горизонтальный скролл вкладок настроек
- Увеличенные touch-targets (44px) для пагинации и кнопок
- Уменьшенная высота графиков на мобильных
- Поддержка safe-area-inset для устройств с вырезами
- theme-color в index.html

Made-with: Cursor
2026-03-10 11:50:36 +03:00
vakabunga
a895bb4b2f fix: 404 при обновлении, стрелки периода, фильтры в URL, авто-категории и очистка истории
- Nginx: проксирование /api на backend (единая точка входа)
- История: стрелки ← → для переключения недель/месяцев/годов
- История: сохранение фильтров и пагинации в URL при F5
- Импорт: миграция 003 — дефолтные правила категорий (PYATEROCHK, AUCHAN и др.)
- Настройки: вкладка «Данные» с кнопкой «Очистить историю»
- Backend: DELETE /api/transactions для удаления всех транзакций
- ClearHistoryModal: подтверждение чекбоксами и вводом «УДАЛИТЬ»
2026-03-10 06:53:56 +03:00
vakabunga
792b4ca4ad fix: fixes compose file 2026-03-02 23:32:40 +03:00
vakabunga
b990149bc3 chore: adds docker files 2026-03-02 23:22:40 +03:00
Anton
6b6ce5cba1 chore: removes extra file 2026-03-02 11:39:04 +03:00
52 changed files with 2252 additions and 359 deletions

6
.gitattributes vendored Normal file
View File

@@ -0,0 +1,6 @@
# Единые правила переносов строк для всех машин
* text=auto eol=lf
# Windows-скрипты — исключение, им нужен CRLF
*.bat text eol=crlf
*.cmd text eol=crlf

9
.gitignore vendored
View File

@@ -1,5 +1,6 @@
node_modules/ node_modules/
dist/ dist/
dist-node/
build/ build/
.env .env
@@ -15,3 +16,11 @@ npm-debug.log*
Thumbs.db Thumbs.db
coverage/ coverage/
jan_feb.json
jan-feb.json
history.xlsx
match_analysis.py
match_report.txt
statements/
.cursor/

37
Dockerfile.backend Normal file
View File

@@ -0,0 +1,37 @@
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json* ./
COPY shared ./shared
COPY backend ./backend
# Увеличенные таймауты для нестабильной сети
RUN npm config set fetch-retry-mintimeout 20000 && \
npm config set fetch-retry-maxtimeout 120000 && \
npm config set fetch-timeout 300000
RUN npm install
FROM node:20-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY --from=deps /app/package.json ./
COPY --from=deps /app/shared ./shared
COPY --from=deps /app/backend ./backend
RUN npm run build -w @family-budget/shared
RUN npm run build -w @family-budget/backend
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/backend/dist ./dist
COPY --from=build /app/backend/package.json ./package.json
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/backend/node_modules/ ./node_modules/
COPY backend/.env .env
EXPOSE 3000
CMD ["node","dist/app.js"]

30
Dockerfile.frontend Normal file
View File

@@ -0,0 +1,30 @@
FROM node:20-alpine AS builder
WORKDIR /app
# Копируем корень и конфигурации
COPY package.json package-lock.json* tsconfig.json* ./
# Копируем исходники воркспэйсов
COPY shared ./shared
COPY frontend ./frontend
# Увеличиваем таймауты и повторы для нестабильной сети
RUN npm config set fetch-retry-mintimeout 20000 && \
npm config set fetch-retry-maxtimeout 120000 && \
npm config set fetch-timeout 300000
# Устанавливаем зависимости из корня
RUN npm install
# Сначала собираем shared (если там есть скрипт build)
RUN npm run build -w @family-budget/shared || echo "No build script in shared, skipping..."
# Теперь собираем frontend
RUN npm run build -w @family-budget/frontend
# Финальный образ — nginx для раздачи статики
FROM nginx:alpine
COPY --from=builder /app/frontend/dist /usr/share/nginx/html
COPY frontend/nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80

View File

@@ -4,7 +4,7 @@ Local SPA for family budget tracking: import bank statements, categorize transac
## Monorepo structure ## Monorepo structure
``` ```text
family_budget/ family_budget/
├── backend/ — Node.js API server (Express + TypeScript) ├── backend/ — Node.js API server (Express + TypeScript)
├── frontend/ — React SPA (Vite + TypeScript) ├── frontend/ — React SPA (Vite + TypeScript)
@@ -15,13 +15,13 @@ family_budget/
## Tech stack ## Tech stack
| Layer | Choice | Rationale | | Layer | Choice | Rationale |
|----------|------------------------|--------------------------------------------------------| |---------- |------------------------|--------------------------------------------------------|
| Backend | Express + TypeScript | Simple, well-known, sufficient for a small local app | | Backend | Express + TypeScript | Simple, well-known, sufficient for a small local app |
| Frontend | React + Vite + TS | Fast dev experience, modern tooling | | Frontend | React + Vite + TS | Fast dev experience, modern tooling |
| Database | PostgreSQL | Deployed on Synology NAS | | Database | PostgreSQL | Deployed on Synology NAS |
| Migrations | Knex | Lightweight, SQL-close, supports seeds | | Migrations | Knex | Lightweight, SQL-close, supports seeds |
| Shared | Pure TypeScript types | Zero-runtime, imported by both backend and frontend | | Shared | Pure TypeScript types | Zero-runtime, imported by both backend and frontend |
## Prerequisites ## Prerequisites

View File

@@ -10,3 +10,14 @@ APP_USER_PASSWORD=changeme
SESSION_TIMEOUT_MS=10800000 SESSION_TIMEOUT_MS=10800000
PORT=3000 PORT=3000
# Ключ OpenAI API для конвертации PDF-выписок в JSON (опционально)
# Без него импорт PDF будет недоступен (503)
LLM_API_KEY=
# Базовый URL API LLM (опционально). По умолчанию https://api.openai.com
# Примеры: Ollama — http://localhost:11434/v1, Azure — https://YOUR_RESOURCE.openai.azure.com/openai/deployments/YOUR_DEPLOYMENT
LLM_API_BASE_URL=
# Имя модели LLM (опционально). Для OpenAI: gpt-4o-mini. Для Ollama: qwen2.5:7b, qwen3:7b
LLM_MODEL=

View File

@@ -45,6 +45,8 @@ npm run dev -w backend
| `APP_USER_PASSWORD` | `changeme` | Пароль для входа в приложение | | `APP_USER_PASSWORD` | `changeme` | Пароль для входа в приложение |
| `SESSION_TIMEOUT_MS` | `10800000` | Таймаут сессии по бездействию (3 часа) | | `SESSION_TIMEOUT_MS` | `10800000` | Таймаут сессии по бездействию (3 часа) |
| `PORT` | `3000` | Порт HTTP-сервера | | `PORT` | `3000` | Порт HTTP-сервера |
| `LLM_API_KEY` | — | Ключ OpenAI API для конвертации PDF в JSON; без него импорт PDF возвращает 503 |
| `LLM_API_BASE_URL` | `https://api.openai.com/v1` | Адрес LLM API (OpenAI-совместимый); для локальной модели, напр. Ollama: `http://localhost:11434/v1` |
## Структура проекта ## Структура проекта
@@ -61,6 +63,7 @@ backend/src/
├── services/ ├── services/
│ ├── auth.ts — login / logout / me │ ├── auth.ts — login / logout / me
│ ├── import.ts — валидация, fingerprint, direction, атомарный импорт │ ├── import.ts — валидация, fingerprint, direction, атомарный импорт
│ ├── pdfToStatement.ts — конвертация PDF → JSON 1.0 через LLM (OpenAI)
│ ├── transactions.ts — список с фильтрами + обновление (categoryId, comment) │ ├── transactions.ts — список с фильтрами + обновление (categoryId, comment)
│ ├── accounts.ts — список счетов, обновление алиаса │ ├── accounts.ts — список счетов, обновление алиаса
│ ├── categories.ts — список категорий (фильтр isActive) │ ├── categories.ts — список категорий (фильтр isActive)
@@ -90,7 +93,7 @@ backend/src/
| Метод | URL | Описание | | Метод | URL | Описание |
|--------|----------------------------|-----------------------------------------| |--------|----------------------------|-----------------------------------------|
| POST | `/api/import/statement` | Импорт банковской выписки (JSON 1.0) | | POST | `/api/import/statement` | Импорт банковской выписки (PDF или JSON 1.0; PDF конвертируется через LLM) |
### Транзакции ### Транзакции

View File

@@ -1,12 +1,14 @@
{ {
"name": "@family-budget/backend", "name": "@family-budget/backend",
"version": "0.1.0", "version": "0.5.12",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "tsx watch src/app.ts", "dev": "tsx watch src/app.ts",
"build": "tsc", "build": "tsc",
"start": "node dist/app.js", "start": "node dist/app.js",
"migrate": "tsx src/db/migrate.ts" "migrate": "tsx src/db/migrate.ts",
"migrate:prod": "node dist/db/migrate.js",
"test:llm": "tsx src/scripts/testLlm.ts"
}, },
"dependencies": { "dependencies": {
"@family-budget/shared": "*", "@family-budget/shared": "*",
@@ -14,6 +16,9 @@
"cors": "^2.8.6", "cors": "^2.8.6",
"dotenv": "^17.3.1", "dotenv": "^17.3.1",
"express": "^5.2.1", "express": "^5.2.1",
"multer": "^2.1.1",
"openai": "^6.27.0",
"pdf-parse": "1.1.1",
"pg": "^8.19.0", "pg": "^8.19.0",
"uuid": "^13.0.0" "uuid": "^13.0.0"
}, },
@@ -21,6 +26,7 @@
"@types/cookie-parser": "^1.4.10", "@types/cookie-parser": "^1.4.10",
"@types/cors": "^2.8.19", "@types/cors": "^2.8.19",
"@types/express": "^5.0.6", "@types/express": "^5.0.6",
"@types/multer": "^2.1.0",
"@types/node": "^25.3.3", "@types/node": "^25.3.3",
"@types/pg": "^8.18.0", "@types/pg": "^8.18.0",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",

View File

@@ -1,3 +1,5 @@
import fs from 'fs';
import path from 'path';
import express from 'express'; import express from 'express';
import cookieParser from 'cookie-parser'; import cookieParser from 'cookie-parser';
import cors from 'cors'; import cors from 'cors';
@@ -5,8 +7,13 @@ import { config } from './config';
import { runMigrations } from './db/migrate'; import { runMigrations } from './db/migrate';
import { requireAuth } from './middleware/auth'; import { requireAuth } from './middleware/auth';
const pkg = JSON.parse(
fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'),
) as { version: string };
import authRouter from './routes/auth'; import authRouter from './routes/auth';
import importRouter from './routes/import'; import importRouter from './routes/import';
import importsRouter from './routes/imports';
import transactionsRouter from './routes/transactions'; import transactionsRouter from './routes/transactions';
import accountsRouter from './routes/accounts'; import accountsRouter from './routes/accounts';
import categoriesRouter from './routes/categories'; import categoriesRouter from './routes/categories';
@@ -14,6 +21,7 @@ import categoryRulesRouter from './routes/categoryRules';
import analyticsRouter from './routes/analytics'; import analyticsRouter from './routes/analytics';
const app = express(); const app = express();
app.set('trust proxy', 1);
app.use(express.json({ limit: '10mb' })); app.use(express.json({ limit: '10mb' }));
app.use(cookieParser()); app.use(cookieParser());
@@ -22,9 +30,14 @@ app.use(cors({ origin: true, credentials: true }));
// Auth routes (login is public; me/logout apply auth internally) // Auth routes (login is public; me/logout apply auth internally)
app.use('/api/auth', authRouter); app.use('/api/auth', authRouter);
app.get('/api/version', (_req, res) => {
res.json({ version: pkg.version });
});
// All remaining /api routes require authentication // All remaining /api routes require authentication
app.use('/api', requireAuth); app.use('/api', requireAuth);
app.use('/api/import', importRouter); app.use('/api/import', importRouter);
app.use('/api/imports', importsRouter);
app.use('/api/transactions', transactionsRouter); app.use('/api/transactions', transactionsRouter);
app.use('/api/accounts', accountsRouter); app.use('/api/accounts', accountsRouter);
app.use('/api/categories', categoriesRouter); app.use('/api/categories', categoriesRouter);

View File

@@ -18,4 +18,13 @@ export const config = {
appUserPassword: process.env.APP_USER_PASSWORD || 'changeme', appUserPassword: process.env.APP_USER_PASSWORD || 'changeme',
sessionTimeoutMs: parseInt(process.env.SESSION_TIMEOUT_MS || '10800000', 10), sessionTimeoutMs: parseInt(process.env.SESSION_TIMEOUT_MS || '10800000', 10),
/** API-ключ для LLM (OpenAI или совместимый). Обязателен для конвертации PDF. */
llmApiKey: process.env.LLM_API_KEY || '',
/** Базовый URL API LLM. По умолчанию https://api.openai.com. Для Ollama: http://localhost:11434/v1 */
llmApiBaseUrl: process.env.LLM_API_BASE_URL || undefined,
/** Имя модели LLM. Для OpenAI: gpt-4o-mini. Для Ollama: qwen2.5:7b, qwen3:7b и т.п. */
llmModel: process.env.LLM_MODEL || 'gpt-4o-mini',
}; };

View File

@@ -96,6 +96,122 @@ const migrations: { name: string; sql: string }[] = [
ON CONFLICT DO NOTHING; ON CONFLICT DO NOTHING;
`, `,
}, },
{
name: '003_seed_category_rules',
sql: `
INSERT INTO category_rules (pattern, match_type, category_id, priority, requires_confirmation)
SELECT pattern, match_type, category_id, priority, requires_confirmation
FROM (VALUES
('PYATEROCHK', 'contains', (SELECT id FROM categories WHERE name = 'Продукты' AND type = 'expense' LIMIT 1), 20, false),
('ПЯТЕРОЧКА', 'contains', (SELECT id FROM categories WHERE name = 'Продукты' AND type = 'expense' LIMIT 1), 20, false),
('AUCHAN', 'contains', (SELECT id FROM categories WHERE name = 'Продукты' AND type = 'expense' LIMIT 1), 20, false),
('DOSTAVKA IZ PYATEROCHK', 'contains', (SELECT id FROM categories WHERE name = 'Продукты' AND type = 'expense' LIMIT 1), 25, false),
('МЕТРО', 'contains', (SELECT id FROM categories WHERE name = 'Продукты' AND type = 'expense' LIMIT 1), 20, false),
('PEREKRESTOK', 'contains', (SELECT id FROM categories WHERE name = 'Продукты' AND type = 'expense' LIMIT 1), 20, false),
('MAGNIT', 'contains', (SELECT id FROM categories WHERE name = 'Продукты' AND type = 'expense' LIMIT 1), 20, false),
('LENTA', 'contains', (SELECT id FROM categories WHERE name = 'Продукты' AND type = 'expense' LIMIT 1), 20, false),
('SPORTMASTER', 'contains', (SELECT id FROM categories WHERE name = 'Спорт' AND type = 'expense' LIMIT 1), 20, false),
('DECATHLON', 'contains', (SELECT id FROM categories WHERE name = 'Спорт' AND type = 'expense' LIMIT 1), 20, false),
('PAYPARKING', 'contains', (SELECT id FROM categories WHERE name = 'Авто' AND type = 'expense' LIMIT 1), 20, false),
('PARKING', 'contains', (SELECT id FROM categories WHERE name = 'Авто' AND type = 'expense' LIMIT 1), 15, false),
('AZS', 'contains', (SELECT id FROM categories WHERE name = 'Авто' AND type = 'expense' LIMIT 1), 20, false),
('ЯНДЕКС.ЕДА', 'contains', (SELECT id FROM categories WHERE name = 'Общепит' AND type = 'expense' LIMIT 1), 25, false),
('MCDONALD', 'contains', (SELECT id FROM categories WHERE name = 'Общепит' AND type = 'expense' LIMIT 1), 20, false),
('KFC', 'contains', (SELECT id FROM categories WHERE name = 'Общепит' AND type = 'expense' LIMIT 1), 20, false),
('STARBUCKS', 'contains', (SELECT id FROM categories WHERE name = 'Общепит' AND type = 'expense' LIMIT 1), 20, false),
('tapper.ru', 'contains', (SELECT id FROM categories WHERE name = 'Развлечения' AND type = 'expense' LIMIT 1), 20, false),
('АПТЕКА', 'contains', (SELECT id FROM categories WHERE name = 'Здоровье' AND type = 'expense' LIMIT 1), 20, false),
('РИГЛА', 'contains', (SELECT id FROM categories WHERE name = 'Здоровье' AND type = 'expense' LIMIT 1), 20, false),
('OZON', 'contains', (SELECT id FROM categories WHERE name = 'Дом' AND type = 'expense' LIMIT 1), 15, false),
('WILDBERRIES', 'contains', (SELECT id FROM categories WHERE name = 'Дом' AND type = 'expense' LIMIT 1), 15, false),
('ЯНДЕКС ПЛЮС', 'contains', (SELECT id FROM categories WHERE name = 'Подписки' AND type = 'expense' LIMIT 1), 20, false),
('SPOTIFY', 'contains', (SELECT id FROM categories WHERE name = 'Подписки' AND type = 'expense' LIMIT 1), 20, false),
('НЕТФЛИКС', 'contains', (SELECT id FROM categories WHERE name = 'Подписки' AND type = 'expense' LIMIT 1), 20, false)
) AS v(pattern, match_type, category_id, priority, requires_confirmation)
WHERE category_id IS NOT NULL
AND EXISTS (SELECT 1 FROM categories LIMIT 1)
AND NOT EXISTS (SELECT 1 FROM category_rules LIMIT 1);
`,
},
{
name: '005_imports_table',
sql: `
CREATE TABLE IF NOT EXISTS imports (
id BIGSERIAL PRIMARY KEY,
imported_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
account_id BIGINT REFERENCES accounts(id),
bank TEXT NOT NULL,
account_number_masked TEXT NOT NULL,
imported_count INT NOT NULL,
duplicates_skipped INT NOT NULL,
total_in_file INT NOT NULL
);
ALTER TABLE transactions
ADD COLUMN IF NOT EXISTS import_id BIGINT REFERENCES imports(id);
`,
},
{
name: '004_seed_category_rules_extended',
sql: `
INSERT INTO category_rules (pattern, match_type, category_id, priority, requires_confirmation)
SELECT pattern, match_type, category_id, priority, requires_confirmation
FROM (VALUES
('STOLOVAYA', 'contains', (SELECT id FROM categories WHERE name = 'Общепит' LIMIT 1), 25, false),
('KOFEJNYA', 'contains', (SELECT id FROM categories WHERE name = 'Общепит' LIMIT 1), 25, false),
('JEFFREY S COFFEESHOP', 'contains', (SELECT id FROM categories WHERE name = 'Общепит' LIMIT 1), 25, false),
('TA TORRRO GRIL', 'contains', (SELECT id FROM categories WHERE name = 'Общепит' LIMIT 1), 25, false),
('OUSHEN FRENDS', 'contains', (SELECT id FROM categories WHERE name = 'Общепит' LIMIT 1), 25, false),
('mkad', 'contains', (SELECT id FROM categories WHERE name = 'Авто' LIMIT 1), 20, false),
('RNAZK', 'contains', (SELECT id FROM categories WHERE name = 'Авто' LIMIT 1), 20, false),
('IP SHEVELEV', 'contains', (SELECT id FROM categories WHERE name = 'Авто' LIMIT 1), 20, false),
('PAVELETSKAYA', 'contains', (SELECT id FROM categories WHERE name = 'Проезд' LIMIT 1), 25, false),
('CPPK-', 'contains', (SELECT id FROM categories WHERE name = 'Проезд' LIMIT 1), 25, false),
('MOS.TRANSPORT', 'contains', (SELECT id FROM categories WHERE name = 'Проезд' LIMIT 1), 25, false),
('Lab4uru', 'contains', (SELECT id FROM categories WHERE name = 'Здоровье' LIMIT 1), 25, false),
('APTEKA', 'contains', (SELECT id FROM categories WHERE name = 'Здоровье' LIMIT 1), 20, false),
('IP SHARAFETDINOV', 'contains', (SELECT id FROM categories WHERE name = 'Арчи' LIMIT 1), 25, false),
('ZOOMAGAZIN CHETYRE LAP', 'contains', (SELECT id FROM categories WHERE name = 'Арчи' LIMIT 1), 25, false),
('VKUSVILL', 'contains', (SELECT id FROM categories WHERE name = 'Продукты' LIMIT 1), 20, false),
('GLOBUS', 'contains', (SELECT id FROM categories WHERE name = 'Продукты' LIMIT 1), 20, false),
('VERNYJ', 'contains', (SELECT id FROM categories WHERE name = 'Продукты' LIMIT 1), 20, false),
('GOLD APPLE', 'contains', (SELECT id FROM categories WHERE name = 'Косметика' LIMIT 1), 25, false),
('SPIRITFIT', 'contains', (SELECT id FROM categories WHERE name = 'Спорт' LIMIT 1), 25, false),
('insanity', 'contains', (SELECT id FROM categories WHERE name = 'Спорт' LIMIT 1), 25, false),
('anta-sport', 'contains', (SELECT id FROM categories WHERE name = 'Спорт' LIMIT 1), 25, false),
('VANYAVPN', 'contains', (SELECT id FROM categories WHERE name = 'Подписки' LIMIT 1), 25, false),
('ГРАНЛАЙН', 'contains', (SELECT id FROM categories WHERE name = 'Подписки' LIMIT 1), 20, false),
('Мобильная связь', 'contains', (SELECT id FROM categories WHERE name = 'Подписки' LIMIT 1), 25, false),
('OSTROVOK', 'contains', (SELECT id FROM categories WHERE name = 'Отпуск' LIMIT 1), 25, false),
('sutochno', 'contains', (SELECT id FROM categories WHERE name = 'Отпуск' LIMIT 1), 25, false),
('УФК', 'contains', (SELECT id FROM categories WHERE name = 'Штрафы' LIMIT 1), 30, false),
('ГИБДД', 'contains', (SELECT id FROM categories WHERE name = 'Штрафы' LIMIT 1), 30, false),
('Поступление заработной платы', 'contains', (SELECT id FROM categories WHERE name = 'Поступления' LIMIT 1), 30, false),
('avito', 'contains', (SELECT id FROM categories WHERE name = 'Поступления' LIMIT 1), 25, false),
('Init payout', 'contains', (SELECT id FROM categories WHERE name = 'Поступления' LIMIT 1), 25, false)
) AS v(pattern, match_type, category_id, priority, requires_confirmation)
WHERE category_id IS NOT NULL
AND EXISTS (SELECT 1 FROM categories LIMIT 1);
`,
},
{
name: '005_imports_table',
sql: `
CREATE TABLE IF NOT EXISTS imports (
id BIGSERIAL PRIMARY KEY,
imported_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
account_id BIGINT REFERENCES accounts(id),
bank TEXT NOT NULL,
account_number_masked TEXT NOT NULL,
imported_count INT NOT NULL,
duplicates_skipped INT NOT NULL,
total_in_file INT NOT NULL
);
ALTER TABLE transactions
ADD COLUMN IF NOT EXISTS import_id BIGINT REFERENCES imports(id);
`,
},
]; ];
export async function runMigrations(): Promise<void> { export async function runMigrations(): Promise<void> {

View File

@@ -7,4 +7,5 @@ export const pool = new Pool({
database: config.db.database, database: config.db.database,
user: config.db.user, user: config.db.user,
password: config.db.password, password: config.db.password,
connectionTimeoutMillis: 5000,
}); });

View File

@@ -16,12 +16,13 @@ router.post(
const result = await authService.login({ login, password }); const result = await authService.login({ login, password });
if (!result) { if (!result) {
res.status(401).json({ error: 'UNAUTHORIZED', message: 'Invalid credentials' }); res.status(401).json({ error: 'UNAUTHORIZED', message: 'Неверный логин или пароль' });
return; return;
} }
res.cookie('sid', result.sessionId, { res.cookie('sid', result.sessionId, {
httpOnly: true, httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax', sameSite: 'lax',
path: '/', path: '/',
}); });

View File

@@ -1,13 +1,81 @@
import { Router } from 'express'; import { Router } from 'express';
import multer from 'multer';
import { asyncHandler } from '../utils'; import { asyncHandler } from '../utils';
import { importStatement, isValidationError } from '../services/import'; import { importStatement, isValidationError } from '../services/import';
import {
convertPdfToStatement,
isPdfConversionError,
} from '../services/pdfToStatement';
const upload = multer({
storage: multer.memoryStorage(),
limits: { fileSize: 15 * 1024 * 1024 },
});
function isPdfFile(file: { mimetype: string; originalname: string }): boolean {
const name = file.originalname.toLowerCase();
return (
file.mimetype === 'application/pdf' ||
name.endsWith('.pdf')
);
}
function isJsonFile(file: { mimetype: string; originalname: string }): boolean {
const name = file.originalname.toLowerCase();
return (
file.mimetype === 'application/json' ||
name.endsWith('.json')
);
}
const router = Router(); const router = Router();
router.post( router.post(
'/statement', '/statement',
upload.single('file'),
asyncHandler(async (req, res) => { asyncHandler(async (req, res) => {
const result = await importStatement(req.body); const file = req.file;
if (!file) {
res.status(400).json({
error: 'BAD_REQUEST',
message: 'Файл не загружен',
});
return;
}
if (!isPdfFile(file) && !isJsonFile(file)) {
res.status(400).json({
error: 'BAD_REQUEST',
message: 'Допустимы только файлы PDF или JSON',
});
return;
}
let body: unknown;
if (isPdfFile(file)) {
const converted = await convertPdfToStatement(file.buffer);
if (isPdfConversionError(converted)) {
res.status(converted.status).json({
error: converted.error,
message: converted.message,
});
return;
}
body = converted;
} else {
try {
body = JSON.parse(file.buffer.toString('utf-8'));
} catch {
res.status(400).json({
error: 'BAD_REQUEST',
message: 'Некорректный JSON-файл',
});
return;
}
}
const result = await importStatement(body);
if (isValidationError(result)) { if (isValidationError(result)) {
res.status((result as { status: number }).status).json({ res.status((result as { status: number }).status).json({
error: (result as { error: string }).error, error: (result as { error: string }).error,

View File

@@ -0,0 +1,36 @@
import { Router } from 'express';
import { pool } from '../db/pool';
import { asyncHandler } from '../utils';
import * as importsService from '../services/imports';
const router = Router();
router.get(
'/',
asyncHandler(async (_req, res) => {
const imports = await importsService.getImports();
res.json(imports);
}),
);
router.delete(
'/:id',
asyncHandler(async (req, res) => {
const id = Number(req.params.id);
if (isNaN(id)) {
res.status(400).json({ error: 'BAD_REQUEST', message: 'Invalid import id' });
return;
}
const { rows } = await pool.query('SELECT 1 FROM imports WHERE id = $1', [id]);
if (rows.length === 0) {
res.status(404).json({ error: 'NOT_FOUND', message: 'Import not found' });
return;
}
const result = await importsService.deleteImport(id);
res.json(result);
}),
);
export default router;

View File

@@ -4,6 +4,14 @@ import * as transactionService from '../services/transactions';
const router = Router(); const router = Router();
router.delete(
'/',
asyncHandler(async (_req, res) => {
const result = await transactionService.clearAllTransactions();
res.json(result);
}),
);
router.get( router.get(
'/', '/',
asyncHandler(async (req, res) => { asyncHandler(async (req, res) => {

View File

@@ -0,0 +1,42 @@
/**
* Тестовый запрос к LLM серверу.
* Запуск: npm run test:llm
*/
import OpenAI from 'openai';
import { config } from '../config';
async function main() {
if (!config.llmApiKey || config.llmApiKey.trim() === '') {
console.error('Ошибка: LLM_API_KEY не задан в .env');
process.exit(1);
}
const openai = new OpenAI({
apiKey: config.llmApiKey,
...(config.llmApiBaseUrl && { baseURL: config.llmApiBaseUrl }),
});
const url = config.llmApiBaseUrl || 'https://api.openai.com/v1';
console.log('LLM сервер:', url);
console.log('Модель:', config.llmModel);
console.log('---');
try {
const completion = await openai.chat.completions.create({
model: config.llmModel,
messages: [{ role: 'user', content: 'Ответь одним словом: какой цвет у неба?' }],
temperature: 0,
max_tokens: 50,
});
const content = completion.choices[0]?.message?.content;
console.log('Ответ:', content || '(пусто)');
console.log('---');
console.log('OK');
} catch (err) {
console.error('Ошибка:', err instanceof Error ? err.message : err);
process.exit(1);
}
}
main();

View File

@@ -155,6 +155,16 @@ export async function importStatement(
isNewAccount = true; isNewAccount = true;
} }
// Create import record (counts updated after loop)
const accountNumberMasked = maskAccountNumber(data.statement.accountNumber);
const importResult = await client.query(
`INSERT INTO imports (account_id, bank, account_number_masked, imported_count, duplicates_skipped, total_in_file)
VALUES ($1, $2, $3, 0, 0, $4)
RETURNING id`,
[accountId, data.bank, accountNumberMasked, data.transactions.length],
);
const importId = Number(importResult.rows[0].id);
// Insert transactions // Insert transactions
const insertedIds: number[] = []; const insertedIds: number[] = [];
@@ -164,11 +174,11 @@ export async function importStatement(
const result = await client.query( const result = await client.query(
`INSERT INTO transactions `INSERT INTO transactions
(account_id, operation_at, amount_signed, commission, description, direction, fingerprint, category_id, is_category_confirmed) (account_id, operation_at, amount_signed, commission, description, direction, fingerprint, category_id, is_category_confirmed, import_id)
VALUES ($1, $2, $3, $4, $5, $6, $7, NULL, FALSE) VALUES ($1, $2, $3, $4, $5, $6, $7, NULL, FALSE, $8)
ON CONFLICT (account_id, fingerprint) DO NOTHING ON CONFLICT (account_id, fingerprint) DO NOTHING
RETURNING id`, RETURNING id`,
[accountId, tx.operationAt, tx.amountSigned, tx.commission, tx.description, dir, fp], [accountId, tx.operationAt, tx.amountSigned, tx.commission, tx.description, dir, fp, importId],
); );
if (result.rows.length > 0) { if (result.rows.length > 0) {
@@ -176,6 +186,13 @@ export async function importStatement(
} }
} }
// Update import record with actual counts
const duplicatesSkipped = data.transactions.length - insertedIds.length;
await client.query(
`UPDATE imports SET imported_count = $1, duplicates_skipped = $2 WHERE id = $3`,
[insertedIds.length, duplicatesSkipped, importId],
);
// Auto-categorize newly inserted transactions // Auto-categorize newly inserted transactions
if (insertedIds.length > 0) { if (insertedIds.length > 0) {
await client.query( await client.query(
@@ -208,7 +225,7 @@ export async function importStatement(
return { return {
accountId, accountId,
isNewAccount, isNewAccount,
accountNumberMasked: maskAccountNumber(data.statement.accountNumber), accountNumberMasked,
imported: insertedIds.length, imported: insertedIds.length,
duplicatesSkipped: data.transactions.length - insertedIds.length, duplicatesSkipped: data.transactions.length - insertedIds.length,
totalInFile: data.transactions.length, totalInFile: data.transactions.length,

View File

@@ -0,0 +1,44 @@
import { pool } from '../db/pool';
import type { Import } from '@family-budget/shared';
export async function getImports(): Promise<Import[]> {
const result = await pool.query(
`SELECT
i.id,
i.imported_at,
i.account_id,
a.alias AS account_alias,
i.bank,
i.account_number_masked,
i.imported_count,
i.duplicates_skipped,
i.total_in_file
FROM imports i
LEFT JOIN accounts a ON a.id = i.account_id
ORDER BY i.imported_at DESC`,
);
return result.rows.map((r) => ({
id: Number(r.id),
importedAt: r.imported_at.toISOString(),
accountId: r.account_id != null ? Number(r.account_id) : null,
accountAlias: r.account_alias ?? null,
bank: r.bank,
accountNumberMasked: r.account_number_masked,
importedCount: Number(r.imported_count),
duplicatesSkipped: Number(r.duplicates_skipped),
totalInFile: Number(r.total_in_file),
}));
}
export async function deleteImport(id: number): Promise<{ deleted: number }> {
const result = await pool.query(
'DELETE FROM transactions WHERE import_id = $1 RETURNING id',
[id],
);
const deleted = result.rowCount ?? 0;
await pool.query('DELETE FROM imports WHERE id = $1', [id]);
return { deleted };
}

View File

@@ -0,0 +1,179 @@
import OpenAI from 'openai';
import { config } from '../config';
import type { StatementFile } from '@family-budget/shared';
const PDF2JSON_PROMPT = `Ты — конвертер банковских выписок. Твоя задача: извлечь данные из текста банковской выписки ниже и вернуть строго один валидный JSON-объект в формате ниже. Никакого текста до или после JSON, только сам объект.
## Структура выходного JSON
{
"schemaVersion": "1.0",
"bank": "<название банка из выписки>",
"statement": {
"accountNumber": "<номер счёта, только цифры, без пробелов>",
"currency": "RUB",
"openingBalance": <число в копейках, целое>,
"closingBalance": <число в копейках, целое>,
"exportedAt": "<дата экспорта в формате ISO 8601 с offset, например 2026-02-27T13:23:00+03:00>"
},
"transactions": [
{
"operationAt": "<дата и время операции в формате ISO 8601 с offset>",
"amountSigned": <число: положительное для прихода, отрицательное для расхода; в копейках>,
"commission": <число, целое, >= 0, в копейках>,
"description": "<полное описание операции из выписки>"
}
]
}
## Правила конвертации
1. Суммы — всегда в копейках (рубли × 100). Пример: 500,00 ₽ → 50000, -1234,56 ₽ → -123456.
2. amountSigned: приход — положительное, расход — отрицательное.
3. operationAt — дата и время, если не указано — 00:00:00, offset +03:00 для МСК.
4. commission — если не указана — 0.
5. description — полный текст операции как в выписке.
6. accountNumber — только цифры, без пробелов и дефисов.
7. openingBalance / closingBalance — в копейках.
8. bank — краткое название (VTB, Sberbank, Тинькофф).
9. exportedAt — дата формирования выписки.
10. transactions — хронологический порядок.
## Требования
- transactions не должен быть пустым.
- Все числа — целые.
- Даты — ISO 8601 с offset.
- currency всегда "RUB".
- schemaVersion всегда "1.0".`;
export interface PdfConversionError {
status: number;
error: string;
message: string;
}
export function isPdfConversionError(r: unknown): r is PdfConversionError {
return (
typeof r === 'object' &&
r !== null &&
'status' in r &&
'error' in r &&
'message' in r
);
}
// Lazy-loaded so the app starts even if pdf-parse is missing from node_modules.
let _pdfParse: ((buf: Buffer) => Promise<{ text: string }>) | undefined;
function getPdfParse() {
if (!_pdfParse) {
// eslint-disable-next-line @typescript-eslint/no-require-imports
_pdfParse = require('pdf-parse') as (buf: Buffer) => Promise<{ text: string }>;
}
return _pdfParse;
}
export async function convertPdfToStatement(
buffer: Buffer,
): Promise<StatementFile | PdfConversionError> {
if (!config.llmApiKey || config.llmApiKey.trim() === '') {
return {
status: 503,
error: 'SERVICE_UNAVAILABLE',
message: 'Конвертация PDF недоступна: не задан LLM_API_KEY',
};
}
let text: string;
try {
const result = await getPdfParse()(buffer);
text = result.text || '';
} catch (err) {
console.error('PDF extraction error:', err);
return {
status: 400,
error: 'BAD_REQUEST',
message: 'Не удалось обработать PDF-файл',
};
}
if (!text || text.trim().length === 0) {
return {
status: 400,
error: 'BAD_REQUEST',
message: 'Не удалось извлечь текст из PDF',
};
}
const openai = new OpenAI({
apiKey: config.llmApiKey,
...(config.llmApiBaseUrl && { baseURL: config.llmApiBaseUrl }),
timeout: 5 * 60 * 1000,
});
try {
const completion = await openai.chat.completions.create({
model: config.llmModel,
messages: [
{ role: 'system', content: PDF2JSON_PROMPT },
{ role: 'user', content: `Текст выписки:\n\n${text}` },
],
temperature: 0,
max_tokens: 32768,
});
const content = completion.choices[0]?.message?.content?.trim();
if (!content) {
return {
status: 422,
error: 'VALIDATION_ERROR',
message: 'Результат конвертации пуст',
};
}
const jsonMatch = content.match(/\{[\s\S]*\}/);
const jsonStr = jsonMatch ? jsonMatch[0] : content;
let parsed: unknown;
try {
parsed = JSON.parse(jsonStr);
} catch {
return {
status: 422,
error: 'VALIDATION_ERROR',
message: 'Результат конвертации не является валидным JSON',
};
}
const data = parsed as Record<string, unknown>;
if (data.schemaVersion !== '1.0') {
return {
status: 422,
error: 'VALIDATION_ERROR',
message: 'Результат конвертации не соответствует схеме 1.0',
};
}
return parsed as StatementFile;
} catch (err) {
console.error('LLM conversion error:', err);
return {
status: 502,
error: 'BAD_GATEWAY',
message: extractLlmErrorMessage(err),
};
}
}
function extractLlmErrorMessage(err: unknown): string {
const raw = String(
(err as Record<string, unknown>)?.message ??
(err as Record<string, Record<string, unknown>>)?.error?.message ?? '',
);
if (/context.length|n_ctx|too.many.tokens|maximum.context/i.test(raw)) {
return 'PDF-файл слишком большой для обработки. Попробуйте файл с меньшим количеством операций или используйте модель с большим контекстным окном.';
}
if (/timeout|timed?\s*out|ETIMEDOUT|ECONNREFUSED/i.test(raw)) {
return 'LLM-сервер не отвечает. Проверьте, что сервер запущен и доступен.';
}
return 'Временная ошибка конвертации';
}

View File

@@ -168,3 +168,9 @@ export async function updateTransaction(
comment: r.comment ?? null, comment: r.comment ?? null,
}; };
} }
export async function clearAllTransactions(): Promise<{ deleted: number }> {
const result = await pool.query('DELETE FROM transactions RETURNING id');
await pool.query('DELETE FROM imports');
return { deleted: result.rowCount ?? 0 };
}

36
docker-compose.yml Normal file
View File

@@ -0,0 +1,36 @@
# NPM: указывайте Forward на порт 3002 (frontend). Frontend проксирует /api на backend.
# Backend, frontend и PostgreSQL — в одной сети (postgres_default).
services:
backend:
build:
context: .
dockerfile: Dockerfile.backend
container_name: family-budget-backend
environment:
# Имя контейнера/сервиса PostgreSQL — postgres или postgres_budget
- DB_HOST=postgres_budget
- DB_PORT=5432
- DB_NAME=family_budget
- DB_USER=budget_user
- DB_PASSWORD=difficult_Paaaaaasword
ports:
- "3000:3000"
restart: unless-stopped
networks:
- postgres_default
frontend:
build:
context: .
dockerfile: Dockerfile.frontend
container_name: family-budget-frontend
ports:
- "3002:80"
restart: unless-stopped
networks:
- postgres_default
networks:
postgres_default:
external: true

View File

@@ -2,7 +2,7 @@
## Назначение ## Назначение
Эндпоинт принимает банковскую выписку в формате JSON 1.0 (см. `format.md`) и атомарно импортирует транзакции в БД. Эндпоинт принимает банковскую выписку (PDF или JSON) и атомарно импортирует транзакции в БД.
## Метод и URL ## Метод и URL
@@ -16,7 +16,15 @@
## Тело запроса ## Тело запроса
JSON строго по схеме 1.0 (`format.md`). Content-Type: `application/json`. **Content-Type:** `multipart/form-data`. Поле: `file`.
Допустимые типы файлов:
- **PDF** — банковская выписка. Конвертируется в JSON 1.0 через LLM (требуется `LLM_API_KEY`).
- **JSON** — файл по схеме 1.0 (см. `format.md`).
При другом типе файла — `400 Bad Request`: «Допустимы только файлы PDF или JSON».
Пример структуры JSON 1.0:
```json ```json
{ {
@@ -207,6 +215,8 @@ accountNumber|operationAt|amountSigned|commission|normalizedDescription
| Код | Ситуация | | Код | Ситуация |
|-----|--------------------------------------------------------------------------| |-----|--------------------------------------------------------------------------|
| 200 | Импорт выполнен успешно | | 200 | Импорт выполнен успешно |
| 400 | Невалидный JSON или нарушение структуры схемы 1.0 | | 400 | Файл не загружен; неверный тип (не PDF и не JSON); некорректный JSON; ошибка извлечения текста из PDF |
| 401 | Нет действующей сессии | | 401 | Нет действующей сессии |
| 422 | Семантическая ошибка валидации (некорректные данные, ошибка при вставке) | | 422 | Семантическая ошибка валидации; результат конвертации PDF не соответствует схеме 1.0 |
| 502 | Ошибка LLM при конвертации PDF |
| 503 | Конвертация PDF недоступна (не задан LLM_API_KEY) |

View File

@@ -1,205 +0,0 @@
"{""id"": 11, ""rawSms"": ""Оплата 4606.11р Карта*4215 RNAZK MJ056 ROS Баланс 15942.54р 11:59"", ""rawJson"": {""action"": ""payment"", ""amount"": 4606.11, ""balance"": 15942.54, ""raw_sms"": ""Оплата 4606.11р Карта*4215 RNAZK MJ056 ROS Баланс 15942.54р 11:59"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 4606.11р Карта*4215 RNAZK MJ056 ROS Баланс 15942.54р 11:59"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-02T08:59:25.474Z"", ""sms_contact"": """", ""signed_amount"": -4606.11}, ""smsText"": ""Оплата 4606.11р Карта*4215 RNAZK MJ056 ROS Баланс 15942.54р 11:59"", ""category"": ""Авто"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-02T08:59:25.474+00:00"", ""counterparty"": ""Роснефть"", ""signedAmount"": -4606.11, ""humanVerified"": false, ""humanVerifiedAt"": null}"
"{""id"": 12, ""rawSms"": ""Оплата 1250р Карта*2249 mkad Баланс 14692.54р 12:08"", ""rawJson"": {""action"": ""payment"", ""amount"": 1250, ""balance"": 14692.54, ""raw_sms"": ""Оплата 1250р Карта*2249 mkad Баланс 14692.54р 12:08"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 1250р Карта*2249 mkad Баланс 14692.54р 12:08"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-02T09:08:35.206Z"", ""sms_contact"": """", ""signed_amount"": -1250}, ""smsText"": ""Оплата 1250р Карта*2249 mkad Баланс 14692.54р 12:08"", ""category"": ""Авто"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-02T09:08:35.206+00:00"", ""counterparty"": ""Автомойка"", ""signedAmount"": -1250.00, ""humanVerified"": false, ""humanVerifiedAt"": null}"
"{""id"": 13, ""rawSms"": ""Оплата 1194.55р Карта*4215 MAGNIT MM AVIAD Баланс 13497.99р 13:20"", ""rawJson"": {""action"": ""payment"", ""amount"": 1194.55, ""balance"": 13497.99, ""raw_sms"": ""Оплата 1194.55р Карта*4215 MAGNIT MM AVIAD Баланс 13497.99р 13:20"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 1194.55р Карта*4215 MAGNIT MM AVIAD Баланс 13497.99р 13:20"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-02T10:20:56.565Z"", ""sms_contact"": """", ""signed_amount"": -1194.55}, ""smsText"": ""Оплата 1194.55р Карта*4215 MAGNIT MM AVIAD Баланс 13497.99р 13:20"", ""category"": ""Продукты"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-02T10:20:56.565+00:00"", ""counterparty"": ""Магнит"", ""signedAmount"": -1194.55, ""humanVerified"": false, ""humanVerifiedAt"": null}"
"{""id"": 14, ""rawSms"": ""Оплата 610.94р Карта*2249 DOSTAVKA IZ PYA Баланс 12887.05р 16:41"", ""rawJson"": {""action"": ""payment"", ""amount"": 610.94, ""balance"": 12887.05, ""raw_sms"": ""Оплата 610.94р Карта*2249 DOSTAVKA IZ PYA Баланс 12887.05р 16:41"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 610.94р Карта*2249 DOSTAVKA IZ PYA Баланс 12887.05р 16:41"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-02T13:41:40.860Z"", ""sms_contact"": """", ""signed_amount"": -610.94}, ""smsText"": ""Оплата 610.94р Карта*2249 DOSTAVKA IZ PYA Баланс 12887.05р 16:41"", ""category"": ""Продукты"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-02T13:41:40.86+00:00"", ""counterparty"": ""Пятёрочка Доставка"", ""signedAmount"": -610.94, ""humanVerified"": false, ""humanVerifiedAt"": null}"
"{""id"": 15, ""rawSms"": ""Отмена: Оплата 610.94р Карта*2249 DOSTAVKA IZ PYA Баланс 13497.99р 16:41"", ""rawJson"": {""action"": ""reversal"", ""amount"": 610.94, ""balance"": 13497.99, ""raw_sms"": ""Отмена: Оплата 610.94р Карта*2249 DOSTAVKA IZ PYA Баланс 13497.99р 16:41"", ""currency"": ""RUB"", ""sms_text"": ""Отмена: Оплата 610.94р Карта*2249 DOSTAVKA IZ PYA Баланс 13497.99р 16:41"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-02T13:51:46.634Z"", ""sms_contact"": """", ""signed_amount"": 610.94}, ""smsText"": ""Отмена: Оплата 610.94р Карта*2249 DOSTAVKA IZ PYA Баланс 13497.99р 16:41"", ""category"": ""Продукты"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-02T13:51:46.634+00:00"", ""counterparty"": ""Пятёрочка Доставка"", ""signedAmount"": 610.94, ""humanVerified"": false, ""humanVerifiedAt"": null}"
"{""id"": 16, ""rawSms"": ""Оплата 342р Карта*4215 IP SHVETSOV A.A Баланс 13155.99р 18:28"", ""rawJson"": {""action"": ""payment"", ""amount"": 342, ""balance"": 13155.99, ""raw_sms"": ""Оплата 342р Карта*4215 IP SHVETSOV A.A Баланс 13155.99р 18:28"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 342р Карта*4215 IP SHVETSOV A.A Баланс 13155.99р 18:28"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-02T15:28:47.636Z"", ""sms_contact"": """", ""signed_amount"": -342}, ""smsText"": ""Оплата 342р Карта*4215 IP SHVETSOV A.A Баланс 13155.99р 18:28"", ""category"": ""Продукты"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-02T15:28:47.636+00:00"", ""counterparty"": ""ИП Швецов"", ""signedAmount"": -342.00, ""humanVerified"": false, ""humanVerifiedAt"": null}"
"{""id"": 17, ""rawSms"": ""Оплата 429.95р Карта*4215 PYATEROCHKA Баланс 12726.04р 18:33"", ""rawJson"": {""action"": ""payment"", ""amount"": 429.95, ""balance"": 12726.04, ""raw_sms"": ""Оплата 429.95р Карта*4215 PYATEROCHKA Баланс 12726.04р 18:33"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 429.95р Карта*4215 PYATEROCHKA Баланс 12726.04р 18:33"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-02T15:33:53.557Z"", ""sms_contact"": """", ""signed_amount"": -429.95}, ""smsText"": ""Оплата 429.95р Карта*4215 PYATEROCHKA Баланс 12726.04р 18:33"", ""category"": ""Продукты"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-02T15:33:53.557+00:00"", ""counterparty"": ""Пятёрочка"", ""signedAmount"": -429.95, ""humanVerified"": false, ""humanVerifiedAt"": null}"
"{""id"": 21, ""rawSms"": ""Снятие 5000р Карта*4215 D. 37/1, MKR. B Баланс 7726.04р 14:45"", ""rawJson"": {""action"": ""unknown"", ""amount"": 5000, ""balance"": 7726.04, ""raw_sms"": ""Снятие 5000р Карта*4215 D. 37/1, MKR. B Баланс 7726.04р 14:45"", ""currency"": ""RUB"", ""sms_text"": ""Снятие 5000р Карта*4215 D. 37/1, MKR. B Баланс 7726.04р 14:45"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-03T11:45:36.294Z"", ""sms_contact"": """", ""signed_amount"": 5000}, ""smsText"": ""Снятие 5000р Карта*4215 D. 37/1, MKR. B Баланс 7726.04р 14:45"", ""category"": ""Наличные"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-03T11:45:36.294+00:00"", ""counterparty"": ""Банкомат"", ""signedAmount"": 5000.00, ""humanVerified"": false, ""humanVerifiedAt"": null}"
"{""id"": 22, ""rawSms"": ""Перевод 650р Счет*5611 МегаФон. Баланс 7076.04р 21:36"", ""rawJson"": {""action"": ""transfer"", ""amount"": 650, ""balance"": 7076.04, ""raw_sms"": ""Перевод 650р Счет*5611 МегаФон. Баланс 7076.04р 21:36"", ""currency"": ""RUB"", ""sms_text"": ""Перевод 650р Счет*5611 МегаФон. Баланс 7076.04р 21:36"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-03T18:36:07.839Z"", ""sms_contact"": """", ""signed_amount"": -650}, ""smsText"": ""Перевод 650р Счет*5611 МегаФон. Баланс 7076.04р 21:36"", ""category"": ""Подписки"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-03T18:36:07.839+00:00"", ""counterparty"": ""МегаФон"", ""signedAmount"": -650.00, ""humanVerified"": false, ""humanVerifiedAt"": null}"
"{""id"": 23, ""rawSms"": ""Оплата 1010р Карта*4215 HaNoi Deli Баланс 5270.70р 11:51"", ""rawJson"": {""action"": ""payment"", ""amount"": 1010, ""balance"": 5270.7, ""raw_sms"": ""Оплата 1010р Карта*4215 HaNoi Deli Баланс 5270.70р 11:51"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 1010р Карта*4215 HaNoi Deli Баланс 5270.70р 11:51"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-04T08:51:41.825Z"", ""sms_contact"": """", ""signed_amount"": -1010}, ""smsText"": ""Оплата 1010р Карта*4215 HaNoi Deli Баланс 5270.70р 11:51"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-04T08:51:41.825+00:00"", ""counterparty"": ""HaNoi Deli"", ""signedAmount"": -1010.00, ""humanVerified"": false, ""humanVerifiedAt"": null}"
"{""id"": 24, ""rawSms"": ""Оплата 700р Карта*4215 Vetnamskoe kafe Баланс 4570.70р 11:54"", ""rawJson"": {""action"": ""payment"", ""amount"": 700, ""balance"": 4570.7, ""raw_sms"": ""Оплата 700р Карта*4215 Vetnamskoe kafe Баланс 4570.70р 11:54"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 700р Карта*4215 Vetnamskoe kafe Баланс 4570.70р 11:54"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-04T08:55:02.801Z"", ""sms_contact"": """", ""signed_amount"": -700}, ""smsText"": ""Оплата 700р Карта*4215 Vetnamskoe kafe Баланс 4570.70р 11:54"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-04T08:55:02.801+00:00"", ""counterparty"": ""Вьетнамское кафе"", ""signedAmount"": -700.00, ""humanVerified"": false, ""humanVerifiedAt"": null}"
"{""id"": 25, ""rawSms"": ""Оплата 100р Карта*8214 w_games Баланс 4470.70р 09:36"", ""rawJson"": {""action"": ""payment"", ""amount"": 100, ""balance"": 4470.7, ""raw_sms"": ""Оплата 100р Карта*8214 w_games Баланс 4470.70р 09:36"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 100р Карта*8214 w_games Баланс 4470.70р 09:36"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-05T06:37:00.782Z"", ""sms_contact"": """", ""signed_amount"": -100}, ""smsText"": ""Оплата 100р Карта*8214 w_games Баланс 4470.70р 09:36"", ""category"": ""Развлечения"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-05T06:37:00.782+00:00"", ""counterparty"": ""W Games"", ""signedAmount"": -100.00, ""humanVerified"": false, ""humanVerifiedAt"": null}"
"{""id"": 54, ""rawSms"": ""Оплата 670р Карта*2249 OZON Баланс 3800.70р 09:33"", ""rawJson"": {""action"": ""payment"", ""amount"": 670, ""balance"": 3800.7, ""raw_sms"": ""Оплата 670р Карта*2249 OZON Баланс 3800.70р 09:33"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 670р Карта*2249 OZON Баланс 3800.70р 09:33"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-06T06:33:34.637Z"", ""sms_contact"": """", ""signed_amount"": -670}, ""smsText"": ""Оплата 670р Карта*2249 OZON Баланс 3800.70р 09:33"", ""category"": ""Здоровье"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-06T06:33:34.637+00:00"", ""counterparty"": ""Озон"", ""signedAmount"": -670.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 55, ""rawSms"": ""Оплата 1265.95р Карта*4215 PYATEROCHKA Баланс 7534.75р 10:23"", ""rawJson"": {""action"": ""payment"", ""amount"": 1265.95, ""balance"": 7534.75, ""raw_sms"": ""Оплата 1265.95р Карта*4215 PYATEROCHKA Баланс 7534.75р 10:23"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 1265.95р Карта*4215 PYATEROCHKA Баланс 7534.75р 10:23"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-06T07:23:31.475Z"", ""sms_contact"": """", ""signed_amount"": -1265.95}, ""smsText"": ""Оплата 1265.95р Карта*4215 PYATEROCHKA Баланс 7534.75р 10:23"", ""category"": ""Продукты"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-06T07:23:31.475+00:00"", ""counterparty"": ""Пятерочка"", ""signedAmount"": -1265.95, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 56, ""rawSms"": ""Оплата 1835р Карта*4215 IP SHVETSOV A.A Баланс 5699.75р 10:28"", ""rawJson"": {""action"": ""payment"", ""amount"": 1835, ""balance"": 5699.75, ""raw_sms"": ""Оплата 1835р Карта*4215 IP SHVETSOV A.A Баланс 5699.75р 10:28"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 1835р Карта*4215 IP SHVETSOV A.A Баланс 5699.75р 10:28"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-06T07:28:24.491Z"", ""sms_contact"": """", ""signed_amount"": -1835}, ""smsText"": ""Оплата 1835р Карта*4215 IP SHVETSOV A.A Баланс 5699.75р 10:28"", ""category"": ""Продукты"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-06T07:28:24.491+00:00"", ""counterparty"": ""Рынок"", ""signedAmount"": -1835.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 57, ""rawSms"": ""Оплата 951.60р Карта*8214 ZOOMAGAZIN CHET Баланс 4748.15р 11:04"", ""rawJson"": {""action"": ""payment"", ""amount"": 951.6, ""balance"": 4748.15, ""raw_sms"": ""Оплата 951.60р Карта*8214 ZOOMAGAZIN CHET Баланс 4748.15р 11:04"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 951.60р Карта*8214 ZOOMAGAZIN CHET Баланс 4748.15р 11:04"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-06T08:05:02.145Z"", ""sms_contact"": """", ""signed_amount"": -951.6}, ""smsText"": ""Оплата 951.60р Карта*8214 ZOOMAGAZIN CHET Баланс 4748.15р 11:04"", ""category"": ""Зоотовары"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-06T08:05:02.145+00:00"", ""counterparty"": ""Четыре лапы"", ""signedAmount"": -951.60, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 58, ""rawSms"": ""Оплата 1293.80р Карта*4215 MAGNIT MM OSHMY Баланс 3454.35р 11:29"", ""rawJson"": {""action"": ""payment"", ""amount"": 1293.8, ""balance"": 3454.35, ""raw_sms"": ""Оплата 1293.80р Карта*4215 MAGNIT MM OSHMY Баланс 3454.35р 11:29"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 1293.80р Карта*4215 MAGNIT MM OSHMY Баланс 3454.35р 11:29"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-06T08:29:32.171Z"", ""sms_contact"": """", ""signed_amount"": -1293.8}, ""smsText"": ""Оплата 1293.80р Карта*4215 MAGNIT MM OSHMY Баланс 3454.35р 11:29"", ""category"": ""Продукты"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-06T08:29:32.171+00:00"", ""counterparty"": ""Магнит"", ""signedAmount"": -1293.80, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 59, ""rawSms"": ""Перевод 500р Счет*5611 Ивченко Н. Баланс 2954.35р 19:08"", ""rawJson"": {""action"": ""transfer"", ""amount"": 500, ""balance"": 2954.35, ""raw_sms"": ""Перевод 500р Счет*5611 Ивченко Н. Баланс 2954.35р 19:08"", ""currency"": ""RUB"", ""sms_text"": ""Перевод 500р Счет*5611 Ивченко Н. Баланс 2954.35р 19:08"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-06T16:08:43.385Z"", ""sms_contact"": """", ""signed_amount"": -500}, ""smsText"": ""Перевод 500р Счет*5611 Ивченко Н. Баланс 2954.35р 19:08"", ""category"": ""Спорт"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-06T16:08:43.385+00:00"", ""counterparty"": ""Буран"", ""signedAmount"": -500.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 61, ""rawSms"": ""Оплата 2539р Карта*2249 OTO*Telegram Баланс 415.35р 23:23"", ""rawJson"": {""action"": ""payment"", ""amount"": 2539, ""balance"": 415.35, ""raw_sms"": ""Оплата 2539р Карта*2249 OTO*Telegram Баланс 415.35р 23:23"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 2539р Карта*2249 OTO*Telegram Баланс 415.35р 23:23"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-06T20:23:42.223Z"", ""sms_contact"": """", ""signed_amount"": -2539}, ""smsText"": ""Оплата 2539р Карта*2249 OTO*Telegram Баланс 415.35р 23:23"", ""category"": ""Подписка"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-06T20:23:42.223+00:00"", ""counterparty"": ""Не определён"", ""signedAmount"": -2539.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 73, ""rawSms"": ""Оплата 1р Карта*2249 DOSTAVKA PEREKR Баланс 5414.35р 10:47"", ""rawJson"": {""action"": ""payment"", ""amount"": 1, ""balance"": 5414.35, ""raw_sms"": ""Оплата 1р Карта*2249 DOSTAVKA PEREKR Баланс 5414.35р 10:47"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 1р Карта*2249 DOSTAVKA PEREKR Баланс 5414.35р 10:47"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-07T07:48:01.526Z"", ""sms_contact"": """", ""signed_amount"": -1}, ""smsText"": ""Оплата 1р Карта*2249 DOSTAVKA PEREKR Баланс 5414.35р 10:47"", ""category"": ""Продукты"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-07T07:48:01.526+00:00"", ""counterparty"": ""Перекрёсток Доставка"", ""signedAmount"": -1.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-07T10:48:00+00:00""}"
"{""id"": 74, ""rawSms"": ""Отмена: Оплата 1р Карта*2249 DOSTAVKA PEREKR Баланс 5415.35р 10:47"", ""rawJson"": {""action"": ""reversal"", ""amount"": 1, ""balance"": 5415.35, ""raw_sms"": ""Отмена: Оплата 1р Карта*2249 DOSTAVKA PEREKR Баланс 5415.35р 10:47"", ""currency"": ""RUB"", ""sms_text"": ""Отмена: Оплата 1р Карта*2249 DOSTAVKA PEREKR Баланс 5415.35р 10:47"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-07T07:48:02.711Z"", ""sms_contact"": """", ""signed_amount"": 1}, ""smsText"": ""Отмена: Оплата 1р Карта*2249 DOSTAVKA PEREKR Баланс 5415.35р 10:47"", ""category"": ""Продукты"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-07T07:48:02.711+00:00"", ""counterparty"": ""Перекрёсток Доставка"", ""signedAmount"": 1.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-07T10:48:00+00:00""}"
"{""id"": 75, ""rawSms"": ""Оплата 595.67р Карта*2249 DOSTAVKA PEREKR Баланс 4819.68р 10:53"", ""rawJson"": {""action"": ""payment"", ""amount"": 595.67, ""balance"": 4819.68, ""raw_sms"": ""Оплата 595.67р Карта*2249 DOSTAVKA PEREKR Баланс 4819.68р 10:53"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 595.67р Карта*2249 DOSTAVKA PEREKR Баланс 4819.68р 10:53"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-07T07:53:09.728Z"", ""sms_contact"": """", ""signed_amount"": -595.67}, ""smsText"": ""Оплата 595.67р Карта*2249 DOSTAVKA PEREKR Баланс 4819.68р 10:53"", ""category"": ""продукты"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-07T07:53:09.728+00:00"", ""counterparty"": ""перекресток"", ""signedAmount"": -595.67, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 76, ""rawSms"": ""Оплата 132.98р Карта*4215 PYATEROCHKA Баланс 4686.70р 10:56"", ""rawJson"": {""action"": ""payment"", ""amount"": 132.98, ""balance"": 4686.7, ""raw_sms"": ""Оплата 132.98р Карта*4215 PYATEROCHKA Баланс 4686.70р 10:56"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 132.98р Карта*4215 PYATEROCHKA Баланс 4686.70р 10:56"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-07T07:57:05.422Z"", ""sms_contact"": """", ""signed_amount"": -132.98}, ""smsText"": ""Оплата 132.98р Карта*4215 PYATEROCHKA Баланс 4686.70р 10:56"", ""category"": ""продукты"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-07T07:57:05.422+00:00"", ""counterparty"": ""пятерочка"", ""signedAmount"": -132.98, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 77, ""rawSms"": ""Списание 550р Счет*5611 МИХАИЛ К. Баланс 4136.70р 10:00"", ""rawJson"": {""action"": ""writeoff"", ""amount"": 550, ""balance"": 4136.7, ""raw_sms"": ""Списание 550р Счет*5611 МИХАИЛ К. Баланс 4136.70р 10:00"", ""currency"": ""RUB"", ""sms_text"": ""Списание 550р Счет*5611 МИХАИЛ К. Баланс 4136.70р 10:00"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-08T07:00:54.503Z"", ""sms_contact"": """", ""signed_amount"": -550}, ""smsText"": ""Списание 550р Счет*5611 МИХАИЛ К. Баланс 4136.70р 10:00"", ""category"": ""Авто"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-08T07:00:54.503+00:00"", ""counterparty"": ""Михаил К."", ""signedAmount"": -550.00, ""humanVerified"": false, ""humanVerifiedAt"": null}"
"{""id"": 78, ""rawSms"": ""Оплата 89.99р Карта*8214 PEREKRESTOKVALP Баланс 4046.71р 11:28"", ""rawJson"": {""action"": ""payment"", ""amount"": 89.99, ""balance"": 4046.71, ""raw_sms"": ""Оплата 89.99р Карта*8214 PEREKRESTOKVALP Баланс 4046.71р 11:28"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 89.99р Карта*8214 PEREKRESTOKVALP Баланс 4046.71р 11:28"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-08T08:29:03.706Z"", ""sms_contact"": """", ""signed_amount"": -89.99}, ""smsText"": ""Оплата 89.99р Карта*8214 PEREKRESTOKVALP Баланс 4046.71р 11:28"", ""category"": ""Продукты"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-08T08:29:03.706+00:00"", ""counterparty"": ""Перекресток"", ""signedAmount"": -89.99, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 79, ""rawSms"": ""Списание 500р Счет*5611 Рубен А. Баланс 3546.71р 11:52"", ""rawJson"": {""action"": ""writeoff"", ""amount"": 500, ""balance"": 3546.71, ""raw_sms"": ""Списание 500р Счет*5611 Рубен А. Баланс 3546.71р 11:52"", ""currency"": ""RUB"", ""sms_text"": ""Списание 500р Счет*5611 Рубен А. Баланс 3546.71р 11:52"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-08T08:52:25.687Z"", ""sms_contact"": """", ""signed_amount"": -500}, ""smsText"": ""Списание 500р Счет*5611 Рубен А. Баланс 3546.71р 11:52"", ""category"": ""Спорт"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-08T08:52:25.687+00:00"", ""counterparty"": ""Буран"", ""signedAmount"": -500.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 80, ""rawSms"": ""Оплата 205.98р Карта*4215 \"" VP 32536 Баланс 3340.73р 12:11"", ""rawJson"": {""action"": ""payment"", ""amount"": 205.98, ""balance"": 3340.73, ""raw_sms"": ""Оплата 205.98р Карта*4215 \"" VP 32536 Баланс 3340.73р 12:11"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 205.98р Карта*4215 \"" VP 32536 Баланс 3340.73р 12:11"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-08T09:12:01.930Z"", ""sms_contact"": """", ""signed_amount"": -205.98}, ""smsText"": ""Оплата 205.98р Карта*4215 \"" VP 32536 Баланс 3340.73р 12:11"", ""category"": ""Продукты"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-08T09:12:01.93+00:00"", ""counterparty"": ""Пятерочка"", ""signedAmount"": -205.98, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 81, ""rawSms"": ""Оплата 3441.60р Карта*4215 CHETYRE LAPY Баланс 4899.13р 14:08"", ""rawJson"": {""action"": ""payment"", ""amount"": 3441.6, ""balance"": 4899.13, ""raw_sms"": ""Оплата 3441.60р Карта*4215 CHETYRE LAPY Баланс 4899.13р 14:08"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 3441.60р Карта*4215 CHETYRE LAPY Баланс 4899.13р 14:08"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-08T11:08:21.442Z"", ""sms_contact"": """", ""signed_amount"": -3441.6}, ""smsText"": ""Оплата 3441.60р Карта*4215 CHETYRE LAPY Баланс 4899.13р 14:08"", ""category"": ""Животные"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-08T11:08:21.442+00:00"", ""counterparty"": ""Четыре лапы"", ""signedAmount"": -3441.60, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 82, ""rawSms"": ""Оплата 295р Карта*4215 MASTERKOM 1 Баланс 4604.13р 14:23"", ""rawJson"": {""action"": ""payment"", ""amount"": 295, ""balance"": 4604.13, ""raw_sms"": ""Оплата 295р Карта*4215 MASTERKOM 1 Баланс 4604.13р 14:23"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 295р Карта*4215 MASTERKOM 1 Баланс 4604.13р 14:23"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-08T11:23:13.541Z"", ""sms_contact"": """", ""signed_amount"": -295}, ""smsText"": ""Оплата 295р Карта*4215 MASTERKOM 1 Баланс 4604.13р 14:23"", ""category"": ""Ремонт"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-08T11:23:13.541+00:00"", ""counterparty"": ""Стройматериалы"", ""signedAmount"": -295.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 84, ""rawSms"": ""Оплата 3480р Карта*2249 OZON Баланс 1124.13р 20:15"", ""rawJson"": {""action"": ""payment"", ""amount"": 3480, ""balance"": 1124.13, ""raw_sms"": ""Оплата 3480р Карта*2249 OZON Баланс 1124.13р 20:15"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 3480р Карта*2249 OZON Баланс 1124.13р 20:15"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-08T17:15:26.647Z"", ""sms_contact"": """", ""signed_amount"": -3480}, ""smsText"": ""Оплата 3480р Карта*2249 OZON Баланс 1124.13р 20:15"", ""category"": ""Дом"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-08T17:15:26.647+00:00"", ""counterparty"": ""Озон"", ""signedAmount"": -3480.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 85, ""rawSms"": ""Оплата 209р Карта*2249 OZON Баланс 915.13р 20:56"", ""rawJson"": {""action"": ""payment"", ""amount"": 209, ""balance"": 915.13, ""raw_sms"": ""Оплата 209р Карта*2249 OZON Баланс 915.13р 20:56"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 209р Карта*2249 OZON Баланс 915.13р 20:56"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-08T17:56:32.852Z"", ""sms_contact"": """", ""signed_amount"": -209}, ""smsText"": ""Оплата 209р Карта*2249 OZON Баланс 915.13р 20:56"", ""category"": ""Ремонт"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-08T17:56:32.852+00:00"", ""counterparty"": ""Озон"", ""signedAmount"": -209.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 87, ""rawSms"": ""Оплата 446р Карта*8214 MSKAPT 708 Баланс 469.13р 20:30"", ""rawJson"": {""action"": ""payment"", ""amount"": 446, ""balance"": 469.13, ""raw_sms"": ""Оплата 446р Карта*8214 MSKAPT 708 Баланс 469.13р 20:30"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 446р Карта*8214 MSKAPT 708 Баланс 469.13р 20:30"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-09T17:31:04.549Z"", ""sms_contact"": """", ""signed_amount"": -446}, ""smsText"": ""Оплата 446р Карта*8214 MSKAPT 708 Баланс 469.13р 20:30"", ""category"": ""Здоровье"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-09T17:31:04.549+00:00"", ""counterparty"": ""Аптека"", ""signedAmount"": -446.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 88, ""rawSms"": ""Оплата 271.97р Карта*8214 VERNYJ 1473 Баланс 197.16р 20:34"", ""rawJson"": {""action"": ""payment"", ""amount"": 271.97, ""balance"": 197.16, ""raw_sms"": ""Оплата 271.97р Карта*8214 VERNYJ 1473 Баланс 197.16р 20:34"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 271.97р Карта*8214 VERNYJ 1473 Баланс 197.16р 20:34"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-09T17:34:35.096Z"", ""sms_contact"": """", ""signed_amount"": -271.97}, ""smsText"": ""Оплата 271.97р Карта*8214 VERNYJ 1473 Баланс 197.16р 20:34"", ""category"": ""Продукты"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-09T17:34:35.096+00:00"", ""counterparty"": ""Верный"", ""signedAmount"": -271.97, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 90, ""rawSms"": ""Перевод 500р Счет*5611 МТС Mobile. Баланс 4697.16р 10:06"", ""rawJson"": {""action"": ""transfer"", ""amount"": 500, ""balance"": 4697.16, ""raw_sms"": ""Перевод 500р Счет*5611 МТС Mobile. Баланс 4697.16р 10:06"", ""currency"": ""RUB"", ""sms_text"": ""Перевод 500р Счет*5611 МТС Mobile. Баланс 4697.16р 10:06"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-10T07:06:31.816Z"", ""sms_contact"": """", ""signed_amount"": -500}, ""smsText"": ""Перевод 500р Счет*5611 МТС Mobile. Баланс 4697.16р 10:06"", ""category"": ""Подписки"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-10T07:06:31.816+00:00"", ""counterparty"": ""МТС"", ""signedAmount"": -500.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 91, ""rawSms"": ""Оплата 4650р Карта*4215 IP SHARAFETDINO Баланс 5047.16р 12:19"", ""rawJson"": {""action"": ""payment"", ""amount"": 4650, ""balance"": 5047.16, ""raw_sms"": ""Оплата 4650р Карта*4215 IP SHARAFETDINO Баланс 5047.16р 12:19"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 4650р Карта*4215 IP SHARAFETDINO Баланс 5047.16р 12:19"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-10T09:19:18.679Z"", ""sms_contact"": """", ""signed_amount"": -4650}, ""smsText"": ""Оплата 4650р Карта*4215 IP SHARAFETDINO Баланс 5047.16р 12:19"", ""category"": ""Арчи"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-10T09:19:18.679+00:00"", ""counterparty"": ""IP SHARAFETDINO"", ""signedAmount"": -4650.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 92, ""rawSms"": ""Оплата 500р Карта*2249 VANYAVPN Баланс 4547.16р 10:28"", ""rawJson"": {""action"": ""payment"", ""amount"": 500, ""balance"": 4547.16, ""raw_sms"": ""Оплата 500р Карта*2249 VANYAVPN Баланс 4547.16р 10:28"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 500р Карта*2249 VANYAVPN Баланс 4547.16р 10:28"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-11T07:28:41.837Z"", ""sms_contact"": """", ""signed_amount"": -500}, ""smsText"": ""Оплата 500р Карта*2249 VANYAVPN Баланс 4547.16р 10:28"", ""category"": ""Подписки"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-11T07:28:41.837+00:00"", ""counterparty"": ""Ванивпн"", ""signedAmount"": -500.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-11T10:29:00+00:00""}"
"{""id"": 93, ""rawSms"": ""Оплата 2075р Карта*2249 YM*sutochno.ru Баланс 2472.16р 11:30"", ""rawJson"": {""action"": ""payment"", ""amount"": 2075, ""balance"": 2472.16, ""raw_sms"": ""Оплата 2075р Карта*2249 YM*sutochno.ru Баланс 2472.16р 11:30"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 2075р Карта*2249 YM*sutochno.ru Баланс 2472.16р 11:30"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-11T08:30:52.559Z"", ""sms_contact"": """", ""signed_amount"": -2075}, ""smsText"": ""Оплата 2075р Карта*2249 YM*sutochno.ru Баланс 2472.16р 11:30"", ""category"": ""Развлечения"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-11T08:30:52.559+00:00"", ""counterparty"": ""Сутонь"", ""signedAmount"": -2075.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 94, ""rawSms"": ""Перевод 1000р Счет*5611 ООО ГРАНЛАЙН - . Баланс 3120.16р 19:14"", ""rawJson"": {""action"": ""transfer"", ""amount"": 1000, ""balance"": 3120.16, ""raw_sms"": ""Перевод 1000р Счет*5611 ООО ГРАНЛАЙН - . Баланс 3120.16р 19:14"", ""currency"": ""RUB"", ""sms_text"": ""Перевод 1000р Счет*5611 ООО ГРАНЛАЙН - . Баланс 3120.16р 19:14"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-11T16:14:25.818Z"", ""sms_contact"": """", ""signed_amount"": -1000}, ""smsText"": ""Перевод 1000р Счет*5611 ООО ГРАНЛАЙН - . Баланс 3120.16р 19:14"", ""category"": ""Подписки"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-11T16:14:25.818+00:00"", ""counterparty"": ""ООО Гранлайн"", ""signedAmount"": -1000.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 95, ""rawSms"": ""Зачисление кешбэка по программе лояльности 1648р Счет*5611 Баланс 3120.16р 19:07"", ""rawJson"": {""action"": ""income"", ""amount"": 1648, ""balance"": 3120.16, ""raw_sms"": ""Зачисление кешбэка по программе лояльности 1648р Счет*5611 Баланс 3120.16р 19:07"", ""currency"": ""RUB"", ""sms_text"": ""Зачисление кешбэка по программе лояльности 1648р Счет*5611 Баланс 3120.16р 19:07"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-11T16:35:10.521Z"", ""sms_contact"": """", ""signed_amount"": 1648}, ""smsText"": ""Зачисление кешбэка по программе лояльности 1648р Счет*5611 Баланс 3120.16р 19:07"", ""category"": ""Поступления"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-11T16:35:10.521+00:00"", ""counterparty"": ""ВТБ"", ""signedAmount"": 1648.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 96, ""rawSms"": ""Оплата 163р Карта*2249 OZON Баланс 2957.16р 20:46"", ""rawJson"": {""action"": ""payment"", ""amount"": 163, ""balance"": 2957.16, ""raw_sms"": ""Оплата 163р Карта*2249 OZON Баланс 2957.16р 20:46"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 163р Карта*2249 OZON Баланс 2957.16р 20:46"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-11T17:46:26.933Z"", ""sms_contact"": """", ""signed_amount"": -163}, ""smsText"": ""Оплата 163р Карта*2249 OZON Баланс 2957.16р 20:46"", ""category"": ""Дом"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-11T17:46:26.933+00:00"", ""counterparty"": ""OZON"", ""signedAmount"": -163.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-11T21:49:00+00:00""}"
"{""id"": 97, ""rawSms"": ""Оплата 601.94р Карта*4215 PYATEROCHKA Баланс 2355.22р 20:56"", ""rawJson"": {""action"": ""payment"", ""amount"": 601.94, ""balance"": 2355.22, ""raw_sms"": ""Оплата 601.94р Карта*4215 PYATEROCHKA Баланс 2355.22р 20:56"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 601.94р Карта*4215 PYATEROCHKA Баланс 2355.22р 20:56"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-11T17:56:14.202Z"", ""sms_contact"": """", ""signed_amount"": -601.94}, ""smsText"": ""Оплата 601.94р Карта*4215 PYATEROCHKA Баланс 2355.22р 20:56"", ""category"": ""Продукты"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-11T17:56:14.202+00:00"", ""counterparty"": ""Пятёрочка"", ""signedAmount"": -601.94, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-11T21:49:00+00:00""}"
"{""id"": 98, ""rawSms"": ""Оплата 100р Карта*4215 IP SHEVELEV S.A Баланс 2255.22р 08:12"", ""rawJson"": {""action"": ""payment"", ""amount"": 100, ""balance"": 2255.22, ""raw_sms"": ""Оплата 100р Карта*4215 IP SHEVELEV S.A Баланс 2255.22р 08:12"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 100р Карта*4215 IP SHEVELEV S.A Баланс 2255.22р 08:12"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-12T05:12:14.999Z"", ""sms_contact"": """", ""signed_amount"": -100}, ""smsText"": ""Оплата 100р Карта*4215 IP SHEVELEV S.A Баланс 2255.22р 08:12"", ""category"": ""Авто"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-12T05:12:14.999+00:00"", ""counterparty"": ""IP SHEVELEV S.A"", ""signedAmount"": -100.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 99, ""rawSms"": ""Оплата 280р Карта*4215 STOLOVAYA 2 Баланс 1975.22р 13:33"", ""rawJson"": {""action"": ""payment"", ""amount"": 280, ""balance"": 1975.22, ""raw_sms"": ""Оплата 280р Карта*4215 STOLOVAYA 2 Баланс 1975.22р 13:33"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 280р Карта*4215 STOLOVAYA 2 Баланс 1975.22р 13:33"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-12T10:33:25.113Z"", ""sms_contact"": """", ""signed_amount"": -280}, ""smsText"": ""Оплата 280р Карта*4215 STOLOVAYA 2 Баланс 1975.22р 13:33"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-12T10:33:25.113+00:00"", ""counterparty"": ""Столовая"", ""signedAmount"": -280.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-12T14:00:00+00:00""}"
"{""id"": 100, ""rawSms"": ""Оплата 225р Карта*2249 YP*anta-sport.r Баланс 1750.22р 17:02"", ""rawJson"": {""action"": ""payment"", ""amount"": 225, ""balance"": 1750.22, ""raw_sms"": ""Оплата 225р Карта*2249 YP*anta-sport.r Баланс 1750.22р 17:02"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 225р Карта*2249 YP*anta-sport.r Баланс 1750.22р 17:02"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-12T14:02:17.343Z"", ""sms_contact"": """", ""signed_amount"": -225}, ""smsText"": ""Оплата 225р Карта*2249 YP*anta-sport.r Баланс 1750.22р 17:02"", ""category"": ""Спорт"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-12T14:02:17.343+00:00"", ""counterparty"": ""Анта-спорт"", ""signedAmount"": -225.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-12T17:02:00+00:00""}"
"{""id"": 101, ""rawSms"": ""Оплата 256р Карта*4215 STOLOVAYA 2 Баланс 1494.22р 17:53"", ""rawJson"": {""action"": ""payment"", ""amount"": 256, ""balance"": 1494.22, ""raw_sms"": ""Оплата 256р Карта*4215 STOLOVAYA 2 Баланс 1494.22р 17:53"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 256р Карта*4215 STOLOVAYA 2 Баланс 1494.22р 17:53"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-12T14:53:29.555Z"", ""sms_contact"": """", ""signed_amount"": -256}, ""smsText"": ""Оплата 256р Карта*4215 STOLOVAYA 2 Баланс 1494.22р 17:53"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-12T14:53:29.555+00:00"", ""counterparty"": ""Столовая"", ""signedAmount"": -256.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-12T17:53:00+00:00""}"
"{""id"": 102, ""rawSms"": ""Оплата 389р Карта*2249 OZON.RU Баланс 1105.22р 18:35"", ""rawJson"": {""action"": ""payment"", ""amount"": 389, ""balance"": 1105.22, ""raw_sms"": ""Оплата 389р Карта*2249 OZON.RU Баланс 1105.22р 18:35"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 389р Карта*2249 OZON.RU Баланс 1105.22р 18:35"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-12T15:35:18.793Z"", ""sms_contact"": """", ""signed_amount"": -389}, ""smsText"": ""Оплата 389р Карта*2249 OZON.RU Баланс 1105.22р 18:35"", ""category"": ""Дом"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-12T15:35:18.793+00:00"", ""counterparty"": ""OZON"", ""signedAmount"": -389.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-12T19:18:00+00:00""}"
"{""id"": 103, ""rawSms"": ""Оплата 507р Карта*2249 OZON Баланс 598.22р 18:59"", ""rawJson"": {""action"": ""payment"", ""amount"": 507, ""balance"": 598.22, ""raw_sms"": ""Оплата 507р Карта*2249 OZON Баланс 598.22р 18:59"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 507р Карта*2249 OZON Баланс 598.22р 18:59"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-12T16:00:07.127Z"", ""sms_contact"": """", ""signed_amount"": -507}, ""smsText"": ""Оплата 507р Карта*2249 OZON Баланс 598.22р 18:59"", ""category"": ""Дом"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-12T16:00:07.127+00:00"", ""counterparty"": ""OZON"", ""signedAmount"": -507.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-12T19:18:00+00:00""}"
"{""id"": 104, ""rawSms"": ""Оплата 1250р Карта*2249 mkad Баланс 14348.22р 19:17"", ""rawJson"": {""action"": ""payment"", ""amount"": 1250, ""balance"": 14348.22, ""raw_sms"": ""Оплата 1250р Карта*2249 mkad Баланс 14348.22р 19:17"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 1250р Карта*2249 mkad Баланс 14348.22р 19:17"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-12T16:17:28.942Z"", ""sms_contact"": """", ""signed_amount"": -1250}, ""smsText"": ""Оплата 1250р Карта*2249 mkad Баланс 14348.22р 19:17"", ""category"": ""Авто"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-12T16:17:28.942+00:00"", ""counterparty"": ""Автомойка"", ""signedAmount"": -1250.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 105, ""rawSms"": ""Оплата 139р Карта*2249 OZON Баланс 4628.22р 20:11"", ""rawJson"": {""action"": ""payment"", ""amount"": 139, ""balance"": 4628.22, ""raw_sms"": ""Оплата 139р Карта*2249 OZON Баланс 4628.22р 20:11"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 139р Карта*2249 OZON Баланс 4628.22р 20:11"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-12T17:12:07.406Z"", ""sms_contact"": """", ""signed_amount"": -139}, ""smsText"": ""Оплата 139р Карта*2249 OZON Баланс 4628.22р 20:11"", ""category"": ""Дом"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-12T17:12:07.406+00:00"", ""counterparty"": ""OZON"", ""signedAmount"": -139.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-12T22:09:00+00:00""}"
"{""id"": 106, ""rawSms"": ""Оплата 194р Карта*4215 STOLOVAYA 2 Баланс 4434.22р 09:05"", ""rawJson"": {""action"": ""payment"", ""amount"": 194, ""balance"": 4434.22, ""raw_sms"": ""Оплата 194р Карта*4215 STOLOVAYA 2 Баланс 4434.22р 09:05"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 194р Карта*4215 STOLOVAYA 2 Баланс 4434.22р 09:05"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-13T06:06:04.027Z"", ""sms_contact"": """", ""signed_amount"": -194}, ""smsText"": ""Оплата 194р Карта*4215 STOLOVAYA 2 Баланс 4434.22р 09:05"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-13T06:06:04.027+00:00"", ""counterparty"": ""Столовая"", ""signedAmount"": -194.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-13T09:07:00+00:00""}"
"{""id"": 107, ""rawSms"": ""Оплата 280р Карта*4215 STOLOVAYA 2 Баланс 4154.22р 13:09"", ""rawJson"": {""action"": ""payment"", ""amount"": 280, ""balance"": 4154.22, ""raw_sms"": ""Оплата 280р Карта*4215 STOLOVAYA 2 Баланс 4154.22р 13:09"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 280р Карта*4215 STOLOVAYA 2 Баланс 4154.22р 13:09"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-13T10:09:35.273Z"", ""sms_contact"": """", ""signed_amount"": -280}, ""smsText"": ""Оплата 280р Карта*4215 STOLOVAYA 2 Баланс 4154.22р 13:09"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-13T10:09:35.273+00:00"", ""counterparty"": ""Столовая"", ""signedAmount"": -280.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-13T13:34:00+00:00""}"
"{""id"": 108, ""rawSms"": ""Оплата 1р Карта*2249 DOSTAVKA PEREKR Баланс 4153.22р 13:48"", ""rawJson"": {""action"": ""payment"", ""amount"": 1, ""balance"": 4153.22, ""raw_sms"": ""Оплата 1р Карта*2249 DOSTAVKA PEREKR Баланс 4153.22р 13:48"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 1р Карта*2249 DOSTAVKA PEREKR Баланс 4153.22р 13:48"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-13T10:48:40.602Z"", ""sms_contact"": """", ""signed_amount"": -1}, ""smsText"": ""Оплата 1р Карта*2249 DOSTAVKA PEREKR Баланс 4153.22р 13:48"", ""category"": ""Продукты"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-13T10:48:40.602+00:00"", ""counterparty"": ""Перекрёсток"", ""signedAmount"": -1.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-13T13:48:00+00:00""}"
"{""id"": 109, ""rawSms"": ""Отмена: Оплата 1р Карта*2249 DOSTAVKA PEREKR Баланс 4154.22р 13:48"", ""rawJson"": {""action"": ""reversal"", ""amount"": 1, ""balance"": 4154.22, ""raw_sms"": ""Отмена: Оплата 1р Карта*2249 DOSTAVKA PEREKR Баланс 4154.22р 13:48"", ""currency"": ""RUB"", ""sms_text"": ""Отмена: Оплата 1р Карта*2249 DOSTAVKA PEREKR Баланс 4154.22р 13:48"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-13T10:48:44.636Z"", ""sms_contact"": """", ""signed_amount"": 1}, ""smsText"": ""Отмена: Оплата 1р Карта*2249 DOSTAVKA PEREKR Баланс 4154.22р 13:48"", ""category"": ""Подписки"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-13T10:48:44.636+00:00"", ""counterparty"": ""Перекрёсток"", ""signedAmount"": 1.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-13T13:49:00+00:00""}"
"{""id"": 110, ""rawSms"": ""Оплата 1962.11р Карта*2249 DOSTAVKA PEREKR Баланс 2192.11р 14:21"", ""rawJson"": {""action"": ""payment"", ""amount"": 1962.11, ""balance"": 2192.11, ""raw_sms"": ""Оплата 1962.11р Карта*2249 DOSTAVKA PEREKR Баланс 2192.11р 14:21"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 1962.11р Карта*2249 DOSTAVKA PEREKR Баланс 2192.11р 14:21"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-13T11:21:44.898Z"", ""sms_contact"": """", ""signed_amount"": -1962.11}, ""smsText"": ""Оплата 1962.11р Карта*2249 DOSTAVKA PEREKR Баланс 2192.11р 14:21"", ""category"": ""Продукты"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-13T11:21:44.898+00:00"", ""counterparty"": ""Перекрёсток"", ""signedAmount"": -1962.11, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-13T14:37:00+00:00""}"
"{""id"": 111, ""rawSms"": ""Оплата 203р Карта*4215 STOLOVAYA 2 Баланс 1989.11р 17:02"", ""rawJson"": {""action"": ""payment"", ""amount"": 203, ""balance"": 1989.11, ""raw_sms"": ""Оплата 203р Карта*4215 STOLOVAYA 2 Баланс 1989.11р 17:02"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 203р Карта*4215 STOLOVAYA 2 Баланс 1989.11р 17:02"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-13T14:02:48.094Z"", ""sms_contact"": """", ""signed_amount"": -203}, ""smsText"": ""Оплата 203р Карта*4215 STOLOVAYA 2 Баланс 1989.11р 17:02"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-13T14:02:48.094+00:00"", ""counterparty"": ""Столовая"", ""signedAmount"": -203.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-13T17:13:00+00:00""}"
"{""id"": 112, ""rawSms"": ""Списание 4000р Счет*5611 Дарья М. Баланс 1989.11р 17:04"", ""rawJson"": {""action"": ""writeoff"", ""amount"": 4000, ""balance"": 1989.11, ""raw_sms"": ""Списание 4000р Счет*5611 Дарья М. Баланс 1989.11р 17:04"", ""currency"": ""RUB"", ""sms_text"": ""Списание 4000р Счет*5611 Дарья М. Баланс 1989.11р 17:04"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-13T14:04:53.729Z"", ""sms_contact"": """", ""signed_amount"": -4000}, ""smsText"": ""Списание 4000р Счет*5611 Дарья М. Баланс 1989.11р 17:04"", ""category"": ""Арчи"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-13T14:04:53.729+00:00"", ""counterparty"": ""Даша Кинолог"", ""signedAmount"": -4000.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 113, ""rawSms"": ""Оплата 269р Карта*2249 OZON Баланс 6720.11р 18:00"", ""rawJson"": {""action"": ""payment"", ""amount"": 269, ""balance"": 6720.11, ""raw_sms"": ""Оплата 269р Карта*2249 OZON Баланс 6720.11р 18:00"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 269р Карта*2249 OZON Баланс 6720.11р 18:00"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-13T15:00:36.237Z"", ""sms_contact"": """", ""signed_amount"": -269}, ""smsText"": ""Оплата 269р Карта*2249 OZON Баланс 6720.11р 18:00"", ""category"": ""Дом"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-13T15:00:36.237+00:00"", ""counterparty"": ""OZON"", ""signedAmount"": -269.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-13T18:04:00+00:00""}"
"{""id"": 114, ""rawSms"": ""Оплата 329.97р Карта*8214 PYATEROCHKA Баланс 6390.14р 18:03"", ""rawJson"": {""action"": ""payment"", ""amount"": 329.97, ""balance"": 6390.14, ""raw_sms"": ""Оплата 329.97р Карта*8214 PYATEROCHKA Баланс 6390.14р 18:03"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 329.97р Карта*8214 PYATEROCHKA Баланс 6390.14р 18:03"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-13T15:03:55.122Z"", ""sms_contact"": """", ""signed_amount"": -329.97}, ""smsText"": ""Оплата 329.97р Карта*8214 PYATEROCHKA Баланс 6390.14р 18:03"", ""category"": ""Продукты"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-13T15:03:55.122+00:00"", ""counterparty"": ""Пятёрочка"", ""signedAmount"": -329.97, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-13T18:04:00+00:00""}"
"{""id"": 115, ""rawSms"": ""Оплата 5280р Карта*4215 NEFTMAGISTRAL 2 Баланс 1110.14р 21:24"", ""rawJson"": {""action"": ""payment"", ""amount"": 5280, ""balance"": 1110.14, ""raw_sms"": ""Оплата 5280р Карта*4215 NEFTMAGISTRAL 2 Баланс 1110.14р 21:24"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 5280р Карта*4215 NEFTMAGISTRAL 2 Баланс 1110.14р 21:24"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-13T18:24:20.370Z"", ""sms_contact"": """", ""signed_amount"": -5280}, ""smsText"": ""Оплата 5280р Карта*4215 NEFTMAGISTRAL 2 Баланс 1110.14р 21:24"", ""category"": ""Авто"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-13T18:24:20.37+00:00"", ""counterparty"": ""Нефтьмагистраль"", ""signedAmount"": -5280.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 116, ""rawSms"": ""Оплата 280р Карта*4215 JEFFREY S COFFE Баланс 637.14р 12:00"", ""rawJson"": {""action"": ""payment"", ""amount"": 280, ""balance"": 637.14, ""raw_sms"": ""Оплата 280р Карта*4215 JEFFREY S COFFE Баланс 637.14р 12:00"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 280р Карта*4215 JEFFREY S COFFE Баланс 637.14р 12:00"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-14T09:00:14.054Z"", ""sms_contact"": """", ""signed_amount"": -280}, ""smsText"": ""Оплата 280р Карта*4215 JEFFREY S COFFE Баланс 637.14р 12:00"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-14T09:00:14.054+00:00"", ""counterparty"": ""Jeffrey S Coffee"", ""signedAmount"": -280.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-14T12:00:00+00:00""}"
"{""id"": 117, ""rawSms"": ""Оплата 280р Карта*4215 STOLOVAYA 2 Баланс 357.14р 13:12"", ""rawJson"": {""action"": ""payment"", ""amount"": 280, ""balance"": 357.14, ""raw_sms"": ""Оплата 280р Карта*4215 STOLOVAYA 2 Баланс 357.14р 13:12"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 280р Карта*4215 STOLOVAYA 2 Баланс 357.14р 13:12"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-14T10:12:51.510Z"", ""sms_contact"": """", ""signed_amount"": -280}, ""smsText"": ""Оплата 280р Карта*4215 STOLOVAYA 2 Баланс 357.14р 13:12"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-14T10:12:51.51+00:00"", ""counterparty"": ""Столовая"", ""signedAmount"": -280.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-14T13:36:00+00:00""}"
"{""id"": 118, ""rawSms"": ""Списание 342р Счет*5611 Антон Ш. Баланс 15.14р 17:30"", ""rawJson"": {""action"": ""writeoff"", ""amount"": 342, ""balance"": 15.14, ""raw_sms"": ""Списание 342р Счет*5611 Антон Ш. Баланс 15.14р 17:30"", ""currency"": ""RUB"", ""sms_text"": ""Списание 342р Счет*5611 Антон Ш. Баланс 15.14р 17:30"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-14T14:30:32.365Z"", ""sms_contact"": """", ""signed_amount"": -342}, ""smsText"": ""Списание 342р Счет*5611 Антон Ш. Баланс 15.14р 17:30"", ""category"": ""Перевод"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-14T14:30:32.365+00:00"", ""counterparty"": ""Шестеров Альфа"", ""signedAmount"": -342.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 119, ""rawSms"": ""Недостаточно средств 1250р Карта*2249 mkad Баланс 15.14р 19:26"", ""rawJson"": {""action"": ""unknown"", ""amount"": 1250, ""balance"": 15.14, ""raw_sms"": ""Недостаточно средств 1250р Карта*2249 mkad Баланс 15.14р 19:26"", ""currency"": ""RUB"", ""sms_text"": ""Недостаточно средств 1250р Карта*2249 mkad Баланс 15.14р 19:26"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-14T16:26:15.369Z"", ""sms_contact"": """", ""signed_amount"": 1250}, ""smsText"": ""Недостаточно средств 1250р Карта*2249 mkad Баланс 15.14р 19:26"", ""category"": ""Проезд"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-14T16:26:15.369+00:00"", ""counterparty"": ""MKAD"", ""signedAmount"": 1250.00, ""humanVerified"": false, ""humanVerifiedAt"": null}"
"{""id"": 120, ""rawSms"": ""Оплата 1250р Карта*2249 mkad Баланс 3765.14р 19:26"", ""rawJson"": {""action"": ""payment"", ""amount"": 1250, ""balance"": 3765.14, ""raw_sms"": ""Оплата 1250р Карта*2249 mkad Баланс 3765.14р 19:26"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 1250р Карта*2249 mkad Баланс 3765.14р 19:26"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-14T16:27:06.566Z"", ""sms_contact"": """", ""signed_amount"": -1250}, ""smsText"": ""Оплата 1250р Карта*2249 mkad Баланс 3765.14р 19:26"", ""category"": ""Авто"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-14T16:27:06.566+00:00"", ""counterparty"": ""Автомойка"", ""signedAmount"": -1250.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 121, ""rawSms"": ""Оплата 203р Карта*4215 STOLOVAYA 2 Баланс 3562.14р 09:21"", ""rawJson"": {""action"": ""payment"", ""amount"": 203, ""balance"": 3562.14, ""raw_sms"": ""Оплата 203р Карта*4215 STOLOVAYA 2 Баланс 3562.14р 09:21"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 203р Карта*4215 STOLOVAYA 2 Баланс 3562.14р 09:21"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-15T06:21:59.175Z"", ""sms_contact"": """", ""signed_amount"": -203}, ""smsText"": ""Оплата 203р Карта*4215 STOLOVAYA 2 Баланс 3562.14р 09:21"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-15T06:21:59.175+00:00"", ""counterparty"": ""Столовая"", ""signedAmount"": -203.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-15T09:56:00+00:00""}"
"{""id"": 122, ""rawSms"": ""Оплата 290р Карта*2249 OZON Баланс 3272.14р 10:04"", ""rawJson"": {""action"": ""payment"", ""amount"": 290, ""balance"": 3272.14, ""raw_sms"": ""Оплата 290р Карта*2249 OZON Баланс 3272.14р 10:04"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 290р Карта*2249 OZON Баланс 3272.14р 10:04"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-15T07:04:52.492Z"", ""sms_contact"": """", ""signed_amount"": -290}, ""smsText"": ""Оплата 290р Карта*2249 OZON Баланс 3272.14р 10:04"", ""category"": ""Дом"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-15T07:04:52.492+00:00"", ""counterparty"": ""OZON"", ""signedAmount"": -290.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-15T10:05:00+00:00""}"
"{""id"": 123, ""rawSms"": ""Оплата 65р Карта*4215 STOLOVAYA 2 Баланс 3207.14р 11:16"", ""rawJson"": {""action"": ""payment"", ""amount"": 65, ""balance"": 3207.14, ""raw_sms"": ""Оплата 65р Карта*4215 STOLOVAYA 2 Баланс 3207.14р 11:16"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 65р Карта*4215 STOLOVAYA 2 Баланс 3207.14р 11:16"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-15T08:16:29.745Z"", ""sms_contact"": """", ""signed_amount"": -65}, ""smsText"": ""Оплата 65р Карта*4215 STOLOVAYA 2 Баланс 3207.14р 11:16"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-15T08:16:29.745+00:00"", ""counterparty"": ""Столовая"", ""signedAmount"": -65.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-15T11:31:00+00:00""}"
"{""id"": 124, ""rawSms"": ""Оплата 989.66р Карта*8214 PYATEROCHKA Баланс 529.98р 11:27"", ""rawJson"": {""action"": ""payment"", ""amount"": 989.66, ""balance"": 529.98, ""raw_sms"": ""Оплата 989.66р Карта*8214 PYATEROCHKA Баланс 529.98р 11:27"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 989.66р Карта*8214 PYATEROCHKA Баланс 529.98р 11:27"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-15T08:27:25.005Z"", ""sms_contact"": """", ""signed_amount"": -989.66}, ""smsText"": ""Оплата 989.66р Карта*8214 PYATEROCHKA Баланс 529.98р 11:27"", ""category"": ""Продукты"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-15T08:27:25.005+00:00"", ""counterparty"": ""Пятёрочка"", ""signedAmount"": -989.66, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-15T11:31:00+00:00""}"
"{""id"": 125, ""rawSms"": ""Недостаточно средств 542.98р Карта*8214 MAGNIT MK STARO Баланс 529.98р 11:33"", ""rawJson"": {""action"": ""unknown"", ""amount"": 542.98, ""balance"": 529.98, ""raw_sms"": ""Недостаточно средств 542.98р Карта*8214 MAGNIT MK STARO Баланс 529.98р 11:33"", ""currency"": ""RUB"", ""sms_text"": ""Недостаточно средств 542.98р Карта*8214 MAGNIT MK STARO Баланс 529.98р 11:33"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-15T08:33:11.490Z"", ""sms_contact"": """", ""signed_amount"": 542.98}, ""smsText"": ""Недостаточно средств 542.98р Карта*8214 MAGNIT MK STARO Баланс 529.98р 11:33"", ""category"": ""Продукты"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-15T08:33:11.49+00:00"", ""counterparty"": ""Магнит"", ""signedAmount"": 542.98, ""humanVerified"": false, ""humanVerifiedAt"": null}"
"{""id"": 126, ""rawSms"": ""Оплата 542.98р Карта*8214 MAGNIT MK STARO Баланс 4987р 11:33"", ""rawJson"": {""action"": ""payment"", ""amount"": 542.98, ""balance"": 4987, ""raw_sms"": ""Оплата 542.98р Карта*8214 MAGNIT MK STARO Баланс 4987р 11:33"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 542.98р Карта*8214 MAGNIT MK STARO Баланс 4987р 11:33"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-15T08:34:00.145Z"", ""sms_contact"": """", ""signed_amount"": -542.98}, ""smsText"": ""Оплата 542.98р Карта*8214 MAGNIT MK STARO Баланс 4987р 11:33"", ""category"": ""Продукты"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-15T08:34:00.145+00:00"", ""counterparty"": ""Магнит"", ""signedAmount"": -542.98, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-15T11:34:00+00:00""}"
"{""id"": 127, ""rawSms"": ""Оплата 280р Карта*4215 STOLOVAYA. Баланс 4707р 13:49"", ""rawJson"": {""action"": ""payment"", ""amount"": 280, ""balance"": 4707, ""raw_sms"": ""Оплата 280р Карта*4215 STOLOVAYA. Баланс 4707р 13:49"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 280р Карта*4215 STOLOVAYA. Баланс 4707р 13:49"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-15T10:49:52.999Z"", ""sms_contact"": """", ""signed_amount"": -280}, ""smsText"": ""Оплата 280р Карта*4215 STOLOVAYA. Баланс 4707р 13:49"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-15T10:49:52.999+00:00"", ""counterparty"": ""Столовая"", ""signedAmount"": -280.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-15T13:56:00+00:00""}"
"{""id"": 128, ""rawSms"": ""Оплата 225р Карта*2249 YP*anta-sport.r Баланс 4482р 20:45"", ""rawJson"": {""action"": ""payment"", ""amount"": 225, ""balance"": 4482, ""raw_sms"": ""Оплата 225р Карта*2249 YP*anta-sport.r Баланс 4482р 20:45"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 225р Карта*2249 YP*anta-sport.r Баланс 4482р 20:45"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-15T17:45:47.563Z"", ""sms_contact"": """", ""signed_amount"": -225}, ""smsText"": ""Оплата 225р Карта*2249 YP*anta-sport.r Баланс 4482р 20:45"", ""category"": ""Спорт"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-15T17:45:47.563+00:00"", ""counterparty"": ""Анта-спорт"", ""signedAmount"": -225.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-15T22:28:00+00:00""}"
"{""id"": 129, ""rawSms"": ""Оплата 9240р Карта*4215 IP SHARAFETDINO Баланс 10556.80р 12:36"", ""rawJson"": {""action"": ""payment"", ""amount"": 9240, ""balance"": 10556.8, ""raw_sms"": ""Оплата 9240р Карта*4215 IP SHARAFETDINO Баланс 10556.80р 12:36"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 9240р Карта*4215 IP SHARAFETDINO Баланс 10556.80р 12:36"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-16T09:37:13.189Z"", ""sms_contact"": """", ""signed_amount"": -9240}, ""smsText"": ""Оплата 9240р Карта*4215 IP SHARAFETDINO Баланс 10556.80р 12:36"", ""category"": ""Арчи"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-16T09:37:13.189+00:00"", ""counterparty"": ""IP SHARAFETDINO"", ""signedAmount"": -9240.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 130, ""rawSms"": ""Оплата 859.95р Карта*4215 PYATEROCHKA Баланс 9696.85р 13:31"", ""rawJson"": {""action"": ""payment"", ""amount"": 859.95, ""balance"": 9696.85, ""raw_sms"": ""Оплата 859.95р Карта*4215 PYATEROCHKA Баланс 9696.85р 13:31"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 859.95р Карта*4215 PYATEROCHKA Баланс 9696.85р 13:31"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-16T10:31:45.727Z"", ""sms_contact"": """", ""signed_amount"": -859.95}, ""smsText"": ""Оплата 859.95р Карта*4215 PYATEROCHKA Баланс 9696.85р 13:31"", ""category"": ""Продукты"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-16T10:31:45.727+00:00"", ""counterparty"": ""Пятёрочка"", ""signedAmount"": -859.95, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-16T13:43:00+00:00""}"
"{""id"": 132, ""rawSms"": ""Зачисление 8929р Карта*2249 OZON Баланс 17625.85р 00:00"", ""rawJson"": {""action"": ""income"", ""amount"": 8929, ""balance"": 17625.85, ""raw_sms"": ""Зачисление 8929р Карта*2249 OZON Баланс 17625.85р 00:00"", ""currency"": ""RUB"", ""sms_text"": ""Зачисление 8929р Карта*2249 OZON Баланс 17625.85р 00:00"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-17T11:13:54.135Z"", ""sms_contact"": """", ""signed_amount"": 8929}, ""smsText"": ""Зачисление 8929р Карта*2249 OZON Баланс 17625.85р 00:00"", ""category"": ""Поступления"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-17T11:13:54.135+00:00"", ""counterparty"": ""OZON"", ""signedAmount"": 8929.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-17T15:18:00+00:00""}"
"{""id"": 133, ""rawSms"": ""Оплата 2000р Карта*4215 SP__NORD & NILS Баланс 15625.85р 15:20"", ""rawJson"": {""action"": ""payment"", ""amount"": 2000, ""balance"": 15625.85, ""raw_sms"": ""Оплата 2000р Карта*4215 SP__NORD & NILS Баланс 15625.85р 15:20"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 2000р Карта*4215 SP__NORD & NILS Баланс 15625.85р 15:20"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-17T12:20:33.189Z"", ""sms_contact"": """", ""signed_amount"": -2000}, ""smsText"": ""Оплата 2000р Карта*4215 SP__NORD & NILS Баланс 15625.85р 15:20"", ""category"": ""Перевод"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-17T12:20:33.189+00:00"", ""counterparty"": ""СП_НОРД & НИЛС"", ""signedAmount"": -2000.00, ""humanVerified"": false, ""humanVerifiedAt"": null}"
"{""id"": 134, ""rawSms"": ""Оплата 140.98р Карта*4215 PYATEROCHKA Баланс 15309.42р 17:44"", ""rawJson"": {""action"": ""payment"", ""amount"": 140.98, ""balance"": 15309.42, ""raw_sms"": ""Оплата 140.98р Карта*4215 PYATEROCHKA Баланс 15309.42р 17:44"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 140.98р Карта*4215 PYATEROCHKA Баланс 15309.42р 17:44"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-18T14:45:08.717Z"", ""sms_contact"": """", ""signed_amount"": -140.98}, ""smsText"": ""Оплата 140.98р Карта*4215 PYATEROCHKA Баланс 15309.42р 17:44"", ""category"": ""Продукты"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-18T14:45:08.717+00:00"", ""counterparty"": ""Пятёрочка"", ""signedAmount"": -140.98, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-18T18:02:00+00:00""}"
"{""id"": 135, ""rawSms"": ""Оплата 1219р Карта*2249 PT*Aliexpress Баланс 14090.42р 18:02"", ""rawJson"": {""action"": ""payment"", ""amount"": 1219, ""balance"": 14090.42, ""raw_sms"": ""Оплата 1219р Карта*2249 PT*Aliexpress Баланс 14090.42р 18:02"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 1219р Карта*2249 PT*Aliexpress Баланс 14090.42р 18:02"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-18T15:02:39.286Z"", ""sms_contact"": """", ""signed_amount"": -1219}, ""smsText"": ""Оплата 1219р Карта*2249 PT*Aliexpress Баланс 14090.42р 18:02"", ""category"": ""Подарки"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-18T15:02:39.286+00:00"", ""counterparty"": ""Алиэкспресс"", ""signedAmount"": -1219.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 136, ""rawSms"": ""Оплата 1021р Карта*2249 OZON.RU Баланс 13069.42р 19:31"", ""rawJson"": {""action"": ""payment"", ""amount"": 1021, ""balance"": 13069.42, ""raw_sms"": ""Оплата 1021р Карта*2249 OZON.RU Баланс 13069.42р 19:31"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 1021р Карта*2249 OZON.RU Баланс 13069.42р 19:31"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-18T16:31:41.474Z"", ""sms_contact"": """", ""signed_amount"": -1021}, ""smsText"": ""Оплата 1021р Карта*2249 OZON.RU Баланс 13069.42р 19:31"", ""category"": ""Дом"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-18T16:31:41.474+00:00"", ""counterparty"": ""OZON"", ""signedAmount"": -1021.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-18T19:32:00+00:00""}"
"{""id"": 137, ""rawSms"": ""Оплата 170р Карта*4215 STOLOVAYA 2 Баланс 12899.42р 10:10"", ""rawJson"": {""action"": ""payment"", ""amount"": 170, ""balance"": 12899.42, ""raw_sms"": ""Оплата 170р Карта*4215 STOLOVAYA 2 Баланс 12899.42р 10:10"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 170р Карта*4215 STOLOVAYA 2 Баланс 12899.42р 10:10"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-19T07:10:19.605Z"", ""sms_contact"": """", ""signed_amount"": -170}, ""smsText"": ""Оплата 170р Карта*4215 STOLOVAYA 2 Баланс 12899.42р 10:10"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-19T07:10:19.605+00:00"", ""counterparty"": ""Столовая"", ""signedAmount"": -170.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-19T10:12:00+00:00""}"
"{""id"": 138, ""rawSms"": ""Оплата 9546.35р Карта*8214 Bethowen Баланс 3353.07р 11:23"", ""rawJson"": {""action"": ""payment"", ""amount"": 9546.35, ""balance"": 3353.07, ""raw_sms"": ""Оплата 9546.35р Карта*8214 Bethowen Баланс 3353.07р 11:23"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 9546.35р Карта*8214 Bethowen Баланс 3353.07р 11:23"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-19T08:24:04.563Z"", ""sms_contact"": """", ""signed_amount"": -9546.35}, ""smsText"": ""Оплата 9546.35р Карта*8214 Bethowen Баланс 3353.07р 11:23"", ""category"": ""Арчи"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-19T08:24:04.563+00:00"", ""counterparty"": ""Бетховен"", ""signedAmount"": -9546.35, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 139, ""rawSms"": ""Оплата 280р Карта*4215 STOLOVAYA 2 Баланс 3073.07р 13:54"", ""rawJson"": {""action"": ""payment"", ""amount"": 280, ""balance"": 3073.07, ""raw_sms"": ""Оплата 280р Карта*4215 STOLOVAYA 2 Баланс 3073.07р 13:54"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 280р Карта*4215 STOLOVAYA 2 Баланс 3073.07р 13:54"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-19T10:54:50.856Z"", ""sms_contact"": """", ""signed_amount"": -280}, ""smsText"": ""Оплата 280р Карта*4215 STOLOVAYA 2 Баланс 3073.07р 13:54"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-19T10:54:50.856+00:00"", ""counterparty"": ""Столовая"", ""signedAmount"": -280.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-19T13:55:00+00:00""}"
"{""id"": 140, ""rawSms"": ""Оплата 4315.94р Карта*4215 RNAZK MJ056 ROS Баланс 4757.13р 08:11"", ""rawJson"": {""action"": ""payment"", ""amount"": 4315.94, ""balance"": 4757.13, ""raw_sms"": ""Оплата 4315.94р Карта*4215 RNAZK MJ056 ROS Баланс 4757.13р 08:11"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 4315.94р Карта*4215 RNAZK MJ056 ROS Баланс 4757.13р 08:11"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-20T05:11:41.879Z"", ""sms_contact"": """", ""signed_amount"": -4315.94}, ""smsText"": ""Оплата 4315.94р Карта*4215 RNAZK MJ056 ROS Баланс 4757.13р 08:11"", ""category"": ""Авто"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-20T05:11:41.879+00:00"", ""counterparty"": ""Транснефть"", ""signedAmount"": -4315.94, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 141, ""rawSms"": ""Оплата 153р Карта*4215 STOLOVAYA 2 Баланс 4604.13р 09:32"", ""rawJson"": {""action"": ""payment"", ""amount"": 153, ""balance"": 4604.13, ""raw_sms"": ""Оплата 153р Карта*4215 STOLOVAYA 2 Баланс 4604.13р 09:32"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 153р Карта*4215 STOLOVAYA 2 Баланс 4604.13р 09:32"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-20T06:32:51.794Z"", ""sms_contact"": """", ""signed_amount"": -153}, ""smsText"": ""Оплата 153р Карта*4215 STOLOVAYA 2 Баланс 4604.13р 09:32"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-20T06:32:51.794+00:00"", ""counterparty"": ""Столовая"", ""signedAmount"": -153.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-20T09:53:00+00:00""}"
"{""id"": 142, ""rawSms"": ""Списание 4000р Счет*5611 Дарья М. Баланс 604.13р 12:34"", ""rawJson"": {""action"": ""writeoff"", ""amount"": 4000, ""balance"": 604.13, ""raw_sms"": ""Списание 4000р Счет*5611 Дарья М. Баланс 604.13р 12:34"", ""currency"": ""RUB"", ""sms_text"": ""Списание 4000р Счет*5611 Дарья М. Баланс 604.13р 12:34"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-20T09:34:18.739Z"", ""sms_contact"": """", ""signed_amount"": -4000}, ""smsText"": ""Списание 4000р Счет*5611 Дарья М. Баланс 604.13р 12:34"", ""category"": ""Арчи"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-20T09:34:18.739+00:00"", ""counterparty"": ""Кинолог"", ""signedAmount"": -4000.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 143, ""rawSms"": ""Оплата 472р Карта*4215 STOLOVAYA 2 Баланс 1132.13р 13:14"", ""rawJson"": {""action"": ""payment"", ""amount"": 472, ""balance"": 1132.13, ""raw_sms"": ""Оплата 472р Карта*4215 STOLOVAYA 2 Баланс 1132.13р 13:14"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 472р Карта*4215 STOLOVAYA 2 Баланс 1132.13р 13:14"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-20T10:14:35.169Z"", ""sms_contact"": """", ""signed_amount"": -472}, ""smsText"": ""Оплата 472р Карта*4215 STOLOVAYA 2 Баланс 1132.13р 13:14"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-20T10:14:35.169+00:00"", ""counterparty"": ""Столовая"", ""signedAmount"": -472.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-20T13:37:00+00:00""}"
"{""id"": 144, ""rawSms"": ""Оплата 719.01р Карта*8214 PYATEROCHKA Баланс 413.12р 13:17"", ""rawJson"": {""action"": ""payment"", ""amount"": 719.01, ""balance"": 413.12, ""raw_sms"": ""Оплата 719.01р Карта*8214 PYATEROCHKA Баланс 413.12р 13:17"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 719.01р Карта*8214 PYATEROCHKA Баланс 413.12р 13:17"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-20T10:17:24.734Z"", ""sms_contact"": """", ""signed_amount"": -719.01}, ""smsText"": ""Оплата 719.01р Карта*8214 PYATEROCHKA Баланс 413.12р 13:17"", ""category"": ""Продукты"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-20T10:17:24.734+00:00"", ""counterparty"": ""Пятёрочка"", ""signedAmount"": -719.01, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-20T13:37:00+00:00""}"
"{""id"": 145, ""rawSms"": ""Оплата 252р Карта*4215 JEFFREY S COFFE Баланс 161.12р 17:10"", ""rawJson"": {""action"": ""payment"", ""amount"": 252, ""balance"": 161.12, ""raw_sms"": ""Оплата 252р Карта*4215 JEFFREY S COFFE Баланс 161.12р 17:10"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 252р Карта*4215 JEFFREY S COFFE Баланс 161.12р 17:10"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-20T14:10:47.286Z"", ""sms_contact"": """", ""signed_amount"": -252}, ""smsText"": ""Оплата 252р Карта*4215 JEFFREY S COFFE Баланс 161.12р 17:10"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-20T14:10:47.286+00:00"", ""counterparty"": ""Jeffrey S Coffee"", ""signedAmount"": -252.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-20T17:12:00+00:00""}"
"{""id"": 146, ""rawSms"": ""Оплата 225р Карта*2249 YP*anta-sport.r Баланс 3936.12р 17:17"", ""rawJson"": {""action"": ""payment"", ""amount"": 225, ""balance"": 3936.12, ""raw_sms"": ""Оплата 225р Карта*2249 YP*anta-sport.r Баланс 3936.12р 17:17"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 225р Карта*2249 YP*anta-sport.r Баланс 3936.12р 17:17"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-20T14:17:51.925Z"", ""sms_contact"": """", ""signed_amount"": -225}, ""smsText"": ""Оплата 225р Карта*2249 YP*anta-sport.r Баланс 3936.12р 17:17"", ""category"": ""Спорт"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-20T14:17:51.925+00:00"", ""counterparty"": ""Анта-спорт"", ""signedAmount"": -225.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-20T17:19:00+00:00""}"
"{""id"": 147, ""rawSms"": ""Оплата 1496р Карта*4215 3234 ZOO Баланс 2440.12р 18:48"", ""rawJson"": {""action"": ""payment"", ""amount"": 1496, ""balance"": 2440.12, ""raw_sms"": ""Оплата 1496р Карта*4215 3234 ZOO Баланс 2440.12р 18:48"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 1496р Карта*4215 3234 ZOO Баланс 2440.12р 18:48"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-20T15:48:57.515Z"", ""sms_contact"": """", ""signed_amount"": -1496}, ""smsText"": ""Оплата 1496р Карта*4215 3234 ZOO Баланс 2440.12р 18:48"", ""category"": ""Арчи"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-20T15:48:57.515+00:00"", ""counterparty"": ""Зоопарк"", ""signedAmount"": -1496.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 148, ""rawSms"": ""Оплата 200р Карта*4215 IP Kibickaja Баланс 2240.12р 21:59"", ""rawJson"": {""action"": ""payment"", ""amount"": 200, ""balance"": 2240.12, ""raw_sms"": ""Оплата 200р Карта*4215 IP Kibickaja Баланс 2240.12р 21:59"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 200р Карта*4215 IP Kibickaja Баланс 2240.12р 21:59"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-20T19:00:07.700Z"", ""sms_contact"": """", ""signed_amount"": -200}, ""smsText"": ""Оплата 200р Карта*4215 IP Kibickaja Баланс 2240.12р 21:59"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-20T19:00:07.7+00:00"", ""counterparty"": ""Кибикя"", ""signedAmount"": -200.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 149, ""rawSms"": ""Оплата 10р Карта*2249 YM*AMPP Баланс 2230.12р 22:13"", ""rawJson"": {""action"": ""payment"", ""amount"": 10, ""balance"": 2230.12, ""raw_sms"": ""Оплата 10р Карта*2249 YM*AMPP Баланс 2230.12р 22:13"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 10р Карта*2249 YM*AMPP Баланс 2230.12р 22:13"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-20T19:13:43.445Z"", ""sms_contact"": """", ""signed_amount"": -10}, ""smsText"": ""Оплата 10р Карта*2249 YM*AMPP Баланс 2230.12р 22:13"", ""category"": ""Авто"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-20T19:13:43.445+00:00"", ""counterparty"": ""Парковка"", ""signedAmount"": -10.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 150, ""rawSms"": ""Оплата 158р Карта*4215 STOLOVAYA 2 Баланс 2072.12р 10:20"", ""rawJson"": {""action"": ""payment"", ""amount"": 158, ""balance"": 2072.12, ""raw_sms"": ""Оплата 158р Карта*4215 STOLOVAYA 2 Баланс 2072.12р 10:20"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 158р Карта*4215 STOLOVAYA 2 Баланс 2072.12р 10:20"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-21T07:21:00.675Z"", ""sms_contact"": """", ""signed_amount"": -158}, ""smsText"": ""Оплата 158р Карта*4215 STOLOVAYA 2 Баланс 2072.12р 10:20"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-21T07:21:00.675+00:00"", ""counterparty"": ""Столовая"", ""signedAmount"": -158.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-21T10:21:00+00:00""}"
"{""id"": 151, ""rawSms"": ""Оплата 280р Карта*4215 STOLOVAYA 2 Баланс 1792.12р 13:45"", ""rawJson"": {""action"": ""payment"", ""amount"": 280, ""balance"": 1792.12, ""raw_sms"": ""Оплата 280р Карта*4215 STOLOVAYA 2 Баланс 1792.12р 13:45"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 280р Карта*4215 STOLOVAYA 2 Баланс 1792.12р 13:45"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-21T10:45:38.266Z"", ""sms_contact"": """", ""signed_amount"": -280}, ""smsText"": ""Оплата 280р Карта*4215 STOLOVAYA 2 Баланс 1792.12р 13:45"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-21T10:45:38.266+00:00"", ""counterparty"": ""Столовая"", ""signedAmount"": -280.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-21T14:08:00+00:00""}"
"{""id"": 152, ""rawSms"": ""Перевод 1000р Счет*5611 Милана К. Баланс 792.12р 14:17"", ""rawJson"": {""action"": ""transfer"", ""amount"": 1000, ""balance"": 792.12, ""raw_sms"": ""Перевод 1000р Счет*5611 Милана К. Баланс 792.12р 14:17"", ""currency"": ""RUB"", ""sms_text"": ""Перевод 1000р Счет*5611 Милана К. Баланс 792.12р 14:17"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-21T11:17:51.249Z"", ""sms_contact"": """", ""signed_amount"": -1000}, ""smsText"": ""Перевод 1000р Счет*5611 Милана К. Баланс 792.12р 14:17"", ""category"": ""Перевод"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-21T11:17:51.249+00:00"", ""counterparty"": ""Милана К."", ""signedAmount"": -1000.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-21T14:18:00+00:00""}"
"{""id"": 153, ""rawSms"": ""Оплата 354.97р Карта*8214 PYATEROCHKA Баланс 437.15р 17:29"", ""rawJson"": {""action"": ""payment"", ""amount"": 354.97, ""balance"": 437.15, ""raw_sms"": ""Оплата 354.97р Карта*8214 PYATEROCHKA Баланс 437.15р 17:29"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 354.97р Карта*8214 PYATEROCHKA Баланс 437.15р 17:29"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-21T14:29:19.469Z"", ""sms_contact"": """", ""signed_amount"": -354.97}, ""smsText"": ""Оплата 354.97р Карта*8214 PYATEROCHKA Баланс 437.15р 17:29"", ""category"": ""Продукты"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-21T14:29:19.469+00:00"", ""counterparty"": ""Пятёрочка"", ""signedAmount"": -354.97, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-21T17:36:00+00:00""}"
"{""id"": 154, ""rawSms"": ""Оплата 154р Карта*2249 OZON Баланс 283.15р 20:09"", ""rawJson"": {""action"": ""payment"", ""amount"": 154, ""balance"": 283.15, ""raw_sms"": ""Оплата 154р Карта*2249 OZON Баланс 283.15р 20:09"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 154р Карта*2249 OZON Баланс 283.15р 20:09"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-21T17:09:32.249Z"", ""sms_contact"": """", ""signed_amount"": -154}, ""smsText"": ""Оплата 154р Карта*2249 OZON Баланс 283.15р 20:09"", ""category"": ""Дом"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-21T17:09:32.249+00:00"", ""counterparty"": ""OZON"", ""signedAmount"": -154.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-21T20:52:00+00:00""}"
"{""id"": 155, ""rawSms"": ""Недостаточно средств 943.91р Карта*4215 PYATEROCHKA Баланс 283.15р 20:23"", ""rawJson"": {""action"": ""unknown"", ""amount"": 943.91, ""balance"": 283.15, ""raw_sms"": ""Недостаточно средств 943.91р Карта*4215 PYATEROCHKA Баланс 283.15р 20:23"", ""currency"": ""RUB"", ""sms_text"": ""Недостаточно средств 943.91р Карта*4215 PYATEROCHKA Баланс 283.15р 20:23"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-21T17:23:48.134Z"", ""sms_contact"": """", ""signed_amount"": 943.91}, ""smsText"": ""Недостаточно средств 943.91р Карта*4215 PYATEROCHKA Баланс 283.15р 20:23"", ""category"": ""Продукты"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-21T17:23:48.134+00:00"", ""counterparty"": ""Пятёрочка"", ""signedAmount"": 943.91, ""humanVerified"": false, ""humanVerifiedAt"": null}"
"{""id"": 156, ""rawSms"": ""Оплата 943.91р Карта*4215 PYATEROCHKA Баланс 4339.24р 20:24"", ""rawJson"": {""action"": ""payment"", ""amount"": 943.91, ""balance"": 4339.24, ""raw_sms"": ""Оплата 943.91р Карта*4215 PYATEROCHKA Баланс 4339.24р 20:24"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 943.91р Карта*4215 PYATEROCHKA Баланс 4339.24р 20:24"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-21T17:24:35.143Z"", ""sms_contact"": """", ""signed_amount"": -943.91}, ""smsText"": ""Оплата 943.91р Карта*4215 PYATEROCHKA Баланс 4339.24р 20:24"", ""category"": ""Продукты"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-21T17:24:35.143+00:00"", ""counterparty"": ""Пятёрочка"", ""signedAmount"": -943.91, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-21T20:52:00+00:00""}"
"{""id"": 157, ""rawSms"": ""Оплата 200р Карта*8214 CPPK-2000012-BP Баланс 4139.24р 10:02"", ""rawJson"": {""action"": ""payment"", ""amount"": 200, ""balance"": 4139.24, ""raw_sms"": ""Оплата 200р Карта*8214 CPPK-2000012-BP Баланс 4139.24р 10:02"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 200р Карта*8214 CPPK-2000012-BP Баланс 4139.24р 10:02"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-22T07:02:13.974Z"", ""sms_contact"": """", ""signed_amount"": -200}, ""smsText"": ""Оплата 200р Карта*8214 CPPK-2000012-BP Баланс 4139.24р 10:02"", ""category"": ""Проезд"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-22T07:02:13.974+00:00"", ""counterparty"": ""РЖД"", ""signedAmount"": -200.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 158, ""rawSms"": ""Оплата 268р Карта*4215 STOLOVAYA 2 Баланс 3871.24р 10:58"", ""rawJson"": {""action"": ""payment"", ""amount"": 268, ""balance"": 3871.24, ""raw_sms"": ""Оплата 268р Карта*4215 STOLOVAYA 2 Баланс 3871.24р 10:58"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 268р Карта*4215 STOLOVAYA 2 Баланс 3871.24р 10:58"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-22T07:58:47.245Z"", ""sms_contact"": """", ""signed_amount"": -268}, ""smsText"": ""Оплата 268р Карта*4215 STOLOVAYA 2 Баланс 3871.24р 10:58"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-22T07:58:47.245+00:00"", ""counterparty"": ""Столовая"", ""signedAmount"": -268.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-22T11:11:00+00:00""}"
"{""id"": 159, ""rawSms"": ""Оплата 1090р Карта*8214 TA TORRRO GRIL Баланс 2781.24р 14:08"", ""rawJson"": {""action"": ""payment"", ""amount"": 1090, ""balance"": 2781.24, ""raw_sms"": ""Оплата 1090р Карта*8214 TA TORRRO GRIL Баланс 2781.24р 14:08"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 1090р Карта*8214 TA TORRRO GRIL Баланс 2781.24р 14:08"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-22T11:09:03.392Z"", ""sms_contact"": """", ""signed_amount"": -1090}, ""smsText"": ""Оплата 1090р Карта*8214 TA TORRRO GRIL Баланс 2781.24р 14:08"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-22T11:09:03.392+00:00"", ""counterparty"": ""Торрро Гриль"", ""signedAmount"": -1090.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-22T14:09:00+00:00""}"
"{""id"": 160, ""rawSms"": ""Оплата 370р Карта*4215 KOFEPOINT Баланс 12411.24р 16:06"", ""rawJson"": {""action"": ""payment"", ""amount"": 370, ""balance"": 12411.24, ""raw_sms"": ""Оплата 370р Карта*4215 KOFEPOINT Баланс 12411.24р 16:06"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 370р Карта*4215 KOFEPOINT Баланс 12411.24р 16:06"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-22T13:03:18.615Z"", ""sms_contact"": """", ""signed_amount"": -370}, ""smsText"": ""Оплата 370р Карта*4215 KOFEPOINT Баланс 12411.24р 16:06"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-22T13:03:18.615+00:00"", ""counterparty"": ""Кофепоинт"", ""signedAmount"": -370.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-22T16:03:00+00:00""}"
"{""id"": 161, ""rawSms"": ""Зарплата 96023.86р Счет *5611 Баланс 108435.10р 16:07"", ""rawJson"": {""action"": ""salary"", ""amount"": 96023.86, ""balance"": 108435.1, ""raw_sms"": ""Зарплата 96023.86р Счет *5611 Баланс 108435.10р 16:07"", ""currency"": ""RUB"", ""sms_text"": ""Зарплата 96023.86р Счет *5611 Баланс 108435.10р 16:07"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-22T13:07:27.728Z"", ""sms_contact"": """", ""signed_amount"": 96023.86}, ""smsText"": ""Зарплата 96023.86р Счет *5611 Баланс 108435.10р 16:07"", ""category"": ""Поступления"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-22T13:07:27.728+00:00"", ""counterparty"": ""НИУ ВШЭ"", ""signedAmount"": 96023.86, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 162, ""rawSms"": ""Оплата 6250р Карта*4215 KLINIKA DEKABRI Баланс 102185.10р 16:11"", ""rawJson"": {""action"": ""payment"", ""amount"": 6250, ""balance"": 102185.1, ""raw_sms"": ""Оплата 6250р Карта*4215 KLINIKA DEKABRI Баланс 102185.10р 16:11"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 6250р Карта*4215 KLINIKA DEKABRI Баланс 102185.10р 16:11"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-22T13:11:55.710Z"", ""sms_contact"": """", ""signed_amount"": -6250}, ""smsText"": ""Оплата 6250р Карта*4215 KLINIKA DEKABRI Баланс 102185.10р 16:11"", ""category"": ""Здоровье"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-22T13:11:55.71+00:00"", ""counterparty"": ""КЛИНИКА ДЕКАБРЬ"", ""signedAmount"": -6250.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-22T16:17:00+00:00""}"
"{""id"": 163, ""rawSms"": ""Оплата 599р Карта*8214 OSTIN Баланс 101586.10р 16:58"", ""rawJson"": {""action"": ""payment"", ""amount"": 599, ""balance"": 101586.1, ""raw_sms"": ""Оплата 599р Карта*8214 OSTIN Баланс 101586.10р 16:58"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 599р Карта*8214 OSTIN Баланс 101586.10р 16:58"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-22T13:59:12.257Z"", ""sms_contact"": """", ""signed_amount"": -599}, ""smsText"": ""Оплата 599р Карта*8214 OSTIN Баланс 101586.10р 16:58"", ""category"": ""Одежда"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-22T13:59:12.257+00:00"", ""counterparty"": ""Остин"", ""signedAmount"": -599.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 164, ""rawSms"": ""Оплата 605.16р Карта*8214 PYATEROCHKA Баланс 100980.94р 18:34"", ""rawJson"": {""action"": ""payment"", ""amount"": 605.16, ""balance"": 100980.94, ""raw_sms"": ""Оплата 605.16р Карта*8214 PYATEROCHKA Баланс 100980.94р 18:34"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 605.16р Карта*8214 PYATEROCHKA Баланс 100980.94р 18:34"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-22T15:34:27.692Z"", ""sms_contact"": """", ""signed_amount"": -605.16}, ""smsText"": ""Оплата 605.16р Карта*8214 PYATEROCHKA Баланс 100980.94р 18:34"", ""category"": ""Продукты"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-22T15:34:27.692+00:00"", ""counterparty"": ""Пятёрочка"", ""signedAmount"": -605.16, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-22T19:18:00+00:00""}"
"{""id"": 165, ""rawSms"": ""Оплата 434р Карта*4215 STOLOVAYA 2 Баланс 100546.94р 10:34"", ""rawJson"": {""action"": ""payment"", ""amount"": 434, ""balance"": 100546.94, ""raw_sms"": ""Оплата 434р Карта*4215 STOLOVAYA 2 Баланс 100546.94р 10:34"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 434р Карта*4215 STOLOVAYA 2 Баланс 100546.94р 10:34"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-23T07:34:31.792Z"", ""sms_contact"": """", ""signed_amount"": -434}, ""smsText"": ""Оплата 434р Карта*4215 STOLOVAYA 2 Баланс 100546.94р 10:34"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-23T07:34:31.792+00:00"", ""counterparty"": ""Столовая"", ""signedAmount"": -434.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-23T10:36:00+00:00""}"
"{""id"": 166, ""rawSms"": ""Оплата 280р Карта*4215 STOLOVAYA 2 Баланс 100266.94р 13:32"", ""rawJson"": {""action"": ""payment"", ""amount"": 280, ""balance"": 100266.94, ""raw_sms"": ""Оплата 280р Карта*4215 STOLOVAYA 2 Баланс 100266.94р 13:32"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 280р Карта*4215 STOLOVAYA 2 Баланс 100266.94р 13:32"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-23T10:32:17.939Z"", ""sms_contact"": """", ""signed_amount"": -280}, ""smsText"": ""Оплата 280р Карта*4215 STOLOVAYA 2 Баланс 100266.94р 13:32"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-23T10:32:17.939+00:00"", ""counterparty"": ""Столовая"", ""signedAmount"": -280.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-23T13:44:00+00:00""}"
"{""id"": 167, ""rawSms"": ""Оплата 273р Карта*8214 ULYBKA RADUGI Баланс 99993.94р 18:31"", ""rawJson"": {""action"": ""payment"", ""amount"": 273, ""balance"": 99993.94, ""raw_sms"": ""Оплата 273р Карта*8214 ULYBKA RADUGI Баланс 99993.94р 18:31"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 273р Карта*8214 ULYBKA RADUGI Баланс 99993.94р 18:31"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-23T15:31:12.438Z"", ""sms_contact"": """", ""signed_amount"": -273}, ""smsText"": ""Оплата 273р Карта*8214 ULYBKA RADUGI Баланс 99993.94р 18:31"", ""category"": ""Косметика"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-23T15:31:12.438+00:00"", ""counterparty"": ""УЛЫБКА РАДУГИ"", ""signedAmount"": -273.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 168, ""rawSms"": ""Оплата 459.50р Карта*8214 FIXPRICE 3082 Баланс 99534.44р 18:35"", ""rawJson"": {""action"": ""payment"", ""amount"": 459.5, ""balance"": 99534.44, ""raw_sms"": ""Оплата 459.50р Карта*8214 FIXPRICE 3082 Баланс 99534.44р 18:35"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 459.50р Карта*8214 FIXPRICE 3082 Баланс 99534.44р 18:35"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-23T15:35:56.366Z"", ""sms_contact"": """", ""signed_amount"": -459.5}, ""smsText"": ""Оплата 459.50р Карта*8214 FIXPRICE 3082 Баланс 99534.44р 18:35"", ""category"": ""Продукты"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-23T15:35:56.366+00:00"", ""counterparty"": ""Фикс Прайс"", ""signedAmount"": -459.50, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-23T19:25:00+00:00""}"
"{""id"": 169, ""rawSms"": ""Оплата 269.70р Карта*8214 ZOOMAGAZIN CHET Баланс 99264.74р 18:40"", ""rawJson"": {""action"": ""payment"", ""amount"": 269.7, ""balance"": 99264.74, ""raw_sms"": ""Оплата 269.70р Карта*8214 ZOOMAGAZIN CHET Баланс 99264.74р 18:40"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 269.70р Карта*8214 ZOOMAGAZIN CHET Баланс 99264.74р 18:40"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-23T15:40:19.349Z"", ""sms_contact"": """", ""signed_amount"": -269.7}, ""smsText"": ""Оплата 269.70р Карта*8214 ZOOMAGAZIN CHET Баланс 99264.74р 18:40"", ""category"": ""Арчи"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-23T15:40:19.349+00:00"", ""counterparty"": ""Чет"", ""signedAmount"": -269.70, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 170, ""rawSms"": ""Оплата 4852.64р Карта*4215 RNAZK MC099 Баланс 94412.10р 19:04"", ""rawJson"": {""action"": ""payment"", ""amount"": 4852.64, ""balance"": 94412.1, ""raw_sms"": ""Оплата 4852.64р Карта*4215 RNAZK MC099 Баланс 94412.10р 19:04"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 4852.64р Карта*4215 RNAZK MC099 Баланс 94412.10р 19:04"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-24T16:04:32.788Z"", ""sms_contact"": """", ""signed_amount"": -4852.64}, ""smsText"": ""Оплата 4852.64р Карта*4215 RNAZK MC099 Баланс 94412.10р 19:04"", ""category"": ""Авто"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-24T16:04:32.788+00:00"", ""counterparty"": ""Транснефть"", ""signedAmount"": -4852.64, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 171, ""rawSms"": ""Оплата 811.94р Карта*4215 PYATEROCHKA Баланс 93600.16р 19:45"", ""rawJson"": {""action"": ""payment"", ""amount"": 811.94, ""balance"": 93600.16, ""raw_sms"": ""Оплата 811.94р Карта*4215 PYATEROCHKA Баланс 93600.16р 19:45"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 811.94р Карта*4215 PYATEROCHKA Баланс 93600.16р 19:45"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-24T16:45:24.658Z"", ""sms_contact"": """", ""signed_amount"": -811.94}, ""smsText"": ""Оплата 811.94р Карта*4215 PYATEROCHKA Баланс 93600.16р 19:45"", ""category"": ""Продукты"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-24T16:45:24.658+00:00"", ""counterparty"": ""Пятёрочка"", ""signedAmount"": -811.94, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-24T19:57:00+00:00""}"
"{""id"": 172, ""rawSms"": ""Оплата 15600р Карта*4215 OOO Vetradiolog Баланс 78000.16р 18:43"", ""rawJson"": {""action"": ""payment"", ""amount"": 15600, ""balance"": 78000.16, ""raw_sms"": ""Оплата 15600р Карта*4215 OOO Vetradiolog Баланс 78000.16р 18:43"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 15600р Карта*4215 OOO Vetradiolog Баланс 78000.16р 18:43"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-25T15:43:59.036Z"", ""sms_contact"": """", ""signed_amount"": -15600}, ""smsText"": ""Оплата 15600р Карта*4215 OOO Vetradiolog Баланс 78000.16р 18:43"", ""category"": ""Арчи"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-25T15:43:59.036+00:00"", ""counterparty"": ""Ветрдиагностика"", ""signedAmount"": -15600.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 173, ""rawSms"": ""Оплата 1040р Карта*4215 EVO_BUBBLES TEA Баланс 76960.16р 20:04"", ""rawJson"": {""action"": ""payment"", ""amount"": 1040, ""balance"": 76960.16, ""raw_sms"": ""Оплата 1040р Карта*4215 EVO_BUBBLES TEA Баланс 76960.16р 20:04"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 1040р Карта*4215 EVO_BUBBLES TEA Баланс 76960.16р 20:04"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-25T17:04:13.766Z"", ""sms_contact"": """", ""signed_amount"": -1040}, ""smsText"": ""Оплата 1040р Карта*4215 EVO_BUBBLES TEA Баланс 76960.16р 20:04"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-25T17:04:13.766+00:00"", ""counterparty"": ""EVO БУБЛИКИ ТЕА"", ""signedAmount"": -1040.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 174, ""rawSms"": ""Оплата 600р Карта*4215 kofeinya kondit Баланс 76360.16р 20:10"", ""rawJson"": {""action"": ""payment"", ""amount"": 600, ""balance"": 76360.16, ""raw_sms"": ""Оплата 600р Карта*4215 kofeinya kondit Баланс 76360.16р 20:10"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 600р Карта*4215 kofeinya kondit Баланс 76360.16р 20:10"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-25T17:10:43.889Z"", ""sms_contact"": """", ""signed_amount"": -600}, ""smsText"": ""Оплата 600р Карта*4215 kofeinya kondit Баланс 76360.16р 20:10"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-25T17:10:43.889+00:00"", ""counterparty"": ""Кофейня Кондит"", ""signedAmount"": -600.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-25T20:14:00+00:00""}"
"{""id"": 175, ""rawSms"": ""Оплата 7540р Карта*4215 STOCKMANN KUSTY Баланс 68820.16р 20:36"", ""rawJson"": {""action"": ""payment"", ""amount"": 7540, ""balance"": 68820.16, ""raw_sms"": ""Оплата 7540р Карта*4215 STOCKMANN KUSTY Баланс 68820.16р 20:36"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 7540р Карта*4215 STOCKMANN KUSTY Баланс 68820.16р 20:36"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-25T17:36:20.285Z"", ""sms_contact"": """", ""signed_amount"": -7540}, ""smsText"": ""Оплата 7540р Карта*4215 STOCKMANN KUSTY Баланс 68820.16р 20:36"", ""category"": ""Одежда"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-25T17:36:20.285+00:00"", ""counterparty"": ""Стокманн"", ""signedAmount"": -7540.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-25T20:54:00+00:00""}"
"{""id"": 176, ""rawSms"": ""Оплата 4830р Карта*4215 OOO Vetradiolog Баланс 63990.16р 20:53"", ""rawJson"": {""action"": ""payment"", ""amount"": 4830, ""balance"": 63990.16, ""raw_sms"": ""Оплата 4830р Карта*4215 OOO Vetradiolog Баланс 63990.16р 20:53"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 4830р Карта*4215 OOO Vetradiolog Баланс 63990.16р 20:53"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-25T17:53:53.743Z"", ""sms_contact"": """", ""signed_amount"": -4830}, ""smsText"": ""Оплата 4830р Карта*4215 OOO Vetradiolog Баланс 63990.16р 20:53"", ""category"": ""Арчи"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-25T17:53:53.743+00:00"", ""counterparty"": ""Ветра diolog"", ""signedAmount"": -4830.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 177, ""rawSms"": ""Оплата 1р Карта*2249 DOSTAVKA PEREKR Баланс 63989.16р 09:19"", ""rawJson"": {""action"": ""payment"", ""amount"": 1, ""balance"": 63989.16, ""raw_sms"": ""Оплата 1р Карта*2249 DOSTAVKA PEREKR Баланс 63989.16р 09:19"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 1р Карта*2249 DOSTAVKA PEREKR Баланс 63989.16р 09:19"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-26T06:19:33.226Z"", ""sms_contact"": """", ""signed_amount"": -1}, ""smsText"": ""Оплата 1р Карта*2249 DOSTAVKA PEREKR Баланс 63989.16р 09:19"", ""category"": ""Продукты"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-26T06:19:33.226+00:00"", ""counterparty"": ""Перекрёсток"", ""signedAmount"": -1.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-26T09:19:00+00:00""}"
"{""id"": 178, ""rawSms"": ""Отмена: Оплата 1р Карта*2249 DOSTAVKA PEREKR Баланс 63990.16р 09:19"", ""rawJson"": {""action"": ""reversal"", ""amount"": 1, ""balance"": 63990.16, ""raw_sms"": ""Отмена: Оплата 1р Карта*2249 DOSTAVKA PEREKR Баланс 63990.16р 09:19"", ""currency"": ""RUB"", ""sms_text"": ""Отмена: Оплата 1р Карта*2249 DOSTAVKA PEREKR Баланс 63990.16р 09:19"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-26T06:19:37.735Z"", ""sms_contact"": """", ""signed_amount"": 1}, ""smsText"": ""Отмена: Оплата 1р Карта*2249 DOSTAVKA PEREKR Баланс 63990.16р 09:19"", ""category"": ""Продукты"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-26T06:19:37.735+00:00"", ""counterparty"": ""Доставка Перекрёсток"", ""signedAmount"": 1.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-26T09:19:00+00:00""}"
"{""id"": 179, ""rawSms"": ""Оплата 120р Карта*4215 STOLOVAYA 2 Баланс 63870.16р 09:41"", ""rawJson"": {""action"": ""payment"", ""amount"": 120, ""balance"": 63870.16, ""raw_sms"": ""Оплата 120р Карта*4215 STOLOVAYA 2 Баланс 63870.16р 09:41"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 120р Карта*4215 STOLOVAYA 2 Баланс 63870.16р 09:41"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-26T06:41:47.293Z"", ""sms_contact"": """", ""signed_amount"": -120}, ""smsText"": ""Оплата 120р Карта*4215 STOLOVAYA 2 Баланс 63870.16р 09:41"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-26T06:41:47.293+00:00"", ""counterparty"": ""Столовая"", ""signedAmount"": -120.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-26T09:42:00+00:00""}"
"{""id"": 180, ""rawSms"": ""Оплата 3732.39р Карта*2249 DOSTAVKA PEREKR Баланс 60137.77р 10:03"", ""rawJson"": {""action"": ""payment"", ""amount"": 3732.39, ""balance"": 60137.77, ""raw_sms"": ""Оплата 3732.39р Карта*2249 DOSTAVKA PEREKR Баланс 60137.77р 10:03"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 3732.39р Карта*2249 DOSTAVKA PEREKR Баланс 60137.77р 10:03"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-26T07:03:15.917Z"", ""sms_contact"": """", ""signed_amount"": -3732.39}, ""smsText"": ""Оплата 3732.39р Карта*2249 DOSTAVKA PEREKR Баланс 60137.77р 10:03"", ""category"": ""Продукты"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-26T07:03:15.917+00:00"", ""counterparty"": ""Перекрёсток"", ""signedAmount"": -3732.39, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-26T10:03:00+00:00""}"
"{""id"": 181, ""rawSms"": ""Оплата 280р Карта*4215 JEFFREY S COFFE Баланс 59857.77р 11:42"", ""rawJson"": {""action"": ""payment"", ""amount"": 280, ""balance"": 59857.77, ""raw_sms"": ""Оплата 280р Карта*4215 JEFFREY S COFFE Баланс 59857.77р 11:42"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 280р Карта*4215 JEFFREY S COFFE Баланс 59857.77р 11:42"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-26T08:42:15.405Z"", ""sms_contact"": """", ""signed_amount"": -280}, ""smsText"": ""Оплата 280р Карта*4215 JEFFREY S COFFE Баланс 59857.77р 11:42"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-26T08:42:15.405+00:00"", ""counterparty"": ""Джейфри С Кофе"", ""signedAmount"": -280.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-26T12:24:00+00:00""}"
"{""id"": 182, ""rawSms"": ""Оплата 310р Карта*2249 OZON.RU Баланс 59547.77р 13:57"", ""rawJson"": {""action"": ""payment"", ""amount"": 310, ""balance"": 59547.77, ""raw_sms"": ""Оплата 310р Карта*2249 OZON.RU Баланс 59547.77р 13:57"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 310р Карта*2249 OZON.RU Баланс 59547.77р 13:57"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-26T10:57:18.774Z"", ""sms_contact"": """", ""signed_amount"": -310}, ""smsText"": ""Оплата 310р Карта*2249 OZON.RU Баланс 59547.77р 13:57"", ""category"": ""Дом"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-26T10:57:18.774+00:00"", ""counterparty"": ""OZON"", ""signedAmount"": -310.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-26T13:57:00+00:00""}"
"{""id"": 183, ""rawSms"": ""Оплата 5000р Карта*8214 IP KOVTUN L.B. Баланс 54547.77р 14:45"", ""rawJson"": {""action"": ""payment"", ""amount"": 5000, ""balance"": 54547.77, ""raw_sms"": ""Оплата 5000р Карта*8214 IP KOVTUN L.B. Баланс 54547.77р 14:45"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 5000р Карта*8214 IP KOVTUN L.B. Баланс 54547.77р 14:45"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-26T11:45:17.764Z"", ""sms_contact"": """", ""signed_amount"": -5000}, ""smsText"": ""Оплата 5000р Карта*8214 IP KOVTUN L.B. Баланс 54547.77р 14:45"", ""category"": ""Авто"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-26T11:45:17.764+00:00"", ""counterparty"": ""Парковка"", ""signedAmount"": -5000.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 184, ""rawSms"": ""Оплата 571.84р Карта*8214 PYATEROCHKA Баланс 53975.93р 10:50"", ""rawJson"": {""action"": ""payment"", ""amount"": 571.84, ""balance"": 53975.93, ""raw_sms"": ""Оплата 571.84р Карта*8214 PYATEROCHKA Баланс 53975.93р 10:50"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 571.84р Карта*8214 PYATEROCHKA Баланс 53975.93р 10:50"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-27T07:51:08.212Z"", ""sms_contact"": """", ""signed_amount"": -571.84}, ""smsText"": ""Оплата 571.84р Карта*8214 PYATEROCHKA Баланс 53975.93р 10:50"", ""category"": ""Продукты"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-27T07:51:08.212+00:00"", ""counterparty"": ""Пятёрочка"", ""signedAmount"": -571.84, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-27T12:27:00+00:00""}"
"{""id"": 185, ""rawSms"": ""Оплата 280р Карта*4215 STOLOVAYA 2 Баланс 53695.93р 13:34"", ""rawJson"": {""action"": ""payment"", ""amount"": 280, ""balance"": 53695.93, ""raw_sms"": ""Оплата 280р Карта*4215 STOLOVAYA 2 Баланс 53695.93р 13:34"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 280р Карта*4215 STOLOVAYA 2 Баланс 53695.93р 13:34"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-27T10:35:03.934Z"", ""sms_contact"": """", ""signed_amount"": -280}, ""smsText"": ""Оплата 280р Карта*4215 STOLOVAYA 2 Баланс 53695.93р 13:34"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-27T10:35:03.934+00:00"", ""counterparty"": ""Столовая"", ""signedAmount"": -280.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-27T13:58:00+00:00""}"
"{""id"": 186, ""rawSms"": ""Оплата 240р Карта*4215 STOLOVAYA 2 Баланс 53455.93р 17:16"", ""rawJson"": {""action"": ""payment"", ""amount"": 240, ""balance"": 53455.93, ""raw_sms"": ""Оплата 240р Карта*4215 STOLOVAYA 2 Баланс 53455.93р 17:16"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 240р Карта*4215 STOLOVAYA 2 Баланс 53455.93р 17:16"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-27T14:16:54.198Z"", ""sms_contact"": """", ""signed_amount"": -240}, ""smsText"": ""Оплата 240р Карта*4215 STOLOVAYA 2 Баланс 53455.93р 17:16"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-27T14:16:54.198+00:00"", ""counterparty"": ""Столовая"", ""signedAmount"": -240.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-27T18:06:00+00:00""}"
"{""id"": 187, ""rawSms"": ""Оплата 266р Карта*2249 OZON.ru Баланс 53189.93р 17:18"", ""rawJson"": {""action"": ""payment"", ""amount"": 266, ""balance"": 53189.93, ""raw_sms"": ""Оплата 266р Карта*2249 OZON.ru Баланс 53189.93р 17:18"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 266р Карта*2249 OZON.ru Баланс 53189.93р 17:18"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-27T14:18:58.621Z"", ""sms_contact"": """", ""signed_amount"": -266}, ""smsText"": ""Оплата 266р Карта*2249 OZON.ru Баланс 53189.93р 17:18"", ""category"": ""Дом"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-27T14:18:58.621+00:00"", ""counterparty"": ""OZON"", ""signedAmount"": -266.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-27T18:06:00+00:00""}"
"{""id"": 188, ""rawSms"": ""Перевод 562.50р Счет*5611 УФК по г. Москв. Баланс 52064.93р 05:50"", ""rawJson"": {""action"": ""transfer"", ""amount"": 562.5, ""balance"": 52064.93, ""raw_sms"": ""Перевод 562.50р Счет*5611 УФК по г. Москв. Баланс 52064.93р 05:50"", ""currency"": ""RUB"", ""sms_text"": ""Перевод 562.50р Счет*5611 УФК по г. Москв. Баланс 52064.93р 05:50"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-28T02:50:31.856Z"", ""sms_contact"": """", ""signed_amount"": -562.5}, ""smsText"": ""Перевод 562.50р Счет*5611 УФК по г. Москв. Баланс 52064.93р 05:50"", ""category"": ""Штрафы"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-28T02:50:31.856+00:00"", ""counterparty"": ""УФК по г. Москве"", ""signedAmount"": -562.50, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 189, ""rawSms"": ""Оплата 205р Карта*4215 STOLOVAYA 2 Баланс 51859.93р 09:50"", ""rawJson"": {""action"": ""payment"", ""amount"": 205, ""balance"": 51859.93, ""raw_sms"": ""Оплата 205р Карта*4215 STOLOVAYA 2 Баланс 51859.93р 09:50"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 205р Карта*4215 STOLOVAYA 2 Баланс 51859.93р 09:50"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-28T06:50:20.636Z"", ""sms_contact"": """", ""signed_amount"": -205}, ""smsText"": ""Оплата 205р Карта*4215 STOLOVAYA 2 Баланс 51859.93р 09:50"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-28T06:50:20.636+00:00"", ""counterparty"": ""Столовая"", ""signedAmount"": -205.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-28T09:51:00+00:00""}"
"{""id"": 190, ""rawSms"": ""Оплата 999р Карта*2249 OZON Баланс 50860.93р 11:03"", ""rawJson"": {""action"": ""payment"", ""amount"": 999, ""balance"": 50860.93, ""raw_sms"": ""Оплата 999р Карта*2249 OZON Баланс 50860.93р 11:03"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 999р Карта*2249 OZON Баланс 50860.93р 11:03"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-28T08:03:40.125Z"", ""sms_contact"": """", ""signed_amount"": -999}, ""smsText"": ""Оплата 999р Карта*2249 OZON Баланс 50860.93р 11:03"", ""category"": ""Продукты"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-28T08:03:40.125+00:00"", ""counterparty"": ""OZON"", ""signedAmount"": -999.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 191, ""rawSms"": ""Оплата 394р Карта*4215 STOLOVAYA 2 Баланс 50466.93р 13:16"", ""rawJson"": {""action"": ""payment"", ""amount"": 394, ""balance"": 50466.93, ""raw_sms"": ""Оплата 394р Карта*4215 STOLOVAYA 2 Баланс 50466.93р 13:16"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 394р Карта*4215 STOLOVAYA 2 Баланс 50466.93р 13:16"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-28T10:16:44.623Z"", ""sms_contact"": """", ""signed_amount"": -394}, ""smsText"": ""Оплата 394р Карта*4215 STOLOVAYA 2 Баланс 50466.93р 13:16"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-28T10:16:44.623+00:00"", ""counterparty"": ""Столовая"", ""signedAmount"": -394.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-28T15:45:00+00:00""}"
"{""id"": 192, ""rawSms"": ""Оплата 185.77р Карта*8214 PYATEROCHKA Баланс 50281.16р 15:34"", ""rawJson"": {""action"": ""payment"", ""amount"": 185.77, ""balance"": 50281.16, ""raw_sms"": ""Оплата 185.77р Карта*8214 PYATEROCHKA Баланс 50281.16р 15:34"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 185.77р Карта*8214 PYATEROCHKA Баланс 50281.16р 15:34"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-28T12:34:37.686Z"", ""sms_contact"": """", ""signed_amount"": -185.77}, ""smsText"": ""Оплата 185.77р Карта*8214 PYATEROCHKA Баланс 50281.16р 15:34"", ""category"": ""Продукты"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-28T12:34:37.686+00:00"", ""counterparty"": ""Пятёрочка"", ""signedAmount"": -185.77, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-28T15:44:00+00:00""}"
"{""id"": 193, ""rawSms"": ""Оплата 573р Карта*2249 YANDEX*5399*MAR Баланс 49708.16р 15:36"", ""rawJson"": {""action"": ""payment"", ""amount"": 573, ""balance"": 49708.16, ""raw_sms"": ""Оплата 573р Карта*2249 YANDEX*5399*MAR Баланс 49708.16р 15:36"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 573р Карта*2249 YANDEX*5399*MAR Баланс 49708.16р 15:36"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-28T12:36:35.628Z"", ""sms_contact"": """", ""signed_amount"": -573}, ""smsText"": ""Оплата 573р Карта*2249 YANDEX*5399*MAR Баланс 49708.16р 15:36"", ""category"": ""Подарки"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-28T12:36:35.628+00:00"", ""counterparty"": ""Яндекс"", ""signedAmount"": -573.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 194, ""rawSms"": ""Оплата 160р Карта*4215 STOLOVAYA 2 Баланс 49548.16р 09:04"", ""rawJson"": {""action"": ""payment"", ""amount"": 160, ""balance"": 49548.16, ""raw_sms"": ""Оплата 160р Карта*4215 STOLOVAYA 2 Баланс 49548.16р 09:04"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 160р Карта*4215 STOLOVAYA 2 Баланс 49548.16р 09:04"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-29T06:05:04.411Z"", ""sms_contact"": """", ""signed_amount"": -160}, ""smsText"": ""Оплата 160р Карта*4215 STOLOVAYA 2 Баланс 49548.16р 09:04"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-29T06:05:04.411+00:00"", ""counterparty"": ""Столовая"", ""signedAmount"": -160.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-29T09:06:00+00:00""}"
"{""id"": 195, ""rawSms"": ""Оплата 225р Карта*2249 YP*anta-sport.r Баланс 49323.16р 09:43"", ""rawJson"": {""action"": ""payment"", ""amount"": 225, ""balance"": 49323.16, ""raw_sms"": ""Оплата 225р Карта*2249 YP*anta-sport.r Баланс 49323.16р 09:43"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 225р Карта*2249 YP*anta-sport.r Баланс 49323.16р 09:43"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-29T06:43:26.735Z"", ""sms_contact"": """", ""signed_amount"": -225}, ""smsText"": ""Оплата 225р Карта*2249 YP*anta-sport.r Баланс 49323.16р 09:43"", ""category"": ""Спорт"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-29T06:43:26.735+00:00"", ""counterparty"": ""Анта-спорт"", ""signedAmount"": -225.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-29T09:43:00+00:00""}"
"{""id"": 196, ""rawSms"": ""Оплата 12832р Карта*2249 SANTEKHNIKA ONL Баланс 36491.16р 09:49"", ""rawJson"": {""action"": ""payment"", ""amount"": 12832, ""balance"": 36491.16, ""raw_sms"": ""Оплата 12832р Карта*2249 SANTEKHNIKA ONL Баланс 36491.16р 09:49"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 12832р Карта*2249 SANTEKHNIKA ONL Баланс 36491.16р 09:49"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-29T06:49:54.523Z"", ""sms_contact"": """", ""signed_amount"": -12832}, ""smsText"": ""Оплата 12832р Карта*2249 SANTEKHNIKA ONL Баланс 36491.16р 09:49"", ""category"": ""Дом"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-29T06:49:54.523+00:00"", ""counterparty"": ""Сантехника Онлайн"", ""signedAmount"": -12832.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-29T09:50:00+00:00""}"
"{""id"": 197, ""rawSms"": ""Оплата 280р Карта*4215 STOLOVAYA 2 Баланс 36211.16р 12:48"", ""rawJson"": {""action"": ""payment"", ""amount"": 280, ""balance"": 36211.16, ""raw_sms"": ""Оплата 280р Карта*4215 STOLOVAYA 2 Баланс 36211.16р 12:48"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 280р Карта*4215 STOLOVAYA 2 Баланс 36211.16р 12:48"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-29T09:48:25.045Z"", ""sms_contact"": """", ""signed_amount"": -280}, ""smsText"": ""Оплата 280р Карта*4215 STOLOVAYA 2 Баланс 36211.16р 12:48"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-29T09:48:25.045+00:00"", ""counterparty"": ""Столовая"", ""signedAmount"": -280.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-29T13:13:00+00:00""}"
"{""id"": 198, ""rawSms"": ""Оплата 200р Карта*8214 CPPK-2000012-BP Баланс 36011.16р 12:58"", ""rawJson"": {""action"": ""payment"", ""amount"": 200, ""balance"": 36011.16, ""raw_sms"": ""Оплата 200р Карта*8214 CPPK-2000012-BP Баланс 36011.16р 12:58"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 200р Карта*8214 CPPK-2000012-BP Баланс 36011.16р 12:58"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-29T09:59:01.642Z"", ""sms_contact"": """", ""signed_amount"": -200}, ""smsText"": ""Оплата 200р Карта*8214 CPPK-2000012-BP Баланс 36011.16р 12:58"", ""category"": ""Проезд"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-29T09:59:01.642+00:00"", ""counterparty"": ""ЦППК"", ""signedAmount"": -200.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 199, ""rawSms"": ""Оплата 83р Карта*8214 Mos.Transport Баланс 35928.16р 13:34"", ""rawJson"": {""action"": ""payment"", ""amount"": 83, ""balance"": 35928.16, ""raw_sms"": ""Оплата 83р Карта*8214 Mos.Transport Баланс 35928.16р 13:34"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 83р Карта*8214 Mos.Transport Баланс 35928.16р 13:34"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-29T11:12:21.107Z"", ""sms_contact"": """", ""signed_amount"": -83}, ""smsText"": ""Оплата 83р Карта*8214 Mos.Transport Баланс 35928.16р 13:34"", ""category"": ""Проезд"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-29T11:12:21.107+00:00"", ""counterparty"": ""Московские Транспортные Системы"", ""signedAmount"": -83.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-29T14:44:00+00:00""}"
"{""id"": 200, ""rawSms"": ""Оплата 258р Карта*8214 ULYBKA RADUGI Баланс 35670.16р 14:41"", ""rawJson"": {""action"": ""payment"", ""amount"": 258, ""balance"": 35670.16, ""raw_sms"": ""Оплата 258р Карта*8214 ULYBKA RADUGI Баланс 35670.16р 14:41"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 258р Карта*8214 ULYBKA RADUGI Баланс 35670.16р 14:41"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-29T11:41:31.862Z"", ""sms_contact"": """", ""signed_amount"": -258}, ""smsText"": ""Оплата 258р Карта*8214 ULYBKA RADUGI Баланс 35670.16р 14:41"", ""category"": ""Здоровье"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-29T11:41:31.862+00:00"", ""counterparty"": ""УЛЫБКА РАДУГИ"", ""signedAmount"": -258.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 201, ""rawSms"": ""Оплата 2500р Карта*2249 card-runc Баланс 33170.16р 14:54"", ""rawJson"": {""action"": ""payment"", ""amount"": 2500, ""balance"": 33170.16, ""raw_sms"": ""Оплата 2500р Карта*2249 card-runc Баланс 33170.16р 14:54"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 2500р Карта*2249 card-runc Баланс 33170.16р 14:54"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-29T11:54:28.137Z"", ""sms_contact"": """", ""signed_amount"": -2500}, ""smsText"": ""Оплата 2500р Карта*2249 card-runc Баланс 33170.16р 14:54"", ""category"": ""Спорт"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-29T11:54:28.137+00:00"", ""counterparty"": ""RUNC.RUN"", ""signedAmount"": -2500.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 202, ""rawSms"": ""Оплата 300р Карта*8214 Moskva Metro Баланс 32870.16р 15:12"", ""rawJson"": {""action"": ""payment"", ""amount"": 300, ""balance"": 32870.16, ""raw_sms"": ""Оплата 300р Карта*8214 Moskva Metro Баланс 32870.16р 15:12"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 300р Карта*8214 Moskva Metro Баланс 32870.16р 15:12"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-29T12:12:39.451Z"", ""sms_contact"": """", ""signed_amount"": -300}, ""smsText"": ""Оплата 300р Карта*8214 Moskva Metro Баланс 32870.16р 15:12"", ""category"": ""Проезд"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-29T12:12:39.451+00:00"", ""counterparty"": ""Московский метрополитен"", ""signedAmount"": -300.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-29T15:18:00+00:00""}"
"{""id"": 203, ""rawSms"": ""Оплата 293р Карта*4215 STOLOVAYA 2 Баланс 32577.16р 17:42"", ""rawJson"": {""action"": ""payment"", ""amount"": 293, ""balance"": 32577.16, ""raw_sms"": ""Оплата 293р Карта*4215 STOLOVAYA 2 Баланс 32577.16р 17:42"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 293р Карта*4215 STOLOVAYA 2 Баланс 32577.16р 17:42"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-29T14:42:58.043Z"", ""sms_contact"": """", ""signed_amount"": -293}, ""smsText"": ""Оплата 293р Карта*4215 STOLOVAYA 2 Баланс 32577.16р 17:42"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-29T14:42:58.043+00:00"", ""counterparty"": ""Столовая"", ""signedAmount"": -293.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-29T17:57:00+00:00""}"
"{""id"": 204, ""rawSms"": ""Перевод 6400р Счет*5611 Милана К. Баланс 26177.16р 18:40"", ""rawJson"": {""action"": ""transfer"", ""amount"": 6400, ""balance"": 26177.16, ""raw_sms"": ""Перевод 6400р Счет*5611 Милана К. Баланс 26177.16р 18:40"", ""currency"": ""RUB"", ""sms_text"": ""Перевод 6400р Счет*5611 Милана К. Баланс 26177.16р 18:40"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-29T15:40:16.808Z"", ""sms_contact"": """", ""signed_amount"": -6400}, ""smsText"": ""Перевод 6400р Счет*5611 Милана К. Баланс 26177.16р 18:40"", ""category"": ""Спорт"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-29T15:40:16.808+00:00"", ""counterparty"": ""Милана К."", ""signedAmount"": -6400.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 205, ""rawSms"": ""Оплата 83р Карта*4215 Mos.Transport Баланс 26094.16р 19:20"", ""rawJson"": {""action"": ""payment"", ""amount"": 83, ""balance"": 26094.16, ""raw_sms"": ""Оплата 83р Карта*4215 Mos.Transport Баланс 26094.16р 19:20"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 83р Карта*4215 Mos.Transport Баланс 26094.16р 19:20"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-29T17:03:33.778Z"", ""sms_contact"": """", ""signed_amount"": -83}, ""smsText"": ""Оплата 83р Карта*4215 Mos.Transport Баланс 26094.16р 19:20"", ""category"": ""Проезд"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-29T17:03:33.778+00:00"", ""counterparty"": ""Московские Транспортные Системы"", ""signedAmount"": -83.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-30T00:18:00+00:00""}"
"{""id"": 206, ""rawSms"": ""Оплата 83р Карта*4215 Mos.Transport Баланс 26011.16р 22:11"", ""rawJson"": {""action"": ""payment"", ""amount"": 83, ""balance"": 26011.16, ""raw_sms"": ""Оплата 83р Карта*4215 Mos.Transport Баланс 26011.16р 22:11"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 83р Карта*4215 Mos.Transport Баланс 26011.16р 22:11"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-29T19:51:56.328Z"", ""sms_contact"": """", ""signed_amount"": -83}, ""smsText"": ""Оплата 83р Карта*4215 Mos.Transport Баланс 26011.16р 22:11"", ""category"": ""Проезд"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-29T19:51:56.328+00:00"", ""counterparty"": ""Московские Транспортные Системы"", ""signedAmount"": -83.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-30T00:18:00+00:00""}"
"{""id"": 207, ""rawSms"": ""Оплата 200р Карта*4215 SKBDSS403159 Баланс 25811.16р 22:52"", ""rawJson"": {""action"": ""payment"", ""amount"": 200, ""balance"": 25811.16, ""raw_sms"": ""Оплата 200р Карта*4215 SKBDSS403159 Баланс 25811.16р 22:52"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 200р Карта*4215 SKBDSS403159 Баланс 25811.16р 22:52"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-29T19:52:10.152Z"", ""sms_contact"": """", ""signed_amount"": -200}, ""smsText"": ""Оплата 200р Карта*4215 SKBDSS403159 Баланс 25811.16р 22:52"", ""category"": ""Авто"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-29T19:52:10.152+00:00"", ""counterparty"": ""Парковка"", ""signedAmount"": -200.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 208, ""rawSms"": ""Оплата 203р Карта*2249 OZON.ru Баланс 25608.16р 14:48"", ""rawJson"": {""action"": ""payment"", ""amount"": 203, ""balance"": 25608.16, ""raw_sms"": ""Оплата 203р Карта*2249 OZON.ru Баланс 25608.16р 14:48"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 203р Карта*2249 OZON.ru Баланс 25608.16р 14:48"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-30T11:48:31.965Z"", ""sms_contact"": """", ""signed_amount"": -203}, ""smsText"": ""Оплата 203р Карта*2249 OZON.ru Баланс 25608.16р 14:48"", ""category"": ""Дом"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-30T11:48:31.965+00:00"", ""counterparty"": ""OZON"", ""signedAmount"": -203.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-30T14:58:00+00:00""}"
"{""id"": 209, ""rawSms"": ""Оплата 4765.66р Карта*4215 RNAZK MC013 Баланс 20842.50р 16:16"", ""rawJson"": {""action"": ""payment"", ""amount"": 4765.66, ""balance"": 20842.5, ""raw_sms"": ""Оплата 4765.66р Карта*4215 RNAZK MC013 Баланс 20842.50р 16:16"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 4765.66р Карта*4215 RNAZK MC013 Баланс 20842.50р 16:16"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-30T13:16:16.509Z"", ""sms_contact"": """", ""signed_amount"": -4765.66}, ""smsText"": ""Оплата 4765.66р Карта*4215 RNAZK MC013 Баланс 20842.50р 16:16"", ""category"": ""Авто"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-30T13:16:16.509+00:00"", ""counterparty"": ""Транснефть"", ""signedAmount"": -4765.66, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 210, ""rawSms"": ""Оплата 348р Карта*4215 STOLOVAYA 2 Баланс 20494.50р 18:04"", ""rawJson"": {""action"": ""payment"", ""amount"": 348, ""balance"": 20494.5, ""raw_sms"": ""Оплата 348р Карта*4215 STOLOVAYA 2 Баланс 20494.50р 18:04"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 348р Карта*4215 STOLOVAYA 2 Баланс 20494.50р 18:04"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-30T15:04:35.373Z"", ""sms_contact"": """", ""signed_amount"": -348}, ""smsText"": ""Оплата 348р Карта*4215 STOLOVAYA 2 Баланс 20494.50р 18:04"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-30T15:04:35.373+00:00"", ""counterparty"": ""Столовая"", ""signedAmount"": -348.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-01-30T18:16:00+00:00""}"
"{""id"": 211, ""rawSms"": ""Оплата 3500р Карта*4215 RESTORAN Баланс 16994.50р 19:15"", ""rawJson"": {""action"": ""payment"", ""amount"": 3500, ""balance"": 16994.5, ""raw_sms"": ""Оплата 3500р Карта*4215 RESTORAN Баланс 16994.50р 19:15"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 3500р Карта*4215 RESTORAN Баланс 16994.50р 19:15"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-30T16:15:27.642Z"", ""sms_contact"": """", ""signed_amount"": -3500}, ""smsText"": ""Оплата 3500р Карта*4215 RESTORAN Баланс 16994.50р 19:15"", ""category"": ""Развлечения"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-30T16:15:27.642+00:00"", ""counterparty"": ""Рублевские бани"", ""signedAmount"": -3500.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 212, ""rawSms"": ""Оплата 13860р Карта*4215 RUBLEVSKIE BANI Баланс 3134.50р 22:46"", ""rawJson"": {""action"": ""payment"", ""amount"": 13860, ""balance"": 3134.5, ""raw_sms"": ""Оплата 13860р Карта*4215 RUBLEVSKIE BANI Баланс 3134.50р 22:46"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 13860р Карта*4215 RUBLEVSKIE BANI Баланс 3134.50р 22:46"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-30T19:46:29.341Z"", ""sms_contact"": """", ""signed_amount"": -13860}, ""smsText"": ""Оплата 13860р Карта*4215 RUBLEVSKIE BANI Баланс 3134.50р 22:46"", ""category"": ""Развлечения"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-30T19:46:29.341+00:00"", ""counterparty"": ""Бани Русские"", ""signedAmount"": -13860.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 215, ""rawSms"": ""Поступление 3300р Счет*5611 от АНДРЕЙ С. Баланс 11934.50р 23:00"", ""rawJson"": {""action"": ""income"", ""amount"": 3300, ""balance"": 11934.5, ""raw_sms"": ""Поступление 3300р Счет*5611 от АНДРЕЙ С. Баланс 11934.50р 23:00"", ""currency"": ""RUB"", ""sms_text"": ""Поступление 3300р Счет*5611 от АНДРЕЙ С. Баланс 11934.50р 23:00"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-01-30T20:01:03.803Z"", ""sms_contact"": """", ""signed_amount"": 3300}, ""smsText"": ""Поступление 3300р Счет*5611 от АНДРЕЙ С. Баланс 11934.50р 23:00"", ""category"": ""Развлечения"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-01-30T20:01:03.803+00:00"", ""counterparty"": ""Соколов"", ""signedAmount"": 3300.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 216, ""rawSms"": ""Оплата 5050р Карта*4215 Deli Баланс 527000.70р 11:51"", ""rawJson"": {""action"": ""payment"", ""amount"": 5050, ""balance"": 527000.7, ""raw_sms"": ""Оплата 5050р Карта*4215 Deli Баланс 527000.70р 11:51"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 5050р Карта*4215 Deli Баланс 527000.70р 11:51"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-02-02T20:12:39.119Z"", ""sms_contact"": null, ""signed_amount"": -5050}, ""smsText"": ""Оплата 5050р Карта*4215 Deli Баланс 527000.70р 11:51"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-02-02T20:12:39.119+00:00"", ""counterparty"": ""Deli"", ""signedAmount"": -5050.00, ""humanVerified"": false, ""humanVerifiedAt"": null}"
"{""id"": 217, ""rawSms"": ""Оплата 265р Карта*4215 STOLOVAYA 2 Баланс 4223.31р 09:51"", ""rawJson"": {""action"": ""payment"", ""amount"": 265, ""balance"": 4223.31, ""raw_sms"": ""Оплата 265р Карта*4215 STOLOVAYA 2 Баланс 4223.31р 09:51"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 265р Карта*4215 STOLOVAYA 2 Баланс 4223.31р 09:51"", ""sms_sender"": ""VTB"", ""received_at"": ""2026-02-03T06:51:51.879Z"", ""sms_contact"": """", ""signed_amount"": -265}, ""smsText"": ""Оплата 265р Карта*4215 STOLOVAYA 2 Баланс 4223.31р 09:51"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": ""VTB"", ""receivedAt"": ""2026-02-03T06:51:51.879+00:00"", ""counterparty"": ""Столовая"", ""signedAmount"": -265.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-02-03T09:52:00+00:00""}"
"{""id"": 218, ""rawSms"": ""Оплата 618р Карта*2249 PT*Aliexpress Баланс 3605.31р 13:01"", ""rawJson"": {""action"": ""payment"", ""amount"": 618, ""balance"": 3605.31, ""raw_sms"": ""Оплата 618р Карта*2249 PT*Aliexpress Баланс 3605.31р 13:01"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 618р Карта*2249 PT*Aliexpress Баланс 3605.31р 13:01"", ""sms_sender"": null, ""received_at"": ""2026-02-03T10:01:13.838Z"", ""sms_contact"": null, ""signed_amount"": -618}, ""smsText"": ""Оплата 618р Карта*2249 PT*Aliexpress Баланс 3605.31р 13:01"", ""category"": ""Подарки"", ""currency"": ""RUB"", ""smsSender"": null, ""receivedAt"": ""2026-02-03T10:01:13.838+00:00"", ""counterparty"": ""Алиэкспресс"", ""signedAmount"": -618.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 219, ""rawSms"": ""Оплата 1289р Карта*2249 PT*Aliexpress Баланс 2316.31р 13:01"", ""rawJson"": {""action"": ""payment"", ""amount"": 1289, ""balance"": 2316.31, ""raw_sms"": ""Оплата 1289р Карта*2249 PT*Aliexpress Баланс 2316.31р 13:01"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 1289р Карта*2249 PT*Aliexpress Баланс 2316.31р 13:01"", ""sms_sender"": null, ""received_at"": ""2026-02-03T10:01:55.496Z"", ""sms_contact"": null, ""signed_amount"": -1289}, ""smsText"": ""Оплата 1289р Карта*2249 PT*Aliexpress Баланс 2316.31р 13:01"", ""category"": ""Одежда"", ""currency"": ""RUB"", ""smsSender"": null, ""receivedAt"": ""2026-02-03T10:01:55.496+00:00"", ""counterparty"": ""Алиэкспресс"", ""signedAmount"": -1289.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 220, ""rawSms"": ""Оплата 280р Карта*4215 STOLOVAYA 2 Баланс 2036.31р 14:04"", ""rawJson"": {""action"": ""payment"", ""amount"": 280, ""balance"": 2036.31, ""raw_sms"": ""Оплата 280р Карта*4215 STOLOVAYA 2 Баланс 2036.31р 14:04"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 280р Карта*4215 STOLOVAYA 2 Баланс 2036.31р 14:04"", ""sms_sender"": null, ""received_at"": ""2026-02-03T11:04:16.430Z"", ""sms_contact"": null, ""signed_amount"": -280}, ""smsText"": ""Оплата 280р Карта*4215 STOLOVAYA 2 Баланс 2036.31р 14:04"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": null, ""receivedAt"": ""2026-02-03T11:04:16.43+00:00"", ""counterparty"": ""Столовая"", ""signedAmount"": -280.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-02-03T21:36:00+00:00""}"
"{""id"": 221, ""rawSms"": ""Перевод 650р Счет*5611 МегаФон. Баланс 1386.31р 21:35"", ""rawJson"": {""action"": ""transfer"", ""amount"": 650, ""balance"": 1386.31, ""raw_sms"": ""Перевод 650р Счет*5611 МегаФон. Баланс 1386.31р 21:35"", ""currency"": ""RUB"", ""sms_text"": ""Перевод 650р Счет*5611 МегаФон. Баланс 1386.31р 21:35"", ""sms_sender"": null, ""received_at"": ""2026-02-03T18:35:42.070Z"", ""sms_contact"": null, ""signed_amount"": -650}, ""smsText"": ""Перевод 650р Счет*5611 МегаФон. Баланс 1386.31р 21:35"", ""category"": ""Подписки"", ""currency"": ""RUB"", ""smsSender"": null, ""receivedAt"": ""2026-02-03T18:35:42.07+00:00"", ""counterparty"": ""МегаФон"", ""signedAmount"": -650.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-02-03T21:36:00+00:00""}"
"{""id"": 222, ""rawSms"": ""Оплата 120р Карта*4215 STOLOVAYA 2 Баланс 1266.31р 09:34"", ""rawJson"": {""action"": ""payment"", ""amount"": 120, ""balance"": 1266.31, ""raw_sms"": ""Оплата 120р Карта*4215 STOLOVAYA 2 Баланс 1266.31р 09:34"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 120р Карта*4215 STOLOVAYA 2 Баланс 1266.31р 09:34"", ""sms_sender"": null, ""received_at"": ""2026-02-04T06:34:22.819Z"", ""sms_contact"": null, ""signed_amount"": -120}, ""smsText"": ""Оплата 120р Карта*4215 STOLOVAYA 2 Баланс 1266.31р 09:34"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": null, ""receivedAt"": ""2026-02-04T06:34:22.819+00:00"", ""counterparty"": ""Столовая"", ""signedAmount"": -120.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-02-04T09:35:00+00:00""}"
"{""id"": 223, ""rawSms"": ""Оплата 145р Карта*4215 STOLOVAYA 2 Баланс 1121.31р 09:45"", ""rawJson"": {""action"": ""payment"", ""amount"": 145, ""balance"": 1121.31, ""raw_sms"": ""Оплата 145р Карта*4215 STOLOVAYA 2 Баланс 1121.31р 09:45"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 145р Карта*4215 STOLOVAYA 2 Баланс 1121.31р 09:45"", ""sms_sender"": null, ""received_at"": ""2026-02-04T06:45:21.347Z"", ""sms_contact"": null, ""signed_amount"": -145}, ""smsText"": ""Оплата 145р Карта*4215 STOLOVAYA 2 Баланс 1121.31р 09:45"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": null, ""receivedAt"": ""2026-02-04T06:45:21.347+00:00"", ""counterparty"": ""Столовая"", ""signedAmount"": -145.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-02-04T09:45:00+00:00""}"
"{""id"": 224, ""rawSms"": ""Оплата 614.41р Карта*8214 PYATEROCHKA Баланс 506.90р 11:22"", ""rawJson"": {""action"": ""payment"", ""amount"": 614.41, ""balance"": 506.9, ""raw_sms"": ""Оплата 614.41р Карта*8214 PYATEROCHKA Баланс 506.90р 11:22"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 614.41р Карта*8214 PYATEROCHKA Баланс 506.90р 11:22"", ""sms_sender"": null, ""received_at"": ""2026-02-04T08:22:17.990Z"", ""sms_contact"": null, ""signed_amount"": -614.41}, ""smsText"": ""Оплата 614.41р Карта*8214 PYATEROCHKA Баланс 506.90р 11:22"", ""category"": ""Продукты"", ""currency"": ""RUB"", ""smsSender"": null, ""receivedAt"": ""2026-02-04T08:22:17.99+00:00"", ""counterparty"": ""Пятёрочка"", ""signedAmount"": -614.41, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-02-04T11:28:00+00:00""}"
"{""id"": 225, ""rawSms"": ""Оплата 280р Карта*4215 STOLOVAYA. Баланс 226.90р 12:44"", ""rawJson"": {""action"": ""payment"", ""amount"": 280, ""balance"": 226.9, ""raw_sms"": ""Оплата 280р Карта*4215 STOLOVAYA. Баланс 226.90р 12:44"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 280р Карта*4215 STOLOVAYA. Баланс 226.90р 12:44"", ""sms_sender"": null, ""received_at"": ""2026-02-04T09:44:32.838Z"", ""sms_contact"": null, ""signed_amount"": -280}, ""smsText"": ""Оплата 280р Карта*4215 STOLOVAYA. Баланс 226.90р 12:44"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": null, ""receivedAt"": ""2026-02-04T09:44:32.838+00:00"", ""counterparty"": ""Столовая"", ""signedAmount"": -280.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-02-04T13:12:00+00:00""}"
"{""id"": 226, ""rawSms"": ""Недостаточно средств 500р Карта*4215 STARS COFFEE 2F Баланс 226.90р 15:33"", ""rawJson"": {""action"": ""unknown"", ""amount"": 500, ""balance"": 226.9, ""raw_sms"": ""Недостаточно средств 500р Карта*4215 STARS COFFEE 2F Баланс 226.90р 15:33"", ""currency"": ""RUB"", ""sms_text"": ""Недостаточно средств 500р Карта*4215 STARS COFFEE 2F Баланс 226.90р 15:33"", ""sms_sender"": null, ""received_at"": ""2026-02-04T12:33:18.491Z"", ""sms_contact"": null, ""signed_amount"": 500}, ""smsText"": ""Недостаточно средств 500р Карта*4215 STARS COFFEE 2F Баланс 226.90р 15:33"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": null, ""receivedAt"": ""2026-02-04T12:33:18.491+00:00"", ""counterparty"": ""STARS COFFEE"", ""signedAmount"": 500.00, ""humanVerified"": false, ""humanVerifiedAt"": null}"
"{""id"": 227, ""rawSms"": ""Оплата 500р Карта*4215 STARS COFFEE 2F Баланс 3440.32р 15:34"", ""rawJson"": {""action"": ""payment"", ""amount"": 500, ""balance"": 3440.32, ""raw_sms"": ""Оплата 500р Карта*4215 STARS COFFEE 2F Баланс 3440.32р 15:34"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 500р Карта*4215 STARS COFFEE 2F Баланс 3440.32р 15:34"", ""sms_sender"": null, ""received_at"": ""2026-02-04T12:34:35.465Z"", ""sms_contact"": null, ""signed_amount"": -500}, ""smsText"": ""Оплата 500р Карта*4215 STARS COFFEE 2F Баланс 3440.32р 15:34"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": null, ""receivedAt"": ""2026-02-04T12:34:35.465+00:00"", ""counterparty"": ""СтарБакс"", ""signedAmount"": -500.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-02-04T16:10:00+00:00""}"
"{""id"": 228, ""rawSms"": ""Оплата 264р Карта*4215 STOLOVAYA 2 Баланс 3176.32р 17:46"", ""rawJson"": {""action"": ""payment"", ""amount"": 264, ""balance"": 3176.32, ""raw_sms"": ""Оплата 264р Карта*4215 STOLOVAYA 2 Баланс 3176.32р 17:46"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 264р Карта*4215 STOLOVAYA 2 Баланс 3176.32р 17:46"", ""sms_sender"": null, ""received_at"": ""2026-02-04T14:46:58.449Z"", ""sms_contact"": null, ""signed_amount"": -264}, ""smsText"": ""Оплата 264р Карта*4215 STOLOVAYA 2 Баланс 3176.32р 17:46"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": null, ""receivedAt"": ""2026-02-04T14:46:58.449+00:00"", ""counterparty"": ""Столовая"", ""signedAmount"": -264.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-02-04T18:08:00+00:00""}"
"{""id"": 229, ""rawSms"": ""Оплата 605.97р Карта*4215 PYATEROCHKA Баланс 2570.35р 20:41"", ""rawJson"": {""action"": ""payment"", ""amount"": 605.97, ""balance"": 2570.35, ""raw_sms"": ""Оплата 605.97р Карта*4215 PYATEROCHKA Баланс 2570.35р 20:41"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 605.97р Карта*4215 PYATEROCHKA Баланс 2570.35р 20:41"", ""sms_sender"": null, ""received_at"": ""2026-02-04T17:41:34.567Z"", ""sms_contact"": null, ""signed_amount"": -605.97}, ""smsText"": ""Оплата 605.97р Карта*4215 PYATEROCHKA Баланс 2570.35р 20:41"", ""category"": ""Продукты"", ""currency"": ""RUB"", ""smsSender"": null, ""receivedAt"": ""2026-02-04T17:41:34.567+00:00"", ""counterparty"": ""Пятёрочка"", ""signedAmount"": -605.97, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-02-04T22:00:00+00:00""}"
"{""id"": 230, ""rawSms"": ""Оплата 4545.24р Карта*4215 RNAZK MC013 Баланс 3025.11р 09:16"", ""rawJson"": {""action"": ""payment"", ""amount"": 4545.24, ""balance"": 3025.11, ""raw_sms"": ""Оплата 4545.24р Карта*4215 RNAZK MC013 Баланс 3025.11р 09:16"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 4545.24р Карта*4215 RNAZK MC013 Баланс 3025.11р 09:16"", ""sms_sender"": null, ""received_at"": ""2026-02-05T06:16:31.300Z"", ""sms_contact"": null, ""signed_amount"": -4545.24}, ""smsText"": ""Оплата 4545.24р Карта*4215 RNAZK MC013 Баланс 3025.11р 09:16"", ""category"": ""Авто"", ""currency"": ""RUB"", ""smsSender"": null, ""receivedAt"": ""2026-02-05T06:16:31.3+00:00"", ""counterparty"": ""Роснефть"", ""signedAmount"": -4545.24, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 231, ""rawSms"": ""Перевод 800р Счет*5611 Али А. Баланс 2225.11р 09:20"", ""rawJson"": {""action"": ""transfer"", ""amount"": 800, ""balance"": 2225.11, ""raw_sms"": ""Перевод 800р Счет*5611 Али А. Баланс 2225.11р 09:20"", ""currency"": ""RUB"", ""sms_text"": ""Перевод 800р Счет*5611 Али А. Баланс 2225.11р 09:20"", ""sms_sender"": null, ""received_at"": ""2026-02-05T06:20:28.634Z"", ""sms_contact"": null, ""signed_amount"": -800}, ""smsText"": ""Перевод 800р Счет*5611 Али А. Баланс 2225.11р 09:20"", ""category"": ""Авто"", ""currency"": ""RUB"", ""smsSender"": null, ""receivedAt"": ""2026-02-05T06:20:28.634+00:00"", ""counterparty"": ""Али А."", ""signedAmount"": -800.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 232, ""rawSms"": ""Оплата 290р Карта*4215 STOLOVAYA 2 Баланс 1935.11р 09:50"", ""rawJson"": {""action"": ""payment"", ""amount"": 290, ""balance"": 1935.11, ""raw_sms"": ""Оплата 290р Карта*4215 STOLOVAYA 2 Баланс 1935.11р 09:50"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 290р Карта*4215 STOLOVAYA 2 Баланс 1935.11р 09:50"", ""sms_sender"": null, ""received_at"": ""2026-02-05T06:50:13.019Z"", ""sms_contact"": null, ""signed_amount"": -290}, ""smsText"": ""Оплата 290р Карта*4215 STOLOVAYA 2 Баланс 1935.11р 09:50"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": null, ""receivedAt"": ""2026-02-05T06:50:13.019+00:00"", ""counterparty"": ""Столовая"", ""signedAmount"": -290.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-02-05T09:51:00+00:00""}"
"{""id"": 233, ""rawSms"": ""Оплата 178р Карта*8214 CPPK-2000012-BP Баланс 1757.11р 10:00"", ""rawJson"": {""action"": ""payment"", ""amount"": 178, ""balance"": 1757.11, ""raw_sms"": ""Оплата 178р Карта*8214 CPPK-2000012-BP Баланс 1757.11р 10:00"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 178р Карта*8214 CPPK-2000012-BP Баланс 1757.11р 10:00"", ""sms_sender"": null, ""received_at"": ""2026-02-05T07:01:02.889Z"", ""sms_contact"": null, ""signed_amount"": -178}, ""smsText"": ""Оплата 178р Карта*8214 CPPK-2000012-BP Баланс 1757.11р 10:00"", ""category"": ""Проезд"", ""currency"": ""RUB"", ""smsSender"": null, ""receivedAt"": ""2026-02-05T07:01:02.889+00:00"", ""counterparty"": ""ЦППК"", ""signedAmount"": -178.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 234, ""rawSms"": ""Оплата 72р Карта*4215 STOLOVAYA 2 Баланс 1685.11р 11:32"", ""rawJson"": {""action"": ""payment"", ""amount"": 72, ""balance"": 1685.11, ""raw_sms"": ""Оплата 72р Карта*4215 STOLOVAYA 2 Баланс 1685.11р 11:32"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 72р Карта*4215 STOLOVAYA 2 Баланс 1685.11р 11:32"", ""sms_sender"": null, ""received_at"": ""2026-02-05T08:32:37.643Z"", ""sms_contact"": null, ""signed_amount"": -72}, ""smsText"": ""Оплата 72р Карта*4215 STOLOVAYA 2 Баланс 1685.11р 11:32"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": null, ""receivedAt"": ""2026-02-05T08:32:37.643+00:00"", ""counterparty"": ""Столовая"", ""signedAmount"": -72.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-02-05T11:32:00+00:00""}"
"{""id"": 235, ""rawSms"": ""Оплата 36р Карта*4215 STOLOVAYA 2 Баланс 1649.11р 11:33"", ""rawJson"": {""action"": ""payment"", ""amount"": 36, ""balance"": 1649.11, ""raw_sms"": ""Оплата 36р Карта*4215 STOLOVAYA 2 Баланс 1649.11р 11:33"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 36р Карта*4215 STOLOVAYA 2 Баланс 1649.11р 11:33"", ""sms_sender"": null, ""received_at"": ""2026-02-05T08:33:16.463Z"", ""sms_contact"": null, ""signed_amount"": -36}, ""smsText"": ""Оплата 36р Карта*4215 STOLOVAYA 2 Баланс 1649.11р 11:33"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": null, ""receivedAt"": ""2026-02-05T08:33:16.463+00:00"", ""counterparty"": ""Столовая"", ""signedAmount"": -36.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-02-05T11:40:00+00:00""}"
"{""id"": 236, ""rawSms"": ""Оплата 300р Карта*8214 OKHOTNY RYAD SE Баланс 1349.11р 12:22"", ""rawJson"": {""action"": ""payment"", ""amount"": 300, ""balance"": 1349.11, ""raw_sms"": ""Оплата 300р Карта*8214 OKHOTNY RYAD SE Баланс 1349.11р 12:22"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 300р Карта*8214 OKHOTNY RYAD SE Баланс 1349.11р 12:22"", ""sms_sender"": null, ""received_at"": ""2026-02-05T09:22:23.515Z"", ""sms_contact"": null, ""signed_amount"": -300}, ""smsText"": ""Оплата 300р Карта*8214 OKHOTNY RYAD SE Баланс 1349.11р 12:22"", ""category"": ""Проезд"", ""currency"": ""RUB"", ""smsSender"": null, ""receivedAt"": ""2026-02-05T09:22:23.515+00:00"", ""counterparty"": ""Охотный ряд"", ""signedAmount"": -300.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 237, ""rawSms"": ""Оплата 1130р Карта*8214 TA TORRRO GRIL Баланс 219.11р 13:56"", ""rawJson"": {""action"": ""payment"", ""amount"": 1130, ""balance"": 219.11, ""raw_sms"": ""Оплата 1130р Карта*8214 TA TORRRO GRIL Баланс 219.11р 13:56"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 1130р Карта*8214 TA TORRRO GRIL Баланс 219.11р 13:56"", ""sms_sender"": null, ""received_at"": ""2026-02-05T10:57:06.037Z"", ""sms_contact"": null, ""signed_amount"": -1130}, ""smsText"": ""Оплата 1130р Карта*8214 TA TORRRO GRIL Баланс 219.11р 13:56"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": null, ""receivedAt"": ""2026-02-05T10:57:06.037+00:00"", ""counterparty"": ""Торрро Гриль"", ""signedAmount"": -1130.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-02-05T14:47:00+00:00""}"
"{""id"": 238, ""rawSms"": ""Списание 700р Счет*5611 АНАСТАСИЯ Ш. Баланс 4519.11р 16:40"", ""rawJson"": {""action"": ""writeoff"", ""amount"": 700, ""balance"": 4519.11, ""raw_sms"": ""Списание 700р Счет*5611 АНАСТАСИЯ Ш. Баланс 4519.11р 16:40"", ""currency"": ""RUB"", ""sms_text"": ""Списание 700р Счет*5611 АНАСТАСИЯ Ш. Баланс 4519.11р 16:40"", ""sms_sender"": null, ""received_at"": ""2026-02-05T13:40:48.922Z"", ""sms_contact"": null, ""signed_amount"": -700}, ""smsText"": ""Списание 700р Счет*5611 АНАСТАСИЯ Ш. Баланс 4519.11р 16:40"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": null, ""receivedAt"": ""2026-02-05T13:40:48.922+00:00"", ""counterparty"": ""Шумилова А."", ""signedAmount"": -700.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 239, ""rawSms"": ""Оплата 112.66р Карта*2249 YM*AMPP Баланс 4406.45р 19:07"", ""rawJson"": {""action"": ""payment"", ""amount"": 112.66, ""balance"": 4406.45, ""raw_sms"": ""Оплата 112.66р Карта*2249 YM*AMPP Баланс 4406.45р 19:07"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 112.66р Карта*2249 YM*AMPP Баланс 4406.45р 19:07"", ""sms_sender"": null, ""received_at"": ""2026-02-05T16:07:23.979Z"", ""sms_contact"": null, ""signed_amount"": -112.66}, ""smsText"": ""Оплата 112.66р Карта*2249 YM*AMPP Баланс 4406.45р 19:07"", ""category"": ""Авто"", ""currency"": ""RUB"", ""smsSender"": null, ""receivedAt"": ""2026-02-05T16:07:23.979+00:00"", ""counterparty"": ""АМПП"", ""signedAmount"": -112.66, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 240, ""rawSms"": ""Оплата 51.33р Карта*2249 YM*AMPP Баланс 4355.12р 10:29"", ""rawJson"": {""action"": ""payment"", ""amount"": 51.33, ""balance"": 4355.12, ""raw_sms"": ""Оплата 51.33р Карта*2249 YM*AMPP Баланс 4355.12р 10:29"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 51.33р Карта*2249 YM*AMPP Баланс 4355.12р 10:29"", ""sms_sender"": null, ""received_at"": ""2026-02-06T07:29:40.207Z"", ""sms_contact"": null, ""signed_amount"": -51.33}, ""smsText"": ""Оплата 51.33р Карта*2249 YM*AMPP Баланс 4355.12р 10:29"", ""category"": ""Авто"", ""currency"": ""RUB"", ""smsSender"": null, ""receivedAt"": ""2026-02-06T07:29:40.207+00:00"", ""counterparty"": ""АМПП"", ""signedAmount"": -51.33, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 241, ""rawSms"": ""Оплата 280р Карта*4215 STOLOVAYA 2 Баланс 4075.12р 13:01"", ""rawJson"": {""action"": ""payment"", ""amount"": 280, ""balance"": 4075.12, ""raw_sms"": ""Оплата 280р Карта*4215 STOLOVAYA 2 Баланс 4075.12р 13:01"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 280р Карта*4215 STOLOVAYA 2 Баланс 4075.12р 13:01"", ""sms_sender"": null, ""received_at"": ""2026-02-06T10:01:28.002Z"", ""sms_contact"": null, ""signed_amount"": -280}, ""smsText"": ""Оплата 280р Карта*4215 STOLOVAYA 2 Баланс 4075.12р 13:01"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": null, ""receivedAt"": ""2026-02-06T10:01:28.002+00:00"", ""counterparty"": ""Столовая"", ""signedAmount"": -280.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-02-06T13:26:00+00:00""}"
"{""id"": 242, ""rawSms"": ""Оплата 10250.50р Карта*8214 Russian Railway Баланс 3824.62р 13:13"", ""rawJson"": {""action"": ""payment"", ""amount"": 10250.5, ""balance"": 3824.62, ""raw_sms"": ""Оплата 10250.50р Карта*8214 Russian Railway Баланс 3824.62р 13:13"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 10250.50р Карта*8214 Russian Railway Баланс 3824.62р 13:13"", ""sms_sender"": null, ""received_at"": ""2026-02-06T10:13:45.206Z"", ""sms_contact"": null, ""signed_amount"": -10250.5}, ""smsText"": ""Оплата 10250.50р Карта*8214 Russian Railway Баланс 3824.62р 13:13"", ""category"": ""Проезд"", ""currency"": ""RUB"", ""smsSender"": null, ""receivedAt"": ""2026-02-06T10:13:45.206+00:00"", ""counterparty"": ""Российские Рельсы"", ""signedAmount"": -10250.50, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-02-06T13:27:00+00:00""}"
"{""id"": 243, ""rawSms"": ""Оплата 2307.86р Карта*2249 DOSTAVKA IZ PYA Баланс 1516.76р 15:23"", ""rawJson"": {""action"": ""payment"", ""amount"": 2307.86, ""balance"": 1516.76, ""raw_sms"": ""Оплата 2307.86р Карта*2249 DOSTAVKA IZ PYA Баланс 1516.76р 15:23"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 2307.86р Карта*2249 DOSTAVKA IZ PYA Баланс 1516.76р 15:23"", ""sms_sender"": null, ""received_at"": ""2026-02-06T12:23:50.765Z"", ""sms_contact"": null, ""signed_amount"": -2307.86}, ""smsText"": ""Оплата 2307.86р Карта*2249 DOSTAVKA IZ PYA Баланс 1516.76р 15:23"", ""category"": ""Продукты"", ""currency"": ""RUB"", ""smsSender"": null, ""receivedAt"": ""2026-02-06T12:23:50.765+00:00"", ""counterparty"": ""Доставка из Пятерочки"", ""signedAmount"": -2307.86, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-02-06T15:44:00+00:00""}"
"{""id"": 244, ""rawSms"": ""Оплата 259.99р Карта*2249 DOSTAVKA IZ PYA Баланс 6881.77р 16:01"", ""rawJson"": {""action"": ""payment"", ""amount"": 259.99, ""balance"": 6881.77, ""raw_sms"": ""Оплата 259.99р Карта*2249 DOSTAVKA IZ PYA Баланс 6881.77р 16:01"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 259.99р Карта*2249 DOSTAVKA IZ PYA Баланс 6881.77р 16:01"", ""sms_sender"": null, ""received_at"": ""2026-02-06T13:01:35.378Z"", ""sms_contact"": null, ""signed_amount"": -259.99}, ""smsText"": ""Оплата 259.99р Карта*2249 DOSTAVKA IZ PYA Баланс 6881.77р 16:01"", ""category"": ""Продукты"", ""currency"": ""RUB"", ""smsSender"": null, ""receivedAt"": ""2026-02-06T13:01:35.378+00:00"", ""counterparty"": ""Доставка из Пятерочки"", ""signedAmount"": -259.99, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-02-06T16:03:00+00:00""}"
"{""id"": 245, ""rawSms"": ""Зачисление кешбэка по программе лояльности 5625р Счет*5611 Баланс 6881.77р 15:38"", ""rawJson"": {""action"": ""income"", ""amount"": 5625, ""balance"": 6881.77, ""raw_sms"": ""Зачисление кешбэка по программе лояльности 5625р Счет*5611 Баланс 6881.77р 15:38"", ""currency"": ""RUB"", ""sms_text"": ""Зачисление кешбэка по программе лояльности 5625р Счет*5611 Баланс 6881.77р 15:38"", ""sms_sender"": null, ""received_at"": ""2026-02-06T13:39:06.646Z"", ""sms_contact"": null, ""signed_amount"": 5625}, ""smsText"": ""Зачисление кешбэка по программе лояльности 5625р Счет*5611 Баланс 6881.77р 15:38"", ""category"": ""Поступления"", ""currency"": ""RUB"", ""smsSender"": null, ""receivedAt"": ""2026-02-06T13:39:06.646+00:00"", ""counterparty"": ""ВТБ кэшбэк"", ""signedAmount"": 5625.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 246, ""rawSms"": ""Оплата 500р Карта*4215 SKULPTOR TELA Баланс 6381.77р 10:01"", ""rawJson"": {""action"": ""payment"", ""amount"": 500, ""balance"": 6381.77, ""raw_sms"": ""Оплата 500р Карта*4215 SKULPTOR TELA Баланс 6381.77р 10:01"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 500р Карта*4215 SKULPTOR TELA Баланс 6381.77р 10:01"", ""sms_sender"": null, ""received_at"": ""2026-02-07T07:01:22.891Z"", ""sms_contact"": null, ""signed_amount"": -500}, ""smsText"": ""Оплата 500р Карта*4215 SKULPTOR TELA Баланс 6381.77р 10:01"", ""category"": ""Спорт"", ""currency"": ""RUB"", ""smsSender"": null, ""receivedAt"": ""2026-02-07T07:01:22.891+00:00"", ""counterparty"": ""Скультор Тела"", ""signedAmount"": -500.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 247, ""rawSms"": ""Оплата 500р Карта*2249 VANYAVPN Баланс 5881.77р 10:29"", ""rawJson"": {""action"": ""payment"", ""amount"": 500, ""balance"": 5881.77, ""raw_sms"": ""Оплата 500р Карта*2249 VANYAVPN Баланс 5881.77р 10:29"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 500р Карта*2249 VANYAVPN Баланс 5881.77р 10:29"", ""sms_sender"": null, ""received_at"": ""2026-02-07T07:29:36.281Z"", ""sms_contact"": null, ""signed_amount"": -500}, ""smsText"": ""Оплата 500р Карта*2249 VANYAVPN Баланс 5881.77р 10:29"", ""category"": ""Подписки"", ""currency"": ""RUB"", ""smsSender"": null, ""receivedAt"": ""2026-02-07T07:29:36.281+00:00"", ""counterparty"": ""VANYAVPN"", ""signedAmount"": -500.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-02-07T11:55:00+00:00""}"
"{""id"": 248, ""rawSms"": ""Оплата 200р Карта*2249 YM*avito Баланс 252.18р 18:39"", ""rawJson"": {""action"": ""payment"", ""amount"": 200, ""balance"": 252.18, ""raw_sms"": ""Оплата 200р Карта*2249 YM*avito Баланс 252.18р 18:39"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 200р Карта*2249 YM*avito Баланс 252.18р 18:39"", ""sms_sender"": null, ""received_at"": ""2026-02-08T15:39:44.989Z"", ""sms_contact"": null, ""signed_amount"": -200}, ""smsText"": ""Оплата 200р Карта*2249 YM*avito Баланс 252.18р 18:39"", ""category"": ""Подписки"", ""currency"": ""RUB"", ""smsSender"": null, ""receivedAt"": ""2026-02-08T15:39:44.989+00:00"", ""counterparty"": ""Авито"", ""signedAmount"": -200.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-02-08T18:39:00+00:00""}"
"{""id"": 249, ""rawSms"": ""Зарплата 399871.64р Счет *5611 Баланс 400123.82р 15:13"", ""rawJson"": {""action"": ""salary"", ""amount"": 399871.64, ""balance"": 400123.82, ""raw_sms"": ""Зарплата 399871.64р Счет *5611 Баланс 400123.82р 15:13"", ""currency"": ""RUB"", ""sms_text"": ""Зарплата 399871.64р Счет *5611 Баланс 400123.82р 15:13"", ""sms_sender"": null, ""received_at"": ""2026-02-09T12:13:16.793Z"", ""sms_contact"": null, ""signed_amount"": 399871.64}, ""smsText"": ""Зарплата 399871.64р Счет *5611 Баланс 400123.82р 15:13"", ""category"": ""Поступления"", ""currency"": ""RUB"", ""smsSender"": null, ""receivedAt"": ""2026-02-09T12:13:16.793+00:00"", ""counterparty"": ""НИУ ВШЭ"", ""signedAmount"": 399871.64, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 250, ""rawSms"": ""Оплата 2502.85р Карта*2249 DOSTAVKA IZ PYA Баланс 397620.97р 17:12"", ""rawJson"": {""action"": ""payment"", ""amount"": 2502.85, ""balance"": 397620.97, ""raw_sms"": ""Оплата 2502.85р Карта*2249 DOSTAVKA IZ PYA Баланс 397620.97р 17:12"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 2502.85р Карта*2249 DOSTAVKA IZ PYA Баланс 397620.97р 17:12"", ""sms_sender"": null, ""received_at"": ""2026-02-09T14:12:26.668Z"", ""sms_contact"": null, ""signed_amount"": -2502.85}, ""smsText"": ""Оплата 2502.85р Карта*2249 DOSTAVKA IZ PYA Баланс 397620.97р 17:12"", ""category"": ""Продукты"", ""currency"": ""RUB"", ""smsSender"": null, ""receivedAt"": ""2026-02-09T14:12:26.668+00:00"", ""counterparty"": ""Доставка из Пятерочки"", ""signedAmount"": -2502.85, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-02-09T17:52:00+00:00""}"
"{""id"": 251, ""rawSms"": ""Оплата 56.72р Карта*2249 DOSTAVKA IZ PYA Баланс 397564.25р 18:12"", ""rawJson"": {""action"": ""payment"", ""amount"": 56.72, ""balance"": 397564.25, ""raw_sms"": ""Оплата 56.72р Карта*2249 DOSTAVKA IZ PYA Баланс 397564.25р 18:12"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 56.72р Карта*2249 DOSTAVKA IZ PYA Баланс 397564.25р 18:12"", ""sms_sender"": null, ""received_at"": ""2026-02-09T15:12:25.842Z"", ""sms_contact"": null, ""signed_amount"": -56.72}, ""smsText"": ""Оплата 56.72р Карта*2249 DOSTAVKA IZ PYA Баланс 397564.25р 18:12"", ""category"": ""Продукты"", ""currency"": ""RUB"", ""smsSender"": null, ""receivedAt"": ""2026-02-09T15:12:25.842+00:00"", ""counterparty"": ""Доставка из Пятерочки"", ""signedAmount"": -56.72, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-02-09T18:25:00+00:00""}"
"{""id"": 252, ""rawSms"": ""Перевод 12768.40р Счет*5611 МосОблЕИРЦ. Баланс 374695.85р 18:28"", ""rawJson"": {""action"": ""transfer"", ""amount"": 12768.4, ""balance"": 374695.85, ""raw_sms"": ""Перевод 12768.40р Счет*5611 МосОблЕИРЦ. Баланс 374695.85р 18:28"", ""currency"": ""RUB"", ""sms_text"": ""Перевод 12768.40р Счет*5611 МосОблЕИРЦ. Баланс 374695.85р 18:28"", ""sms_sender"": null, ""received_at"": ""2026-02-09T15:28:54.421Z"", ""sms_contact"": null, ""signed_amount"": -12768.4}, ""smsText"": ""Перевод 12768.40р Счет*5611 МосОблЕИРЦ. Баланс 374695.85р 18:28"", ""category"": ""ЖКХ"", ""currency"": ""RUB"", ""smsSender"": null, ""receivedAt"": ""2026-02-09T15:28:54.421+00:00"", ""counterparty"": ""МосОблЕИРЦ"", ""signedAmount"": -12768.40, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 253, ""rawSms"": ""Перевод 10000р, комиссия 100р Счет*5611 ANTON S. Баланс 374695.85р 18:28"", ""rawJson"": {""action"": ""transfer"", ""amount"": 10000, ""balance"": 374695.85, ""raw_sms"": ""Перевод 10000р, комиссия 100р Счет*5611 ANTON S. Баланс 374695.85р 18:28"", ""currency"": ""RUB"", ""sms_text"": ""Перевод 10000р, комиссия 100р Счет*5611 ANTON S. Баланс 374695.85р 18:28"", ""sms_sender"": null, ""received_at"": ""2026-02-09T15:29:49.057Z"", ""sms_contact"": null, ""signed_amount"": -10000}, ""smsText"": ""Перевод 10000р, комиссия 100р Счет*5611 ANTON S. Баланс 374695.85р 18:28"", ""category"": ""Перевод"", ""currency"": ""RUB"", ""smsSender"": null, ""receivedAt"": ""2026-02-09T15:29:49.057+00:00"", ""counterparty"": ""Бакай Банк"", ""signedAmount"": -10000.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 254, ""rawSms"": ""Оплата 120р Карта*4215 STOLOVAYA 2 Баланс 224575.85р 09:37"", ""rawJson"": {""action"": ""payment"", ""amount"": 120, ""balance"": 224575.85, ""raw_sms"": ""Оплата 120р Карта*4215 STOLOVAYA 2 Баланс 224575.85р 09:37"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 120р Карта*4215 STOLOVAYA 2 Баланс 224575.85р 09:37"", ""sms_sender"": null, ""received_at"": ""2026-02-10T06:37:37.395Z"", ""sms_contact"": null, ""signed_amount"": -120}, ""smsText"": ""Оплата 120р Карта*4215 STOLOVAYA 2 Баланс 224575.85р 09:37"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": null, ""receivedAt"": ""2026-02-10T06:37:37.395+00:00"", ""counterparty"": ""Столовая"", ""signedAmount"": -120.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-02-10T10:37:00+00:00""}"
"{""id"": 255, ""rawSms"": ""Перевод 500р Счет*5611 МТС Mobile. Баланс 224075.85р 10:05"", ""rawJson"": {""action"": ""transfer"", ""amount"": 500, ""balance"": 224075.85, ""raw_sms"": ""Перевод 500р Счет*5611 МТС Mobile. Баланс 224075.85р 10:05"", ""currency"": ""RUB"", ""sms_text"": ""Перевод 500р Счет*5611 МТС Mobile. Баланс 224075.85р 10:05"", ""sms_sender"": null, ""received_at"": ""2026-02-10T07:05:30.553Z"", ""sms_contact"": null, ""signed_amount"": -500}, ""smsText"": ""Перевод 500р Счет*5611 МТС Mobile. Баланс 224075.85р 10:05"", ""category"": ""Подписки"", ""currency"": ""RUB"", ""smsSender"": null, ""receivedAt"": ""2026-02-10T07:05:30.553+00:00"", ""counterparty"": ""МТС"", ""signedAmount"": -500.00, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 256, ""rawSms"": ""Оплата 171р Карта*4215 STOLOVAYA 2 Баланс 223904.85р 10:47"", ""rawJson"": {""action"": ""payment"", ""amount"": 171, ""balance"": 223904.85, ""raw_sms"": ""Оплата 171р Карта*4215 STOLOVAYA 2 Баланс 223904.85р 10:47"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 171р Карта*4215 STOLOVAYA 2 Баланс 223904.85р 10:47"", ""sms_sender"": null, ""received_at"": ""2026-02-10T07:47:27.873Z"", ""sms_contact"": null, ""signed_amount"": -171}, ""smsText"": ""Оплата 171р Карта*4215 STOLOVAYA 2 Баланс 223904.85р 10:47"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": null, ""receivedAt"": ""2026-02-10T07:47:27.873+00:00"", ""counterparty"": ""Столовая"", ""signedAmount"": -171.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-02-10T11:01:00+00:00""}"
"{""id"": 257, ""rawSms"": ""Оплата 611р Карта*2249 OZON.RU Баланс 223293.85р 13:33"", ""rawJson"": {""action"": ""payment"", ""amount"": 611, ""balance"": 223293.85, ""raw_sms"": ""Оплата 611р Карта*2249 OZON.RU Баланс 223293.85р 13:33"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 611р Карта*2249 OZON.RU Баланс 223293.85р 13:33"", ""sms_sender"": null, ""received_at"": ""2026-02-10T10:33:52.816Z"", ""sms_contact"": null, ""signed_amount"": -611}, ""smsText"": ""Оплата 611р Карта*2249 OZON.RU Баланс 223293.85р 13:33"", ""category"": ""Дом"", ""currency"": ""RUB"", ""smsSender"": null, ""receivedAt"": ""2026-02-10T10:33:52.816+00:00"", ""counterparty"": ""OZON"", ""signedAmount"": -611.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-02-10T14:15:00+00:00""}"
"{""id"": 258, ""rawSms"": ""Оплата 280р Карта*4215 STOLOVAYA 2 Баланс 223013.85р 13:49"", ""rawJson"": {""action"": ""payment"", ""amount"": 280, ""balance"": 223013.85, ""raw_sms"": ""Оплата 280р Карта*4215 STOLOVAYA 2 Баланс 223013.85р 13:49"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 280р Карта*4215 STOLOVAYA 2 Баланс 223013.85р 13:49"", ""sms_sender"": null, ""received_at"": ""2026-02-10T10:49:57.633Z"", ""sms_contact"": null, ""signed_amount"": -280}, ""smsText"": ""Оплата 280р Карта*4215 STOLOVAYA 2 Баланс 223013.85р 13:49"", ""category"": ""Общепит"", ""currency"": ""RUB"", ""smsSender"": null, ""receivedAt"": ""2026-02-10T10:49:57.633+00:00"", ""counterparty"": ""Столовая"", ""signedAmount"": -280.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-02-10T14:14:00+00:00""}"
"{""id"": 259, ""rawSms"": ""Оплата 1183р Карта*2249 OZON.ru Баланс 221830.85р 15:20"", ""rawJson"": {""action"": ""payment"", ""amount"": 1183, ""balance"": 221830.85, ""raw_sms"": ""Оплата 1183р Карта*2249 OZON.ru Баланс 221830.85р 15:20"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 1183р Карта*2249 OZON.ru Баланс 221830.85р 15:20"", ""sms_sender"": null, ""received_at"": ""2026-02-10T12:20:34.466Z"", ""sms_contact"": null, ""signed_amount"": -1183}, ""smsText"": ""Оплата 1183р Карта*2249 OZON.ru Баланс 221830.85р 15:20"", ""category"": ""Дом"", ""currency"": ""RUB"", ""smsSender"": null, ""receivedAt"": ""2026-02-10T12:20:34.466+00:00"", ""counterparty"": ""OZON"", ""signedAmount"": -1183.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-02-10T16:02:00+00:00""}"
"{""id"": 260, ""rawSms"": ""Счёт карты MIR-3846 16:07 зачисление 15 000р Т-Bank Баланс: 939 988.16р"", ""rawJson"": {""action"": ""income"", ""amount"": 15000, ""balance"": 939988.16, ""raw_sms"": ""Счёт карты MIR-3846 16:07 зачисление 15 000р Т-Bank Баланс: 939 988.16р"", ""currency"": ""RUB"", ""sms_text"": ""Счёт карты MIR-3846 16:07 зачисление 15 000р Т-Bank Баланс: 939 988.16р"", ""sms_sender"": null, ""received_at"": ""2026-02-10T13:07:43.911Z"", ""sms_contact"": null, ""signed_amount"": 15000}, ""smsText"": ""Счёт карты MIR-3846 16:07 зачисление 15 000р Т-Bank Баланс: 939 988.16р"", ""category"": ""Перевод"", ""currency"": ""RUB"", ""smsSender"": null, ""receivedAt"": ""2026-02-10T13:07:43.911+00:00"", ""counterparty"": ""Т-Bank"", ""signedAmount"": 15000.00, ""humanVerified"": false, ""humanVerifiedAt"": null}"
"{""id"": 261, ""rawSms"": ""Оплата 225р Карта*2249 YP*anta-sport.r Баланс 221605.85р 17:15"", ""rawJson"": {""action"": ""payment"", ""amount"": 225, ""balance"": 221605.85, ""raw_sms"": ""Оплата 225р Карта*2249 YP*anta-sport.r Баланс 221605.85р 17:15"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 225р Карта*2249 YP*anta-sport.r Баланс 221605.85р 17:15"", ""sms_sender"": null, ""received_at"": ""2026-02-10T14:15:52.091Z"", ""sms_contact"": null, ""signed_amount"": -225}, ""smsText"": ""Оплата 225р Карта*2249 YP*anta-sport.r Баланс 221605.85р 17:15"", ""category"": ""Спорт"", ""currency"": ""RUB"", ""smsSender"": null, ""receivedAt"": ""2026-02-10T14:15:52.091+00:00"", ""counterparty"": ""Анта-спорт"", ""signedAmount"": -225.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-02-10T17:16:00+00:00""}"
"{""id"": 262, ""rawSms"": ""Оплата 3991.80р Карта*4215 APTEKA 77-1409 Баланс 217614.05р 21:08"", ""rawJson"": {""action"": ""payment"", ""amount"": 3991.8, ""balance"": 217614.05, ""raw_sms"": ""Оплата 3991.80р Карта*4215 APTEKA 77-1409 Баланс 217614.05р 21:08"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 3991.80р Карта*4215 APTEKA 77-1409 Баланс 217614.05р 21:08"", ""sms_sender"": null, ""received_at"": ""2026-02-10T18:08:57.030Z"", ""sms_contact"": null, ""signed_amount"": -3991.8}, ""smsText"": ""Оплата 3991.80р Карта*4215 APTEKA 77-1409 Баланс 217614.05р 21:08"", ""category"": ""Здоровье"", ""currency"": ""RUB"", ""smsSender"": null, ""receivedAt"": ""2026-02-10T18:08:57.03+00:00"", ""counterparty"": ""Аптека"", ""signedAmount"": -3991.80, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-02-10T21:53:00+00:00""}"
"{""id"": 264, ""rawSms"": ""Оплата 4388.94р Карта*4215 RNAZK MC099 Баланс 214525.11р 21:36"", ""rawJson"": {""action"": ""payment"", ""amount"": 4388.94, ""balance"": 214525.11, ""raw_sms"": ""Оплата 4388.94р Карта*4215 RNAZK MC099 Баланс 214525.11р 21:36"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 4388.94р Карта*4215 RNAZK MC099 Баланс 214525.11р 21:36"", ""sms_sender"": null, ""received_at"": ""2026-02-10T18:36:19.870Z"", ""sms_contact"": null, ""signed_amount"": -4388.94}, ""smsText"": ""Оплата 4388.94р Карта*4215 RNAZK MC099 Баланс 214525.11р 21:36"", ""category"": ""Авто"", ""currency"": ""RUB"", ""smsSender"": null, ""receivedAt"": ""2026-02-10T18:36:19.87+00:00"", ""counterparty"": ""Транснефть"", ""signedAmount"": -4388.94, ""humanVerified"": true, ""humanVerifiedAt"": null}"
"{""id"": 265, ""rawSms"": ""Оплата 225р Карта*2249 YP*anta-sport.r Баланс 214300.11р 23:41"", ""rawJson"": {""action"": ""payment"", ""amount"": 225, ""balance"": 214300.11, ""raw_sms"": ""Оплата 225р Карта*2249 YP*anta-sport.r Баланс 214300.11р 23:41"", ""currency"": ""RUB"", ""sms_text"": ""Оплата 225р Карта*2249 YP*anta-sport.r Баланс 214300.11р 23:41"", ""sms_sender"": null, ""received_at"": ""2026-02-10T20:42:00.495Z"", ""sms_contact"": null, ""signed_amount"": -225}, ""smsText"": ""Оплата 225р Карта*2249 YP*anta-sport.r Баланс 214300.11р 23:41"", ""category"": ""Спорт"", ""currency"": ""RUB"", ""smsSender"": null, ""receivedAt"": ""2026-02-10T20:42:00.495+00:00"", ""counterparty"": ""Анта-спорт"", ""signedAmount"": -225.00, ""humanVerified"": true, ""humanVerifiedAt"": ""2026-02-10T23:58:00+00:00""}"

View File

@@ -3,6 +3,8 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#0f172a" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<title>Семейный бюджет</title> <title>Семейный бюджет</title>
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />

37
frontend/nginx.conf Normal file
View File

@@ -0,0 +1,37 @@
server {
listen 80;
root /usr/share/nginx/html;
index index.html;
# Import endpoint — long timeout for LLM processing
location /api/import {
proxy_pass http://family-budget-backend:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cookie_path / /;
proxy_connect_timeout 5s;
proxy_read_timeout 600s;
client_max_body_size 15m;
}
# API — проксируем на backend (сервис из docker-compose)
location /api {
proxy_pass http://family-budget-backend:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cookie_path / /;
proxy_connect_timeout 5s;
proxy_read_timeout 30s;
}
# SPA — все остальные пути отдаём index.html
location / {
try_files $uri $uri/ /index.html;
}
}

View File

@@ -1,6 +1,6 @@
{ {
"name": "@family-budget/frontend", "name": "@family-budget/frontend",
"version": "0.1.0", "version": "0.8.5",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<rect width="32" height="32" rx="8" fill="#0f172a"/>
<text x="16" y="23" font-family="Georgia, serif" font-size="20" font-weight="bold" fill="#3b82f6" text-anchor="middle">&#8381;</text>
</svg>

After

Width:  |  Height:  |  Size: 271 B

View File

@@ -26,8 +26,16 @@ async function request<T>(url: string, options: RequestInit = {}): Promise<T> {
}); });
if (res.status === 401) { if (res.status === 401) {
onUnauthorized?.(); let body: ApiError;
throw new ApiException(401, { error: 'UNAUTHORIZED', message: 'Сессия истекла' }); try {
body = await res.json();
} catch {
body = { error: 'UNAUTHORIZED', message: 'Сессия истекла' };
}
if (!url.includes('/api/auth/login')) {
onUnauthorized?.();
}
throw new ApiException(401, body);
} }
if (!res.ok) { if (!res.ok) {
@@ -45,6 +53,40 @@ async function request<T>(url: string, options: RequestInit = {}): Promise<T> {
return res.json(); return res.json();
} }
async function requestFormData<T>(url: string, formData: FormData): Promise<T> {
const res = await fetch(url, {
method: 'POST',
body: formData,
credentials: 'include',
// Do not set Content-Type — browser sets multipart/form-data with boundary
});
if (res.status === 401) {
let body: ApiError;
try {
body = await res.json();
} catch {
body = { error: 'UNAUTHORIZED', message: 'Сессия истекла' };
}
if (!url.includes('/api/auth/login')) {
onUnauthorized?.();
}
throw new ApiException(401, body);
}
if (!res.ok) {
let body: ApiError;
try {
body = await res.json();
} catch {
body = { error: 'UNKNOWN', message: res.statusText };
}
throw new ApiException(res.status, body);
}
return res.json();
}
export const api = { export const api = {
get: <T>(url: string) => request<T>(url), get: <T>(url: string) => request<T>(url),
@@ -54,9 +96,15 @@ export const api = {
body: body != null ? JSON.stringify(body) : undefined, body: body != null ? JSON.stringify(body) : undefined,
}), }),
postFormData: <T>(url: string, formData: FormData) =>
requestFormData<T>(url, formData),
put: <T>(url: string, body: unknown) => put: <T>(url: string, body: unknown) =>
request<T>(url, { method: 'PUT', body: JSON.stringify(body) }), request<T>(url, { method: 'PUT', body: JSON.stringify(body) }),
patch: <T>(url: string, body: unknown) => patch: <T>(url: string, body: unknown) =>
request<T>(url, { method: 'PATCH', body: JSON.stringify(body) }), request<T>(url, { method: 'PATCH', body: JSON.stringify(body) }),
delete: <T>(url: string) =>
request<T>(url, { method: 'DELETE' }),
}; };

View File

@@ -1,8 +1,26 @@
import type { ImportStatementResponse } from '@family-budget/shared'; import type {
ImportStatementResponse,
Import,
} from '@family-budget/shared';
import { api } from './client'; import { api } from './client';
export async function importStatement( export async function importStatement(
file: unknown, file: File,
): Promise<ImportStatementResponse> { ): Promise<ImportStatementResponse> {
return api.post<ImportStatementResponse>('/api/import/statement', file); const formData = new FormData();
formData.append('file', file);
return api.postFormData<ImportStatementResponse>(
'/api/import/statement',
formData,
);
}
export async function getImports(): Promise<Import[]> {
return api.get<Import[]>('/api/imports');
}
export async function deleteImport(
id: number,
): Promise<{ deleted: number }> {
return api.delete<{ deleted: number }>(`/api/imports/${id}`);
} }

View File

@@ -25,3 +25,7 @@ export async function updateTransaction(
): Promise<Transaction> { ): Promise<Transaction> {
return api.put<Transaction>(`/api/transactions/${id}`, data); return api.put<Transaction>(`/api/transactions/${id}`, data);
} }
export async function clearAllTransactions(): Promise<{ deleted: number }> {
return api.delete<{ deleted: number }>('/api/transactions');
}

View File

@@ -0,0 +1,5 @@
import { api } from './client';
export async function getBackendVersion(): Promise<{ version: string }> {
return api.get<{ version: string }>('/api/version');
}

View File

@@ -7,6 +7,7 @@ import {
} from 'recharts'; } from 'recharts';
import type { ByCategoryItem } from '@family-budget/shared'; import type { ByCategoryItem } from '@family-budget/shared';
import { formatAmount } from '../utils/format'; import { formatAmount } from '../utils/format';
import { useMediaQuery } from '../hooks/useMediaQuery';
interface Props { interface Props {
data: ByCategoryItem[]; data: ByCategoryItem[];
@@ -25,6 +26,9 @@ const rubFormatter = new Intl.NumberFormat('ru-RU', {
}); });
export function CategoryChart({ data }: Props) { export function CategoryChart({ data }: Props) {
const isMobile = useMediaQuery('(max-width: 600px)');
const chartHeight = isMobile ? 250 : 300;
if (data.length === 0) { if (data.length === 0) {
return <div className="chart-empty">Нет данных за период</div>; return <div className="chart-empty">Нет данных за период</div>;
} }
@@ -38,7 +42,7 @@ export function CategoryChart({ data }: Props) {
return ( return (
<div className="category-chart-wrapper"> <div className="category-chart-wrapper">
<ResponsiveContainer width="100%" height={300}> <ResponsiveContainer width="100%" height={chartHeight}>
<PieChart> <PieChart>
<Pie <Pie
data={chartData} data={chartData}

View File

@@ -0,0 +1,90 @@
import { useState } from 'react';
import { clearAllTransactions } from '../api/transactions';
interface Props {
onClose: () => void;
onDone: () => void;
}
export function ClearHistoryModal({ onClose, onDone }: Props) {
const [check1, setCheck1] = useState(false);
const [check2, setCheck2] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const canConfirm = check1 && check2;
const handleConfirm = async () => {
if (!canConfirm || loading) return;
setLoading(true);
setError('');
try {
await clearAllTransactions();
onDone();
} catch (e) {
setError(
e instanceof Error ? e.message : 'Ошибка при очистке истории',
);
} finally {
setLoading(false);
}
};
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h2>Очистить историю операций</h2>
<button className="btn-close" onClick={onClose}>
&times;
</button>
</div>
<div className="modal-body">
<p className="clear-history-warn">
Все транзакции будут безвозвратно удалены. Счета и категории
сохранятся.
</p>
{error && <div className="alert alert-error">{error}</div>}
<div className="form-group form-group-checkbox clear-history-check">
<label>
<input
type="checkbox"
checked={check1}
onChange={(e) => setCheck1(e.target.checked)}
/>
Я хочу очистить историю операций
</label>
</div>
<div className="form-group form-group-checkbox clear-history-check">
<label>
<input
type="checkbox"
checked={check2}
onChange={(e) => setCheck2(e.target.checked)}
/>
Я понимаю, что действие необратимо и все данные об операциях
будут потеряны навсегда
</label>
</div>
</div>
<div className="modal-footer">
<button
className="btn btn-danger"
onClick={handleConfirm}
disabled={!canConfirm || loading}
>
{loading ? 'Удаление…' : 'Удалить всё'}
</button>
<button className="btn btn-secondary" onClick={onClose}>
Отмена
</button>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,119 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { ClearHistoryModal } from './ClearHistoryModal';
import { DeleteImportModal } from './DeleteImportModal';
import { getImports } from '../api/import';
import type { Import } from '@family-budget/shared';
function formatDate(iso: string): string {
const d = new Date(iso);
return d.toLocaleString('ru-RU', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
}
export function DataSection() {
const [showClearModal, setShowClearModal] = useState(false);
const [imports, setImports] = useState<Import[]>([]);
const [impToDelete, setImpToDelete] = useState<Import | null>(null);
const navigate = useNavigate();
useEffect(() => {
getImports().then(setImports).catch(() => {});
}, []);
const handleImportDeleted = () => {
setImpToDelete(null);
getImports().then(setImports).catch(() => {});
navigate('/history');
};
return (
<div className="data-section">
<div className="section-block">
<h3>История импортов</h3>
<p className="section-desc">
Список импортов выписок. Можно удалить операции конкретного импорта.
</p>
{imports.length === 0 ? (
<p className="muted">Импортов пока нет.</p>
) : (
<div className="table-responsive">
<table className="table">
<thead>
<tr>
<th>Дата</th>
<th>Счёт</th>
<th>Банк</th>
<th>Импортировано</th>
<th>Дубликаты</th>
<th></th>
</tr>
</thead>
<tbody>
{imports.map((imp) => (
<tr key={imp.id}>
<td>{formatDate(imp.importedAt)}</td>
<td>
{imp.accountAlias || imp.accountNumberMasked || '—'}
</td>
<td>{imp.bank}</td>
<td>{imp.importedCount}</td>
<td>{imp.duplicatesSkipped}</td>
<td>
<button
type="button"
className="btn btn-sm btn-danger"
onClick={() => setImpToDelete(imp)}
disabled={imp.importedCount === 0}
>
Удалить
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
<div className="section-block">
<h3>Очистка данных</h3>
<p className="section-desc">
Очистить историю операций (все транзакции). Счета, категории и
правила сохранятся.
</p>
<button
type="button"
className="btn btn-danger"
onClick={() => setShowClearModal(true)}
>
Очистить историю
</button>
</div>
{showClearModal && (
<ClearHistoryModal
onClose={() => setShowClearModal(false)}
onDone={() => {
setShowClearModal(false);
navigate('/history');
}}
/>
)}
{impToDelete && (
<DeleteImportModal
imp={impToDelete}
onClose={() => setImpToDelete(null)}
onDone={handleImportDeleted}
/>
)}
</div>
);
}

View File

@@ -0,0 +1,70 @@
import { useState } from 'react';
import { deleteImport } from '../api/import';
import type { Import } from '@family-budget/shared';
interface Props {
imp: Import;
onClose: () => void;
onDone: () => void;
}
export function DeleteImportModal({ imp, onClose, onDone }: Props) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const accountLabel =
imp.accountAlias || imp.accountNumberMasked || `ID ${imp.accountId}`;
const handleConfirm = async () => {
setLoading(true);
setError('');
try {
await deleteImport(imp.id);
onDone();
} catch (e) {
setError(
e instanceof Error ? e.message : 'Ошибка при удалении импорта',
);
} finally {
setLoading(false);
}
};
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h2>Удалить импорт</h2>
<button className="btn-close" onClick={onClose}>
&times;
</button>
</div>
<div className="modal-body">
<p className="clear-history-warn">
Будут удалены все операции этого импорта ({imp.importedCount}{' '}
шт.): {imp.bank} / {accountLabel}
</p>
{error && <div className="alert alert-error">{error}</div>}
<p>Действие необратимо.</p>
</div>
<div className="modal-footer">
<button
type="button"
className="btn btn-danger"
onClick={handleConfirm}
disabled={loading}
>
{loading ? 'Удаление…' : 'Удалить'}
</button>
<button type="button" className="btn btn-secondary" onClick={onClose}>
Отмена
</button>
</div>
</div>
</div>
);
}

View File

@@ -22,23 +22,27 @@ export function ImportModal({ onClose, onDone }: Props) {
const file = e.target.files?.[0]; const file = e.target.files?.[0];
if (!file) return; if (!file) return;
const name = file.name.toLowerCase();
const type = file.type;
const isPdf = type === 'application/pdf' || name.endsWith('.pdf');
const isJson = type === 'application/json' || name.endsWith('.json');
if (!isPdf && !isJson) {
setError('Допустимы только файлы PDF или JSON');
return;
}
setLoading(true); setLoading(true);
setError(''); setError('');
setResult(null); setResult(null);
try { try {
const text = await file.text(); const resp = await importStatement(file);
const json = JSON.parse(text);
const resp = await importStatement(json);
setResult(resp); setResult(resp);
} catch (err: unknown) { } catch (err: unknown) {
if (err instanceof SyntaxError) { const msg =
setError('Некорректный JSON-файл'); err instanceof Error ? err.message : 'Ошибка импорта';
} else { setError(msg);
const msg =
err instanceof Error ? err.message : 'Ошибка импорта';
setError(msg);
}
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -69,11 +73,11 @@ export function ImportModal({ onClose, onDone }: Props) {
{!result && ( {!result && (
<div className="import-upload"> <div className="import-upload">
<p>Выберите JSON-файл выписки (формат 1.0)</p> <p>Выберите файл выписки (PDF или JSON, формат 1.0)</p>
<input <input
ref={fileRef} ref={fileRef}
type="file" type="file"
accept=".json" accept=".pdf,.json,application/pdf,application/json"
onChange={handleFileChange} onChange={handleFileChange}
className="file-input" className="file-input"
/> />
@@ -85,7 +89,7 @@ export function ImportModal({ onClose, onDone }: Props) {
{result && ( {result && (
<div className="import-result"> <div className="import-result">
<div className="import-result-icon">&check;</div> <div className="import-result-icon" aria-hidden="true"></div>
<h3>Импорт завершён</h3> <h3>Импорт завершён</h3>
<table className="import-stats"> <table className="import-stats">
<tbody> <tbody>

View File

@@ -1,13 +1,45 @@
import type { ReactNode } from 'react'; import { useState, useEffect, type ReactNode } from 'react';
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router-dom';
import { useAuth } from '../context/AuthContext'; import { useAuth } from '../context/AuthContext';
import { getBackendVersion } from '../api/version';
export function Layout({ children }: { children: ReactNode }) { export function Layout({ children }: { children: ReactNode }) {
const { user, logout } = useAuth(); const { user, logout } = useAuth();
const [drawerOpen, setDrawerOpen] = useState(false);
const [beVersion, setBeVersion] = useState<string | null>(null);
const closeDrawer = () => setDrawerOpen(false);
useEffect(() => {
getBackendVersion()
.then((r) => setBeVersion(r.version))
.catch(() => setBeVersion(null));
}, []);
return ( return (
<div className="layout"> <div className="layout">
<aside className="sidebar"> <button
type="button"
className="burger-btn"
aria-label="Открыть меню"
onClick={() => setDrawerOpen(true)}
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="3" y1="6" x2="21" y2="6" />
<line x1="3" y1="12" x2="21" y2="12" />
<line x1="3" y1="18" x2="21" y2="18" />
</svg>
</button>
{drawerOpen && (
<div
className="sidebar-overlay"
aria-hidden="true"
onClick={closeDrawer}
/>
)}
<aside className={`sidebar ${drawerOpen ? 'sidebar-open' : ''}`}>
<div className="sidebar-brand"> <div className="sidebar-brand">
<span className="sidebar-brand-icon"></span> <span className="sidebar-brand-icon"></span>
<span className="sidebar-brand-text">Семейный бюджет</span> <span className="sidebar-brand-text">Семейный бюджет</span>
@@ -19,6 +51,7 @@ export function Layout({ children }: { children: ReactNode }) {
className={({ isActive }) => className={({ isActive }) =>
`nav-link${isActive ? ' active' : ''}` `nav-link${isActive ? ' active' : ''}`
} }
onClick={closeDrawer}
> >
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" /> <path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" />
@@ -35,6 +68,7 @@ export function Layout({ children }: { children: ReactNode }) {
className={({ isActive }) => className={({ isActive }) =>
`nav-link${isActive ? ' active' : ''}` `nav-link${isActive ? ' active' : ''}`
} }
onClick={closeDrawer}
> >
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="18" y1="20" x2="18" y2="10" /> <line x1="18" y1="20" x2="18" y2="10" />
@@ -49,6 +83,7 @@ export function Layout({ children }: { children: ReactNode }) {
className={({ isActive }) => className={({ isActive }) =>
`nav-link${isActive ? ' active' : ''}` `nav-link${isActive ? ' active' : ''}`
} }
onClick={closeDrawer}
> >
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="3" /> <circle cx="12" cy="12" r="3" />
@@ -59,10 +94,18 @@ export function Layout({ children }: { children: ReactNode }) {
</nav> </nav>
<div className="sidebar-footer"> <div className="sidebar-footer">
<span className="sidebar-user">{user?.login}</span> <div className="sidebar-footer-top">
<button className="btn-logout" onClick={() => logout()}> <span className="sidebar-user">{user?.login}</span>
Выход <button className="btn-logout" onClick={() => logout()}>
</button> Выход
</button>
</div>
<div className="sidebar-footer-bottom">
<span className="sidebar-version">
FE {__FE_VERSION__} · BE {beVersion ?? '…'}
</span>
<span className="sidebar-copyright">© 2025 Семейный бюджет</span>
</div>
</div> </div>
</aside> </aside>

View File

@@ -9,6 +9,7 @@ import {
ResponsiveContainer, ResponsiveContainer,
} from 'recharts'; } from 'recharts';
import type { TimeseriesItem } from '@family-budget/shared'; import type { TimeseriesItem } from '@family-budget/shared';
import { useMediaQuery } from '../hooks/useMediaQuery';
interface Props { interface Props {
data: TimeseriesItem[]; data: TimeseriesItem[];
@@ -21,6 +22,9 @@ const rubFormatter = new Intl.NumberFormat('ru-RU', {
}); });
export function TimeseriesChart({ data }: Props) { export function TimeseriesChart({ data }: Props) {
const isMobile = useMediaQuery('(max-width: 600px)');
const chartHeight = isMobile ? 250 : 300;
if (data.length === 0) { if (data.length === 0) {
return <div className="chart-empty">Нет данных за период</div>; return <div className="chart-empty">Нет данных за период</div>;
} }
@@ -32,7 +36,7 @@ export function TimeseriesChart({ data }: Props) {
})); }));
return ( return (
<ResponsiveContainer width="100%" height={300}> <ResponsiveContainer width="100%" height={chartHeight}>
<BarChart data={chartData}> <BarChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" stroke="#e2e8f0" /> <CartesianGrid strokeDasharray="3 3" stroke="#e2e8f0" />
<XAxis <XAxis

View File

@@ -6,7 +6,10 @@ import type {
} from '@family-budget/shared'; } from '@family-budget/shared';
import { toISODate } from '../utils/format'; import { toISODate } from '../utils/format';
export type PeriodMode = 'week' | 'month' | 'year' | 'custom';
export interface FiltersState { export interface FiltersState {
periodMode: PeriodMode;
from: string; from: string;
to: string; to: string;
accountId: string; accountId: string;
@@ -58,7 +61,57 @@ export function TransactionFilters({
from = new Date(now.getFullYear(), 0, 1); from = new Date(now.getFullYear(), 0, 1);
break; break;
} }
onChange({ ...filters, from: toISODate(from), to: toISODate(now) }); onChange({
...filters,
periodMode: preset,
from: toISODate(from),
to: toISODate(now),
});
};
const navigate = (direction: -1 | 1) => {
const fromDate = new Date(filters.from);
let newFrom: Date;
let newTo: Date;
switch (filters.periodMode) {
case 'week':
newFrom = new Date(fromDate);
newFrom.setDate(fromDate.getDate() + 7 * direction);
newTo = new Date(newFrom);
newTo.setDate(newFrom.getDate() + 6);
break;
case 'month':
newFrom = new Date(
fromDate.getFullYear(),
fromDate.getMonth() + direction,
1,
);
newTo = new Date(
newFrom.getFullYear(),
newFrom.getMonth() + 1,
0,
);
break;
case 'year':
newFrom = new Date(fromDate.getFullYear() + direction, 0, 1);
newTo = new Date(newFrom.getFullYear(), 11, 31);
break;
default:
return;
}
onChange({
...filters,
from: toISODate(newFrom),
to: toISODate(newTo),
});
};
const handleDateChange = (field: 'from' | 'to', value: string) => {
onChange({
...filters,
periodMode: 'custom',
[field]: value,
});
}; };
return ( return (
@@ -66,34 +119,56 @@ export function TransactionFilters({
<div className="filters-row"> <div className="filters-row">
<div className="filter-group"> <div className="filter-group">
<label>Период</label> <label>Период</label>
<div className="filter-dates"> <div className="filter-dates-wrap">
<input {filters.periodMode !== 'custom' && (
type="date" <button
value={filters.from} type="button"
onChange={(e) => set('from', e.target.value)} className="btn-page"
/> onClick={() => navigate(-1)}
<span className="filter-separator">&mdash;</span> title="Предыдущий период"
<input >
type="date" &larr;
value={filters.to} </button>
onChange={(e) => set('to', e.target.value)} )}
/> <div className="filter-dates">
<input
type="date"
value={filters.from}
onChange={(e) => handleDateChange('from', e.target.value)}
/>
<span className="filter-separator">&mdash;</span>
<input
type="date"
value={filters.to}
onChange={(e) => handleDateChange('to', e.target.value)}
/>
</div>
{filters.periodMode !== 'custom' && (
<button
type="button"
className="btn-page"
onClick={() => navigate(1)}
title="Следующий период"
>
&rarr;
</button>
)}
</div> </div>
<div className="filter-presets"> <div className="filter-presets">
<button <button
className="btn-preset" className={`btn-preset ${filters.periodMode === 'week' ? 'active' : ''}`}
onClick={() => applyPreset('week')} onClick={() => applyPreset('week')}
> >
Неделя Неделя
</button> </button>
<button <button
className="btn-preset" className={`btn-preset ${filters.periodMode === 'month' ? 'active' : ''}`}
onClick={() => applyPreset('month')} onClick={() => applyPreset('month')}
> >
Месяц Месяц
</button> </button>
<button <button
className="btn-preset" className={`btn-preset ${filters.periodMode === 'year' ? 'active' : ''}`}
onClick={() => applyPreset('year')} onClick={() => applyPreset('year')}
> >
Год Год

View File

@@ -7,18 +7,75 @@ interface Props {
onEdit: (tx: Transaction) => void; onEdit: (tx: Transaction) => void;
} }
const DIRECTION_LABELS: Record<string, string> = {
income: 'Приход',
expense: 'Расход',
transfer: 'Перевод',
};
const DIRECTION_CLASSES: Record<string, string> = { const DIRECTION_CLASSES: Record<string, string> = {
income: 'amount-income', income: 'amount-income',
expense: 'amount-expense', expense: 'amount-expense',
transfer: 'amount-transfer', transfer: 'amount-transfer',
}; };
function TransactionCard({
tx,
onEdit,
}: {
tx: Transaction;
onEdit: (tx: Transaction) => void;
}) {
const directionClass = DIRECTION_CLASSES[tx.direction] ?? '';
const isUnconfirmed =
!tx.isCategoryConfirmed && tx.categoryId != null;
return (
<div
className={`transaction-card ${isUnconfirmed ? 'row-unconfirmed' : ''}`}
>
<div className="transaction-card-header">
<span className="transaction-card-date">
{formatDateTime(tx.operationAt)}
</span>
<span className={`transaction-card-amount ${directionClass}`}>
{formatAmount(tx.amountSigned)}
</span>
</div>
<div className="transaction-card-body">
<span className="description-text">{tx.description}</span>
{tx.comment && (
<span className="comment-badge" title={tx.comment}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z" />
</svg>
</span>
)}
</div>
<div className="transaction-card-footer">
<span className="transaction-card-meta">
{tx.accountAlias || '—'} · {tx.categoryName || '—'}
</span>
<div className="transaction-card-actions">
{tx.categoryId != null && !tx.isCategoryConfirmed && (
<span
className="badge badge-warning"
title="Категория не подтверждена"
>
?
</span>
)}
<button
type="button"
className="btn-icon btn-icon-touch"
onClick={() => onEdit(tx)}
title="Редактировать"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7" />
<path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z" />
</svg>
</button>
</div>
</div>
</div>
);
}
export function TransactionTable({ transactions, loading, onEdit }: Props) { export function TransactionTable({ transactions, loading, onEdit }: Props) {
if (loading) { if (loading) {
return <div className="table-loading">Загрузка операций...</div>; return <div className="table-loading">Загрузка операций...</div>;
@@ -29,79 +86,87 @@ export function TransactionTable({ transactions, loading, onEdit }: Props) {
} }
return ( return (
<div className="table-wrapper"> <>
<table className="data-table"> <div className="table-wrapper table-desktop">
<thead> <table className="data-table">
<tr> <thead>
<th>Дата</th> <tr>
<th>Счёт</th> <th>Дата</th>
<th>Сумма</th> <th>Счёт</th>
<th>Описание</th> <th>Сумма</th>
<th>Категория</th> <th>Описание</th>
<th className="th-center">Статус</th> <th>Категория</th>
<th></th> <th className="th-center">Статус</th>
</tr> <th></th>
</thead>
<tbody>
{transactions.map((tx) => (
<tr
key={tx.id}
className={
!tx.isCategoryConfirmed && tx.categoryId
? 'row-unconfirmed'
: ''
}
>
<td className="td-nowrap">
{formatDateTime(tx.operationAt)}
</td>
<td className="td-nowrap">{tx.accountAlias || '—'}</td>
<td
className={`td-nowrap td-amount ${DIRECTION_CLASSES[tx.direction] ?? ''}`}
>
{formatAmount(tx.amountSigned)}
</td>
<td className="td-description">
<span className="description-text">{tx.description}</span>
{tx.comment && (
<span className="comment-badge" title={tx.comment}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z" />
</svg>
</span>
)}
</td>
<td className="td-nowrap">
{tx.categoryName || (
<span className="text-muted"></span>
)}
</td>
<td className="td-center">
{tx.categoryId != null && !tx.isCategoryConfirmed && (
<span
className="badge badge-warning"
title="Категория не подтверждена"
>
?
</span>
)}
</td>
<td>
<button
className="btn-icon"
onClick={() => onEdit(tx)}
title="Редактировать"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7" />
<path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z" />
</svg>
</button>
</td>
</tr> </tr>
))} </thead>
</tbody> <tbody>
</table> {transactions.map((tx) => (
</div> <tr
key={tx.id}
className={
!tx.isCategoryConfirmed && tx.categoryId
? 'row-unconfirmed'
: ''
}
>
<td className="td-nowrap">
{formatDateTime(tx.operationAt)}
</td>
<td className="td-nowrap">{tx.accountAlias || '—'}</td>
<td
className={`td-nowrap td-amount ${DIRECTION_CLASSES[tx.direction] ?? ''}`}
>
{formatAmount(tx.amountSigned)}
</td>
<td className="td-description">
<span className="description-text">{tx.description}</span>
{tx.comment && (
<span className="comment-badge" title={tx.comment}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z" />
</svg>
</span>
)}
</td>
<td className="td-nowrap">
{tx.categoryName || (
<span className="text-muted"></span>
)}
</td>
<td className="td-center">
{tx.categoryId != null && !tx.isCategoryConfirmed && (
<span
className="badge badge-warning"
title="Категория не подтверждена"
>
?
</span>
)}
</td>
<td>
<button
className="btn-icon"
onClick={() => onEdit(tx)}
title="Редактировать"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7" />
<path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z" />
</svg>
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="transaction-cards transaction-mobile">
{transactions.map((tx) => (
<TransactionCard key={tx.id} tx={tx} onEdit={onEdit} />
))}
</div>
</>
); );
} }

View File

@@ -43,8 +43,11 @@ export function AuthProvider({ children }: { children: ReactNode }) {
const me = await getMe(); const me = await getMe();
setUser({ login: me.login }); setUser({ login: me.login });
} catch (e: unknown) { } catch (e: unknown) {
const msg = const msg = e instanceof Error && e.message === 'Failed to fetch'
e instanceof Error ? e.message : 'Ошибка входа'; ? 'Сервер недоступен. Проверьте, что backend запущен и NPM проксирует /api на порт 3000.'
: e instanceof Error
? e.message
: 'Ошибка входа';
setError(msg); setError(msg);
throw e; throw e;
} }

View File

@@ -0,0 +1,18 @@
import { useState, useEffect } from 'react';
export function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(() => {
if (typeof window === 'undefined') return false;
return window.matchMedia(query).matches;
});
useEffect(() => {
const mql = window.matchMedia(query);
const handler = (e: MediaQueryListEvent) => setMatches(e.matches);
setMatches(mql.matches);
mql.addEventListener('change', handler);
return () => mql.removeEventListener('change', handler);
}, [query]);
return matches;
}

View File

@@ -1,4 +1,5 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { useSearchParams } from 'react-router-dom';
import type { import type {
Transaction, Transaction,
Account, Account,
@@ -19,10 +20,26 @@ import { EditTransactionModal } from '../components/EditTransactionModal';
import { ImportModal } from '../components/ImportModal'; import { ImportModal } from '../components/ImportModal';
import { toISODate } from '../utils/format'; import { toISODate } from '../utils/format';
const PARAM_KEYS = [
'from',
'to',
'accountId',
'direction',
'categoryId',
'search',
'amountMin',
'amountMax',
'onlyUnconfirmed',
'sortBy',
'sortOrder',
'periodMode',
] as const;
function getDefaultFilters(): FiltersState { function getDefaultFilters(): FiltersState {
const now = new Date(); const now = new Date();
const from = new Date(now.getFullYear(), now.getMonth(), 1); const from = new Date(now.getFullYear(), now.getMonth(), 1);
return { return {
periodMode: 'month',
from: toISODate(from), from: toISODate(from),
to: toISODate(now), to: toISODate(now),
accountId: '', accountId: '',
@@ -37,10 +54,70 @@ function getDefaultFilters(): FiltersState {
}; };
} }
function filtersFromParams(params: URLSearchParams): Partial<FiltersState> {
const out: Partial<FiltersState> = {};
for (const key of PARAM_KEYS) {
const v = params.get(key);
if (v !== null) {
if (key === 'onlyUnconfirmed') {
out[key] = v === '1' || v === 'true';
} else {
(out as Record<string, unknown>)[key] = v;
}
}
}
return out;
}
function paramsFromUrl(params: URLSearchParams): {
page: number;
pageSize: number;
} {
const p = params.get('page');
const ps = params.get('pageSize');
return {
page: p ? Math.max(1, parseInt(p, 10) || 1) : 1,
pageSize: ps ? parseInt(ps, 10) || 50 : 50,
};
}
function filtersToParams(
f: FiltersState,
pageNum: number,
pageSizeNum: number,
): URLSearchParams {
const p = new URLSearchParams();
if (f.from) p.set('from', f.from);
if (f.to) p.set('to', f.to);
if (f.accountId) p.set('accountId', f.accountId);
if (f.direction) p.set('direction', f.direction);
if (f.categoryId) p.set('categoryId', f.categoryId);
if (f.search) p.set('search', f.search);
if (f.amountMin) p.set('amountMin', f.amountMin);
if (f.amountMax) p.set('amountMax', f.amountMax);
if (f.onlyUnconfirmed) p.set('onlyUnconfirmed', '1');
if (f.sortBy) p.set('sortBy', f.sortBy);
if (f.sortOrder) p.set('sortOrder', f.sortOrder);
if (f.periodMode) p.set('periodMode', f.periodMode);
if (pageNum > 1) p.set('page', String(pageNum));
if (pageSizeNum !== 50) p.set('pageSize', String(pageSizeNum));
return p;
}
export function HistoryPage() { export function HistoryPage() {
const [filters, setFilters] = useState<FiltersState>(getDefaultFilters); const [searchParams, setSearchParams] = useSearchParams();
const [page, setPage] = useState(1); const urlFilters = filtersFromParams(searchParams);
const [pageSize, setPageSize] = useState(50); const { page: urlPage, pageSize: urlPageSize } = paramsFromUrl(searchParams);
const defaultFilters = getDefaultFilters();
const [filters, setFilters] = useState<FiltersState>(() => ({
...defaultFilters,
...urlFilters,
periodMode:
(urlFilters.periodMode as FiltersState['periodMode']) ??
defaultFilters.periodMode,
}));
const [page, setPage] = useState(urlPage);
const [pageSize, setPageSize] = useState(urlPageSize);
const [data, setData] = useState<PaginatedResponse<Transaction> | null>( const [data, setData] = useState<PaginatedResponse<Transaction> | null>(
null, null,
); );
@@ -55,6 +132,22 @@ export function HistoryPage() {
getCategories().then(setCategories).catch(() => {}); getCategories().then(setCategories).catch(() => {});
}, []); }, []);
useEffect(() => {
const urlFilters = filtersFromParams(searchParams);
const { page: p, pageSize: ps } = paramsFromUrl(searchParams);
if (Object.keys(urlFilters).length > 0) {
setFilters((prev) => ({
...getDefaultFilters(),
...urlFilters,
periodMode:
(urlFilters.periodMode as FiltersState['periodMode']) ??
prev.periodMode,
}));
}
setPage(p);
setPageSize(ps);
}, [searchParams]);
const fetchData = useCallback(async () => { const fetchData = useCallback(async () => {
setLoading(true); setLoading(true);
try { try {
@@ -92,8 +185,12 @@ export function HistoryPage() {
const handleFiltersChange = (newFilters: FiltersState) => { const handleFiltersChange = (newFilters: FiltersState) => {
setFilters(newFilters); setFilters(newFilters);
setPage(1); setPage(1);
setSearchParams(filtersToParams(newFilters, 1, pageSize), {
replace: true,
});
}; };
const handleEditSave = () => { const handleEditSave = () => {
setEditingTx(null); setEditingTx(null);
fetchData(); fetchData();
@@ -136,10 +233,20 @@ export function HistoryPage() {
pageSize={data.pageSize} pageSize={data.pageSize}
totalItems={data.totalItems} totalItems={data.totalItems}
totalPages={data.totalPages} totalPages={data.totalPages}
onPageChange={setPage} onPageChange={(p) => {
setPage(p);
setSearchParams(
filtersToParams(filters, p, pageSize),
{ replace: true },
);
}}
onPageSizeChange={(size) => { onPageSizeChange={(size) => {
setPageSize(size); setPageSize(size);
setPage(1); setPage(1);
setSearchParams(
filtersToParams(filters, 1, size),
{ replace: true },
);
}} }}
/> />
)} )}

View File

@@ -2,8 +2,9 @@ import { useState } from 'react';
import { AccountsList } from '../components/AccountsList'; import { AccountsList } from '../components/AccountsList';
import { CategoriesList } from '../components/CategoriesList'; import { CategoriesList } from '../components/CategoriesList';
import { RulesList } from '../components/RulesList'; import { RulesList } from '../components/RulesList';
import { DataSection } from '../components/DataSection';
type Tab = 'accounts' | 'categories' | 'rules'; type Tab = 'accounts' | 'categories' | 'rules' | 'data';
export function SettingsPage() { export function SettingsPage() {
const [tab, setTab] = useState<Tab>('accounts'); const [tab, setTab] = useState<Tab>('accounts');
@@ -33,12 +34,19 @@ export function SettingsPage() {
> >
Правила Правила
</button> </button>
<button
className={`tab ${tab === 'data' ? 'active' : ''}`}
onClick={() => setTab('data')}
>
Данные
</button>
</div> </div>
<div className="tab-content"> <div className="tab-content">
{tab === 'accounts' && <AccountsList />} {tab === 'accounts' && <AccountsList />}
{tab === 'categories' && <CategoriesList />} {tab === 'categories' && <CategoriesList />}
{tab === 'rules' && <RulesList />} {tab === 'rules' && <RulesList />}
{tab === 'data' && <DataSection />}
</div> </div>
</div> </div>
); );

View File

@@ -138,11 +138,34 @@ body {
.sidebar-footer { .sidebar-footer {
padding: 16px 20px; padding: 16px 20px;
border-top: 1px solid rgba(255, 255, 255, 0.1); border-top: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
flex-direction: column;
gap: 10px;
}
.sidebar-footer-top {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
} }
.sidebar-footer-bottom {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 11px;
color: var(--color-sidebar-text);
opacity: 0.85;
}
.sidebar-version {
font-family: ui-monospace, monospace;
}
.sidebar-copyright {
font-size: 10px;
}
.sidebar-user { .sidebar-user {
font-size: 13px; font-size: 13px;
color: var(--color-sidebar-text); color: var(--color-sidebar-text);
@@ -249,6 +272,7 @@ body {
.page { .page {
max-width: 1200px; max-width: 1200px;
margin: 0 auto;
} }
.page-header { .page-header {
@@ -390,6 +414,15 @@ textarea {
border-color: var(--color-border-hover); border-color: var(--color-border-hover);
} }
.btn-danger {
background: var(--color-danger);
color: #fff;
}
.btn-danger:hover:not(:disabled) {
background: #dc2626;
}
.btn-block { .btn-block {
width: 100%; width: 100%;
} }
@@ -497,6 +530,16 @@ textarea {
white-space: nowrap; white-space: nowrap;
} }
.filter-dates-wrap {
display: flex;
align-items: center;
gap: 6px;
}
.filter-dates-wrap .btn-page {
flex-shrink: 0;
}
.filter-dates { .filter-dates {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -579,6 +622,75 @@ textarea {
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm); box-shadow: var(--shadow-sm);
overflow-x: auto; overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
/* Transaction cards (mobile view) */
.transaction-cards {
display: none;
flex-direction: column;
gap: 12px;
}
.transaction-card {
background: var(--color-surface);
border-radius: var(--radius-lg);
padding: 14px 16px;
box-shadow: var(--shadow-sm);
border-left: 4px solid var(--color-border);
}
.transaction-card.row-unconfirmed {
border-left-color: var(--color-warning);
}
.transaction-card-header {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 6px;
}
.transaction-card-date {
font-size: 12px;
color: var(--color-text-secondary);
}
.transaction-card-amount {
font-variant-numeric: tabular-nums;
font-weight: 600;
font-size: 16px;
}
.transaction-card-body {
font-size: 14px;
word-break: break-word;
margin-bottom: 8px;
}
.transaction-card-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
flex-wrap: wrap;
}
.transaction-card-meta {
font-size: 12px;
color: var(--color-text-muted);
}
.transaction-card-actions {
display: flex;
align-items: center;
gap: 6px;
}
.btn-icon-touch {
min-width: 44px;
min-height: 44px;
padding: 10px;
} }
.data-table { .data-table {
@@ -739,8 +851,9 @@ textarea {
.pagination-size { .pagination-size {
width: auto; width: auto;
padding: 4px 8px; padding: 8px 12px;
font-size: 13px; font-size: 13px;
min-height: 44px;
} }
.btn-page { .btn-page {
@@ -752,6 +865,11 @@ textarea {
font-size: 14px; font-size: 14px;
transition: all var(--transition); transition: all var(--transition);
font-family: inherit; font-family: inherit;
min-width: 44px;
min-height: 44px;
display: inline-flex;
align-items: center;
justify-content: center;
} }
.btn-page:hover:not(:disabled) { .btn-page:hover:not(:disabled) {
@@ -818,6 +936,12 @@ textarea {
color: var(--color-text-secondary); color: var(--color-text-secondary);
padding: 0; padding: 0;
line-height: 1; line-height: 1;
min-width: 44px;
min-height: 44px;
display: flex;
align-items: center;
justify-content: center;
margin: -8px -8px -8px 0;
} }
.btn-close:hover { .btn-close:hover {
@@ -967,6 +1091,13 @@ textarea {
gap: 0; gap: 0;
border-bottom: 2px solid var(--color-border); border-bottom: 2px solid var(--color-border);
margin-bottom: 0; margin-bottom: 0;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
}
.tabs::-webkit-scrollbar {
display: none;
} }
.tab { .tab {
@@ -981,6 +1112,8 @@ textarea {
cursor: pointer; cursor: pointer;
transition: all var(--transition); transition: all var(--transition);
font-family: inherit; font-family: inherit;
flex-shrink: 0;
min-height: 44px;
} }
.tab:hover { .tab:hover {
@@ -1003,6 +1136,46 @@ textarea {
padding: 0; padding: 0;
} }
.data-section {
padding: 20px 24px;
}
.section-block {
margin-bottom: 24px;
}
.section-block:last-child {
margin-bottom: 0;
}
.section-block h3 {
font-size: 16px;
font-weight: 600;
margin-bottom: 8px;
}
.section-desc {
color: var(--color-text-secondary);
font-size: 14px;
margin-bottom: 12px;
max-width: 480px;
}
.clear-history-warn {
color: var(--color-danger);
font-weight: 500;
margin-bottom: 16px;
}
.clear-history-check {
margin-bottom: 12px;
}
.input-error {
border-color: var(--color-danger) !important;
box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.15);
}
/* ================================================================ /* ================================================================
Toggle button Toggle button
================================================================ */ ================================================================ */
@@ -1252,6 +1425,61 @@ textarea {
vertical-align: middle; vertical-align: middle;
} }
/* ================================================================
Burger button (mobile)
================================================================ */
.burger-btn {
display: none;
position: fixed;
top: 12px;
left: 12px;
z-index: 101;
width: 44px;
height: 44px;
padding: 0;
background: var(--color-sidebar);
color: #fff;
border: none;
border-radius: var(--radius);
cursor: pointer;
align-items: center;
justify-content: center;
box-shadow: var(--shadow);
transition: all var(--transition);
}
.burger-btn:hover {
background: var(--color-sidebar-hover);
}
.sidebar-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 99;
animation: overlay-fade-in 0.2s ease;
}
@keyframes overlay-fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes drawer-slide-in {
from {
transform: translateX(-100%);
}
to {
transform: translateX(0);
}
}
/* ================================================================ /* ================================================================
Responsive Responsive
================================================================ */ ================================================================ */
@@ -1263,13 +1491,29 @@ textarea {
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.burger-btn {
display: flex;
}
.sidebar-overlay {
display: block;
}
.sidebar { .sidebar {
width: 200px; width: min(280px, 85vw);
transform: translateX(-100%);
transition: transform 0.25s ease;
box-shadow: var(--shadow-lg);
}
.sidebar.sidebar-open {
transform: translateX(0);
animation: drawer-slide-in 0.25s ease;
} }
.main-content { .main-content {
margin-left: 200px; margin-left: 0;
padding: 16px; padding: 16px 16px 16px 60px;
} }
.filters-row { .filters-row {
@@ -1283,4 +1527,114 @@ textarea {
.summary-cards { .summary-cards {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.page-header {
flex-direction: column;
align-items: stretch;
gap: 12px;
}
.page-header .btn {
min-height: 44px;
}
.filter-dates-wrap .btn-page {
min-width: 44px;
min-height: 44px;
padding: 8px;
}
.filter-presets {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
flex-wrap: nowrap;
padding-bottom: 4px;
}
.btn-preset {
flex-shrink: 0;
}
.filter-sort .btn-sort-order {
min-width: 44px;
min-height: 44px;
}
}
@media (max-width: 600px) {
.table-desktop {
display: none !important;
}
.transaction-mobile {
display: flex !important;
}
}
@media (max-width: 480px) {
.modal-overlay {
padding: 0;
align-items: stretch;
}
.modal {
max-width: none;
max-height: none;
height: 100%;
min-height: 100dvh;
border-radius: 0;
display: flex;
flex-direction: column;
}
.modal-header {
flex-shrink: 0;
}
.modal-body {
overflow-y: auto;
flex: 1;
min-height: 0;
-webkit-overflow-scrolling: touch;
}
.modal-footer {
flex-shrink: 0;
}
.modal-footer .btn {
min-height: 44px;
min-width: 44px;
padding: 12px 20px;
}
.page-header h1 {
font-size: 20px;
}
.pagination {
flex-direction: column;
align-items: stretch;
gap: 12px;
}
.pagination-controls {
justify-content: center;
}
}
/* Safe area for devices with notches */
@supports (padding: env(safe-area-inset-bottom)) {
.burger-btn {
top: calc(12px + env(safe-area-inset-top));
left: calc(12px + env(safe-area-inset-left));
}
.main-content {
padding-bottom: calc(16px + env(safe-area-inset-bottom));
}
.modal-footer {
padding-bottom: calc(20px + env(safe-area-inset-bottom));
}
} }

View File

@@ -1 +1,3 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
declare const __FE_VERSION__: string;

View File

@@ -1,8 +1,19 @@
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react'; import react from '@vitejs/plugin-react';
import { readFileSync } from 'fs';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const pkg = JSON.parse(
readFileSync(join(__dirname, 'package.json'), 'utf-8'),
) as { version: string };
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
define: {
__FE_VERSION__: JSON.stringify(pkg.version),
},
server: { server: {
port: 5173, port: 5173,
proxy: { proxy: {

226
package-lock.json generated
View File

@@ -22,6 +22,9 @@
"cors": "^2.8.6", "cors": "^2.8.6",
"dotenv": "^17.3.1", "dotenv": "^17.3.1",
"express": "^5.2.1", "express": "^5.2.1",
"multer": "^2.1.1",
"openai": "^6.27.0",
"pdf-parse": "1.1.1",
"pg": "^8.19.0", "pg": "^8.19.0",
"uuid": "^13.0.0" "uuid": "^13.0.0"
}, },
@@ -29,6 +32,7 @@
"@types/cookie-parser": "^1.4.10", "@types/cookie-parser": "^1.4.10",
"@types/cors": "^2.8.19", "@types/cors": "^2.8.19",
"@types/express": "^5.0.6", "@types/express": "^5.0.6",
"@types/multer": "^2.1.0",
"@types/node": "^25.3.3", "@types/node": "^25.3.3",
"@types/pg": "^8.18.0", "@types/pg": "^8.18.0",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
@@ -36,6 +40,28 @@
"typescript": "^5.9.3" "typescript": "^5.9.3"
} }
}, },
"backend/node_modules/debug": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.1"
}
},
"backend/node_modules/pdf-parse": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/pdf-parse/-/pdf-parse-1.1.1.tgz",
"integrity": "sha512-v6ZJ/efsBpGrGGknjtq9J/oC8tZWq0KWL5vQrk2GlzLEQPUDB1ex+13Rmidl1neNN358Jn9EHZw5y07FFtaC7A==",
"license": "MIT",
"dependencies": {
"debug": "^3.1.0",
"node-ensure": "^0.0.0"
},
"engines": {
"node": ">=6.8.1"
}
},
"frontend": { "frontend": {
"name": "@family-budget/frontend", "name": "@family-budget/frontend",
"version": "0.1.0", "version": "0.1.0",
@@ -1396,6 +1422,16 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/multer": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.1.0.tgz",
"integrity": "sha512-zYZb0+nJhOHtPpGDb3vqPjwpdeGlGC157VpkqNQL+UU2qwoacoQ7MpsAmUptI/0Oa127X32JzWDqQVEXp2RcIA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/express": "*"
}
},
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "25.3.3", "version": "25.3.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.3.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.3.tgz",
@@ -1515,6 +1551,12 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/append-field": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
"license": "MIT"
},
"node_modules/baseline-browser-mapping": { "node_modules/baseline-browser-mapping": {
"version": "2.10.0", "version": "2.10.0",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz",
@@ -1587,6 +1629,23 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
} }
}, },
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"license": "MIT"
},
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
"dependencies": {
"streamsearch": "^1.1.0"
},
"engines": {
"node": ">=10.16.0"
}
},
"node_modules/bytes": { "node_modules/bytes": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -1655,6 +1714,21 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/concat-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
"integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
"engines": [
"node >= 6.0"
],
"license": "MIT",
"dependencies": {
"buffer-from": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.0.2",
"typedarray": "^0.0.6"
}
},
"node_modules/content-disposition": { "node_modules/content-disposition": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
@@ -2474,6 +2548,68 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/multer": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz",
"integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==",
"license": "MIT",
"dependencies": {
"append-field": "^1.0.0",
"busboy": "^1.6.0",
"concat-stream": "^2.0.0",
"type-is": "^1.6.18"
},
"engines": {
"node": ">= 10.16.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/multer/node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/multer/node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/multer/node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/multer/node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"license": "MIT",
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.11", "version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@@ -2502,6 +2638,12 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/node-ensure": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/node-ensure/-/node-ensure-0.0.0.tgz",
"integrity": "sha512-DRI60hzo2oKN1ma0ckc6nQWlHU69RH6xN0sjQTjMpChPfTYvKZdcQFfdYK2RWbJcKyUizSIy/l8OTGxMAM1QDw==",
"license": "MIT"
},
"node_modules/node-releases": { "node_modules/node-releases": {
"version": "2.0.27", "version": "2.0.27",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
@@ -2551,6 +2693,27 @@
"wrappy": "1" "wrappy": "1"
} }
}, },
"node_modules/openai": {
"version": "6.27.0",
"resolved": "https://registry.npmjs.org/openai/-/openai-6.27.0.tgz",
"integrity": "sha512-osTKySlrdYrLYTt0zjhY8yp0JUBmWDCN+Q+QxsV4xMQnnoVFpylgKGgxwN8sSdTNw0G4y+WUXs4eCMWpyDNWZQ==",
"license": "Apache-2.0",
"bin": {
"openai": "bin/cli"
},
"peerDependencies": {
"ws": "^8.18.0",
"zod": "^3.25 || ^4.0"
},
"peerDependenciesMeta": {
"ws": {
"optional": true
},
"zod": {
"optional": true
}
}
},
"node_modules/parseurl": { "node_modules/parseurl": {
"version": "1.3.3", "version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -2939,6 +3102,20 @@
"react-dom": ">=16.6.0" "react-dom": ">=16.6.0"
} }
}, },
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/recharts": { "node_modules/recharts": {
"version": "2.15.4", "version": "2.15.4",
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz",
@@ -3042,6 +3219,26 @@
"node": ">= 18" "node": ">= 18"
} }
}, },
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/safer-buffer": { "node_modules/safer-buffer": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@@ -3221,6 +3418,23 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/tiny-invariant": { "node_modules/tiny-invariant": {
"version": "1.3.3", "version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
@@ -3771,6 +3985,12 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
"license": "MIT"
},
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.9.3", "version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
@@ -3832,6 +4052,12 @@
"browserslist": ">= 4.21.0" "browserslist": ">= 4.21.0"
} }
}, },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/uuid": { "node_modules/uuid": {
"version": "13.0.0", "version": "13.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",

75
pdf2json.md Normal file
View File

@@ -0,0 +1,75 @@
Ты — конвертер банковских выписок. Твоя задача: извлечь данные из прикреплённого PDF банковской выписки и вернуть строго один валидный JSON-объект в формате ниже. Никакого текста до или после JSON, только сам объект.
## Структура выходного JSON
```json
{
"schemaVersion": "1.0",
"bank": "<название банка из выписки>",
"statement": {
"accountNumber": "<номер счёта, только цифры, без пробелов>",
"currency": "RUB",
"openingBalance": <число в копейках, целое>,
"closingBalance": <число в копейках, целое>,
"exportedAt": "<дата экспорта в формате ISO 8601 с offset, например 2026-02-27T13:23:00+03:00>"
},
"transactions": [
{
"operationAt": "<дата и время операции в формате ISO 8601 с offset>",
"amountSigned": <число: положительное для прихода, отрицательное для расхода; в копейках>,
"commission": <число, целое, >= 0, в копейках>,
"description": "<полное описание операции из выписки>"
}
]
}
```
## Правила конвертации
1. **Суммы** — всегда в копейках (рубли × 100). Пример: 500,00 ₽ → 50000, -1234,56 ₽ → -123456.
2. **amountSigned**:
- Приход (зачисление, пополнение) — положительное число.
- Расход (списание, оплата) — отрицательное число.
- Переводы — знак в зависимости от направления движения на счёт.
3. **operationAt** — дата и время операции. Если время не указано, используй 00:00:00. Обязательно указывай offset (+03:00 для МСК).
4. **commission** — комиссия по операции. Если не указана — 0.
5. **description** — полный текст операции как в выписке (назначение платежа, магазин, получатель и т.п.). Не сокращай и не меняй формулировки.
6. **accountNumber** — только цифры, без пробелов и дефисов (например: 40817810825104025611).
7. **openingBalance / closingBalance** — начальный и конечный остаток по счёту в копейках.
8. **bank** — краткое название банка (VTB, Sberbank, Тинькофф и т.п.).
9. **exportedAt** — дата формирования выписки. Если неизвестна — возьми дату последней операции в выписке.
10. **Порядок транзакций** — сохраняй хронологический порядок из выписки (обычно от старых к новым).
## Требования
- Массив `transactions` не должен быть пустым.
- Все числа — целые.
- Даты — строго в формате ISO 8601 с offset.
- currency всегда "RUB".
- schemaVersion всегда "1.0".
## Пример одной транзакции
Выписка: «26.02.2026 14:06 | -500,00 ₽ | 0,00 | Оплата товаров. PAVELETSKAYA по карте *8214»
```json
{
"operationAt": "2026-02-26T14:06:00+03:00",
"amountSigned": -50000,
"commission": 0,
"description": "Оплата товаров. PAVELETSKAYA по карте *8214"
}
```
Обработай прикреплённый PDF и верни один JSON-объект.

View File

@@ -1,3 +1,16 @@
/** Import record from imports table (for import history) */
export interface Import {
id: number;
importedAt: string;
accountId: number | null;
accountAlias: string | null;
bank: string;
accountNumberMasked: string;
importedCount: number;
duplicatesSkipped: number;
totalInFile: number;
}
/** JSON 1.0 statement file — the shape accepted by POST /api/import/statement */ /** JSON 1.0 statement file — the shape accepted by POST /api/import/statement */
export interface StatementFile { export interface StatementFile {
schemaVersion: '1.0'; schemaVersion: '1.0';

View File

@@ -37,6 +37,7 @@ export type {
StatementHeader, StatementHeader,
StatementTransaction, StatementTransaction,
ImportStatementResponse, ImportStatementResponse,
Import,
} from './import'; } from './import';
export type { export type {