Currency core, real-statement PDF fixes, and FX rates (v0.2 Phase 1 + 2) - #17
Open
aafre wants to merge 2 commits into
Open
Currency core, real-statement PDF fixes, and FX rates (v0.2 Phase 1 + 2)#17aafre wants to merge 2 commits into
aafre wants to merge 2 commits into
Conversation
…X rates
Implements Phase 1 (narrow currency core) and the remaining Phase 2 defects from the
v0.2 plan, on top of work already staged on this branch.
Currency core:
- Settings.base_currency, an ISO-4217 SUPPORTED_CURRENCIES dict replacing the GBP-only
reject, and per-currency minor-unit exponents (JPY=0) applied consistently through a
single domain.money.minor_units helper.
- fx_rates table (migration 0004), FxRateRepository with at-or-before/inverse lookup,
domain.fx.to_base, and `pfa fx set/fetch/list` plus /fx/* endpoints. Rates cross every
boundary (API request/response, Frankfurter fetch) as decimal strings, never float.
- Analytics is per-currency throughout (monthly_summary, category/merchant totals,
cashflow, budgets, current_cash), with a mixed GBP/INR regression test.
- A row's currency is validated against its destination account's at preview time
(blocking issue, not a crash) and account currency lookup during preview no longer
creates an account as a side effect.
Statement extraction:
- Fixed: an AMEX payment's own-line "CR" marker was silently dropped during extraction,
so the statement's default debit-positive convention flipped it to spending. The
marker now attaches to its row and marks the direction explicit, which the sign
convention now respects everywhere (PDF debit/credit columns, CSV, inline/own-line CR).
- Fixed: parse_amount used a naive `* 100` instead of the currency's minor-unit exponent
and rounding, corrupting non-2dp currencies (JPY) and truncating instead of rounding.
- Fixed: year-less dates ("Jul31") always took today's year, even during a replay of an
old statement. Batches now infer the statement's year from any other year-bearing date
in the same file and normalize every date up front, with a visible warning when no
date carries a year at all.
- AMEX's duplicate "Date" column no longer leaks into the transaction description.
- Barclaycard two-column layout clustering and Money Out/Money In PDF header coverage.
Also fixes: ruff/format/mypy clean (was failing all three), restores a deleted PDF
header-alias regression test, and adds fetch_and_store_fx_rates coverage that proves
Frankfurter rates are parsed to Decimal rather than round-tripped through float.
pfa.config.get_settings() is @lru_cache'd. Any PdfStatementExtractor() built with
default settings (many unit tests do this) primes that cache with the real
sqlite:///data/pfa.db - and once cached, CliRunner(env={"PFA_DATABASE_URL": ...})
has no effect for the rest of the process, so test_cli_fx_commands ran against
whatever DB happened to be cached first instead of its own tmp_path DB.
Reproduced locally by forcing collection order:
pytest tests/unit/test_pdf_extractor.py tests/integration/test_cli.py
and confirmed the autouse cache-clear fixture fixes it regardless of order.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T8foThd2rk578JQmV3JHLQ
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements v0.2 Phase 1 (narrow currency core) and fixes the remaining Phase 2 defects
found during self-review, per
docs/plans/2026-08-29-v0.2-currency-investments-plan.md.Currency core
Settings.base_currency,SUPPORTED_CURRENCIES(ISO-4217 code → minor-unit exponent)replacing the GBP-only reject, applied consistently via
domain.money.minor_units.fx_ratestable (migration0004),FxRateRepository(at-or-before + inverse lookup),domain.fx.to_base,pfa fx set/fetch/list, and/fx/*endpoints. Rates cross everyboundary — API request/response, CLI, Frankfurter fetch — as decimal strings, never float.
monthly_summary, category/merchant totals,cashflow, budgets,
current_cash), with a mixed GBP/INR regression test proving nocross-currency sum ever happens.
blocking issue (not a crash at commit), and looking up an account during preview no
longer has the side effect of creating one.
Statement extraction bug fixes
CRmarker was being silently dropped during extraction, sothe statement's default debit-positive convention flipped a payment to spending. The
marker now attaches to its row and marks the direction explicit, which the sign
convention now respects everywhere it can occur (PDF debit/credit columns, CSV,
inline/own-line CR).
parse_amountused a naive* 100instead of the currency's own minor-unit exponentand rounding — corrupted JPY (0dp) amounts and truncated instead of rounding.
Jul31) always took today's year, even replaying an old statement.Batches now infer the statement's year from any other year-bearing date in the same
file and normalize every date up front, with a visible warning when nothing in the
file carries a year at all.
Datecolumn no longer leaks into the transaction description.coverage (test was deleted, not superseded, in the prior pass).
Hygiene
ruff check,ruff format --check, andmypy srcare all clean again (previously30/21/2 failures).
fetch_and_store_fx_ratescoverage proving Frankfurter rates are parsed toDecimal, not round-tripped through a binary float.Test plan
uv run ruff check .uv run ruff format --check .uv run mypy srcuv run pytest(122 passed)alembic upgrade headapplies migration0004cleanlyhttps://claude.ai/code/session_01T8foThd2rk578JQmV3JHLQ