aboutsummaryrefslogtreecommitdiffstats
path: root/firefly_cli
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-07-01 10:53:58 +0200
committerDanilo M. <danix@danix.xyz>2026-07-01 10:53:58 +0200
commit1c807048f8c754b421dae5a07ee8c053e3788348 (patch)
tree252dcd2c199c3a8a68e76539eb2a07f695c49549 /firefly_cli
parent69647b1ca6f2e11429850b0782c65d04b86509cb (diff)
downloadfirefly-cli-1c807048f8c754b421dae5a07ee8c053e3788348.tar.gz
firefly-cli-1c807048f8c754b421dae5a07ee8c053e3788348.zip
feat: tx add --skip-dupes for idempotent imports (v0.3.4)
Re-running an import (a retried or double-read batch) created phantom duplicate transactions and drifted balances, with no signal. --skip-dupes searches for an existing tx with the same amount + date + source + destination before writing; on a match it emits {"skipped": "duplicate", "matched_id": "<id>"} and exits 0 without writing. Off by default (one extra search per add only when set). dry-run wins over skip-dupes: a preview never triggers the search. Match is amount+date+ accounts, not description, so genuinely distinct same-value same-day transfers between the same accounts look like duplicates; documented. Also document a Firefly quirk in SKILL.md: a missing/deleted transaction id returns 401 (not 404) on tx get/edit/delete, so a 401 after delete confirms the record is gone rather than signalling an auth failure. Verified live on the test instance: create -> identical --skip-dupes skips with matched_id and no write, --dry-run --skip-dupes previews without searching, no-match --skip-dupes writes; records cleaned up after. PATCH: new optional flag, contract unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diffstat (limited to 'firefly_cli')
-rw-r--r--firefly_cli/__init__.py2
-rw-r--r--firefly_cli/commands/transaction.py14
2 files changed, 15 insertions, 1 deletions
diff --git a/firefly_cli/__init__.py b/firefly_cli/__init__.py
index 23ac8eb..b7a8764 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.3"
+__version__ = "0.3.4"
diff --git a/firefly_cli/commands/transaction.py b/firefly_cli/commands/transaction.py
index 72d2606..8d858b6 100644
--- a/firefly_cli/commands/transaction.py
+++ b/firefly_cli/commands/transaction.py
@@ -32,6 +32,8 @@ def _add_args(p):
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")
+ p.add_argument("--skip-dupes", dest="skip_dupes", action="store_true",
+ help="skip if a tx with same amount+date+source+destination exists")
@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):
@@ -54,8 +56,20 @@ def cmd_add(args, ctx):
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.
+ # dry-run wins over skip-dupes: caller wants a preview, not a write.
output.emit({"dry_run": True, "would_send": split}, human=ctx.human)
return 0
+ if args.skip_dupes:
+ # Firefly's search matches amount numerically; names quoted for spaces.
+ query = (f'amount_is:{split["amount"]} date_on:{split["date"]} '
+ f'source_account_is:"{src["name"]}" '
+ f'destination_account_is:"{dst["name"]}"')
+ hits = output.unwrap(ctx.client.request(
+ "GET", "/api/v1/search/transactions", params={"query": query}))
+ if hits:
+ output.emit({"skipped": "duplicate", "matched_id": hits[0].get("id")},
+ human=ctx.human)
+ return 0
resp = ctx.client.request("POST", "/api/v1/transactions",
body={"transactions": [split]})
output.emit(output.unwrap(resp), human=ctx.human)