summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--SKILL.md10
-rw-r--r--completions/firefly.bash2
-rw-r--r--firefly_cli/__init__.py2
-rw-r--r--firefly_cli/commands/transaction.py6
-rw-r--r--pyproject.toml2
-rw-r--r--tests/unit/test_commands_transaction.py31
6 files changed, 45 insertions, 8 deletions
diff --git a/SKILL.md b/SKILL.md
index 672c97e..42d5fb4 100644
--- a/SKILL.md
+++ b/SKILL.md
@@ -56,7 +56,7 @@ firefly account balance <name|id> [--at YYYY-MM-DD]
firefly account create <name> --type asset|expense|revenue
[--opening-balance N] [--currency CODE]
firefly tx add <amount> --from <acct> --to <acct>
- [--desc TEXT] [--date YYYY-MM-DD] [--category NAME] [--tags a,b] [--type T]
+ [--desc TEXT] [--date YYYY-MM-DD] [--category NAME] [--tags a,b] [--type T] [--dry-run]
firefly tx edit <id>
[--amount N] [--date YYYY-MM-DD] [--desc TEXT] [--from <acct>] [--to <acct>]
[--category NAME] [--tags a,b] [--type T] # only fields passed are changed
@@ -102,6 +102,14 @@ Supports asset, expense, revenue. asset accounts get the default role
automatically. Unlike categories/tags, accounts are NOT auto-created by
`tx add`, create them explicitly here first.
+**Validate a batch before writing any of it:** when importing many rows in a
+loop, a mid-batch failure (a `--to` account that doesn't exist yet) leaves the
+earlier rows already written. Dry-run each row first so the whole batch fails
+fast: `tx add ... --dry-run` resolves `--from`/`--to` and infers the type but
+sends nothing, printing `{"dry_run": true, "would_send": {...}}`. A missing
+account is still a hard error (exit 1). Recipe: dry-run every row, create any
+accounts the errors name, then run the batch for real.
+
**Check a balance:**
```bash
firefly account balance test01 # -> {"id","name","current_balance"}
diff --git a/completions/firefly.bash b/completions/firefly.bash
index b23f081..2dbe218 100644
--- a/completions/firefly.bash
+++ b/completions/firefly.bash
@@ -40,7 +40,7 @@ _firefly() {
"account balance") leaf_opts="--at";;
"account create") leaf_opts="--currency --opening-balance --type";;
"account list") leaf_opts="--type";;
- "tx add") leaf_opts="--category --date --desc --from --tags --to --type";;
+ "tx add") leaf_opts="--category --date --desc --dry-run --from --tags --to --type";;
"tx delete") leaf_opts="--yes";;
"tx edit") leaf_opts="--amount --category --date --desc --from --tags --to --type";;
"tx list") leaf_opts="--account --all --limit --since --until";;
diff --git a/firefly_cli/__init__.py b/firefly_cli/__init__.py
index eb73e59..23ac8eb 100644
--- a/firefly_cli/__init__.py
+++ b/firefly_cli/__init__.py
@@ -2,4 +2,4 @@
# Copyright (C) 2026 Danilo M. <danix@danix.xyz>
# Licensed under the GNU General Public License v2.0 only.
-__version__ = "0.3.2"
+__version__ = "0.3.3"
diff --git a/firefly_cli/commands/transaction.py b/firefly_cli/commands/transaction.py
index 1d6f219..72d2606 100644
--- a/firefly_cli/commands/transaction.py
+++ b/firefly_cli/commands/transaction.py
@@ -30,6 +30,8 @@ def _add_args(p):
p.add_argument("--tags", default=None, help="comma-separated")
p.add_argument("--type", default=None,
help="withdrawal|deposit|transfer (overrides inference)")
+ p.add_argument("--dry-run", dest="dry_run", action="store_true",
+ help="resolve accounts and show what would be sent; write nothing")
@registry.command("tx add", help="record a transaction; source/destination resolve to accounts, category/tags auto-create", args=_add_args)
def cmd_add(args, ctx):
@@ -50,6 +52,10 @@ def cmd_add(args, ctx):
split["category_name"] = args.category
if args.tags:
split["tags"] = [t.strip() for t in args.tags.split(",") if t.strip()]
+ if args.dry_run:
+ # Accounts already resolved above (missing name = hard error). Write nothing.
+ output.emit({"dry_run": True, "would_send": split}, human=ctx.human)
+ return 0
resp = ctx.client.request("POST", "/api/v1/transactions",
body={"transactions": [split]})
output.emit(output.unwrap(resp), human=ctx.human)
diff --git a/pyproject.toml b/pyproject.toml
index fd0d369..4b4bf9b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "firefly-iii-agent"
-version = "0.3.2"
+version = "0.3.3"
description = "CLI tool for agent interaction with Firefly III"
readme = "README.md"
requires-python = ">=3.11"
diff --git a/tests/unit/test_commands_transaction.py b/tests/unit/test_commands_transaction.py
index d8b81da..a6b5228 100644
--- a/tests/unit/test_commands_transaction.py
+++ b/tests/unit/test_commands_transaction.py
@@ -19,7 +19,7 @@ class TestTxAdd(unittest.TestCase):
"attributes": {}}}
args = MagicMock(amount="42.50", source="Checking", dest="Groceries",
desc="food", date="2026-06-30", category=None,
- tags=None, type=None)
+ tags=None, type=None, dry_run=False)
rc = tx.cmd_add(args, ctx)
self.assertEqual(rc, 0)
method, path = client.request.call_args[0][:2]
@@ -39,7 +39,8 @@ class TestTxAdd(unittest.TestCase):
}[n]
client.request.return_value = {"data": {"id": "1", "attributes": {}}}
args = MagicMock(amount="1000", source="Salary", dest="Checking",
- desc="pay", date=None, category=None, tags=None, type=None)
+ desc="pay", date=None, category=None, tags=None,
+ type=None, dry_run=False)
tx.cmd_add(args, ctx)
self.assertEqual(client.request.call_args[1]["body"]["transactions"][0]["type"],
"deposit")
@@ -49,7 +50,8 @@ class TestTxAdd(unittest.TestCase):
resolver.account.side_effect = lambda n: {"id": "1", "type": "asset", "name": n}
client.request.return_value = {"data": {"id": "1", "attributes": {}}}
args = MagicMock(amount="5", source="A", dest="B", desc=None, date=None,
- category=None, tags="food,fun", type="transfer")
+ category=None, tags="food,fun", type="transfer",
+ dry_run=False)
tx.cmd_add(args, ctx)
split = client.request.call_args[1]["body"]["transactions"][0]
self.assertEqual(split["type"], "transfer")
@@ -61,10 +63,31 @@ class TestTxAdd(unittest.TestCase):
resolver.account.side_effect = lambda n: {"id": "1", "type": "asset", "name": n}
client.request.return_value = {"data": {"id": "1", "attributes": {}}}
args = MagicMock(amount="5", source="A", dest="B", desc=None, date=None,
- category="Brand New Cat", tags=None, type="withdrawal")
+ category="Brand New Cat", tags=None, type="withdrawal",
+ dry_run=False)
tx.cmd_add(args, ctx)
split = client.request.call_args[1]["body"]["transactions"][0]
self.assertEqual(split["category_name"], "Brand New Cat")
+
+ def test_dry_run_resolves_but_does_not_post(self):
+ ctx, client, resolver = make_ctx()
+ resolver.account.side_effect = lambda n: {"id": "1", "type": "asset", "name": n}
+ args = MagicMock(amount="5", source="A", dest="B", desc="x", date="2026-06-01",
+ category=None, tags=None, type="withdrawal", dry_run=True)
+ rc = tx.cmd_add(args, ctx)
+ self.assertEqual(rc, 0)
+ client.request.assert_not_called() # accounts resolved, nothing written
+ self.assertEqual(resolver.account.call_count, 2)
+
+ def test_dry_run_missing_account_is_hard_error(self):
+ from firefly_cli.errors import ResolutionError
+ ctx, client, resolver = make_ctx()
+ resolver.account.side_effect = ResolutionError('No account named "B"')
+ args = MagicMock(amount="5", source="A", dest="B", desc=None, date=None,
+ category=None, tags=None, type="withdrawal", dry_run=True)
+ with self.assertRaises(ResolutionError):
+ tx.cmd_add(args, ctx)
+ client.request.assert_not_called()
resolver.category.assert_not_called()
class TestTxEdit(unittest.TestCase):