Skip to content

Currency core, real-statement PDF fixes, and FX rates (v0.2 Phase 1 + 2) - #17

Open
aafre wants to merge 2 commits into
mainfrom
feat/v02-currency-investments
Open

Currency core, real-statement PDF fixes, and FX rates (v0.2 Phase 1 + 2)#17
aafre wants to merge 2 commits into
mainfrom
feat/v02-currency-investments

Conversation

@aafre

@aafre aafre commented Aug 30, 2026

Copy link
Copy Markdown
Owner

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_rates table (migration 0004), FxRateRepository (at-or-before + inverse lookup),
    domain.fx.to_base, pfa fx set/fetch/list, and /fx/* endpoints. Rates cross every
    boundary — API request/response, CLI, 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 proving no
    cross-currency sum ever happens.
  • A row's currency is checked against its destination account's at preview time as a
    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

  • An AMEX payment's own-line CR marker was being silently dropped during extraction, so
    the 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_amount used a naive * 100 instead of the currency's own minor-unit exponent
    and rounding — corrupted JPY (0dp) amounts and truncated instead of rounding.
  • Year-less dates (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.
  • AMEX's duplicate Date column no longer leaks into the transaction description.
  • Barclaycard two-column layout clustering; restored Money Out/Money In PDF header
    coverage (test was deleted, not superseded, in the prior pass).

Hygiene

  • ruff check, ruff format --check, and mypy src are all clean again (previously
    30/21/2 failures).
  • Added fetch_and_store_fx_rates coverage proving Frankfurter rates are parsed to
    Decimal, not round-tripped through a binary float.

Test plan

  • uv run ruff check .
  • uv run ruff format --check .
  • uv run mypy src
  • uv run pytest (122 passed)
  • Fresh alembic upgrade head applies migration 0004 cleanly
  • Manual: run the real statement corpus through the UI per Part 1 of the plan

https://claude.ai/code/session_01T8foThd2rk578JQmV3JHLQ

…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.
@aafre aafre self-assigned this Aug 30, 2026
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant