"""Functional tests for PaymentHold.""" from datetime import date, datetime, timedelta from unittest.mock import patch from zoneinfo import ZoneInfo from abacus_common_logic.utils.dates import safe_format_date from dateutil import tz from abacus_account.constants.error import ( ERROR_INVALID_HOLD_STATUS_REASON, ERROR_INVALID_START_DATE ) from tests.utils.factories import AccountFactory, PaymentHoldFactory def test_get_payment_hold_info_by_account_id(fixture_client): """Tests that we get the payment hold info for a given account ID.""" account = AccountFactory.create() payment_hold = PaymentHoldFactory.create(account=account) response = fixture_client.get(f'/account/{account.account_id}/payment-hold') res_json = response.json assert response.status_code == 200 assert res_json == { 'payment_hold_id': payment_hold.payment_hold_id, 'account_id': payment_hold.account_id, 'is_on_hold': payment_hold.is_on_hold, 'start_date': payment_hold.start_date.isoformat(), 'created_at': payment_hold.created_at.strftime('%Y-%m-%d'), 'created_by': payment_hold.created_by, 'last_modified': payment_hold.last_modified.strftime('%Y-%m-%d'), 'last_modified_by': payment_hold.last_modified_by, 'reason': payment_hold.reason } def test_get_payment_hold_info_error(fixture_client): """Tests that we get an error if an unrecognized account ID is passed in.""" response = fixture_client.get('account/NOT_A_VALID_ID/payment-hold') assert response.status_code == 404 def test_create_payment_hold_success(fixture_client): """Create new payment hold if account has no payment holds.""" account = AccountFactory.create() post_data = { 'is_on_hold': True, 'reason': "To teach 'em a lesson", 'start_date': '2099-05-01' } res = fixture_client.post( f'/account/{account.account_id}/payment-hold', json=post_data ) today_str = datetime.now(tz.tzutc()).strftime('%Y-%m-%d') assert res.status_code == 201 assert account.payment_hold assert res.json == { 'payment_hold_id': 1, 'account_id': account.account_id, 'is_on_hold': post_data['is_on_hold'], 'reason': post_data['reason'], 'start_date': post_data['start_date'], 'created_at': today_str, 'created_by': 'default_user_id', 'last_modified': today_str, 'last_modified_by': 'default_user_id' } @patch('abacus_account.logic.payment_hold.datetime') def test_create_payment_hold_invalid_start_date(mock_datetime, fixture_client): """Raise error if start_date is before today.""" account = AccountFactory.create() today = datetime(2025, 5, 1, tzinfo=ZoneInfo('UTC')) post_data = { 'is_on_hold': True, 'reason': "To teach 'em a lesson", 'start_date': '2000-05-01' } mock_datetime.now.return_value = today res = fixture_client.post( f'/account/{account.account_id}/payment-hold', json=post_data ) assert res.status_code == 400 assert res.json['message'] == ERROR_INVALID_START_DATE.format( start_date=post_data['start_date'], today=today.strftime('%Y-%m-%d'), tz=today.strftime('%Z%z'), ) def test_update_payment_hold(fixture_client): """Update existing payment hold if one already exists for account.""" account = AccountFactory.create() payment_hold = PaymentHoldFactory(account=account) tomorrow = date.today() + timedelta(days=1) post_data = { 'is_on_hold': not payment_hold.is_on_hold, 'reason': "Because I don't understand", 'start_date': safe_format_date(tomorrow) } res = fixture_client.post( f'/account/{account.account_id}/payment-hold', json=post_data ) assert res.status_code == 201 assert res.json == { 'payment_hold_id': payment_hold.payment_hold_id, 'account_id': account.account_id, 'is_on_hold': post_data['is_on_hold'], 'reason': post_data['reason'], 'start_date': post_data['start_date'], 'created_at': payment_hold.created_at.strftime('%Y-%m-%d'), 'created_by': 'default_user_id', 'last_modified': payment_hold.last_modified.strftime('%Y-%m-%d'), 'last_modified_by': 'default_user_id' } def test_update_payment_hold_same_hold_status(fixture_client): """Update of existing payment hold reason without is_on_hold change.""" account = AccountFactory.create() payment_hold = PaymentHoldFactory(account=account) start_date = safe_format_date(date.today() + timedelta(days=1)) post_data = { 'is_on_hold': payment_hold.is_on_hold, 'reason': 'It is ok, just a new reason', 'start_date': start_date } res = fixture_client.post( f'/account/{account.account_id}/payment-hold', json=post_data ) assert res.status_code == 201 assert res.json == { 'payment_hold_id': payment_hold.payment_hold_id, 'account_id': account.account_id, 'is_on_hold': post_data['is_on_hold'], 'reason': 'It is ok, just a new reason', 'start_date': start_date, 'created_at': payment_hold.created_at.strftime('%Y-%m-%d'), 'created_by': 'default_user_id', 'last_modified': payment_hold.last_modified.strftime('%Y-%m-%d'), 'last_modified_by': 'default_user_id' } def test_update_payment_hold_invalid_hold_status_and_reason(fixture_client): """Update of existing payment hold fails for smae is_on_hold and reason fields.""" account = AccountFactory.create() payment_hold = PaymentHoldFactory(account=account) tomorrow = date.today() + timedelta(days=1) post_data = { 'is_on_hold': payment_hold.is_on_hold, 'reason': payment_hold.reason, 'start_date': safe_format_date(tomorrow) } res = fixture_client.post( f'/account/{account.account_id}/payment-hold', json=post_data ) assert res.status_code == 400 assert res.json['message'] == ( ERROR_INVALID_HOLD_STATUS_REASON.format( hold_status='on hold', reason=payment_hold.reason ) ) def test_update_payment_hold_creates_history_record(fixture_client): """Update of existing payment hold should log existing hold in history.""" account = AccountFactory.create() payment_hold = PaymentHoldFactory(account=account) assert len(payment_hold.history) == 0 tomorrow = date.today() + timedelta(days=1) previous_is_on_hold = payment_hold.is_on_hold post_data = { 'is_on_hold': not previous_is_on_hold, 'reason': "Because I don't understand", 'start_date': safe_format_date(tomorrow) } res = fixture_client.post( f'/account/{account.account_id}/payment-hold', json=post_data ) assert res.status_code == 201 assert len(payment_hold.history) == 1 assert payment_hold.history[0].payment_hold_id == payment_hold.payment_hold_id assert payment_hold.history[0].is_on_hold == previous_is_on_hold def test_list_payment_holds_success(fixture_client, fresh_db): """Test listing payment holds with account filter.""" hold_one = PaymentHoldFactory.create() hold_two = PaymentHoldFactory.create() PaymentHoldFactory.create() res = fixture_client.post( '/payment-holds?limit=10&offset=0', json=[hold_one.account.account_id, hold_two.account.account_id], ) assert res.status_code == 200 assert res.json['total_count'] == 2 account_ids = sorted(item['account_id'] for item in res.json['items']) assert account_ids == sorted( [ hold_one.account.account_id, hold_two.account.account_id, ] ) def test_list_payment_holds_without_account_filter(fixture_client, fresh_db): """Test listing payment holds without providing account ids.""" PaymentHoldFactory.create() PaymentHoldFactory.create() res = fixture_client.post('/payment-holds?limit=1&offset=0') assert res.status_code == 200 assert res.json['total_count'] == 2 assert len(res.json['items']) == 1 def test_list_payment_holds_invalid_payload(fixture_client, fresh_db): """Test listing payment holds with invalid account ids payload.""" PaymentHoldFactory.create() res = fixture_client.post('/payment-holds?limit=10&offset=0', json=['invalid']) assert res.status_code == 400 assert res.json == { 'code': 'error', 'message': { 'account_ids': {'0': ['Must be an integer greater or equal to 0.']} }, }