Skip to content

Commit 9b3626d

Browse files
committed
fix: honor gl-report format flags
1 parent 5929459 commit 9b3626d

7 files changed

Lines changed: 162 additions & 56 deletions

File tree

02_docs/architecture.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ flowchart TD
6666
### Output Layer
6767

6868
- `output()` dispatches to text/JSON/TSV modes.
69+
- `_resolve_fmt()` defines the shared precedence rule: command-specific `--output/--format`
70+
overrides global `-f/--format`.
6971
- `_normalize_output_data()` centralizes dict/list normalization shared by text/TSV output.
7072
- `_output_kv()` handles readable single-entity rendering.
7173

@@ -76,6 +78,8 @@ flowchart TD
7678
- Report builders format that tree into text, JSON-like structures, or transaction lists.
7779
- `cmd_gl_report()` remains the orchestration point for account discovery, customer resolution,
7880
date handling, and final rendering.
81+
- `gl-report` follows the shared format resolver for `text`/`json`, adds `txns` and `expanded`,
82+
and rejects unsupported `tsv` explicitly.
7983

8084
## Safety Boundaries
8185

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# 2026-03-19 gl-format review merge
2+
3+
- Scope: review local refactor work, verify merge readiness, push `main`.
4+
- Finding: `gl-report` did not honor shared format resolution for global `-f json` and did not accept
5+
subcommand `--format json`.
6+
- Fix: route `gl-report` through `_resolve_fmt()`, add `--format` alias, reject unsupported `tsv`
7+
explicitly, add parser/handler coverage, update docs.
8+
- Verification:
9+
- `uv run ruff check qbo_cli/cli.py tests/test_commands.py tests/test_live.py README.md`
10+
- `uv run mypy qbo_cli/cli.py tests`
11+
- `uv run pytest -q`
12+
- `uv run pytest -m live -q`
13+
- Note: repo-wide Ruff still reports pre-existing unrelated issues already present on `origin/main`
14+
in untouched test files.

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,10 +214,15 @@ qbo gl-report -c "John Smith" -a 125 --currency USD
214214
# Custom date range
215215
qbo gl-report -c "John Smith" -a "Revenue" --start 2025-01-01 --end 2025-12-31
216216

217+
# Structured JSON output
218+
qbo gl-report -a 125 --format json
219+
217220
# Dates default to: first transaction → today
218221
qbo gl-report -c "John Smith" -a 125
219222
```
220223

224+
`gl-report` supports `text`, `json`, `txns`, and `expanded`. `tsv` is not available for this command.
225+
221226
### Output formats
222227

223228
```bash

THEORY.MD

Lines changed: 47 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,64 @@
1-
# Theory: Safe Internal Refactor for qbo-cli
1+
# Theory: Safe CLI Refactor With Format Contract Verification
22

33
## Problem
44

5-
`qbo_cli/cli.py` is still the center of the application, but before this pass it
6-
mixed parser construction, command dispatch, repeated command glue, and output
7-
normalization in ways that made small changes noisier than necessary. The risk
8-
was not a broken CLI; the risk was that future edits would keep paying a
9-
complexity tax in the least interesting parts of the file.
5+
`qbo_cli/cli.py` still carries the whole CLI surface, so small refactors can
6+
silently change behavior in places where parser defaults, command glue, and
7+
output selection interact. The structural cleanup already improved the file, but
8+
that kind of cleanup is only safe if the format contract stays consistent across
9+
commands. `gl-report` was the highest-risk spot because it has custom output
10+
modes and heavier orchestration than the basic CRUD/report commands.
1011

1112
## Operating Theory
1213

13-
The project is safest to improve at its seams. Unit tests already protect the
14-
public behavior of common command handlers, parser aliases, output formatting,
15-
client pagination/retry behavior, and many pure report helpers. The less-tested
16-
surfaces are OAuth callback handling, token persistence internals, and the
17-
orchestration-heavy parts of `cmd_gl_report()`. That makes the correct lever a
18-
local refactor of parser/dispatch and command plumbing, not a large module
19-
split.
14+
The right way to stabilize this codebase is to keep changes at the seams and
15+
verify the public contract aggressively. The refactor reduced duplication by
16+
centralizing parser construction, runtime creation, dispatch, stdin handling,
17+
report param building, and output emission. That increases maintainability, but
18+
it also means one mismatch in format resolution can affect an entire command
19+
surface. The leverage point is therefore not more extraction; it is making sure
20+
every command follows the same format-selection rules unless it has an explicit
21+
reason not to.
2022

2123
## Strategy
2224

23-
Keep behavior stable while reducing repetition:
25+
Treat the refactor as successful only if the CLI contract is demonstrably
26+
preserved:
2427

25-
- shrink `main()` into orchestration only
26-
- move parser building and dispatch decisions into dedicated helpers
27-
- centralize repeated command concerns: client creation, output emission, stdin
28-
JSON loading, report param building
29-
- centralize output normalization shared by text and TSV rendering
30-
- document the logical architecture so the file can stay understandable even
31-
before a future module split
28+
- review local commits and uncommitted fixes against `origin/main`
29+
- verify parser behavior directly, not only through handler unit tests
30+
- patch only the format-resolution gap at the source
31+
- add tests that lock the contract in place for both command-level and parser-level flows
32+
- validate with unit tests, live tests, and static checks that matter for the touched surface
3233

