feat: Schnellerfassung per Kurzbefehl (Etappe A)

Der Kontoauszug kommt erst am Monatsende; wer im Laden bezahlt hat, weiss
in dem Moment noch, wofuer. iOS laesst keine App fremde Mitteilungen
mitlesen, also fuehrt der Weg ueber einen Kurzbefehl, der die Zahlung an
Certain schickt.

Gebaut:
- Geraetemarken mit eigener Abhaengigkeit statt Sonderfall in
  get_current_user. Eine Marke gilt NUR an /quick – auf allen anderen
  Wegen ist sie keine Anmeldung. Geht das Telefon verloren, ist kein
  Datenabfluss moeglich; Tests halten das fuer Buchungen, Konten,
  Kategorien, Export und Datenbanksicherung fest.
- POST /quick/booking laeuft durch detect_rows, damit Schlagwort- und
  Betragsregeln die Kategorie schon vorschlagen. external_id verhindert,
  dass ein doppelt ausgeloester Kurzbefehl zweimal bucht.
- inbox_items merkt sich die Zahlung ueber das Buchen hinaus. Beim
  spaeteren CSV-Import greift der Import-Schluessel nicht: Der Text vom
  Telefon ist selten wortgleich mit dem des Auszugs und die Bank bucht ein
  bis zwei Tage spaeter. detect_rows vergleicht deshalb zusaetzlich
  unscharf – exakter Betrag, Datum +/- 3 Tage, je Eintrag nur einmal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Guido
2026-08-22 19:34:14 +02:00
co-authored by Claude Opus 5
parent 9d6a53a224
commit 02260d7cd2
9 changed files with 841 additions and 1 deletions
+65
View File
@@ -1,6 +1,8 @@
"""Pydantic-Schemas für Anfragen/Antworten (Phase 0: Auth, Benutzer, Einstellungen)."""
from datetime import date as date_t, datetime, time as time_t
from decimal import Decimal
from typing import Literal
from pydantic import BaseModel, ConfigDict, Field, field_validator
@@ -475,3 +477,66 @@ class FlaggedKeywordOut(BaseModel):
word: str
applied_count: int
overridden_count: int
# --- Gerätemarken und Eingang ------------------------------------------------
class ApiTokenCreate(BaseModel):
name: str = Field(min_length=1, max_length=80)
default_account_id: int | None = None
class ApiTokenOut(BaseModel):
id: int
name: str
default_account_id: int | None
last_used_at: datetime | None
created_at: datetime | None
model_config = ConfigDict(from_attributes=True)
class ApiTokenCreated(ApiTokenOut):
"""Antwort beim Anlegen – der Klartext ist hier das einzige Mal zu sehen."""
token: str
class QuickBookingIn(BaseModel):
"""Was ein Kurzbefehl schickt. Bewusst genügsam: Betrag reicht."""
amount: Decimal
payee: str | None = Field(default=None, max_length=255)
date: date_t | None = None
account_id: int | None = None
note: str | None = None
# Kennung des Kurzbefehls, damit ein doppelt ausgelöster Aufruf nicht
# zweimal bucht (schlechtes Netz, versehentlicher Doppeltipp).
external_id: str | None = Field(default=None, max_length=255)
kind: Literal["expense", "income"] = "expense"
class InboxItemOut(BaseModel):
id: int
source: str
status: str
account_id: int | None
date: date_t
amount_cents: int
currency: str
payee: str | None
note: str | None
category_id: int | None
transaction_id: int | None
created_at: datetime | None
model_config = ConfigDict(from_attributes=True)
class QuickBookingOut(BaseModel):
"""Kurze Rückmeldung – der Kurzbefehl zeigt sie als Mitteilung an."""
ok: bool = True
message: str
item: InboxItemOut