diff --git a/fire_planner/examples/llm_extract.py b/fire_planner/examples/llm_extract.py index 6e65b23..2f7d04b 100644 --- a/fire_planner/examples/llm_extract.py +++ b/fire_planner/examples/llm_extract.py @@ -179,3 +179,22 @@ async def extract_with_fallback( client=client, ) return secondary or primary + + +def to_gbp( + amount: Decimal | None, + currency: str | None, + rates: dict[str, Decimal], +) -> Decimal | None: + """Convert `amount` in `currency` to GBP using `fx.fetch_rates` output. + + `rates[X]` = "how much GBP one unit of X is worth" — the convention + used by `fire_planner/fx.py`. Returns None when amount/currency is + missing or the currency isn't in `rates`. + """ + if amount is None or currency is None: + return None + rate = rates.get(currency.upper()) + if rate is None: + return None + return (amount * rate).quantize(Decimal("0.01")) diff --git a/tests/test_examples_llm_extract.py b/tests/test_examples_llm_extract.py index 4ebc5ac..a28c4f9 100644 --- a/tests/test_examples_llm_extract.py +++ b/tests/test_examples_llm_extract.py @@ -12,6 +12,7 @@ import respx from fire_planner.examples.llm_extract import ( extract_with_fallback, extract_with_qwen, + to_gbp, ) from fire_planner.examples.models import RawPost @@ -171,3 +172,20 @@ async def test_fallback_keeps_high_confidence_qwen_result() -> None: assert out is not None assert out.llm_model == "qwen3-8b" assert claude_route.called is False # high-confidence qwen → claude not hit + + +def test_to_gbp_converts_usd() -> None: + rates = {"GBP": Decimal("1"), "USD": Decimal("0.80")} + assert to_gbp(Decimal("100"), "USD", rates) == Decimal("80.00") + + +def test_to_gbp_passes_through_gbp() -> None: + assert to_gbp(Decimal("100"), "GBP", {"GBP": Decimal("1")}) == Decimal("100.00") + + +def test_to_gbp_returns_none_for_unknown_currency() -> None: + assert to_gbp(Decimal("100"), "XYZ", {"GBP": Decimal("1"), "USD": Decimal("0.8")}) is None + + +def test_to_gbp_returns_none_for_none_amount() -> None: + assert to_gbp(None, "USD", {"USD": Decimal("0.8")}) is None