From 6a8984345bd8a0b6a5a55207915cdef491ce3b0a Mon Sep 17 00:00:00 2001 From: longieirl Date: Thu, 9 Apr 2026 20:54:31 +0100 Subject: [PATCH] fix(#134): append statement_year to yearless CC dates in to_dict() output Add Transaction._enrich_date() which detects yearless date strings (e.g. '3 Feb') and appends statement_year from additional_fields. Called from to_dict() so all output paths (CSV, JSON, Excel, monthly_summary, expense_analysis) automatically receive the enriched date. Nine new tests cover unit behaviour of _enrich_date and integration through to_dict(). --- .../domain/models/transaction.py | 37 +++++++- .../tests/domain/test_transaction.py | 84 +++++++++++++++++++ 2 files changed, 120 insertions(+), 1 deletion(-) diff --git a/packages/parser-core/src/bankstatements_core/domain/models/transaction.py b/packages/parser-core/src/bankstatements_core/domain/models/transaction.py index 1782c8d..2f58c8c 100644 --- a/packages/parser-core/src/bankstatements_core/domain/models/transaction.py +++ b/packages/parser-core/src/bankstatements_core/domain/models/transaction.py @@ -6,6 +6,7 @@ from __future__ import annotations import json +import re from dataclasses import dataclass, field from decimal import Decimal, InvalidOperation @@ -304,6 +305,40 @@ def _get_value(data: dict[str, str | None], keys: list[str]) -> str | None: return data[key] return None + @staticmethod + def _enrich_date(date_str: str, additional_fields: dict[str, str]) -> str: + """Append statement year to yearless CC dates in output. + + CC statement dates like "3 Feb" lack a year. When statement_year is + available in additional_fields, append it so output reads "3 Feb 2025". + + Args: + date_str: Raw date string from the transaction. + additional_fields: Transaction additional_fields dict. + + Returns: + Enriched date string if yearless + year available, else original. + + Examples: + >>> Transaction._enrich_date("3 Feb", {"statement_year": "2025"}) + '3 Feb 2025' + >>> Transaction._enrich_date("01/01/2025", {"statement_year": "2025"}) + '01/01/2025' + """ + if not date_str: + return date_str + # Yearless CC date: digits, optional space, 3+ letter month abbreviation, end + if not re.fullmatch(r"\d{1,2}\s+[A-Za-z]{3,}", date_str.strip()): + return date_str + year_raw = additional_fields.get("statement_year") + if not year_raw: + return date_str + try: + year = int(year_raw) + except ValueError: + return date_str + return f"{date_str.strip()} {year}" + def to_dict(self, currency_symbol: str = "€") -> dict[str, str | None]: """Convert Transaction to dictionary. @@ -325,7 +360,7 @@ def to_dict(self, currency_symbol: str = "€") -> dict[str, str | None]: """ suffix = f" {currency_symbol}" if currency_symbol else "" result: dict[str, str | None] = { - "Date": self.date, + "Date": self._enrich_date(self.date, self.additional_fields), "Details": self.details, f"Debit{suffix}": self.debit, f"Credit{suffix}": self.credit, diff --git a/packages/parser-core/tests/domain/test_transaction.py b/packages/parser-core/tests/domain/test_transaction.py index b1daf58..92273b0 100644 --- a/packages/parser-core/tests/domain/test_transaction.py +++ b/packages/parser-core/tests/domain/test_transaction.py @@ -983,3 +983,87 @@ def test_from_dict_round_trips_pound(self): tx2 = Transaction.from_dict(d) assert tx2.debit == "50.00" assert tx2.balance == "100.00" + + +class TestTransactionEnrichDate: + """Tests for _enrich_date and its integration in to_dict() (issue #134).""" + + def _make_tx(self, date: str, statement_year: str | None = None) -> Transaction: + from bankstatements_core.domain.models.transaction import Transaction + + additional: dict[str, str] = {} + if statement_year is not None: + additional["statement_year"] = statement_year + return Transaction( + date=date, + details="Test", + debit="10.00", + credit=None, + balance="100.00", + filename="cc.pdf", + additional_fields=additional, + ) + + # --- _enrich_date unit tests --- + + def test_yearless_short_month_with_year(self): + """'3 Feb' + statement_year='2025' → '3 Feb 2025'.""" + from bankstatements_core.domain.models.transaction import Transaction + + assert ( + Transaction._enrich_date("3 Feb", {"statement_year": "2025"}) + == "3 Feb 2025" + ) + + def test_yearless_full_month_with_year(self): + """'3 February' + statement_year='2026' → '3 February 2026'.""" + from bankstatements_core.domain.models.transaction import Transaction + + assert ( + Transaction._enrich_date("3 February", {"statement_year": "2026"}) + == "3 February 2026" + ) + + def test_dated_with_year_unchanged(self): + """Full dates are left untouched.""" + from bankstatements_core.domain.models.transaction import Transaction + + assert ( + Transaction._enrich_date("01/01/2025", {"statement_year": "2025"}) + == "01/01/2025" + ) + + def test_yearless_no_statement_year_unchanged(self): + """Yearless date without statement_year returns original string.""" + from bankstatements_core.domain.models.transaction import Transaction + + assert Transaction._enrich_date("3 Feb", {}) == "3 Feb" + + def test_empty_date_unchanged(self): + """Empty date string returns empty string.""" + from bankstatements_core.domain.models.transaction import Transaction + + assert Transaction._enrich_date("", {"statement_year": "2025"}) == "" + + def test_invalid_year_unchanged(self): + """Non-integer statement_year leaves date unchanged.""" + from bankstatements_core.domain.models.transaction import Transaction + + assert Transaction._enrich_date("3 Feb", {"statement_year": "abc"}) == "3 Feb" + + # --- integration: to_dict() applies enrichment --- + + def test_to_dict_yearless_date_enriched(self): + """to_dict() Date field includes year when statement_year present.""" + tx = self._make_tx("3 Feb", statement_year="2025") + assert tx.to_dict()["Date"] == "3 Feb 2025" + + def test_to_dict_full_date_not_changed(self): + """to_dict() Date field is unchanged for full dates.""" + tx = self._make_tx("01/01/2025", statement_year="2025") + assert tx.to_dict()["Date"] == "01/01/2025" + + def test_to_dict_yearless_no_statement_year_unchanged(self): + """to_dict() Date field unchanged when no statement_year in additional_fields.""" + tx = self._make_tx("3 Feb") + assert tx.to_dict()["Date"] == "3 Feb"