"""Test for the collaborator logic.""" from datetime import date from http import HTTPStatus from types import SimpleNamespace from unittest.mock import ANY import pytest from collaborator.constants import error from collaborator.constants.features import BALANCE_CLEARING, SHOW_DP_ACTIVATION_CTA from collaborator.constants.transaction import ( BALANCE_CLEARING_MSG, TYPE_BALANCE_CLEARING, ) from collaborator.logic import collaborator from collaborator.utils import logging from collaborator.utils.error import OwsError from collaborator.utils.typing import Account @pytest.mark.parametrize( "example_participant_id, example_name," "example_currency, collaborator_type, description, internal_id", [ ("666", "Nothing", "USD", "COLLABORATOR", "Mixing Engineer", "ABC-123"), ("999", "Nowhere", "GBP", None, "Lawyer", "ABC-222"), ], ) def test_create_collaborator_success( mocker, mock_user, mock_account, example_participant_id, example_name, example_currency, collaborator_type, description, internal_id, ): """Test successfully creating a collaborator.""" mock_result_id = 42 mock_result = { "id": mock_result_id, "participant_id": example_participant_id, "name": example_name, "vendor_id": mock_account.id, "currency": example_currency, "collaborator_type": collaborator_type, "description": description, "internal_id": internal_id, } mock_create_collaborator = mocker.patch.object( collaborator.CollaboratorPersister, "create_collaborator" ) mocker.patch.object( collaborator.StatementPeriodPersister, "get_or_create_open_statement_period" ) mock_create_collaborator.return_value = (mock_result, True) mock_log_event = mocker.patch.object(collaborator.logging, "log_event") collaborator.create_collaborator( example_participant_id, example_name, mock_account, collaborator_type, example_currency, mock_user, description, internal_id, ) assert mock_create_collaborator.called mock_log_event.assert_called_with( logging.LOG_EVENT_CREATE, "collaborator", mock_result_id, None, mock_result, mock_user, ) @pytest.mark.parametrize( "example_participant_id, example_name", [ ("666", "Nothing"), ("999", "Nowhere"), ], ) def test_create_collaborator_already_exists( mocker, mock_account, mock_user, example_participant_id, example_name ): """Test creating a collaborator that already exists.""" mock_create_collaborator = mocker.patch.object( collaborator.CollaboratorPersister, "create_collaborator" ) mocker.patch.object( collaborator.StatementPeriodPersister, "get_or_create_open_statement_period" ) mock_data = {"id": 1} mock_create_collaborator.return_value = (mock_data, False) collaborator_type = "COLLABORATOR" currency = "USD" mock_log_event = mocker.patch.object(collaborator.logging, "log_event") collaborator.create_collaborator( example_participant_id, example_name, mock_account, collaborator_type, currency, mock_user, ) mock_log_event.assert_called_with( logging.LOG_EVENT_CREATE, "collaborator", mock_data["id"], None, mock_data, mock_user, ) mock_create_collaborator.assert_called_with( example_name, mock_account, None, example_participant_id, collaborator_type, currency, None, None, True, 24601, ) def test_get_by_id_and_account_success(mocker, mock_account): """Test getting collaborators by an account.""" collaborator_id = 1234 mock_get_by_id_and_account = mocker.patch.object( collaborator.CollaboratorPersister, "get_by_id_and_account" ) collaborator.get_by_id_and_account(collaborator_id, mock_account) mock_get_by_id_and_account.assert_called_with(collaborator_id, mock_account) @pytest.mark.parametrize( "account, limit, offset", [ (Account("vendor", "24601"), 0, 0), (Account("vendor", "55555"), 10, 20), ], ) def test_get_for_account_success(mocker, account, limit, offset): """Test getting collaborators by an account.""" mock_get_for_account = mocker.patch.object( collaborator.CollaboratorPersister, "get_for_account" ) collaborator.get_for_account(account, limit, offset) mock_get_for_account.assert_called_with(account, limit, offset, None) @pytest.mark.parametrize( "account, limit, offset", [ (Account("vendor", "24601"), 0, 0), (Account("vendor", "55555"), 10, 20), ], ) def test_get_for_account_success_with_recipient_id(mocker, account, limit, offset): """Test getting collaborators by an account.""" mock_get_for_account = mocker.patch.object( collaborator.CollaboratorPersister, "get_for_account" ) collaborator.get_for_account(account, limit, offset, True) mock_get_for_account.assert_called_with(account, limit, offset, True) @pytest.mark.parametrize( "payload,collaborator_balance,balance_clearing_ff_enabled", [ ({"performance_rights": True, "name": "Json", "currency": "USD"}, 0, False), ({"performance_rights": True, "name": "Mark", "currency": "USD"}, 0, False), ({"dp_enabled": True}, 0, False), ({"dp_enabled": True}, 0, True), ({"dp_enabled": True}, 1, True), ], ) def test_update_collaborator_success( mocker, mock_user, mock_account, mock_features, payload, collaborator_balance, balance_clearing_ff_enabled, ): """Test successfully updating a collaborator.""" mock_features( { SHOW_DP_ACTIVATION_CTA: payload.get("dp_enabled"), BALANCE_CLEARING: balance_clearing_ff_enabled, } ) collaborator_id = 1 mock_update_collaborator_result = { "id": 1, "name": payload.get("name"), "vendor_id": mock_account.id, "performance_rights": True, "currency": payload.get("currency"), "dp_enabled_date": "2021-01-01" if payload.get("dp_enabled") else None, } mock_update_collaborator = mocker.patch.object( collaborator.CollaboratorPersister, "update_collaborator" ) mock_update_collaborator.return_value = mock_update_collaborator_result mock_get_collaborator = mocker.patch.object( collaborator.CollaboratorPersister, "get_by_id_and_account" ) mock_get_collaborator.return_value = {"currency": "USD"} mock_create_collaborator_payee = mocker.patch.object( collaborator.ows_payee, "create_collaborator_payee" ) mock_log_event = mocker.patch.object(collaborator.logging, "log_event") mock_get_open_statement_period = mocker.patch.object( collaborator.StatementPeriodPersister, "get_open_statement_period" ) mock_get_open_statement_period.return_value = SimpleNamespace( statement_period_id=456 ) mock_get_balances_for_ids = mocker.patch.object( collaborator.CollaboratorPersister, "get_balances_for_ids" ) mock_get_balances_for_ids.return_value = [ SimpleNamespace(amount=collaborator_balance) ] mock_create_transaction = mocker.patch.object( collaborator.TransactionPersister, "create_transaction" ) collaborator.update_collaborator(collaborator_id, payload, mock_account, mock_user) mock_get_collaborator.assert_called_with(collaborator_id, mock_account) mock_update_collaborator.assert_called_with(collaborator_id, payload, session=ANY) if mock_update_collaborator_result.get("dp_enabled_date"): mock_create_collaborator_payee.assert_called_with(collaborator_id) if not balance_clearing_ff_enabled: mock_create_transaction.assert_not_called() else: if collaborator_balance == 0: mock_create_transaction.assert_not_called() else: mock_create_transaction.assert_called_with( session=ANY, collaborator_id=collaborator_id, transaction_type=TYPE_BALANCE_CLEARING, transaction_date=ANY, original_amount=-collaborator_balance, chargeable_amount=-collaborator_balance, statement_period_id=456, currency="USD", description=BALANCE_CLEARING_MSG.format( amount=collaborator_balance, currency="USD", ), collaborator_share=None, report_id=None, transferwise_transaction_id=None, voided_transaction_id=None, ) mock_log_event.assert_called_with( logging.LOG_EVENT_UPDATE, "collaborator", collaborator_id, None, mock_update_collaborator_result, mock_user, ) @pytest.mark.parametrize( "payload", [ ({"performance_rights": True, "name": "Json"}), ({"performance_rights": True, "name": "Mark"}), ], ) def test_update_collaborator_failure(mocker, mock_user, mock_account, payload): """Test updating collaborator with failure.""" collaborator_id = 1 mock_update_collaborator = mocker.patch.object( collaborator.CollaboratorPersister, "update_collaborator" ) mock_get_collaborator = mocker.patch.object( collaborator.CollaboratorPersister, "get_by_id_and_account" ) mock_get_collaborator.side_effect = OwsError.not_found( message=error.ERROR_MESSAGE_COLLABORATOR_NOT_FOUND, code=error.ERROR_CODE_COLLABORATOR_NOT_FOUND, ) with pytest.raises(OwsError) as exc_info: collaborator.update_collaborator( collaborator_id, payload, mock_account, mock_user ) mock_get_collaborator.assert_called_with(collaborator_id, mock_account) mock_update_collaborator.assert_not_called() assert exc_info.value.status == HTTPStatus.NOT_FOUND def test_update_collaborator_failure_dp_activation_ff_disabled(mocker): """Test updating collaborator with failure when DP activation feature flag is disabled.""" mock_get_collaborator = mocker.patch.object( collaborator.CollaboratorPersister, "get_by_id_and_account", return_value={}, ) mock_is_feature_enabled = mocker.patch.object( collaborator.features, "is_feature_enabled", return_value=False ) mock_update_collaborator = mocker.patch.object( collaborator.CollaboratorPersister, "update_collaborator" ) collaborator_id = 1 payload = {"dp_enabled": True} with pytest.raises(OwsError) as exc_info: collaborator.update_collaborator(collaborator_id, payload, None, None) mock_get_collaborator.assert_called_with(collaborator_id, None) mock_is_feature_enabled.assert_called_with(SHOW_DP_ACTIVATION_CTA) mock_update_collaborator.assert_not_called() assert exc_info.value.status == HTTPStatus.FORBIDDEN def test_update_collaborator_balance_clearing_transaction_date_is_date_object( mocker, mock_user, mock_account ): """transaction_date must be a date object, not a string. Regression test for PROD-OWS-COLLABORATOR-AH: passing a str from time.strftime caused Transaction.to_dict() to crash on .isoformat(). """ mock_features = mocker.patch.object(collaborator.features, "is_feature_enabled") mock_features.return_value = True mocker.patch.object( collaborator.CollaboratorPersister, "get_by_id_and_account", return_value={"currency": "USD", "dp_enabled_date": None}, ) mocker.patch.object( collaborator.StatementPeriodPersister, "get_open_statement_period", return_value=SimpleNamespace(statement_period_id=1), ) mocker.patch.object( collaborator.CollaboratorPersister, "get_balances_for_ids", return_value=[SimpleNamespace(amount=100)], ) mock_create_transaction = mocker.patch.object( collaborator.TransactionPersister, "create_transaction", return_value={"id": 1}, ) mocker.patch.object(collaborator.ows_payee, "create_collaborator_payee") mocker.patch.object(collaborator.CollaboratorPersister, "update_collaborator") mocker.patch.object(collaborator.logging, "log_event") collaborator.update_collaborator(1, {"dp_enabled": True}, mock_account, mock_user) _, kwargs = mock_create_transaction.call_args assert isinstance(kwargs["transaction_date"], date), ( "transaction_date must be a datetime.date, not a str — " "Transaction.to_dict() calls .isoformat() on it" ) # --------------------------------------------------------------------------- # get_splits_by_collaborator_id # --------------------------------------------------------------------------- def _split(**kwargs): """Build a Split SQLAlchemy instance for use as a persister return value.""" from collaborator.models.rds.split import Split defaults = { "split_id": 1, "identifier": "TUID-1", "split_rate": 0.5, "split_type_id": 2, "collaborator_id": 42, "rate_type": "NET", "created_date": "2020-02-03T04:05:06", } defaults.update(kwargs) return Split(**defaults) def test_get_splits_by_collaborator_id_returns_serialised_splits(mocker): """Returns persister rows serialised via ``to_dict``.""" mock_get = mocker.patch.object( collaborator.SplitPersister, "get_for_collaborator", return_value=[ _split(split_id=1, identifier="A", collaborator_id=42), _split(split_id=2, identifier="B", collaborator_id=42), ], ) result = collaborator.get_splits_by_collaborator_id(collaborator_id=42) mock_get.assert_called_once_with(collaborator_id=42, split_types=None) assert [r.id for r in result] == [1, 2] assert all(r.collaborator_id == 42 for r in result) def test_get_splits_by_collaborator_id_forwards_split_types(mocker): """Forwards split_types through to the persister.""" from collaborator.constants.split_type import SplitTypeId mock_get = mocker.patch.object( collaborator.SplitPersister, "get_for_collaborator", return_value=[], ) collaborator.get_splits_by_collaborator_id( collaborator_id=42, split_types=[SplitTypeId.TRACK] ) mock_get.assert_called_once_with( collaborator_id=42, split_types=[SplitTypeId.TRACK] ) def test_get_splits_by_collaborator_id_returns_empty_when_no_splits(mocker): """Returns empty list when the persister returns nothing.""" mocker.patch.object( collaborator.SplitPersister, "get_for_collaborator", return_value=[] ) result = collaborator.get_splits_by_collaborator_id(collaborator_id=42) assert result == []