33-
The aim is structural clarity with minimal movement of risky code.
34+
The intent is still minimal movement. One root fix in the parser/output path is
35+
better than special-casing downstream rendering.
3436

3537
## Key Discoveries
3638

37-
The earlier readonly pass was useful because it separated environment gaps from
38-
real quality issues. Once the local toolchain was installed, the important fact
39-
was that `pytest` was already green, `ruff` problems were isolated to tests,
40-
and `mypy` failures in `qbo_cli/cli.py` were small enough to eliminate as part
41-
of the refactor. That turned the work from "safe cleanup only" into "safe
42-
cleanup plus static-check hardening" without widening the risk surface.
43-
44-
The current shape now matches the intended strategy: `main()` delegates to
45-
`_build_parser()`, `_build_runtime()`, and `_dispatch_command()`, command
46-
handlers share helper glue, and output normalization is centralized. The code
47-
is still a single-file CLI, but with cleaner internal boundaries.
48-
49-
The strongest confidence signal came from the live suite. Running all
50-
`@pytest.mark.live` tests against the configured QuickBooks account passed
51-
without changes to test code or to the refactored command paths. That matters
52-
because it validates the actual packaged CLI entrypoint, subprocess execution,
53-
auth status, report endpoints, and General Ledger smoke flows against the real
54-
service rather than only mocked unit-level contracts.
39+
The critical discovery from review was not inside report generation itself but
40+
at the CLI boundary: `gl-report` emitted JSON correctly when `args.output` was
41+
set to `json`, but it did not actually honor the shared format contract. Global
42+
`-f json` left `args.output` at `"text"`, and `gl-report --format json` was not
43+
accepted at all. That meant the refactor had made the common `_resolve_fmt()`
44+
path feel shared while `gl-report` still behaved like a special case.
45+
46+
The fix is small and structural. `gl-report` now resolves output through the
47+
same format selector as other commands, accepts `--format` as an alias, and
48+
fails explicitly for `tsv`, which remains unsupported for that command. The
49+
tests now cover both direct handler behavior and parser wiring for `gl-report`,
50+
which was the missing safety net.
51+
52+
Verification stayed strong after the fix. Full non-live `pytest`, full
53+
`@pytest.mark.live` smoke tests, and `mypy` all passed. Ruff also passes for the
54+
touched surface. Repo-wide Ruff failures still exist on `origin/main`, but they
55+
are pre-existing test-file issues unrelated to this refactor.
5556

5657
## Open Questions
5758

58-
The next meaningful architectural step is still blocked on test depth, not on
59-
mechanical extraction work. Before splitting `cli.py` into separate modules, the
60-
project should add stronger tests around `TokenManager`, OAuth callback state
61-
handling, and `cmd_gl_report()` output modes. Without that, larger movement
62-
would trade readability gains for regression risk.
59+
The main remaining uncertainty is still test depth around the most stateful
60+
paths: `TokenManager`, OAuth callback handling, and the more exotic
61+
`cmd_gl_report()` modes such as `txns`, `expanded`, and explicit unsupported
62+
format cases. Future extraction work should wait until those contracts are
63+
covered more directly, otherwise readability gains will come with regression
64+
risk.

qbo_cli/cli.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1163,7 +1163,9 @@ def cmd_gl_report(args, config, token_mgr):
11631163
display_start = actual_first
11641164

11651165
# Output
1166-
out_mode = args.output
1166+
out_mode = _resolve_fmt(args)
1167+
if out_mode == "tsv":
1168+
die("gl-report does not support tsv output. Use text, json, txns, or expanded.")
11671169
title = f"General Ledger Report - {cust_name}" if cust_name else "General Ledger Report"
11681170
date_range = _format_date_range(display_start, end_date)
11691171
currency = args.currency
@@ -1207,7 +1209,7 @@ def tree_to_dict(node):
12071209
report_data["customer"] = cust_name
12081210
report_data["customer_id"] = cust_id
12091211

1210-
output(report_data)
1212+
output(report_data, out_mode)
12111213

12121214
elif out_mode == "txns":
12131215
lines = [title, date_range, ""]
@@ -1654,7 +1656,9 @@ def _build_parser() -> tuple[argparse.ArgumentParser, argparse.ArgumentParser]:
16541656
gl_p.add_argument(
16551657
"-o",
16561658
"--output",
1657-
default="text",
1659+
"--format",
1660+
dest="output",
1661+
default=None,
16581662
choices=GL_OUTPUT_FORMATS,
16591663
help="Output format: text (default), json, txns (flat transaction list), expanded (tree + transactions)",
16601664
)

tests/test_commands.py

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,17 @@
1010

1111
from qbo_cli.cli import (
1212
QBOClient,
13+
_resolve_fmt,
1314
cmd_create,
15+
cmd_gl_report,
1416
cmd_query,
15-
cmd_search,
1617
cmd_report,
18+
cmd_search,
1719
cmd_update,
1820
main,
19-
_resolve_fmt,
2021
)
2122
from tests.conftest import make_args
2223

