Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from __future__ import annotations

import json
import re
from dataclasses import dataclass, field
from decimal import Decimal, InvalidOperation

Expand Down Expand Up @@ -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.

Expand All @@ -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,
Expand Down
84 changes: 84 additions & 0 deletions packages/parser-core/tests/domain/test_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"