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:
@@ -0,0 +1,77 @@
|
||||
"""Eingang fuer extern erfasste Zahlungen und Geraetemarken.
|
||||
|
||||
Der Kontoauszug kommt erst am Monatsende; wer eine Zahlung gleich erfasst,
|
||||
weiss noch, wofuer sie war. ``inbox_items`` haelt solche Zahlungen fest und
|
||||
bleibt auch nach dem Buchen bestehen, damit der spaetere CSV-Import sie
|
||||
wiedererkennt und nicht ein zweites Mal verbucht.
|
||||
|
||||
``api_tokens`` gibt Geraeten ein Zugangsmerkmal, die sich nicht anmelden
|
||||
koennen (iOS-Kurzbefehle). Der Geltungsbereich ist bewusst eng: nur erfassen,
|
||||
nicht lesen.
|
||||
|
||||
Revision ID: 0015
|
||||
Revises: 0014
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision = "0015"
|
||||
down_revision = "0014"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"api_tokens",
|
||||
sa.Column("id", sa.Integer(), primary_key=True),
|
||||
sa.Column("owner_id", sa.Integer(), nullable=False),
|
||||
sa.Column("name", sa.String(length=80), nullable=False),
|
||||
sa.Column("token_hash", sa.String(length=64), nullable=False),
|
||||
sa.Column("scope", sa.String(length=32), nullable=False, server_default="quick_add"),
|
||||
sa.Column("default_account_id", sa.Integer(), nullable=True),
|
||||
sa.Column("last_used_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.ForeignKeyConstraint(["owner_id"], ["users.id"], ondelete="CASCADE"),
|
||||
sa.ForeignKeyConstraint(["default_account_id"], ["accounts.id"], ondelete="SET NULL"),
|
||||
)
|
||||
op.create_index("ix_api_tokens_owner_id", "api_tokens", ["owner_id"])
|
||||
op.create_index("ix_api_tokens_token_hash", "api_tokens", ["token_hash"], unique=True)
|
||||
|
||||
op.create_table(
|
||||
"inbox_items",
|
||||
sa.Column("id", sa.Integer(), primary_key=True),
|
||||
sa.Column("owner_id", sa.Integer(), nullable=False),
|
||||
sa.Column("source", sa.String(length=32), nullable=False),
|
||||
sa.Column("status", sa.String(length=32), nullable=False, server_default="pending"),
|
||||
sa.Column("account_id", sa.Integer(), nullable=True),
|
||||
sa.Column("date", sa.Date(), nullable=False),
|
||||
sa.Column("amount_cents", sa.Integer(), nullable=False),
|
||||
sa.Column("currency", sa.String(length=3), nullable=False, server_default="EUR"),
|
||||
sa.Column("payee", sa.String(length=255), nullable=True),
|
||||
sa.Column("note", sa.Text(), nullable=True),
|
||||
sa.Column("category_id", sa.Integer(), nullable=True),
|
||||
sa.Column("transaction_id", sa.Integer(), nullable=True),
|
||||
sa.Column("external_id", sa.String(length=255), nullable=True),
|
||||
sa.Column("raw_text", sa.Text(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.ForeignKeyConstraint(["owner_id"], ["users.id"], ondelete="CASCADE"),
|
||||
sa.ForeignKeyConstraint(["account_id"], ["accounts.id"], ondelete="CASCADE"),
|
||||
sa.ForeignKeyConstraint(["category_id"], ["categories.id"], ondelete="SET NULL"),
|
||||
# SET NULL, nicht CASCADE: Wird die Buchung geloescht, muss der
|
||||
# Eingangs-Eintrag bleiben – sonst kaeme die Zahlung beim naechsten
|
||||
# Auszug unbemerkt ein zweites Mal herein.
|
||||
sa.ForeignKeyConstraint(["transaction_id"], ["transactions.id"], ondelete="SET NULL"),
|
||||
sa.UniqueConstraint("owner_id", "source", "external_id",
|
||||
name="uq_inbox_owner_source_external"),
|
||||
)
|
||||
op.create_index("ix_inbox_items_owner_id", "inbox_items", ["owner_id"])
|
||||
op.create_index("ix_inbox_items_account_id", "inbox_items", ["account_id"])
|
||||
op.create_index("ix_inbox_items_date", "inbox_items", ["date"])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("inbox_items")
|
||||
op.drop_table("api_tokens")
|
||||
Reference in New Issue
Block a user