# `--from-id`/`--to-id` for `tx add` Date: 2026-07-02 Resolves: ISSUES.md #2 (same-name accounts unresolvable via CLI) ## Problem Two accounts share the name "Nexi": expense (id 52, the 15th debt-payment target) and revenue (id 129, the 1st-of-month plafond-refill source). `firefly tx add --from/--to "Nexi"` fails with `Ambiguous account "Nexi" matches ids 52, 129`. The CLI accepts names only; a numeric id is treated as a literal name and also fails. The intended two-same-name-account cycle cannot be driven by the CLI. ## Goal Add an id escape hatch to `tx add` so an ambiguous (or any) account can be targeted by numeric id, bypassing name resolution. ## Design ### New resolver method (`resolver.py`) ```python def account_by_id(self, acc_id): resp = self.client.request("GET", f"/api/v1/accounts/{acc_id}") item = resp["data"] return {"id": item["id"], **item["attributes"]} ``` GET a single account by id. Returns the same dict shape as `account(name)` (`{"id", "name", "type", ...}`) so all downstream code is path-agnostic. A bad id yields a 404 that `client.request` already surfaces as a `FireflyError` — this is the existence validation (per the "validate id" decision), no extra list scan needed. ### Flags (`transaction.py` `_add_args`) - Add `--from-id` (dest `source_id`) and `--to-id` (dest `dest_id`). - Drop `required=True` from `--from`/`--to`; requirement is now one-of-two per side, enforced in the handler (argparse can't express XOR cleanly). ### Handler validation (`cmd_add`) Per side, exactly one of the name/id pair must be supplied: - source: exactly one of `args.source`, `args.source_id` - dest: exactly one of `args.dest`, `args.dest_id` Zero or both on a side → `FireflyError` with a clear message. Sides are independent: `--from NAME --to-id 129` is valid (name on one side, id on the other). ### Resolution ```python src = (ctx.resolver.account_by_id(args.source_id) if args.source_id else ctx.resolver.account(args.source)) dst = (ctx.resolver.account_by_id(args.dest_id) if args.dest_id else ctx.resolver.account(args.dest)) ``` Everything downstream is unchanged: type inference, the transfer-direction stderr echo, the skip-dupes search query, and the emitted split all use `src`/`dst` dicts identically for both paths. ## Contract impact → PATCH (v0.3.7) New optional flags. `--from`/`--to` are no longer argparse-required, but the handler still requires one per side, so existing name-only callers are unaffected. JSON output shape and exit codes are unchanged. Per the contract-keyed scheme this is a PATCH. ## Tests (mocked) - id path resolves via `account_by_id` and writes the split with that id - missing both name and id on a side → error - both name and id on the same side → error - mixed: name on source, id on dest → works - a 404 from `account_by_id` surfaces as a FireflyError ## Out of scope (YAGNI) - id support on `tx list` or other name-taking commands — add when a real blocker appears; `tx list` was not reported as blocked. - `--from-type`/type-scoped resolution — the id flags cover disambiguation. - ISSUES.md #1 (liability account creation) — separate, larger, next session.