Bisher lag sie nur am Eingangs-Eintrag. Das reicht nicht: Der Eintrag ist eine Zwischenstation und faellt weg, wenn ein Konto geloescht wird oder ein aelteres Backup ihn nicht kannte. Dann war die Zahlung fuer den naechsten Abruf neu, obwohl sie laengst gebucht war. Am schlimmsten bei einem als Uebertrag gebuchten Umsatz: Der ist von der Aehnlichkeitspruefung ausgenommen, dort griff also gar nichts mehr. Die Kennung gehoert deshalb an die Buchung (transactions.source_uid, mit der Quelle davor). Dort ueberlebt sie alles, was mit dem Eingang passiert, und die Eindeutigkeit haelt die Datenbank selbst durch – nicht erst der pruefende Code davor. Gestempelt wird an allen drei Buchungswegen: Eingang, Kurzbefehl und Uebertrag. Vorhandene Buchungen bekommen die Kennung nachgetragen, solange ihr Eingangs-Eintrag noch da ist. Ohne Kennung – von Hand erfasst, aus einer CSV-Datei – bleibt es beim bisherigen Schutz ueber die Aehnlichkeit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
71 lines
2.8 KiB
Python
71 lines
2.8 KiB
Python
"""Die Kennung der Quelle steht an der Buchung selbst.
|
||
|
||
Bisher lag sie nur am Eingangs-Eintrag (``inbox_items.external_id``). Das hat
|
||
gereicht, solange dieser Eintrag da war – und genau das ist er nicht immer: Er
|
||
faellt weg, wenn ein Konto geloescht wird, er fehlt in aelteren Backups, und
|
||
nach einem neu verbundenen Bankkonto passt sein Praefix nicht mehr. Dann war
|
||
die Zahlung fuer den naechsten Abruf neu, obwohl sie laengst gebucht war.
|
||
|
||
Die Kennung gehoert deshalb an die Buchung. Dort ueberlebt sie alles, was mit
|
||
dem Eingang passiert, und die Eindeutigkeit haelt die Datenbank selbst durch –
|
||
nicht erst der pruefende Code davor.
|
||
|
||
Vorhandene Buchungen bekommen die Kennung nachgetragen, solange ihr
|
||
Eingangs-Eintrag noch existiert. Was jetzt schon verloren ist, bleibt verloren;
|
||
ab hier faellt nichts Neues mehr aus.
|
||
|
||
Revision ID: 0027
|
||
Revises: 0026
|
||
"""
|
||
|
||
import sqlalchemy as sa
|
||
from alembic import op
|
||
|
||
revision = "0027"
|
||
down_revision = "0026"
|
||
branch_labels = None
|
||
depends_on = None
|
||
|
||
|
||
def upgrade() -> None:
|
||
with op.batch_alter_table("transactions") as batch:
|
||
batch.add_column(sa.Column("source_uid", sa.String(length=320), nullable=True))
|
||
|
||
# Nachtragen, was der Eingang noch weiss. Der Name der Quelle kommt davor:
|
||
# Eine Nachrichten-Kennung aus dem Postfach und eine Umsatzkennung der Bank
|
||
# duerfen nie zufaellig gleich lauten.
|
||
#
|
||
# Zwei Eingangs-Eintraege koennen auf dieselbe Buchung zeigen (die beiden
|
||
# Seiten eines Uebertrags). Dann gewinnt der aeltere – ``MIN(id)``. Zwei
|
||
# verschiedene Buchungen koennen dagegen nicht dieselbe Kennung bekommen:
|
||
# (owner_id, source, external_id) ist im Eingang bereits eindeutig.
|
||
op.execute(sa.text("""
|
||
UPDATE transactions SET source_uid = (
|
||
SELECT i.source || ':' || i.external_id
|
||
FROM inbox_items i
|
||
WHERE i.transaction_id = transactions.id
|
||
AND i.owner_id = transactions.owner_id
|
||
AND i.external_id IS NOT NULL
|
||
ORDER BY i.id
|
||
LIMIT 1
|
||
)
|
||
WHERE source_uid IS NULL
|
||
"""))
|
||
|
||
# Erst jetzt eindeutig stellen – vorher stuende die Regel ueber leeren
|
||
# Spalten und der Nachtrag koennte sie nicht mehr fuellen.
|
||
#
|
||
# Ein INDEX, keine UNIQUE-Spaltenregel: Letztere ginge nur ueber einen
|
||
# Tabellen-Umbau, und der loescht auf SQLite per ON DELETE CASCADE die
|
||
# Uebertrags-Verknuepfungen und abgelehnten Vorschlaege mit. Genau davor
|
||
# steht tests/test_migrations.py. Der Index haelt dasselbe durch, ohne die
|
||
# Tabelle anzufassen.
|
||
op.create_index("uq_txn_owner_source_uid", "transactions",
|
||
["owner_id", "source_uid"], unique=True)
|
||
|
||
|
||
def downgrade() -> None:
|
||
op.drop_index("uq_txn_owner_source_uid", table_name="transactions")
|
||
with op.batch_alter_table("transactions") as batch:
|
||
batch.drop_column("source_uid")
|