feat(ledger): импорт отчётов в леджер — приём, дедупликация, pending_instrument

Поток: upload -> raw_report_file (sha256 UNIQUE) -> parse -> raw_report_line,
событий в леджере ещё нет -> preview -> POST /imports/{id}/commit ->
ledger/ingest.py резолвит инструмент, считает dedupe_key, пишет event
confirmed | shadow (plan §1.6 B) — сравнивая account.primary_event_source
с источником отчёта, а не гадая. Нерезолвленный инструмент ждёт в
pending_instrument, никогда не угадывается; POST /instruments/pending/{id}/resolve
привязывает и пересобирает лоты.

ledger/dedupe.py — shadow-матчинг случая B двумя проходами (точная дата, затем
±1 рабочий день, жадно 1:1, |price| ±0,5 %). Шаги shadow_dedupe и
report_reconcile зарегистрированы перед quality: оба говорят через FINDINGS.

/instruments/pending регистрируется в app.py ДО routers/instruments.py:
FastAPI сопоставляет маршруты по порядку, и /instruments/{id} с типом int
отвечает 422 на нечисловой сегмент, а не проваливается дальше.

Контракт — docs/ai/import-contract.md, общий для бэкенда и Flutter.
This commit is contained in:
Dmitry
2026-09-19 10:40:04 +03:00
parent 2e742a093b
commit ff3b76871d
16 changed files with 4484 additions and 0 deletions
+12
View File
@@ -50,6 +50,13 @@ from fintracker.models.pricing import (
PriceManual,
RawCbrRate,
)
from fintracker.models.reports import (
PendingInstrument,
PendingInstrumentStatus,
RawReportFile,
RawReportLine,
ReportParseStatus,
)
from fintracker.models.sync import (
JobStatus,
RunStatus,
@@ -121,6 +128,8 @@ __all__ = [
"MetricReturns",
"MetricRunway",
"MetricSpendingByCategory",
"PendingInstrument",
"PendingInstrumentStatus",
"Portfolio",
"PortfolioAccount",
"PositionSnapshot",
@@ -129,6 +138,8 @@ __all__ = [
"PriceLast",
"PriceManual",
"RawCbrRate",
"RawReportFile",
"RawReportLine",
"RawTinvestEvent",
"RawTinvestInstrument",
"RawTinvestOperation",
@@ -136,6 +147,7 @@ __all__ = [
"RawZenmoneyDeletion",
"RawZenmoneyEntity",
"RefreshToken",
"ReportParseStatus",
"Rule",
"RuleKind",
"RuleMatchType",