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:
@@ -531,3 +531,37 @@ def test_szenario_loeschen_roundtrip_gui(client, db):
|
||||
db.commit()
|
||||
assert client.delete(f"/api/scenarios/{sc.id}", headers=H).status_code == 204
|
||||
assert "Wegwerf-Roundtrip" not in client.get("/szenarien").text
|
||||
|
||||
|
||||
def test_szenarien_seite_rechnet_veraltete_projektion_neu(client, db):
|
||||
from sqlalchemy import func, select
|
||||
from app.models.tables import ProjectionPoint
|
||||
from app.services.projection_service import run_projection
|
||||
client.post("/login", data={"username": "admin", "password": "geheim"})
|
||||
sc = Scenario(name="Veraltet", description="", include_recurring=False,
|
||||
include_planned=False)
|
||||
db.add(sc)
|
||||
db.commit()
|
||||
run_projection(db, sc, 10, date.today() - timedelta(days=3))
|
||||
assert client.get("/szenarien").status_code == 200
|
||||
first = db.execute(select(func.min(ProjectionPoint.day))
|
||||
.where(ProjectionPoint.scenario_id == sc.id)).scalar()
|
||||
assert first == date.today() + timedelta(days=1)
|
||||
|
||||
|
||||
def test_refresh_fehler_bricht_seiten_nicht(client, monkeypatch):
|
||||
def kaputt(session, today):
|
||||
raise RuntimeError("absichtlich kaputt")
|
||||
monkeypatch.setattr("app.routers.gui.refresh_stale_projections", kaputt)
|
||||
client.post("/login", data={"username": "admin", "password": "geheim"})
|
||||
for path in ("/", "/szenarien"):
|
||||
assert client.get(path).status_code == 200, path
|
||||
|
||||
|
||||
def test_v_projection_zeigt_ist_serie_und_keine_alten_punkte():
|
||||
from app.models.views import RESERVED_SCENARIO_NAME, VIEWS
|
||||
body = VIEWS["v_projection"]
|
||||
assert f"'{RESERVED_SCENARIO_NAME}'" in body
|
||||
assert "p.day > CURRENT_DATE" in body
|
||||
names = list(VIEWS)
|
||||
assert names.index("v_balance_total") < names.index("v_projection")
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
from datetime import date, timedelta
|
||||
|
||||
from sqlalchemy import func, select
|
||||
|
||||
from app.config import get_settings
|
||||
from app.models.tables import ProjectionPoint, ProjectionResult, Scenario
|
||||
from app.services.projection_service import refresh_stale_projections, run_projection
|
||||
|
||||
TODAY = date(2026, 9, 28)
|
||||
|
||||
|
||||
def _scenario(db, name):
|
||||
sc = Scenario(name=name, description="", include_recurring=False,
|
||||
include_planned=False)
|
||||
db.add(sc)
|
||||
db.commit()
|
||||
return sc
|
||||
|
||||
|
||||
def _first_day(db, sid):
|
||||
return db.execute(select(func.min(ProjectionPoint.day))
|
||||
.where(ProjectionPoint.scenario_id == sid)).scalar()
|
||||
|
||||
|
||||
def _count(db, sid):
|
||||
return db.execute(select(func.count()).select_from(ProjectionPoint)
|
||||
.where(ProjectionPoint.scenario_id == sid)).scalar()
|
||||
|
||||
|
||||
def test_veraltetes_szenario_wird_ab_heute_neu_gerechnet(db):
|
||||
sc = _scenario(db, "Alt")
|
||||
run_projection(db, sc, 10, TODAY - timedelta(days=5))
|
||||
assert _first_day(db, sc.id) == TODAY - timedelta(days=4)
|
||||
assert refresh_stale_projections(db, TODAY) == 1
|
||||
assert _first_day(db, sc.id) == TODAY + timedelta(days=1)
|
||||
assert _count(db, sc.id) == get_settings().horizon_days
|
||||
|
||||
|
||||
def test_gestern_gerechnet_gilt_als_veraltet(db):
|
||||
sc = _scenario(db, "Gestern")
|
||||
run_projection(db, sc, 10, TODAY - timedelta(days=1))
|
||||
assert refresh_stale_projections(db, TODAY) == 1
|
||||
|
||||
|
||||
def test_heute_gerechnet_bleibt_unveraendert(db):
|
||||
sc = _scenario(db, "Heute")
|
||||
run_projection(db, sc, 10, TODAY)
|
||||
before = db.get(ProjectionResult, sc.id).computed_at
|
||||
assert refresh_stale_projections(db, TODAY) == 0
|
||||
assert _count(db, sc.id) == 10
|
||||
assert db.get(ProjectionResult, sc.id).computed_at == before
|
||||
|
||||
|
||||
def test_nie_gerechnetes_szenario_bleibt_ohne_ergebnis(db):
|
||||
sc = _scenario(db, "Nie")
|
||||
assert refresh_stale_projections(db, TODAY) == 0
|
||||
assert db.get(ProjectionResult, sc.id) is None
|
||||
assert _count(db, sc.id) == 0
|
||||
|
||||
|
||||
def test_refresh_ist_idempotent(db):
|
||||
sc = _scenario(db, "Zweimal")
|
||||
run_projection(db, sc, 10, TODAY - timedelta(days=3))
|
||||
assert refresh_stale_projections(db, TODAY) == 1
|
||||
assert refresh_stale_projections(db, TODAY) == 0
|
||||
assert _count(db, sc.id) == get_settings().horizon_days
|
||||
@@ -0,0 +1,82 @@
|
||||
"""Postgres-only-Prüfung der Grafana-Views (Ausbaustufe 11).
|
||||
|
||||
Läuft nur mit FB_TEST_PG_URL (Wegwerf-Postgres, DB-Name muss auf '_test'
|
||||
enden - Schutz gegen versehentliche Ausführung gegen die Produktiv-DB).
|
||||
Beispiel:
|
||||
podman run -d --rm --name a11-pg -e POSTGRES_PASSWORD=wegwerf \
|
||||
-e POSTGRES_DB=fb_test -p 127.0.0.1:55432:5432 docker.io/library/postgres:17.10
|
||||
FB_TEST_PG_URL=postgresql+psycopg://postgres:wegwerf@127.0.0.1:55432/fb_test \
|
||||
.venv/bin/python -m pytest -q tests/test_views_pg.py
|
||||
"""
|
||||
import os
|
||||
from datetime import timedelta
|
||||
from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import create_engine, text
|
||||
from sqlalchemy.engine import make_url
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db import Base
|
||||
import app.models # noqa: F401
|
||||
from app.models.tables import (Account, ProjectionPoint, ProjectionResult,
|
||||
Scenario, Transaction)
|
||||
from app.models.views import RESERVED_SCENARIO_NAME, VIEWS, create_views
|
||||
|
||||
PG_URL = os.environ.get("FB_TEST_PG_URL")
|
||||
pytestmark = pytest.mark.skipif(not PG_URL, reason="FB_TEST_PG_URL nicht gesetzt")
|
||||
|
||||
|
||||
def _reset(engine):
|
||||
with engine.begin() as conn:
|
||||
for name in VIEWS:
|
||||
conn.execute(text(f"DROP VIEW IF EXISTS {name} CASCADE"))
|
||||
Base.metadata.drop_all(engine)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pg():
|
||||
assert make_url(PG_URL).database.endswith("_test"), "nur Wegwerf-DB *_test!"
|
||||
engine = create_engine(PG_URL)
|
||||
_reset(engine)
|
||||
Base.metadata.create_all(engine)
|
||||
create_views(engine)
|
||||
with Session(engine) as session:
|
||||
yield session
|
||||
_reset(engine)
|
||||
engine.dispose()
|
||||
|
||||
|
||||
def test_v_projection_ist_serie_und_szenario_ab_heute(pg):
|
||||
today = pg.execute(text("SELECT CURRENT_DATE")).scalar()
|
||||
acc = Account(bank="t", iban="DE-PG-1", name="Testkonto", type="giro",
|
||||
anchor_date=today - timedelta(days=10),
|
||||
anchor_balance=Decimal("1000.00"))
|
||||
pg.add(acc)
|
||||
pg.flush()
|
||||
pg.add(Transaction(account_id=acc.id, booking_date=today - timedelta(days=2),
|
||||
amount=Decimal("-100.00"), purpose="t", counterparty="t",
|
||||
status="confirmed", dedup_hash="pg-1"))
|
||||
stale = Scenario(name="Veraltet", description="")
|
||||
empty = Scenario(name="Ohne Punkte", description="")
|
||||
pg.add_all([stale, empty])
|
||||
pg.flush()
|
||||
# Veraltete Momentaufnahme: Punkte von heute-5 bis heute+5.
|
||||
for off in range(-5, 6):
|
||||
pg.add(ProjectionPoint(scenario_id=stale.id, day=today + timedelta(days=off),
|
||||
balance=Decimal("5000.00")))
|
||||
pg.add(ProjectionResult(scenario_id=stale.id, computed_at=today,
|
||||
low_point_date=today, low_point_balance=Decimal("0")))
|
||||
pg.commit()
|
||||
|
||||
rows = pg.execute(text(
|
||||
"SELECT scenario, day, balance FROM v_projection ORDER BY scenario, day")).all()
|
||||
ist = [r for r in rows if r.scenario == RESERVED_SCENARIO_NAME]
|
||||
sc = [r for r in rows if r.scenario == "Veraltet"]
|
||||
assert max(r.day for r in ist) == today
|
||||
assert [r.balance for r in ist if r.day == today] == [Decimal("900.00")]
|
||||
# Szenario startet heute am Ist-Saldo, keine alten Punkte davor.
|
||||
assert sc[0].day == today and sc[0].balance == Decimal("900.00")
|
||||
assert all(r.day >= today for r in sc)
|
||||
assert [r.balance for r in sc[1:]] == [Decimal("5000.00")] * 5
|
||||
assert not any(r.scenario == "Ohne Punkte" for r in rows)
|
||||
Reference in New Issue
Block a user