feat: add tests for import polling, login page, money utilities, and query keys

- Implemented tests for import polling functionality, ensuring correct behavior for active and terminal import statuses.
- Created tests for the login page, validating user interactions and error handling.
- Added unit tests for money utility functions, covering parsing, formatting, and summation of monetary values.
- Developed tests for query keys, ensuring proper scoping and invalidation roots.

chore: setup testing environment and configuration

- Added setup file for testing with Vitest and Testing Library.
- Configured TypeScript and Vite for the frontend project.
- Created Docker and Nginx configurations for local development and production environments.
- Established a PostgreSQL initialization script for setting up application roles.

docs: add README and environment configuration for infra

- Included README for the infra directory, detailing local development setup and service descriptions.
- Created an example environment configuration file for local development with placeholders for sensitive information.
This commit is contained in:
k1nq
2026-09-14 09:26:33 +05:00
parent d8b938adfb
commit d582f15681
164 changed files with 25553 additions and 1 deletions
@@ -0,0 +1,277 @@
# План MVP финансового трекера
## Цель
Собрать закрытую hosted beta личного zero-based бюджета. Пользователь по приглашению создаёт личное финансовое пространство, учитывает RUB-счета и кредитки, планирует календарный месяц, импортирует выписки пяти поддержанных банковских контрактов, разбирает неоднозначности, закрывает период и видит детерминированную аналитику.
Критический пользовательский путь:
1. Принять приглашение, выбрать RU/EN и создать личное пространство.
2. Создать дебетовый, наличный или кредитный счёт с начальной позицией.
3. Создать месячный план из ожидаемых доходов и назначений, при необходимости скопировав прошлый месяц.
4. Загрузить выписку, подтвердить привязку к счёту, исправить критические поля, разрешить дубли/переводы и атомарно применить импорт.
5. Категоризировать расходы, распределить фактически поступившие деньги, покрыть обязательные дефициты и закрыть месяц.
6. Проверить счета, бюджет, операции, импорт и четыре отчёта; экспортировать либо удалить свои данные.
## Зафиксированный объём
- Один личный `Financial Space` на пользователя в UI; схема сразу поддерживает membership с ролями `owner` и `member`.
- Полная видимость данных внутри пространства; счёт принадлежит ровно одному пространству.
- RUB only. Все суммы хранятся целым числом копеек, без `float`.
- Счета MVP: дебетовые/текущие, наличные, кредитные.
- Импорт-контракты: Альфа debit XLSX, Альфа credit XLSX, Ozon debit PDF, Ozon credit PDF, Рокетбанк debit PDF.
- Ozon и Рокетбанк: только PDF с извлекаемым текстом, без OCR. Альфа: только XLSX, без XLS.
- Бюджет: календарный месяц, план и обеспечение разделены, ожидаемый доход можно планировать до поступления.
- Категории: плоский список; одна операция имеет не более одной бюджетной категории.
- Два типа категории: месячная категория и постоянный накопительный конверт.
- Отчёты: расходы по категориям; план/факт; доходы/расходы/cash flow; остатки активов и долг по кредиткам.
- Полностью адаптивный RU/EN web для desktop и mobile.
- Масштаб beta: до 100 пользователей, 50 000 подтверждённых операций на пространство, 10 000 строк и 20 МБ на импорт.
## Вне MVP
- AI-категоризация, AI-аналитика и анализ договоров кредиток.
- Семейные приглашения, передача владения, выход из пространства и совместный UI.
- Полный расчёт льготного периода, процентов и тарифов кредиток; кредитка MVP — баланс/долг и резерв.
- Мультивалютность, вклады, кредиты кроме кредиток, инвестиции, цели с суммой/сроком.
- Split одной операции между несколькими категориями и иерархия категорий.
- OCR, сканы, XLS, credit-выписка Рокетбанка, банковские API и автоматическая синхронизация.
- Движок регулярных платежей; вместо него есть явное копирование плана прошлого месяца.
- Публичная регистрация, биллинг и 2FA.
## Доменный язык
При реализации создать корневой `CONTEXT.md` в формате skill `domain-modeling` и перенести туда только следующие краткие определения, без технических деталей.
- **Пользователь**: подтверждённая личность, которая входит в приложение. Не называть финансовым счётом или владельцем операций.
- **Финансовое пространство**: единая граница владения счетами, операциями, бюджетами и справочниками.
- **Участие**: связь пользователя с финансовым пространством и ролью `owner` либо `member`.
- **Финансовый счёт**: место учёта собственных денег либо долга. Карта является банковским инструментом/атрибутом, а не отдельным синонимом счёта.
- **Операция**: пользовательский финансовый факт: расход, доход, внутренний перевод или возврат.
- **Проводка**: изменение остатка одного внутреннего финансового счёта в рамках операции.
- **Внутренний перевод**: одна операция с двумя противоположными проводками по собственным счетам; не доход и не расход.
- **Возврат**: отдельная операция, полностью или частично связанная с исходным расходом; не доход.
- **Импорт выписки**: загруженный документ и процесс его проверки/применения.
- **Попытка разбора**: версионированный результат конкретного parser adapter для одного документа.
- **Исходная запись**: неизменяемый извлечённый банковский факт с происхождением в документе.
- **Получатель**: нормализованное пространство-локальное имя магазина, сервиса или человека с алиасами исходных описаний.
- **Бюджетный период**: календарный месяц в часовом поясе пользователя со статусом open/closed.
- **Общий пул**: фактически доступные, но ещё не распределённые деньги пространства; может быть отрицательным.
- **Плановое назначение**: доля текущих и ожидаемых денег, которую пользователь намерен направить категории.
- **Обеспечение**: фактически поступившие деньги, перемещённые из общего пула в категорию или резерв.
- **Месячная категория**: назначение расходов, остаток которого при закрытии сворачивается в общий пул.
- **Накопительный конверт**: виртуальный обеспеченный резерв без обязательной цели/срока, переносимый между месяцами.
- **Резерв кредитки**: постоянный системный резерв конкретной кредитки для будущего погашения долга.
- **Ожидаемый доход**: плановое поступление с датой, суммой и источником, отдельно сопоставляемое фактическим доходам.
- **Источник дохода**: необязательная классификация происхождения фактического или ожидаемого поступления.
## Архитектура
### Репозиторий и процессы
- Монорепозиторий: `backend/`, `frontend/`, `infra/`, корневые `CONTEXT.md`, `docs/adr/` и setup-документация.
- Backend: FastAPI, Pydantic 2, синхронный SQLAlchemy 2 + psycopg, Alembic, PostgreSQL.
- Worker: Celery + Redis. HTTP-процесс не разбирает файлы; загрузка возвращает import ID, клиент опрашивает активное состояние через TanStack Query и прекращает polling в terminal state.
- Frontend: React + TypeScript + Vite, React Router, TanStack Query, React Hook Form + Zod, i18next; OpenAPI-generated API client.
- Локально: Docker Compose с API, worker, frontend/reverse proxy, PostgreSQL, Redis, MinIO и SMTP test sink.
- Production: provider-neutral OCI images, PostgreSQL, Redis, S3-compatible storage и SMTP; фактическая площадка, база, объекты и backups обязаны находиться в РФ.
- Пакеты фиксируются `uv.lock` и `pnpm-lock.yaml`; секреты не попадают в `VITE_*` и Git.
### Backend-модули
- `identity`: invitations, users, password reset, sessions, locale/timezone.
- `spaces`: financial spaces, memberships, roles, RLS context.
- `ledger`: accounts, operations, postings, transfers, refunds, voiding, balances.
- `budgeting`: periods, plans, expected income, funding, categories, envelopes, credit reserves, close/reopen.
- `imports`: encrypted documents, parser adapters/runs, source records, reconciliation, deduplication, atomic apply/revert.
- `categorization`: payees, aliases, MCC/bank mappings, user rules and suggestion provenance.
- `analytics`: read queries/snapshots for the four fixed reports.
- `exports`: asynchronous ZIP exports and space deletion workflow.
- `audit`: append-only domain/security events without raw financial payloads.
Модули общаются через application services внутри одного процесса/БД. Не создавать микросервисы, event bus или абстрактные AI-provider interfaces в MVP.
### ADR после scaffold
Создать только ADR, которые сохраняют труднообратимый контекст:
1. `0001-financial-space-ownership-and-rls.md`: данные принадлежат пространству; полная видимость; application checks + PostgreSQL RLS.
2. `0002-journal-and-budget-reserves.md`: журнал операций/проводок, отдельные план/обеспечение, переносимые envelopes/credit reserves, не event sourcing и не полная двойная запись.
3. `0003-versioned-statement-imports.md`: неизменяемое происхождение, версионированные parse runs, атомарное применение/отмена и консервативная дедупликация.
4. `0004-provider-neutral-encrypted-data-boundary.md`: размещение в РФ, S3-neutral envelope encryption и отсутствие внешних AI-вызовов.
## Модель данных и инварианты
### Identity и tenancy
- `users`, `invitations`, `sessions`, `financial_spaces`, `memberships`, `audit_events`.
- Регистрация только по одноразовому email invite; пароль хешируется Argon2id. Email нормализован и уникален без учёта регистра.
- Непрозрачный session token хранится в cookie `HttpOnly`, `Secure`, `SameSite=Lax`; в БД хранится только его hash. Ротация после login/reset, revoke one/all sessions, rate limits и CSRF token для изменяющих запросов.
- В MVP invitation создаёт пространство и единственного owner. В БД всегда должен оставаться минимум один owner.
- Все финансовые таблицы имеют `space_id`; составные FK запрещают cross-space ссылки.
- API проверяет membership, затем внутри транзакции выполняет `SET LOCAL app.space_id`; RLS повторно ограничивает запрос. Worker получает IDs, заново проверяет принадлежность и устанавливает тот же RLS context. Bypass role доступна только migrations и audited break-glass tooling.
- Для конкурентно изменяемых budget/operation/import aggregates использовать version column и `409 Conflict`, а для close/apply/revert — row locks и одну DB transaction.
### Ledger
- `accounts`: `cash | debit | credit`, opening date/amount, bank code, masked product identifier, keyed fingerprint, status `active | archived`.
- Остаток не изменяется напрямую: opening position + confirmed, non-voided postings. Asset balance положительный; credit balance отражает долг по единой документированной sign convention.
- `operations`: `expense | income | transfer | refund`; status `confirmed | voided`; positive amount; `occurred_on`; optional `posted_on`; optional category/income source/payee/refund parent; audit/version.
- `postings`: одна для external income/expense/refund, две равные и противоположные для internal transfer. Погашение кредитки — internal transfer и не создаёт второй расход.
- Бюджет и расходная аналитика используют `occurred_on`. Остаток/сверка используют `posted_on`, когда он есть, иначе `occurred_on`. Если выписка содержит одну дату, parser записывает только `occurred_on`, не выдумывая вторую.
- Операция имеет максимум одну категорию. Income source необязателен и отсутствие источника не блокирует close.
- Ошибочная операция voided с причиной; реальный возврат создаётся отдельно. Изменение/void операции закрытого месяца требует каскадного reopen.
- Refund sum по связанной покупке не должна бесконтрольно превышать исходный расход; неоднозначный/избыточный остаток требует ручной классификации.
- Счёт архивируется только с нулевым остатком и сохраняется в истории; удалить можно только никогда не использованный счёт.
### Budget
- `budget_periods`, `plan_assignments`, `expected_incomes`, `expected_income_matches`, `funding_movements`, `period_closures`.
- Плановый остаток = opening projected pool + expected incomes - plan assignments. Он может быть положительным/отрицательным: UI предупреждает, но не блокирует сохранение или close.
- Actual funding выполняется вручную после поступления денег; UI предлагает недообеспеченные назначения. Expected income может быть закрыт несколькими фактическими incomes и показывает amount/date variance.
- Фактический доход сразу увеличивает general pool независимо от наличия income source.
- Debit/cash expense уменьшает funded category/envelope; uncategorized expense уменьшает системный `Unallocated` deficit/general availability и блокирует close.
- Credit expense увеличивает debt. Обеспеченная часть расхода перемещается из category/envelope в постоянный credit reserve; необеспеченная часть остаётся видимым category debt. Uncategorized credit expense остаётся unallocated/unfunded debt.
- Credit payment сначала уменьшает credit reserve, затем general pool; pool может стать отрицательным. Initial credit debt финансируется пользователем вручную.
- Обычная monthly category при close сворачивает положительный или отрицательный остаток в general pool и начинает следующий период с нуля.
- Savings envelope переносит баланс. Его отрицательный баланс блокирует close до покрытия.
- Credit reserve переносится до платежа. Refund на кредитку уменьшает debt; связанный излишек резерва освобождается в текущий pool.
- Refund в том же открытом месяце восстанавливает категорию покупки. Refund после закрытия исходного месяца пополняет текущий general pool и не переписывает closed budget fact.
- Close блокируют: imports без допустимого reconciliation confirmation, unresolved financial fields/dedup/transfer decisions, expenses без category и отрицательные savings envelopes. Plan imbalance, expected-income variance и underfunded credit debt дают warnings, но не блокируют.
- Reopen старого периода показывает impacted periods, атомарно открывает его и все последующие и пересчитывает preliminary pools; повторный close выполняется последовательно.
- `Copy previous month` копирует plan assignments и expected-income templates, сдвигает ожидаемую дату на следующий месяц с clamp к последнему дню, но не копирует факты, matches или funding.
### Categories и локализация
- `categories`: `monthly | savings_envelope`, `system_key` nullable, `custom_name/name_override` nullable, archived flag.
- Standard category отображается через RU/EN translation key. После rename сохраняется override, который больше не переводится. Custom category всегда хранит одно пользовательское имя.
- Использованная category только архивируется; merge и bulk reclassification вне MVP.
- `income_sources` — отдельный плоский пользовательский справочник; неизвестный источник разрешён и в отчёте группируется как unclassified.
- Locale хранится в user profile. API возвращает machine-readable error codes; React и email templates переводят RU/EN. User timezone configurable, default `Europe/Moscow`; формат суммы/даты следует locale.
### Categorization
- `payees`, `payee_aliases`, `categorization_rules`, versioned MCC dictionary и bank-category mappings.
- Parser извлекает raw description, MCC и bank category, но не назначает пользовательскую category.
- Приоритет: explicit operation choice > confirmed payee rule > MCC suggestion > bank category suggestion > uncategorized.
- Payee rule автоматически применяется к будущим операциям пространства и сохраняет provenance; MCC/bank match остаётся предложением до подтверждения.
### Import
- `statement_documents`, `parse_runs`, `source_records`, `source_record_operation_links`, `dedup_candidates`, reconciliation metadata.
- Файл проходит magic/MIME/size check, hash и quotas до сохранения. Не сохранять полный извлечённый текст документа в БД: persist только allowlisted statement metadata/source fields и page/row coordinates.
- Содержимое шифруется до S3 через AES-GCM envelope encryption: случайный DEK на файл, encrypted DEK и nonce metadata в БД, KEK в secret manager. Random object keys не содержат PII. KEK rotation rewraps DEKs.
- XLSX reader: `openpyxl` в `read_only=True`, `data_only=True`, `keep_links=False`, с `defusedxml`, ZIP quotas и явным close.
- PDF reader: `pdfplumber`; table/text settings принадлежат конкретному adapter contract. Worker non-root, без исходящей сети, с CPU/memory/time limits.
- Adapter interface: content detection + parser version + `StatementDraft` с bank/product/account fingerprint, period, currency, balances/control totals и source rows (external ID, dates, signed amount/direction, description, payee hints, MCC, bank category, location, field warnings).
- State machine: uploaded -> queued -> processing -> review_required/ready -> confirmed либо failed; reparse создаёт новый run, старый не изменяет.
- Критические blockers: currency, amount, direction, occurred date, target account, arithmetic mismatch. Описательные uncertainty отправляются в categorization queue.
- Первая account mapping подтверждается пользователем; stable bank fingerprint далее применяется автоматически. Conflict блокирует импорт.
- Reconciliation: при наличии balances/control totals требуется точное равенство. Если формат не даёт полной проверки, только явный `confirmed_without_full_reconciliation` с причиной/audit.
- Dedupe автоматически связывает stable bank ID или единственное полное однозначное совпадение. Повторяющиеся одинаковые кандидаты остаются отдельными до решения; приоритет — не потерять реальную операцию.
- Internal transfer matcher использует amount/currency/time/account candidates; связывает только однозначную пару, остальные подтверждает пользователь.
- Confirm применяет весь run одной DB transaction: создаёт только новые operations/postings, а duplicates получают additional source links. Ни одна строка не влияет на ledger до confirm.
- Reparse создаёт новый run и diff; пользовательские правки не перезаписываются. Apply проходит полную сверку и dedupe заново.
- Revert confirmed import показывает diff, требует cascade reopen affected periods, атомарно удаляет созданные этим import postings/operations и снимает только source links у ранее существовавших duplicates. Файл/audit остаётся до отдельного удаления.
- Source file хранится до explicit deletion. После удаления сохраняются allowlisted source records, checksum и normalized operations. Удаление пространства уничтожает всё по lifecycle ниже.
## UI и API
- Same-origin `/api/v1` REST; OpenAPI schema генерирует frontend client. Mutations принимают idempotency key там, где browser retry может повторить side effect.
- Главная навигация: Accounts, Budget, Operations, Review Queue, Imports, Analytics, Settings/Data.
- Desktop использует data-dense tables для operations/import review; mobile показывает те же действия карточками и пошаговыми dialogs, без обязательного horizontal scroll.
- Operations: server pagination, date/account/type/category/payee/status filters, search, manual income/expense/transfer/refund, void and audit view.
- Budget: month switcher, preliminary/closed badge, planned/funded/spent/available columns, pool, envelopes, card reserves/debt, expected income matching, close/reopen/copy flows.
- Import: upload/progress, adapter/account detection, field warnings, reconciliation summary, duplicate/transfer decisions, atomic confirm/revert/reparse diff.
- Analytics:
- category spend uses purchase occurrence period; linked late refund adjusts original purchase analytics but not closed budget fact;
- cash flow uses actual posting/refund period (`posted_on`, fallback `occurred_on`), excludes internal transfers/opening positions and identifies refund separately;
- plan/fact uses budget-effective `occurred_on` and immutable closure snapshots for closed periods;
- balances/debt use posting timeline and confirmed non-voided postings.
- Любой chart имеет доступную табличную альтернативу; keyboard/focus/error states и contrast соответствуют WCAG AA.
## Безопасность, данные и эксплуатация
- TLS only; private S3 bucket; encrypted PostgreSQL/Redis/backups; secrets from environment/secret manager; structured logs redact filename, description, payee, account identifiers, email and amounts.
- Нет support UI для просмотра финансов. Break-glass использует отдельную роль, обязательную reason, short-lived grant и append-only audit; обычный admin управляет invites/health без содержимого пространств.
- Uploaded files никогда не исполняются. Ограничить decompression, pages, cells, rows, parser time and worker memory; quarantine failed files; одинаковый file hash делает enqueue/apply idempotent.
- Celery tasks idempotent, `acks_late`, prefetch 1, bounded retry/backoff и terminal failure. DB unique constraints защищают от duplicate task side effects.
- Self-service export создаётся worker как short-lived encrypted ZIP с CSV + JSON для accounts, operations, categories, budgets, expected incomes and audit-relevant provenance; download URL одноразовый/короткоживущий.
- Delete space: 30-day cancellation window, пространство немедленно блокируется; затем hard-delete rows/objects и crypto-shred file keys. Backups хранятся 30 дней; disaster restore повторно применяет durable deletion ledger до открытия сервиса.
- Observability provider-neutral: health/readiness, queue depth, parser duration/failure by adapter version, reconciliation outcomes, DB latency and error codes. Метрики/трейсы не содержат финансовый payload.
- До реальных приглашений провести юридическую проверку обработки персональных/финансовых данных и локализации в РФ; задокументировать privacy/retention/incident procedures.
## Последовательность реализации
1. **Scaffold и доменная документация**
- Создать monorepo layout, locked backend/frontend dependencies, lint/type/test commands, Docker Compose и CI.
- Добавить `CONTEXT.md` и четыре ADR выше; дополнить `.gitignore` для private fixtures, object data, secrets и generated exports.
- Поднять PostgreSQL/Redis/MinIO/test SMTP; добавить Alembic naming convention и transactional migrations.
2. **Identity, sessions и tenant boundary**
- Реализовать admin invite, registration, login/logout/reset, server sessions, CSRF, locale/timezone.
- Создавать personal space + owner membership атомарно.
- Включить RLS policies, request/worker transaction context, composite tenant constraints и negative cross-tenant tests.
3. **Ledger vertical slice**
- Реализовать accounts/opening position, operations/postings, sign convention, manual income/expense/transfer/refund, voiding and archive.
- Добавить transactional domain services, optimistic locking and balance queries/snapshots.
- Построить Accounts/Operations UI и property tests для сохранения сумм/переводов/долга.
4. **Budget vertical slice**
- Реализовать periods, plan assignments, expected incomes/matches, manual funding, monthly categories, envelopes and credit reserves.
- Закодировать debit/credit/refund/uncategorized effects, close gates, closure snapshot, cascade reopen and copy-plan behavior.
- Построить Budget UI с предупреждениями plan imbalance/underfunded card и invariant/property tests.
5. **Безопасное import ядро**
- Реализовать upload validation, envelope encryption/S3 lifecycle, Celery state machine, adapter contract, reconciliation, account mapping and versioned parse runs.
- Реализовать conservative dedupe, transfer matching, atomic confirm, reparse diff, revert import and source deletion.
- Добавить synthetic parser inputs в Git и отдельный `PRIVATE_FIXTURES_DIR` test command; сами реальные документы никогда не коммитить.
6. **Пять bank adapter contracts**
- Последовательно добавить Alfa debit/credit XLSX, Ozon debit/credit PDF, Rocketbank debit PDF.
- Для каждого выделить detection signatures, page/sheet parser, control totals and adapter-specific warnings.
- Adapter не считается включённым в beta, пока не проходит минимум два приватных периода своего contract и malformed/duplicate/overlap cases.
7. **Categorization и review queue**
- Добавить standard/custom category localization, income sources, payees/aliases, MCC and bank maps, precedence/provenance.
- Построить unified Review Queue для critical import issues, duplicate/transfer decisions и uncategorized expenses.
- Проверить, что parser не знает пользовательские категории, а confirmed payee rules не пересекают spaces.
8. **Analytics**
- Реализовать четыре read models/queries с правильными occurred/posted/refund semantics и server-side ranges/pagination.
- Добавить charts + accessible tables; закрытые budget facts читать из closure snapshots, balances — из ledger.
- Проверить агрегаты на generated 50k-operation space и reconcile totals с ledger.
9. **Data controls и hardening**
- Реализовать async export, 30-day delete/cancel, object/key purge, backup deletion ledger and restore test.
- Добавить break-glass tooling/audit, redaction, rate limits, parser sandbox limits, key rotation and security headers.
- Завершить responsive RU/EN flows, accessibility and browser E2E.
10. **Staging и закрытая beta**
- Собрать provider-neutral images, one-shot migrations, same-origin reverse proxy, health checks, backup/restore runbook and alerts.
- Сначала прогнать synthetic data и private parser suite; затем legal/privacy gate и ограниченные email invites.
- Включать bank adapters независимо после contract gate; не заявлять отсутствующие форматы.
## Проверка готовности
- Backend: Ruff/format, static typing, pytest unit/integration against real PostgreSQL/Redis/S3-compatible test services, Hypothesis domain properties.
- Database: Alembic upgrade from empty DB, downgrade where safe, constraints/RLS policies, tenant escape tests, concurrent close/apply conflict tests.
- Parsers: synthetic CI tests plus mandatory private E2E command for five contracts; malformed/oversized/encrypted/scanned/wrong-bank files fail with stable diagnostics.
- Frontend: TypeScript, lint, Vitest + Testing Library, i18n key parity, accessibility checks.
- E2E Playwright on desktop and mobile viewport covers invite/login, account setup, plan/funding, manual operations, import review/confirm, categorization, close/reopen, analytics, export and delete cancellation.
- Financial properties: transfer sums zero; no double-counted card payment; balances equal opening + postings; import confirm/revert is idempotent; category/pool/envelope/reserve transitions conserve the defined available-resource equation; closed periods never change without explicit cascade reopen.
- Analytics fixtures verify late refund on two axes, unknown income source, uncategorized expense, posting-vs-occurrence dates and archived entities.
- Performance gate at agreed beta scale; import remains asynchronous and all operation/report lists remain paginated.
- Disaster test restores a backup, reapplies completed deletion ledger and proves purged spaces/keys remain inaccessible.
## Основные риски и gates
- Private fixtures are an external prerequisite: an adapter cannot be declared supported without them, even if synthetic tests pass.
- Bank layout drift must create a new adapter/parser version and review diff, never silently reinterpret historical data.
- RLS context leakage through pooled connections/tasks is a critical test target; use transaction-local settings and fail closed when context is absent.
- Parsing untrusted PDF/XLSX is isolated and resource-limited; library success alone is not a security boundary.
- Envelope encryption protects object storage but is not zero-knowledge: compromised running API/worker + KEK can decrypt files; break-glass, secret isolation and audit remain necessary.
- Russian data-location and consent requirements are release gates requiring qualified legal review, not assumptions encoded only in infrastructure.
Неразрешённых продуктовых решений для MVP не осталось. Репозиторий пуст, поэтому migration path для существующих данных не требуется.