# Sprint 1 – Transfer Pairing v2

## Ziel und Entscheidung

Interne Überträge werden aus bestehenden Budget-Importkandidaten als ein nachvollziehbares Paar abgeleitet. Rohimporte bleiben unverändert. Automatische Erkennung erzeugt nur einen Vorschlag; die produktive Verbuchung bleibt `Preview/Review → Confirm → Audit` und unterliegt unverändert dem fail-closed Write-Gate aus Sprint 0.

## Wiederverwendete Strukturen

- `budget_transaction_candidates` als normalisierte Importkandidaten;
- `budget_accounts` als bekannte eigene Konten;
- `budget_transactions` und `budget_transfers` als bestehendes Budget-Ledger;
- `audit_log` als bestehender Auditmechanismus;
- bestehende Budget-API und `BudgetImportReviewPage` als Review-Pfad.

Es wurde keine zweite Import- oder Ledgerengine angelegt.

## Additive Migration

`budget_transaction_candidates` erhält:

- `signed_amount_original`;
- `value_date`.

Neu ist `budget_transfer_pairs` mit stabiler Pair-ID, Quell-/Zielkandidat, nicht sensitiven Kontenbezügen, signierten Beträgen, Währung, Buchungs-/Valutadaten, Evidence-/Reason-Codes, Status, Entscheidungs- und Auditinformationen sowie `budget_effect_chf='0'`.

Partielle Unique-Indizes verhindern, dass ein Kandidat in mehr als einem bestätigten Paar konsumiert wird. Die Migration verändert keine Rohimporte und greift nicht auf eine produktive Datenbank zu.

## Matcher-Vertrag

Pflichtkriterien für einen Vorschlag:

- beide Seiten gehören zu bekannten aktiven eigenen Konten;
- unterschiedliche Konten;
- entgegengesetzte Vorzeichen;
- exakt gleicher Decimal-Betrag;
- gleiche Währung;
- Buchungs-/Valutadatum innerhalb von standardmäßig drei, maximal sieben Tagen.

Quelle, Importlauf, Referenz, Merchant und Memo sind nur zusätzliche Evidenz. Text allein bestätigt oder verwirft kein Paar. Bei mehreren gleich plausiblen Gegenbuchungen bleibt der Status `unmatched` mit Quality `ambiguous`; Auto-Confirm ist deaktiviert.

Statusvertrag:

- `proposed` – eindeutige starke Gegenbuchung, manuelle Bestätigung erforderlich;
- `confirmed` – beide Kandidaten atomar konsumiert und genau ein Transfer erzeugt;
- `rejected` – Vorschlag abgelehnt, Kandidaten bleiben offen;
- `superseded` – früherer offener Vorschlag durch eine neuere Ableitung ersetzt;
- `unmatched` – Gegenbuchung fehlt oder ist mehrdeutig.

## API-Vertrag

- `GET /api/budget/transfer-pairs`
- `POST /api/budget/transfer-pairs/match`
- `POST /api/budget/transfer-pairs/{pair_id}/confirm` mit explizitem `confirm=true`
- `POST /api/budget/transfer-pairs/{pair_id}/reject`

Der bestehende Kandidaten-Confirm delegiert einen Transfer nur noch an ein vorhandenes `proposed`-Paar. Ohne ausgewählte Gegenbuchung wird keine produktive Transferbuchung erzeugt. Pair-Antworten enthalten keine vollständigen Kontoreferenzen, Rohimportdateinamen oder lokalen Pfade.

## Atomarer Confirm

Confirm validiert beide Kandidaten und die Kontenbeziehung erneut, sperrt die SQLite-Schreibtransaktion, erzeugt zwei signierte `transfer`-Ledgerseiten und genau eine `budget_transfers`-Beziehung, setzt beide Kandidaten gemeinsam auf `confirmed`, hält `budget_effect_chf='0'` fest und schreibt einen redigierten Audit-Eintrag. Deterministische IDs, Statusprüfung und Unique-Indizes machen Wiederholung und konkurrierende Requests idempotent beziehungsweise konfliktfrei.

Es entstehen weder Einkommen noch Konsumausgaben.

## Synthetische UAT

1. Synthetischer Raiffeisen-Kandidat `CHF -100.00` mit Text `Einkauf TWINT SWISSLOS E-COMMERCE` wird importiert: ohne Gegenbuchung bleibt er ungeklärt und ungebucht.
2. Synthetischer AKB-Kandidat `CHF +100.00` aus einem getrennten Importlauf wird ergänzt: ein gemeinsamer Vorschlag erscheint.
3. Die Review zeigt Richtung, `CHF 100.00`, Begründung, Unsicherheit, `Budgeteffekt CHF 0` und den Hinweis, dass erst Confirm produktiv verbucht.
4. Confirm erzeugt genau einen Transfer und konsumiert beide Kandidaten atomar.
5. Erneutes Confirm liefert idempotent denselben Transfer; es entsteht kein zweiter Datensatz.
6. Reject erzeugt keine Buchung und lässt die ursprünglichen Kandidaten offen.

## Rollback

Vor produktiver Nutzung kann der gesamte Commit revertiert werden. Nach Nutzung bleiben Rohimporte unverändert; vor einem Schema-Rollback ist eine Runtime-Datenbanksicherung erforderlich. Die additive Pair-Tabelle und die zwei optionalen Kandidatenspalten können anschließend kontrolliert entfernt werden. Ein Rollback darf das frühere Bestätigen eines einseitigen beziehungsweise ungepaarten Transfers nicht wieder freischalten.

## Nicht-Scope

Keine neue Importquelle, kein allgemeiner Reconciliation-Workflow, keine Portfolio-Performance, TWR/XIRR, Providerintegration, Tailscale-Writes oder Orderausführung.
