- Add dual-sheet architecture to pull attendance from both adult and junior spreadsheets. - Introduce parsing rules to isolate juniors (e.g. above '# Treneri', tier 'J'). - Add new endpoints `/fees-juniors` and `/reconcile-juniors` to track junior attendances and match bank payments. - Display granular attendance components showing adult vs. junior practices. - Add fee rule configuration supporting custom pricing exceptions for specific months (e.g. Sep 2025) and merging billing periods. - Add `make sync-2025` target to the Makefile for convenience. - Document junior fees implementation logic and rules in prompts/outcomes. Co-authored-by: Antigravity <antigravity@google.com>
128 lines
4.9 KiB
Python
128 lines
4.9 KiB
Python
import unittest
|
|
from unittest.mock import patch, MagicMock
|
|
from app import app
|
|
|
|
class TestWebApp(unittest.TestCase):
|
|
def setUp(self):
|
|
# Configure app for testing
|
|
app.config['TESTING'] = True
|
|
self.client = app.test_client()
|
|
|
|
@patch('app.get_members_with_fees')
|
|
def test_index_page(self, mock_get_members):
|
|
"""Test that / returns the refresh meta tag"""
|
|
response = self.client.get('/')
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertIn(b'url=/fees', response.data)
|
|
|
|
@patch('app.get_members_with_fees')
|
|
def test_fees_route(self, mock_get_members):
|
|
"""Test that /fees returns 200 and renders the dashboard"""
|
|
# Mock attendance data
|
|
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_junior_members_with_fees')
|
|
def test_fees_juniors_route(self, mock_get_junior_members):
|
|
"""Test that /fees-juniors returns 200 and renders the junior dashboard"""
|
|
# Mock attendance data: one with string symbol '?', one with integer
|
|
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.fetch_sheet_data')
|
|
@patch('app.get_members_with_fees')
|
|
def test_reconcile_route(self, mock_get_members, mock_fetch_sheet):
|
|
"""Test that /reconcile returns 200 and shows matches"""
|
|
# Mock attendance data
|
|
mock_get_members.return_value = (
|
|
[('Test Member', 'A', {'2026-01': (750, 4)})],
|
|
['2026-01']
|
|
)
|
|
# Mock sheet data - include all keys required by reconcile
|
|
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.fetch_sheet_data')
|
|
def test_payments_route(self, mock_fetch_sheet):
|
|
"""Test that /payments returns 200 and groups transactions"""
|
|
# Mock sheet data
|
|
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.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):
|
|
"""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)
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|