23-
2424
# ─── cmd_query ────────────────────────────────────────────────────────────────
2525

2626

@@ -169,6 +169,83 @@ def test_report_forwards_params(self, fake_config, fake_token_mgr, capsys):
169169
assert params["accounting_method"] == "Cash"
170170

171171

172+
# ─── cmd_gl_report ────────────────────────────────────────────────────────────
173+
174+
175+
class TestCmdGlReport:
176+
def test_gl_report_json_output(self, fake_config, fake_token_mgr, capsys):
177+
client = MagicMock()
178+
client.report = MagicMock(return_value={"Header": {"Option": []}, "Rows": {}})
179+
args = make_args(
180+
command="gl-report",
181+
customer="R-CB1",
182+
account="125",
183+
start="2026-02-01",
184+
end="2026-02-28",
185+
method="Cash",
186+
currency="THB",
187+
list_accounts=False,
188+
output="json",
189+
no_sub=False,
190+
by_customer=False,
191+
sort="alpha",
192+
)
193+
194+
with (
195+
patch("qbo_cli.cli.QBOClient", return_value=client),
196+
patch("qbo_cli.cli._resolve_customer", return_value=("104", "PM:R-CB1")),
197+
patch("qbo_cli.cli._discover_account_tree", return_value={"name": "PM Owner Funds", "id": "125", "children": []}),
198+
patch("qbo_cli.cli._parse_gl_rows", return_value=[]),
199+
patch("qbo_cli.cli._build_section_index", return_value={}),
200+
patch("qbo_cli.cli._extract_dates_from_gl", return_value=(None, None)),
201+
patch("qbo_cli.cli._compute_subtotal", return_value=(123.45, 0)),
202+
patch("qbo_cli.cli._find_gl_section", return_value=None),
203+
):
204+
cmd_gl_report(args, fake_config, fake_token_mgr)
205+
206+
data = json.loads(capsys.readouterr().out)
207+
assert data["customer"] == "PM:R-CB1"
208+
assert data["account"]["name"] == "PM Owner Funds"
209+
assert data["total"] == 123.45
210+
211+
def test_gl_report_respects_global_format_flag(self, fake_config, fake_token_mgr, capsys):
212+
client = MagicMock()
213+
client.report = MagicMock(return_value={"Header": {"Option": []}, "Rows": {}})
214+
args = make_args(
215+
command="gl-report",
216+
customer=None,
217+
account="125",
218+
start="2026-02-01",
219+
end="2026-02-28",
220+
method="Cash",
221+
currency="THB",
222+
list_accounts=False,
223+
output=None,
224+
format="json",
225+
no_sub=False,
226+
by_customer=False,
227+
sort="alpha",
228+
)
229+
230+
with (
231+
patch("qbo_cli.cli.QBOClient", return_value=client),
232+
patch(
233+
"qbo_cli.cli._discover_account_tree",
234+
return_value={"name": "PM Owner Funds", "id": "125", "children": []},
235+
),
236+
patch("qbo_cli.cli._parse_gl_rows", return_value=[]),
237+
patch("qbo_cli.cli._build_section_index", return_value={}),
238+
patch("qbo_cli.cli._extract_dates_from_gl", return_value=(None, None)),
239+
patch("qbo_cli.cli._compute_subtotal", return_value=(123.45, 0)),
240+
patch("qbo_cli.cli._find_gl_section", return_value=None),
241+
):
242+
cmd_gl_report(args, fake_config, fake_token_mgr)
243+
244+
data = json.loads(capsys.readouterr().out)
245+
assert data["account"]["name"] == "PM Owner Funds"
246+
assert data["total"] == 123.45
247+
248+
172249
# ─── cmd_create / cmd_update ──────────────────────────────────────────────────
173250

174251

@@ -252,6 +329,7 @@ class TestSubcommandFormatAlias:
252329
(["qbo", "delete", "Customer", "1", "--format", "json"], "cmd_delete"),
253330
(["qbo", "report", "ProfitAndLoss", "--format", "json"], "cmd_report"),
254331
(["qbo", "raw", "GET", "companyinfo/1", "--format", "json"], "cmd_raw"),
332+
(["qbo", "gl-report", "-a", "125", "--format", "json"], "cmd_gl_report"),
255333
],
256334
)
257335
def test_format_alias_after_subcommand_maps_to_output(self, argv, handler_name):

tests/test_live.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -125,11 +125,10 @@ def test_smoke_gl_report_json():
125125
)
126126
if result.returncode != 0:
127127
pytest.skip(f"GL report returned no data for account {acct_id}")
128-
# -o json in gl-report renders as key-value text (not raw JSON)
129-
# Verify structural text output contains expected fields
130-
assert "start_date" in result.stdout
131-
assert "end_date" in result.stdout
132-
assert "total" in result.stdout
128+
data = json.loads(result.stdout)
129+
assert "start_date" in data
130+
assert "end_date" in data
131+
assert "total" in data
133132

134133

135134
@pytest.mark.live

0 commit comments

Comments
 (0)