aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-07-03 17:43:28 +0200
committerDanilo M. <danix@danix.xyz>2026-07-03 17:43:28 +0200
commit5d4d393a0de8a6c429a34dc1a51cde4e703cc033 (patch)
treeaf1a83bebe3c59f6bd28487531ef4b8bb4a89c0d
parentb125853ab2e5fe3d8d0ad045609a5863e46a607a (diff)
downloadfirefly-cli-5d4d393a0de8a6c429a34dc1a51cde4e703cc033.tar.gz
firefly-cli-5d4d393a0de8a6c429a34dc1a51cde4e703cc033.zip
feat(budget): budget create
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
-rw-r--r--firefly_cli/commands/budget.py31
-rw-r--r--tests/unit/test_commands_budget.py50
2 files changed, 81 insertions, 0 deletions
diff --git a/firefly_cli/commands/budget.py b/firefly_cli/commands/budget.py
index 257bba7..990d2ad 100644
--- a/firefly_cli/commands/budget.py
+++ b/firefly_cli/commands/budget.py
@@ -41,3 +41,34 @@ def cmd_list(args, ctx):
rows = [{**r, "spent": _spent_scalar(r)} for r in rows]
output.emit(rows, human=ctx.human)
return 0
+
+
+def _create_args(p):
+ p.add_argument("name", help="budget name (must be unique)")
+ g = p.add_mutually_exclusive_group()
+ g.add_argument("--active", dest="active", action="store_true", default=True,
+ help="create active (default)")
+ g.add_argument("--inactive", dest="active", action="store_false",
+ help="create inactive")
+ p.add_argument("--currency", default=None, help="auto-budget currency code, e.g. EUR")
+ p.add_argument("--auto-budget-amount", dest="auto_budget_amount", default=None,
+ help="recurring auto-budget amount")
+ p.add_argument("--auto-budget-period", dest="auto_budget_period", default=None,
+ help="daily|weekly|monthly|quarterly|half_year|yearly")
+ p.add_argument("--auto-budget-type", dest="auto_budget_type", default=None,
+ help="reset|rollover|adjusted|none")
+
+@registry.command("budget create", help="create a budget", args=_create_args)
+def cmd_create(args, ctx):
+ body = {"name": args.name, "active": args.active}
+ if args.auto_budget_amount is not None:
+ body["auto_budget_amount"] = str(args.auto_budget_amount)
+ if args.auto_budget_period is not None:
+ body["auto_budget_period"] = args.auto_budget_period
+ if args.auto_budget_type is not None:
+ body["auto_budget_type"] = args.auto_budget_type
+ if args.currency:
+ body["currency_code"] = args.currency
+ resp = ctx.client.request("POST", "/api/v1/budgets", body=body)
+ output.emit(output.unwrap(resp), human=ctx.human)
+ return 0
diff --git a/tests/unit/test_commands_budget.py b/tests/unit/test_commands_budget.py
index 7741840..26e9dae 100644
--- a/tests/unit/test_commands_budget.py
+++ b/tests/unit/test_commands_budget.py
@@ -32,5 +32,55 @@ class TestBudgetList(unittest.TestCase):
{"start": "2026-01-01", "end": "2026-01-31"})
+class TestBudgetCreate(unittest.TestCase):
+ def test_create_minimal_active(self):
+ client = MagicMock()
+ client.request.return_value = {"data": {"id": "9", "attributes": {"name": "Fun"}}}
+ args = SimpleNamespace(name="Fun", active=True, currency=None,
+ auto_budget_amount=None, auto_budget_period=None,
+ auto_budget_type=None)
+ rc = budget.cmd_create(args, _ctx(client=client))
+ self.assertEqual(rc, 0)
+ method, path = client.request.call_args[0][:2]
+ body = client.request.call_args[1]["body"]
+ self.assertEqual((method, path), ("POST", "/api/v1/budgets"))
+ self.assertEqual(body["name"], "Fun")
+ self.assertTrue(body["active"])
+
+ def test_create_inactive_with_auto_budget(self):
+ client = MagicMock()
+ client.request.return_value = {"data": {"id": "9", "attributes": {}}}
+ args = SimpleNamespace(name="Cap", active=False, currency="EUR",
+ auto_budget_amount="500", auto_budget_period="monthly",
+ auto_budget_type="reset")
+ budget.cmd_create(args, _ctx(client=client))
+ body = client.request.call_args[1]["body"]
+ self.assertFalse(body["active"])
+ self.assertEqual(body["auto_budget_amount"], "500")
+ self.assertEqual(body["auto_budget_type"], "reset")
+ self.assertEqual(body["currency_code"], "EUR")
+
+
+class TestBudgetSpent(unittest.TestCase):
+ def test_human_mode_replaces_spent_with_scalar(self):
+ client = MagicMock()
+ client.request.return_value = {"data": [
+ {"id": "1", "attributes": {"name": "Food",
+ "spent": [{"sum": "-12.50"}, {"sum": "-2.50"}]}}]}
+ ctx = _ctx(client=client)
+ ctx.human = True
+ captured = {}
+ import firefly_cli.output as output
+ orig = output.emit
+ def spy(data, human=False, stream=None):
+ captured["rows"] = data
+ output.emit = spy
+ try:
+ budget.cmd_list(SimpleNamespace(start=None, end=None), ctx)
+ finally:
+ output.emit = orig
+ self.assertEqual(captured["rows"][0]["spent"], "-15.00")
+
+
if __name__ == "__main__":
unittest.main()