aboutsummaryrefslogtreecommitdiffstats
path: root/firefly_cli
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-06-30 11:06:44 +0200
committerDanilo M. <danix@danix.xyz>2026-06-30 11:06:44 +0200
commita9b7872fd85cbde483bc65fc1540a9d7f0c5d193 (patch)
tree6632adfe62bb8d05ea2802e5c4f11a3541cb6f75 /firefly_cli
parent28bde5b10abf904212dfc3cae937112134293053 (diff)
downloadfirefly-cli-a9b7872fd85cbde483bc65fc1540a9d7f0c5d193.tar.gz
firefly-cli-a9b7872fd85cbde483bc65fc1540a9d7f0c5d193.zip
feat: transaction add/list/get/search with type inference
Diffstat (limited to 'firefly_cli')
-rw-r--r--firefly_cli/commands/__init__.py2
-rw-r--r--firefly_cli/commands/transaction.py95
2 files changed, 96 insertions, 1 deletions
diff --git a/firefly_cli/commands/__init__.py b/firefly_cli/commands/__init__.py
index 67cab2e..8204db6 100644
--- a/firefly_cli/commands/__init__.py
+++ b/firefly_cli/commands/__init__.py
@@ -1,3 +1,3 @@
# Copyright (C) 2026 Danilo M. <danix@danix.xyz> GPL-2.0-only
# Importing each module runs its @registry.command decorators.
-from firefly_cli.commands import auth, account, category, tag # noqa: F401
+from firefly_cli.commands import auth, account, category, tag, transaction # noqa: F401
diff --git a/firefly_cli/commands/transaction.py b/firefly_cli/commands/transaction.py
new file mode 100644
index 0000000..7d48f78
--- /dev/null
+++ b/firefly_cli/commands/transaction.py
@@ -0,0 +1,95 @@
+# Copyright (C) 2026 Danilo M. <danix@danix.xyz> GPL-2.0-only
+from firefly_cli import registry, output
+
+# Inference table keyed by (source_type, destination_type) -> firefly tx type.
+def _infer_type(src_type, dst_type):
+ s, d = (src_type or "").lower(), (dst_type or "").lower()
+ if s == "asset" and d == "asset":
+ return "transfer"
+ if s in ("revenue",) and d == "asset":
+ return "deposit"
+ if s == "asset" and d in ("expense",):
+ return "withdrawal"
+ # Fallback: asset source -> withdrawal, asset dest -> deposit.
+ if s == "asset":
+ return "withdrawal"
+ if d == "asset":
+ return "deposit"
+ raise ValueError(
+ f"Cannot infer transaction type from {src_type!r}->{dst_type!r}; "
+ "pass --type withdrawal|deposit|transfer.")
+
+def _add_args(p):
+ p.add_argument("amount")
+ p.add_argument("--from", dest="source", required=True, help="source account")
+ p.add_argument("--to", dest="dest", required=True, help="destination account")
+ p.add_argument("--desc", default=None)
+ p.add_argument("--date", default=None, help="YYYY-MM-DD (default today)")
+ p.add_argument("--category", default=None)
+ p.add_argument("--tags", default=None, help="comma-separated")
+ p.add_argument("--type", default=None,
+ help="withdrawal|deposit|transfer (overrides inference)")
+
+@registry.command("tx add", help="record a transaction", args=_add_args)
+def cmd_add(args, ctx):
+ src = ctx.resolver.account(args.source)
+ dst = ctx.resolver.account(args.dest)
+ ttype = args.type or _infer_type(src.get("type"), dst.get("type"))
+ from datetime import date as _date
+ split = {
+ "type": ttype,
+ "date": args.date or _date.today().isoformat(),
+ "amount": str(args.amount),
+ "description": args.desc or "",
+ "source_id": src["id"],
+ "destination_id": dst["id"],
+ }
+ if args.category:
+ split["category_name"] = ctx.resolver.category(args.category).get("name", args.category)
+ if args.tags:
+ split["tags"] = [t.strip() for t in args.tags.split(",") if t.strip()]
+ resp = ctx.client.request("POST", "/api/v1/transactions",
+ body={"transactions": [split]})
+ output.emit(output.unwrap(resp), human=ctx.human)
+ return 0
+
+def _list_args(p):
+ p.add_argument("--since", default=None, help="start date YYYY-MM-DD")
+ p.add_argument("--until", default=None, help="end date YYYY-MM-DD")
+ p.add_argument("--account", default=None, help="filter by account name")
+ p.add_argument("--limit", type=int, default=20)
+
+@registry.command("tx list", help="list transactions", args=_list_args)
+def cmd_list(args, ctx):
+ if args.account:
+ acc = ctx.resolver.account(args.account)
+ path = f"/api/v1/accounts/{acc['id']}/transactions"
+ else:
+ path = "/api/v1/transactions"
+ params = {"limit": args.limit}
+ if args.since:
+ params["start"] = args.since
+ if args.until:
+ params["end"] = args.until
+ resp = ctx.client.request("GET", path, params=params)
+ output.emit(output.unwrap(resp), human=ctx.human)
+ return 0
+
+def _id_arg(p):
+ p.add_argument("id")
+
+@registry.command("tx get", help="show one transaction", args=_id_arg)
+def cmd_get(args, ctx):
+ resp = ctx.client.request("GET", f"/api/v1/transactions/{args.id}")
+ output.emit(output.unwrap(resp), human=ctx.human)
+ return 0
+
+def _query_arg(p):
+ p.add_argument("query")
+
+@registry.command("tx search", help="search transactions", args=_query_arg)
+def cmd_search(args, ctx):
+ resp = ctx.client.request("GET", "/api/v1/search/transactions",
+ params={"query": args.query})
+ output.emit(output.unwrap(resp), human=ctx.human)
+ return 0