aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-07-03 17:36:17 +0200
committerDanilo M. <danix@danix.xyz>2026-07-03 17:36:17 +0200
commit9c1c96b70dad9701498e4da7a3a8fa68e180a118 (patch)
tree55bda44d65085c8e9600ce569fa08058e3b1452d
parente1fbf7439f50b1367c8e7fbd153e4f5ea61ea48b (diff)
downloadfirefly-cli-9c1c96b70dad9701498e4da7a3a8fa68e180a118.tar.gz
firefly-cli-9c1c96b70dad9701498e4da7a3a8fa68e180a118.zip
feat(resolver): budget name/id resolution
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
-rw-r--r--firefly_cli/resolver.py13
-rw-r--r--tests/unit/test_resolver.py33
2 files changed, 46 insertions, 0 deletions
diff --git a/firefly_cli/resolver.py b/firefly_cli/resolver.py
index 7296aad..53144e1 100644
--- a/firefly_cli/resolver.py
+++ b/firefly_cli/resolver.py
@@ -42,3 +42,16 @@ class Resolver:
def category(self, name):
return self._match("category", self._list("/api/v1/categories"), name)
+
+ def budget(self, name_or_id):
+ # Budgets must pre-exist (Firefly does not auto-create them for a
+ # transaction), so we resolve to an id. A numeric-looking ref goes
+ # straight to the show endpoint (lets same-name-safe callers pin an id);
+ # otherwise match by name against the list.
+ if str(name_or_id).isdigit():
+ return self.budget_by_id(name_or_id)
+ return self._match("budget", self._list("/api/v1/budgets"), name_or_id)
+
+ def budget_by_id(self, budget_id):
+ item = self.client.request("GET", f"/api/v1/budgets/{budget_id}")["data"]
+ return {"id": item["id"], **item.get("attributes", {})}
diff --git a/tests/unit/test_resolver.py b/tests/unit/test_resolver.py
index 82ac30b..8c93a84 100644
--- a/tests/unit/test_resolver.py
+++ b/tests/unit/test_resolver.py
@@ -51,3 +51,36 @@ class TestResolver(unittest.TestCase):
acc = r.account_by_id("129")
self.assertEqual(acc, {"id": "129", "name": "Nexi", "type": "revenue"})
c.request.assert_called_once_with("GET", "/api/v1/accounts/129")
+
+
+def _budget_list_resp(budgets):
+ return {"data": [{"id": b["id"], "attributes": {"name": b["name"]}}
+ for b in budgets]}
+
+
+class TestResolverBudget(unittest.TestCase):
+ def test_budget_by_name(self):
+ client = MagicMock()
+ client.request.return_value = _budget_list_resp(
+ [{"id": "3", "name": "Groceries"}, {"id": "4", "name": "Rent"}])
+ r = Resolver(client)
+ self.assertEqual(r.budget("Groceries")["id"], "3")
+
+ def test_budget_missing_raises_with_candidates(self):
+ client = MagicMock()
+ client.request.return_value = _budget_list_resp([{"id": "3", "name": "Rent"}])
+ r = Resolver(client)
+ with self.assertRaises(ResolutionError) as cm:
+ r.budget("Nope")
+ self.assertIn("Rent", str(cm.exception))
+
+ def test_budget_numeric_ref_goes_by_id(self):
+ client = MagicMock()
+ client.request.return_value = {
+ "data": {"id": "7", "attributes": {"name": "Fun"}}}
+ r = Resolver(client)
+ got = r.budget("7")
+ self.assertEqual(got["id"], "7")
+ self.assertEqual(got["name"], "Fun")
+ # Must hit the show endpoint, not list.
+ client.request.assert_called_with("GET", "/api/v1/budgets/7")