local-review.yml (push) Invalid workflow file
Auto-version and Release / release (push) Successful in 12s
CI Pipeline / lint (push) Failing after 31s
CI Pipeline / go-surface (push) Failing after 19s
CI Pipeline / canary (push) Failing after 1m32s
CI Pipeline / best-practices (push) Successful in 7s
CI Pipeline / documentation (push) Successful in 7s
CI Pipeline / type-check (push) Failing after 36s
CI Pipeline / gitea-offline (push) Successful in 19s
CI Pipeline / rust-surface (push) Successful in 4m36s
CI Pipeline / unit-tests (push) Failing after 5m50s
deploy / deploy (push) Failing after 15m12s
CI Pipeline / required-checks (push) Failing after 1s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Successful in 9s
- scripts/finance_tracker.py: CSV ledger (~/.rune/finance/, human-readable, portable), add/budget/report commands, month + week filters, per-category totals vs budgets with over-by flags, income/net, and recurring-payment detection (3+ same-note ~monthly payments) that surfaces subscription cancellation candidates — the exact 'flags subscriptions worth cancelling' acceptance. - tests/test_finance_tracker.py 6/6: sign→kind, budget over-flag, net, week filter, monthly-subscription detection, one-off not flagged.
78 lines
2.6 KiB
Python
78 lines
2.6 KiB
Python
"""test_finance_tracker.py — #3: the ledger, budgets, report, and the
|
|
subscription flag that surfaces cancellation candidates. Isolated via
|
|
RUNE_FINANCE_DIR tmp override."""
|
|
|
|
import importlib.util
|
|
import json
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from datetime import date, timedelta
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def _load(tmp: str):
|
|
os.environ["RUNE_FINANCE_DIR"] = tmp
|
|
spec = importlib.util.spec_from_file_location("finance_tracker", ROOT / "scripts/finance_tracker.py")
|
|
mod = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(mod)
|
|
return mod
|
|
|
|
|
|
class TestFinanceTracker(unittest.TestCase):
|
|
def setUp(self):
|
|
self._tmp = tempfile.TemporaryDirectory()
|
|
self.ft = _load(self._tmp.name)
|
|
|
|
def tearDown(self):
|
|
self._tmp.cleanup()
|
|
|
|
def test_add_records_negative_as_expense(self):
|
|
row = self.ft.add(-30.0, "Groceries", "weekly shop")
|
|
self.assertEqual(row["kind"], "out")
|
|
self.assertEqual(row["amount"], "30.00")
|
|
self.assertEqual(row["category"], "groceries")
|
|
|
|
def test_budget_and_report_flags_over(self):
|
|
self.ft.set_budget("groceries", 100)
|
|
self.ft.add(-60, "groceries", "shop 1")
|
|
self.ft.add(-60, "groceries", "shop 2")
|
|
r = self.ft.report()
|
|
self.assertEqual(r["spend"], 120.0)
|
|
self.assertEqual(
|
|
r["over_budget"], [{"category": "groceries", "spent": 120.0, "budget": 100.0, "over_by": 20.0}]
|
|
)
|
|
|
|
def test_income_and_net(self):
|
|
self.ft.add(1450, "salary", "October pay")
|
|
self.ft.add(-250, "bills", "electric")
|
|
r = self.ft.report()
|
|
self.assertEqual(r["income"], 1450.0)
|
|
self.assertEqual(r["net"], 1200.0)
|
|
|
|
def test_week_filter(self):
|
|
self.ft.add(-5, "coffee", "old", when=(date.today() - timedelta(days=10)).isoformat())
|
|
self.ft.add(-4, "coffee", "recent", when=(date.today() - timedelta(days=2)).isoformat())
|
|
r = self.ft.report(week=True)
|
|
self.assertEqual(r["spend"], 4.0)
|
|
|
|
def test_subscription_detection_monthly(self):
|
|
today = date.today()
|
|
for i in (3, 2, 1):
|
|
self.ft.add(-9.99, "subscriptions", "netflix", when=(today - timedelta(days=30 * i)).isoformat())
|
|
subs = self.ft.find_subscriptions()
|
|
self.assertEqual(len(subs), 1)
|
|
self.assertEqual(subs[0]["note"], "netflix")
|
|
self.assertEqual(subs[0]["monthly"], 9.99)
|
|
|
|
def test_one_off_not_flagged(self):
|
|
self.ft.add(-120, "electronics", "headphones")
|
|
self.assertEqual(self.ft.find_subscriptions(), [])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|