# `--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.
