feat(infer): messages starting with "other:" are not monthly fees
When the bank message starts with "other:" (case-insensitive), inference does not look for a month, neither in the text nor from the payment date. The trimmed message is written to Purpose, so reconcile books the payment as an "other" transaction for the person instead of a monthly fee. Messages containing "other:" elsewhere are unchanged. - matching.IsOtherPurpose / is_other_purpose shared by inference and reconcile - reconcile applies the rule to rows not yet processed by inference - no person found: Purpose is still written, row shows as unmatched - implemented in Go and legacy Python; four new parity fixtures Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from scripts.match_payments import canonical_member_key, fetch_sheet_data, reconcile
|
||||
from scripts.match_payments import (
|
||||
canonical_member_key, fetch_sheet_data, infer_transaction_details, is_other_purpose, reconcile,
|
||||
)
|
||||
|
||||
|
||||
class TestCanonicalMemberKey(unittest.TestCase):
|
||||
@@ -88,3 +90,44 @@ class TestFetchSheetDataVS(unittest.TestCase):
|
||||
|
||||
def test_string_vs_is_kept(self):
|
||||
self.assertEqual(self._fetch("0308")[0]["vs"], "0308")
|
||||
|
||||
|
||||
class TestOtherPurposeInference(unittest.TestCase):
|
||||
"""A message starting with "other:" is not a monthly fee."""
|
||||
|
||||
MEMBERS = ["Jana Nováková", "Adam Novák"]
|
||||
|
||||
def test_is_other_purpose(self):
|
||||
for text in ("other: tournament", "other:shirt", " Other: Tournament", "OTHER:x"):
|
||||
self.assertTrue(is_other_purpose(text), text)
|
||||
for text in ("leden other: x", "other tournament", "another: x", "2026-01", "", None):
|
||||
self.assertFalse(is_other_purpose(text), text)
|
||||
|
||||
def test_message_becomes_purpose_and_no_month_is_inferred(self):
|
||||
tx = {"sender": "Jana Novakova", "message": " other: turnaj leden 2026 ", "date": "2026-04-15"}
|
||||
result = infer_transaction_details(tx, self.MEMBERS)
|
||||
self.assertEqual(result["members"], [("Jana Nováková", "auto")])
|
||||
self.assertEqual(result["months"], ["other: turnaj leden 2026"])
|
||||
|
||||
def test_unknown_member_still_yields_purpose(self):
|
||||
tx = {"sender": "Unknown Payer", "message": "Other: Dres", "date": "2026-04-15"}
|
||||
result = infer_transaction_details(tx, self.MEMBERS)
|
||||
self.assertEqual(result["members"], [])
|
||||
self.assertEqual(result["months"], ["Other: Dres"])
|
||||
|
||||
def test_other_not_at_start_is_handled_the_old_way(self):
|
||||
tx = {"sender": "Jana Novakova", "message": "leden other: dres", "date": "2026-04-15"}
|
||||
result = infer_transaction_details(tx, self.MEMBERS)
|
||||
self.assertEqual(result["months"], ["2026-01"])
|
||||
|
||||
def test_reconcile_fallback_puts_it_in_other_ledger(self):
|
||||
members = [("Jana Nováková", "A", {"2026-01": (750, 3)})]
|
||||
tx = {"date": "2026-01-10", "amount": 300, "person": "", "purpose": "",
|
||||
"sender": "Jana Novakova", "message": "other: dres", "inferred_amount": ""}
|
||||
result = reconcile(members, ["2026-01"], [tx], {})
|
||||
member = result["members"]["Jana Nováková"]
|
||||
self.assertEqual(member["months"]["2026-01"]["paid"], 0)
|
||||
self.assertEqual(len(member["other_transactions"]), 1)
|
||||
self.assertEqual(member["other_transactions"][0]["amount"], 300)
|
||||
self.assertEqual(member["other_transactions"][0]["purpose"], "other: dres")
|
||||
self.assertEqual(result["unmatched"], [])
|
||||
|
||||
Reference in New Issue
Block a user