fix: Szenario-Vergleich zeigt vor heute Ist-Saldo, veraltete Szenarien werden neu gerechnet (A11-T2)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-28 19:10:51 +02:00
co-authored by Claude Opus 5.5
parent 32d78fdbe1
commit 979e1c0edc
7 changed files with 261 additions and 5 deletions
+31 -1
View File
@@ -2,10 +2,11 @@ from datetime import date, timedelta
from datetime import datetime, timezone
from decimal import Decimal
from sqlalchemy import delete, select
from sqlalchemy import delete, func, select
from sqlalchemy.orm import Session
from app.config import get_settings
from app.db import advisory_xact_lock
from app.engine.loans import loan_schedule
from app.engine.projection import project
from app.engine.scenario import (PlainModifier, PlainPlanned, PlainRecurring,
@@ -63,3 +64,32 @@ def run_projection(session: Session, scenario: Scenario, horizon_days: int,
session.add(result)
session.commit()
return result
# Advisory-Lock-Schlüssel (beliebig, aber projektweit eindeutig).
STALE_PROJECTION_LOCK = 110_001
def refresh_stale_projections(session: Session, today: date) -> int:
"""Rechnet jedes bereits durchgerechnete Szenario neu, dessen Projektion
vor `today` beginnt (erster gespeicherter Punkt <= today, d.h. Rechen-Start
< today). Maßstab ist der Projektionsbeginn, nicht computed_at (UTC-
Zeitstempel, würde um Mitternacht falsch schneiden). Nie gerechnete
Szenarien bleiben unberührt. Je Szenario: Lock nehmen, Staleness NEU
prüfen (ein paralleler Seitenaufruf kann schon gerechnet haben), rechnen;
run_projection committet und gibt damit den Lock frei.
Rückgabe: Anzahl neu gerechneter Szenarien."""
horizon = get_settings().horizon_days
ids = list(session.execute(select(ProjectionResult.scenario_id)).scalars())
refreshed = 0
for sid in ids:
advisory_xact_lock(session, STALE_PROJECTION_LOCK)
first = session.execute(select(func.min(ProjectionPoint.day))
.where(ProjectionPoint.scenario_id == sid)).scalar()
scenario = session.get(Scenario, sid)
if scenario is None or first is None or first > today:
session.commit() # Lock freigeben
continue
run_projection(session, scenario, horizon, today)
refreshed += 1
return refreshed