"""Functional tests for Bulk Ledgers.""" import copy from concurrent.futures import ThreadPoolExecutor from datetime import datetime, timedelta from decimal import Decimal from unittest.mock import patch from ledger.logic import bulk_ledgers from ledger.models.ledger_account import LedgerAccount from ledger.models.ledger_deposit import LedgerDeposit from tests.conftest import mock_account from tests.utils.factories import LedgerAccountContractFactory, LedgerAccountFactory from tests.utils.wrappers import delay_response_once # Save original methods get_ledger_current_balances_by_accounts = ( bulk_ledgers.get_ledger_current_balances_by_accounts ) get_ledger_current_balances_by_contracts = ( bulk_ledgers.get_ledger_current_balances_by_contracts ) def test_get_bulk_accounts_balance(fixture_client, mock_event_fixtures): """Test bulk get current balances with filters.""" created_at = datetime.now() for ind in (1, 2, 3): if ind > 1: mock_account(ind) LedgerAccountFactory.create( account_id=ind, current_balance=100 * ind, created_at=created_at + timedelta(minutes=1), ) # no filters res = fixture_client.get('/bulk/accounts-balance') assert res.status_code == 200 assert len(res.json) == 3 # account_ids filter res = fixture_client.get('/bulk/accounts-balance?account_ids=1,2,3') assert res.status_code == 200 response_items = res.json assert len(response_items) == 3 assert sorted(response_items, key=lambda x: x['account_id']) == [ { 'account_id': 1, 'currency_code': 'USD', 'currency_name': 'US Dollar', 'current_balance': '100.00', # 100 * 1 }, { 'account_id': 2, 'currency_code': 'USD', 'currency_name': 'US Dollar', 'current_balance': '200.00', # 100 * 2 }, { 'account_id': 3, 'currency_code': 'USD', 'currency_name': 'US Dollar', 'current_balance': '300.00', # 100 * 3 }, ] # get users with balance >= 200 res = fixture_client.get('/bulk/accounts-balance?balance_min=200') assert res.status_code == 200 response_items = res.json assert len(response_items) == 2 assert response_items == [ { 'currency_name': 'US Dollar', 'current_balance': '200.00', 'currency_code': 'USD', 'account_id': 2, }, { 'currency_name': 'US Dollar', 'current_balance': '300.00', 'currency_code': 'USD', 'account_id': 3, }, ] def test_bulk_create_ledger_account_entries( fixture_client, mock_bulk_ledgers_body, mock_event_fixtures ): """Test bulk ledger creation.""" res = fixture_client.post('/ledger/bulk', json=mock_bulk_ledgers_body) assert res.status_code == 201 account_ledger_entry = LedgerAccount.get_by_account_id(1).first() assert account_ledger_entry.current_balance == Decimal('44.44') assert account_ledger_entry.currency_code == 'USD' assert account_ledger_entry.abacus_event_id == 1 assert account_ledger_entry.previous_balance == Decimal('0.00') assert account_ledger_entry.currency_amount == Decimal('44.44') deposit_ledger = LedgerDeposit.get_by_id(1) assert deposit_ledger.contract_id == 1 assert deposit_ledger.account_id == 1 assert deposit_ledger.abacus_event_id == 1 assert deposit_ledger.currency_code == 'USD' assert deposit_ledger.rounded_amount == Decimal('44.44') assert deposit_ledger.remaining_amount == Decimal('0.004444444444444444') @patch('ledger.logic.bulk_ledgers.get_ledger_current_balances_by_accounts') def test_parallel_bulk_create_ledger_account_entries_handles_race_condition( mock_account_balances, test_app, mock_bulk_ledgers_body, mock_event_fixtures ): """Test parallel bulk ledger creation.""" # Delay the first response by a given duration to # make sure it runs in parallel with other requests mock_account_balances.side_effect = delay_response_once( get_ledger_current_balances_by_accounts, 0.5 ) def make_request(json_body): with test_app.test_client() as client: return client.post('/ledger/bulk', json=json_body) # Make multiple parallel requests max_workers = 5 results = [] with ThreadPoolExecutor(max_workers=max_workers) as executor: futures = [] for _ in range(max_workers): mock_bulk_ledgers_body = copy.deepcopy(mock_bulk_ledgers_body) for item in mock_bulk_ledgers_body: item['abacus_event_id'] = len(futures) + 1 futures.append(executor.submit(make_request, mock_bulk_ledgers_body)) for f in futures: results.append(f.result()) # Verify all requests were successful for res in results: assert res.status_code == 201 # Get all inserted entries account_ledger_entries = list(LedgerAccount.get_by_account_id(1).all()) # Convert DESC entries to ASC account_ledger_entries.reverse() # Verify each ledger entry currency_amount = Decimal('44.44') for index, account_ledger_entry in enumerate(account_ledger_entries, 1): assert account_ledger_entry.current_balance == ( currency_amount * Decimal(index) ) assert account_ledger_entry.currency_code == 'USD' assert 0 < account_ledger_entry.abacus_event_id <= max_workers assert account_ledger_entry.previous_balance == ( currency_amount * Decimal(index - 1) ) assert account_ledger_entry.currency_amount == currency_amount deposit_ledger = LedgerDeposit.get_by_id(index) assert deposit_ledger.contract_id == 1 assert deposit_ledger.account_id == 1 assert 0 < deposit_ledger.abacus_event_id <= max_workers assert deposit_ledger.currency_code == 'USD' assert deposit_ledger.rounded_amount == Decimal('44.44') assert deposit_ledger.remaining_amount == Decimal('0.004444444444444444') # Verify previous amounts assert account_ledger_entries[0].previous_balance == Decimal(0) for index in range(1, max_workers): prev_entry_current_balance = account_ledger_entries[index - 1].current_balance curr_entry_previous_balance = account_ledger_entries[index].previous_balance assert prev_entry_current_balance == curr_entry_previous_balance def test_get_contracts_balances(fixture_client, mock_event_fixtures): """Test get_contracts_balances.""" LedgerAccountContractFactory.create( account_id=1, contract_id=1, current_balance=Decimal(100), currency_code='USD' ) LedgerAccountContractFactory.create( account_id=1, contract_id=2, current_balance=Decimal(200), currency_code='EUR' ) res = fixture_client.get( '/bulk/contracts/closed-balance?contract_ids=1,2,3&account_ids=1' ) assert res.status_code == 200 response_items = res.json assert len(response_items) == 2 assert sorted(response_items, key=lambda x: x['contract_id']) == [ { 'ledger_account_contract_id': 1, 'account_id': 1, 'contract_id': 1, 'currency_code': 'USD', 'current_balance': '100.00', }, { 'ledger_account_contract_id': 2, 'account_id': 1, 'contract_id': 2, 'currency_code': 'EUR', 'current_balance': '200.00', }, ]