from datetime import date from decimal import Decimal from app.models.tables import Account, Category, RecurringItem, Statement, Transaction from app.services.suggestions import suggest_recurring H = {"Authorization": "Bearer test-key"} def _seed_balance(db, amount="1000.00"): acc = Account(bank="dkb", iban="DE01", name="G", type="giro") db.add(acc) db.flush() db.add(Statement(filename="s.pdf", bank="dkb", account_id=acc.id, period_start=date(2026, 6, 1), period_end=date(2026, 6, 30), opening_balance=Decimal("0"), closing_balance=Decimal(amount), status="confirmed")) db.commit() def test_scenario_projection(client, db): _seed_balance(db) client.post("/api/recurring", headers=H, json={ "name": "Miete", "amount": "-600.00", "rhythm": "monthly", "due_day": 1}) sc = client.post("/api/scenarios", headers=H, json={"name": "Basis"}).json() r = client.post(f"/api/scenarios/{sc['id']}/project", headers=H, params={"horizon_days": 92, "start_date": "2026-07-15"}) assert r.status_code == 200 body = r.json() # 3 Mietzahlungen (01.08., 01.09., 01.10.): 400 -> -200 -> -800 assert Decimal(body["low_point_balance"]) == Decimal("-800.00") assert body["below_zero_date"] == "2026-09-01" assert len(body["series"]) == 92 def test_loan_in_scenario_keeps_balance_positive(client, db): _seed_balance(db) client.post("/api/recurring", headers=H, json={ "name": "Miete", "amount": "-600.00", "rhythm": "monthly", "due_day": 1}) loan = client.post("/api/loans", headers=H, json={ "name": "K1", "principal": "5000.00", "annual_rate_pct": "6.0", "term_months": 48, "payout_date": "2026-07-20", "repayment_type": "annuity"}).json() sc = client.post("/api/scenarios", headers=H, json={"name": "Kredit"}).json() client.post(f"/api/scenarios/{sc['id']}/loans/{loan['id']}", headers=H) body = client.post(f"/api/scenarios/{sc['id']}/project", headers=H, params={"horizon_days": 92, "start_date": "2026-07-15"}).json() assert Decimal(body["low_point_balance"]) > Decimal("0") def _tx(acc, d, amount, counterparty, dedup, category_id=None): return Transaction(account_id=acc.id, booking_date=d, amount=Decimal(amount), purpose="", counterparty=counterparty, status="confirmed", dedup_hash=dedup, category_id=category_id) def test_suggest_recurring_three_consecutive_months_with_year_wrap(db): acc = Account(bank="dkb", iban="DE01", name="G", type="giro") db.add(acc) db.flush() cat = Category(name="Miete") db.add(cat) db.flush() # Dez 2025 -> Jan 2026 -> Feb 2026: 3 aufeinanderfolgende Monate ueber den # Jahreswechsel hinweg (prueft die Monats-Linearisierung y*12+m). db.add(_tx(acc, date(2025, 12, 1), "-600.00", "Vermieter", "h1", cat.id)) db.add(_tx(acc, date(2026, 1, 15), "-600.00", "Vermieter", "h2", cat.id)) db.add(_tx(acc, date(2026, 2, 28), "-600.00", "Vermieter", "h3", cat.id)) db.commit() out = suggest_recurring(db) # Median der Tage [1, 15, 28] = 15; Betrag unveraendert uebernommen. assert out == [{"name": "Vermieter", "amount": Decimal("-600.00"), "rhythm": "monthly", "due_day": 15, "category_id": cat.id}] def test_suggest_recurring_two_months_no_suggestion(db): acc = Account(bank="dkb", iban="DE01", name="G", type="giro") db.add(acc) db.flush() db.add(_tx(acc, date(2026, 3, 10), "-50.00", "Zweimonatig", "h1")) db.add(_tx(acc, date(2026, 4, 10), "-50.00", "Zweimonatig", "h2")) db.commit() assert suggest_recurring(db) == [] def test_suggest_recurring_excludes_existing_recurring_item(db): acc = Account(bank="dkb", iban="DE01", name="G", type="giro") db.add(acc) db.flush() db.add(_tx(acc, date(2026, 1, 5), "-30.00", "Streaming", "h1")) db.add(_tx(acc, date(2026, 2, 5), "-30.00", "Streaming", "h2")) db.add(_tx(acc, date(2026, 3, 5), "-30.00", "Streaming", "h3")) db.add(RecurringItem(name="Streaming", amount=Decimal("-30.00"), rhythm="monthly", due_day=5)) db.commit() # Gleicher Name + Betrag wie ein bereits vorhandenes RecurringItem -> ausgelassen. assert suggest_recurring(db) == []