fix: Tolerate diacritic/case/whitespace mismatches in Person column matching
Some checks failed
Deploy to K8s / deploy (push) Successful in 11s
Build and Push / build (push) Successful in 6s
Build and Push / build-go (push) Failing after 6s

- Add canonical_member_key() in match_payments.py to normalize names via
  NFKD + lowercase + whitespace-collapse before ledger lookup; resolves
  payments attributed to e.g. "Maria Maco" to canonical "Mária Maco".
  Emits logger.info when a non-canonical cell is rescued so sheet typos
  are visible in logs without losing the payment allocation.
- Extend group_payments_by_person() in app.py to accept member_names and
  re-key raw-payment groups under the canonical attendance-sheet name so
  the modal's Raw Payments debug section also finds the row correctly.
- Add raw payments collapsible section to member detail modal in adults.html
  and juniors.html for debugging payment attribution issues.
- Remove 4 obsolete tests targeting routes /fees, /fees-juniors, /reconcile,
  /reconcile-juniors that no longer exist; add test_match_payments.py
  covering canonical key equivalence and reconcile() tolerance end-to-end.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-05 17:22:54 +02:00
parent 81b36878b3
commit 394da2e6b8
8 changed files with 498 additions and 120 deletions

View File

@@ -19,67 +19,6 @@ class TestWebApp(unittest.TestCase):
self.assertEqual(response.status_code, 200)
self.assertIn(b'url=/adults', response.data)
@patch('app.get_cached_data', side_effect=_bypass_cache)
@patch('app.get_members_with_fees')
@patch('app.fetch_exceptions', return_value={})
def test_fees_route(self, mock_exceptions, mock_get_members, mock_cache):
"""Test that /fees returns 200 and renders the dashboard"""
mock_get_members.return_value = (
[('Test Member', 'A', {'2026-01': (750, 4)})],
['2026-01']
)
response = self.client.get('/fees')
self.assertEqual(response.status_code, 200)
self.assertIn(b'FUJ Fees Dashboard', response.data)
self.assertIn(b'Test Member', response.data)
@patch('app.get_cached_data', side_effect=_bypass_cache)
@patch('app.get_junior_members_with_fees')
@patch('app.fetch_exceptions', return_value={})
def test_fees_juniors_route(self, mock_exceptions, mock_get_junior_members, mock_cache):
"""Test that /fees-juniors returns 200 and renders the junior dashboard"""
mock_get_junior_members.return_value = (
[
('Test Junior 1', 'J', {'2026-01': ('?', 1, 0, 1)}),
('Test Junior 2', 'J', {'2026-01': (500, 4, 1, 3)})
],
['2026-01']
)
response = self.client.get('/fees-juniors')
self.assertEqual(response.status_code, 200)
self.assertIn(b'FUJ Junior Fees Dashboard', response.data)
self.assertIn(b'Test Junior 1', response.data)
self.assertIn(b'? / 1 (J)', response.data)
self.assertIn(b'500 CZK / 4 (1A+3J)', response.data)
@patch('app.get_cached_data', side_effect=_bypass_cache)
@patch('app.fetch_sheet_data')
@patch('app.fetch_exceptions', return_value={})
@patch('app.get_members_with_fees')
def test_reconcile_route(self, mock_get_members, mock_exceptions, mock_fetch_sheet, mock_cache):
"""Test that /reconcile returns 200 and shows matches"""
mock_get_members.return_value = (
[('Test Member', 'A', {'2026-01': (750, 4)})],
['2026-01']
)
mock_fetch_sheet.return_value = [{
'date': '2026-01-01',
'amount': 750,
'person': 'Test Member',
'purpose': '2026-01',
'message': 'test payment',
'sender': 'External Bank User',
'inferred_amount': 750
}]
response = self.client.get('/reconcile')
self.assertEqual(response.status_code, 200)
self.assertIn(b'Payment Reconciliation', response.data)
self.assertIn(b'Test Member', response.data)
self.assertIn(b'OK', response.data)
@patch('app.get_cached_data', side_effect=_bypass_cache)
@patch('app.fetch_sheet_data')
def test_payments_route(self, mock_fetch_sheet, mock_cache):
@@ -98,38 +37,6 @@ class TestWebApp(unittest.TestCase):
self.assertIn(b'Test Member', response.data)
self.assertIn(b'Direct Member Payment', response.data)
@patch('app.get_cached_data', side_effect=_bypass_cache)
@patch('app.fetch_sheet_data')
@patch('app.fetch_exceptions')
@patch('app.get_junior_members_with_fees')
def test_reconcile_juniors_route(self, mock_get_junior, mock_exceptions, mock_transactions, mock_cache):
"""Test that /reconcile-juniors correctly computes balances for juniors."""
mock_get_junior.return_value = (
[
('Junior One', 'J', {'2026-01': (500, 4, 2, 2)}),
('Junior Two', 'X', {'2026-01': ('?', 1, 0, 1)})
],
['2026-01']
)
mock_exceptions.return_value = {}
mock_transactions.return_value = [{
'date': '2026-01-15',
'amount': 500,
'person': 'Junior One',
'purpose': '2026-01',
'message': '',
'sender': 'Parent',
'inferred_amount': 500
}]
response = self.client.get('/reconcile-juniors')
self.assertEqual(response.status_code, 200)
self.assertIn(b'Junior Payment Reconciliation', response.data)
self.assertIn(b'Junior One', response.data)
self.assertIn(b'Junior Two', response.data)
self.assertIn(b'OK', response.data)
self.assertIn(b'?', response.data)
@patch('app.get_cached_data', side_effect=_bypass_cache)
@patch('app.fetch_sheet_data')
@patch('app.fetch_exceptions', return_value={})