"""Ledger Adjustment Applied logic tests.""" from unittest.mock import MagicMock, patch import pytest from werkzeug.exceptions import HTTPException from ledger.constants.error import ERROR_APPLY_SOFT_DELETED_WORKSHEET_ADJUSTMENT from ledger.logic import ledger_adjustment_applied as logic from tests.conftest import mock_worksheet_adjustment_detail from tests.utils.factories import ( LedgerAdjustmentAppliedFactory, LedgerAdjustmentDetailAppliedFactory, ) @patch('ledger.logic.ledger_adjustment_applied.LedgerAdjustmentDetailApplied') @patch('ledger.logic.ledger_adjustment_applied.LedgerAdjustmentApplied') def test_create_bulk( mock_model, mock_detail_model, mock_event_fixtures, mock_bulk_ledger_adjustment_with_details_applied_request_body, ): """Test creating ledger adjustment applied items.""" ledger_adjustment_applied = LedgerAdjustmentAppliedFactory.create() mock_worksheet_adjustment_detail(worksheet_adjustment_detail_id=3) # 2 items to match the request mock ledger_adjustment_applied_details = [ LedgerAdjustmentDetailAppliedFactory.create(), LedgerAdjustmentDetailAppliedFactory.create(worksheet_adjustment_detail_id=3), ] mock_model.build.return_value = ledger_adjustment_applied mock_detail_model.build.side_effect = ledger_adjustment_applied_details post_data = mock_bulk_ledger_adjustment_with_details_applied_request_body res = logic.bulk_create(post_data) assert res.status == 201 assert res.message == {'message': 'OK', 'created': len(post_data)} assert mock_model.build.call_count == 1 assert mock_detail_model.build.call_count == len(ledger_adjustment_applied_details) assert len(ledger_adjustment_applied.details) == len( ledger_adjustment_applied_details ) assert ledger_adjustment_applied.details == ledger_adjustment_applied_details @patch('ledger.logic.ledger_adjustment_applied.abort') def test_validate_record(mock_abort): """Test for validate_record method.""" test_record = {'currency_code_one': 'USD', 'currency_code_two': 'WIERD'} logic.validate_record( record=test_record, currency_fields=['currency_code_one', 'currency_code_two'] ) mock_abort.assert_called_once_with( status=400, description='Currency code WIERD not recognized' ' for the field currency_code_two', ) class _FakeOrig: def __init__(self, errno): self.args = (errno, 'SIGNAL message') class _FakeDBError(Exception): """Stand-in for a SQLAlchemy error wrapping a driver error with .orig.""" def __init__(self, errno): super().__init__('db error') self.orig = _FakeOrig(errno) def test_is_soft_deleted_worksheet_adjustment_error(): """Only the errno-1644 guard signal is recognized.""" assert logic._is_soft_deleted_worksheet_adjustment_error(_FakeDBError(1644)) is True assert ( logic._is_soft_deleted_worksheet_adjustment_error(_FakeDBError(1062)) is False ) assert logic._is_soft_deleted_worksheet_adjustment_error(Exception('x')) is False @patch('ledger.logic.ledger_adjustment_applied.validate_record') @patch('ledger.logic.ledger_adjustment_applied.db') @patch('ledger.logic.ledger_adjustment_applied.LedgerAdjustmentApplied') def test_bulk_create_maps_soft_delete_guard_to_409( mock_model, mock_db, _mock_validate, ): """The DB guard signal (errno 1644) surfaces as a 409, not a generic 400.""" mock_model.build.return_value = MagicMock(details=[]) mock_db.session.commit.side_effect = _FakeDBError(1644) request = [ { 'worksheet_adjustment_id': 1, 'adjustment_currency_code': 'USD', 'adjustment_payee_currency_code': 'USD', } ] with pytest.raises(HTTPException) as exc: logic.bulk_create(request) assert exc.value.code == 409 assert exc.value.description == ERROR_APPLY_SOFT_DELETED_WORKSHEET_ADJUSTMENT mock_db.session.rollback.assert_called_once() @patch( 'ledger.logic.ledger_adjustment_applied' '._get_applied_worksheet_adjustment_detail_ids' ) @patch( 'ledger.logic.ledger_adjustment_applied' '._get_applied_ledger_adjustments_by_worksheet_adjustment_ids' ) @patch('ledger.logic.ledger_adjustment_applied.db') @patch('ledger.logic.ledger_adjustment_applied.LedgerAdjustmentDetailApplied') @patch('ledger.logic.ledger_adjustment_applied.LedgerAdjustmentApplied') def test_bulk_create_with_deduplication_filters_duplicates( mock_model, mock_detail_model, mock_db, mock_get_applied_adjustments, mock_get_applied_detail_ids, ): """Test deduplication filters out already-applied adjustments.""" ledger_adjustment_applied = MagicMock() ledger_adjustment_applied_details = [MagicMock(), MagicMock()] # Mock an existing adjustment with worksheet_adjustment_id = 1 existing_adjustment = MagicMock() existing_adjustment.worksheet_adjustment_id = 1 existing_adjustment.details = [] mock_model.build.return_value = ledger_adjustment_applied mock_detail_model.build.side_effect = ledger_adjustment_applied_details mock_get_applied_adjustments.return_value = [ existing_adjustment ] # Only id 1 is already applied mock_get_applied_detail_ids.return_value = set() post_data = [ { 'worksheet_adjustment_id': 1, 'abacus_event_id': 1, 'account_id': 1, 'contract_id': 1, 'statement_period_id': 1, 'ledger_adjustment_id': 1, 'adjustment_currency_code': 'USD', 'adjustment_amount': '100.00', 'adjustment_payee_currency_code': 'USD', 'adjustment_amount_payee_currency': '100.00', }, { 'worksheet_adjustment_id': 2, 'abacus_event_id': 1, 'account_id': 1, 'contract_id': 1, 'statement_period_id': 1, 'ledger_adjustment_id': 2, 'adjustment_currency_code': 'USD', 'adjustment_amount': '200.00', 'adjustment_payee_currency_code': 'USD', 'adjustment_amount_payee_currency': '200.00', }, ] res = logic.bulk_create_with_deduplication(post_data) assert res.status == 201 # Only one record should be created (the new one) assert res.message['created'] == 1 assert res.message['filtered'] == 1 @patch( 'ledger.logic.ledger_adjustment_applied' '._get_applied_worksheet_adjustment_detail_ids' ) @patch( 'ledger.logic.ledger_adjustment_applied' '._get_applied_ledger_adjustments_by_worksheet_adjustment_ids' ) def test_bulk_create_with_deduplication_all_duplicates( mock_get_applied_adjustments, mock_get_applied_detail_ids, ): """Test when all adjustments have already been applied.""" # Mock existing adjustments with worksheet_adjustment_ids = 1 and 2 existing_adjustment_1 = MagicMock() existing_adjustment_1.worksheet_adjustment_id = 1 existing_adjustment_1.details = [] existing_adjustment_2 = MagicMock() existing_adjustment_2.worksheet_adjustment_id = 2 existing_adjustment_2.details = [] mock_get_applied_adjustments.return_value = [ existing_adjustment_1, existing_adjustment_2, ] # Both already applied mock_get_applied_detail_ids.return_value = set() post_data = [ { 'worksheet_adjustment_id': 1, 'abacus_event_id': 1, 'account_id': 1, 'contract_id': 1, 'statement_period_id': 1, 'ledger_adjustment_id': 1, 'adjustment_currency_code': 'USD', 'adjustment_amount': '100.00', 'adjustment_payee_currency_code': 'USD', 'adjustment_amount_payee_currency': '100.00', }, { 'worksheet_adjustment_id': 2, 'abacus_event_id': 1, 'account_id': 1, 'contract_id': 1, 'statement_period_id': 1, 'ledger_adjustment_id': 2, 'adjustment_currency_code': 'USD', 'adjustment_amount': '200.00', 'adjustment_payee_currency_code': 'USD', 'adjustment_amount_payee_currency': '200.00', }, ] res = logic.bulk_create_with_deduplication(post_data) # No new records created, all filtered assert res.status == 201 assert res.message['created'] == 0 assert res.message['filtered'] == 2 @patch('ledger.logic.ledger_adjustment_applied.LedgerAdjustmentDetailApplied') @patch('ledger.logic.ledger_adjustment_applied.LedgerAdjustmentApplied') def test_bulk_create_passes_apply_to_flowthrough_payment( mock_model, mock_detail_model, mock_event_fixtures, ): """Test that apply_to_flowthrough_payment is forwarded to the model build call.""" from tests.utils.factories import ( LedgerAdjustmentAppliedFactory, LedgerAdjustmentDetailAppliedFactory, ) ledger_adjustment_applied = LedgerAdjustmentAppliedFactory.create() mock_model.build.return_value = ledger_adjustment_applied mock_detail_model.build.return_value = None post_data = [ { 'abacus_event_id': 1, 'account_id': 1, 'contract_id': 1, 'statement_period_id': 1, 'worksheet_adjustment_id': 1, 'adjustment_currency_code': 'USD', 'adjustment_amount': '3000.00', 'adjustment_payee_currency_code': 'USD', 'adjustment_amount_payee_currency': '3000.00', 'apply_to_flowthrough_payment': True, } ] res = logic.bulk_create(post_data) assert res.status == 201 build_kwargs = mock_model.build.call_args[1] assert build_kwargs.get('apply_to_flowthrough_payment') is True