- 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>
102 lines
3.9 KiB
Python
102 lines
3.9 KiB
Python
import unittest
|
|
from unittest.mock import patch
|
|
from app import app
|
|
|
|
|
|
def _bypass_cache(cache_key, sheet_id, fetch_func, *args, serialize=None, deserialize=None, **kwargs):
|
|
"""Test helper: call fetch_func directly, bypassing the cache layer."""
|
|
return fetch_func(*args, **kwargs)
|
|
|
|
|
|
class TestWebApp(unittest.TestCase):
|
|
def setUp(self):
|
|
app.config['TESTING'] = True
|
|
self.client = app.test_client()
|
|
|
|
def test_index_page(self):
|
|
"""Test that / returns the refresh meta tag"""
|
|
response = self.client.get('/')
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertIn(b'url=/adults', 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):
|
|
"""Test that /payments returns 200 and groups transactions"""
|
|
mock_fetch_sheet.return_value = [{
|
|
'date': '2026-01-01',
|
|
'amount': 750,
|
|
'person': 'Test Member',
|
|
'purpose': '2026-01',
|
|
'message': 'Direct Member Payment',
|
|
'sender': 'External Bank User'
|
|
}]
|
|
response = self.client.get('/payments')
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertIn(b'Payments Ledger', response.data)
|
|
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', return_value={})
|
|
@patch('app.get_members_with_fees')
|
|
def test_adults_route(self, mock_get_members, mock_exceptions, mock_fetch_sheet, mock_cache):
|
|
"""Test that /adults returns 200 and shows combined 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('/adults')
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertIn(b'Adults Dashboard', response.data)
|
|
self.assertIn(b'Test Member', response.data)
|
|
self.assertNotIn(b'OK', response.data)
|
|
self.assertIn(b'750/750 CZK (4)', 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_junior_members_with_fees')
|
|
def test_juniors_route(self, mock_get_junior_members, mock_exceptions, mock_fetch_sheet, mock_cache):
|
|
"""Test that /juniors returns 200, uses single line format, and displays '?' properly"""
|
|
mock_get_junior_members.return_value = (
|
|
[
|
|
('Junior One', 'J', {'2026-01': (500, 3, 0, 3)}),
|
|
('Junior Two', 'X', {'2026-01': ('?', 1, 0, 1)})
|
|
],
|
|
['2026-01']
|
|
)
|
|
mock_exceptions.return_value = {}
|
|
mock_fetch_sheet.return_value = [{
|
|
'date': '2026-01-15',
|
|
'amount': 500,
|
|
'person': 'Junior One',
|
|
'purpose': '2026-01',
|
|
'message': '',
|
|
'sender': 'Parent',
|
|
'inferred_amount': 500
|
|
}]
|
|
|
|
response = self.client.get('/juniors')
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertIn(b'Juniors Dashboard', response.data)
|
|
self.assertIn(b'Junior One', response.data)
|
|
self.assertIn(b'Junior Two', response.data)
|
|
self.assertNotIn(b'OK', response.data)
|
|
self.assertIn(b'500/500 CZK', response.data)
|
|
self.assertIn(b'?', response.data)
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|