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:
@@ -150,27 +150,48 @@ def match_members(
|
||||
# Reconciliation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def is_other_purpose(text) -> bool:
|
||||
"""True when text marks a payment that is not a monthly fee.
|
||||
|
||||
It starts with "other:" (case-insensitive, leading whitespace ignored).
|
||||
Used for both the Purpose column and the bank message.
|
||||
"""
|
||||
return str(text or "").strip().lower().startswith("other:")
|
||||
|
||||
|
||||
def infer_transaction_details(tx: dict, member_names: list[str]) -> dict:
|
||||
"""Infer member(s) and month(s) for a single transaction.
|
||||
|
||||
|
||||
A message that starts with "other:" is not a monthly fee: no month is
|
||||
inferred (neither from the text nor from the date) and 'months' holds the
|
||||
trimmed message as the single purpose.
|
||||
|
||||
Returns:
|
||||
{
|
||||
'members': [(name, confidence)],
|
||||
'months': [YYYY-MM],
|
||||
'months': [YYYY-MM] or ['other:…'],
|
||||
'matched_text': str
|
||||
}
|
||||
"""
|
||||
# Combine sender + message for searching
|
||||
search_text = f"{tx.get('sender', '')} {tx.get('message', '')} {tx.get('user_id', '')}"
|
||||
matched_members = match_members(search_text, member_names)
|
||||
matched_months = parse_month_references(
|
||||
tx.get("message", "") + " " + tx.get("user_id", "")
|
||||
)
|
||||
|
||||
if not matched_members:
|
||||
# Try matching sender name alone with more lenient matching
|
||||
matched_members = match_members(tx.get("sender", ""), member_names)
|
||||
|
||||
if is_other_purpose(tx.get("message", "")):
|
||||
return {
|
||||
"members": matched_members,
|
||||
"months": [str(tx.get("message", "")).strip()],
|
||||
"search_text": search_text
|
||||
}
|
||||
|
||||
matched_months = parse_month_references(
|
||||
tx.get("message", "") + " " + tx.get("user_id", "")
|
||||
)
|
||||
|
||||
if not matched_months:
|
||||
# If no month specified, try to infer from payment date
|
||||
tx_date = tx.get("date")
|
||||
@@ -394,7 +415,7 @@ def reconcile(
|
||||
|
||||
# Strip markers like [?]
|
||||
person_str = re.sub(r"\[\?\]\s*", "", person_str)
|
||||
is_other = purpose_str.lower().startswith("other:")
|
||||
is_other = is_other_purpose(purpose_str)
|
||||
|
||||
if person_str and purpose_str:
|
||||
# We have pre-matched data (either from script or manual)
|
||||
@@ -415,6 +436,10 @@ def reconcile(
|
||||
inference = infer_transaction_details(tx, member_names)
|
||||
matched_members = inference["members"]
|
||||
matched_months = inference["months"]
|
||||
# A message starting with "other:" is inferred as a non-fee payment.
|
||||
if len(matched_months) == 1 and is_other_purpose(matched_months[0]):
|
||||
is_other = True
|
||||
purpose_str = matched_months[0]
|
||||
amount = tx.get("amount", 0)
|
||||
try:
|
||||
amount = float(amount)
|
||||
|
||||
Reference in New Issue
Block a user