"""Test for Split Persister.""" from unittest.mock import ANY from oto import status import pytest from collaborator.constants import error from collaborator.constants.split import RateType from collaborator.constants.split_type import SplitTypeId from collaborator.models.rds.split_persister import SplitPersister from collaborator.schemas.split import ReplacementSplitSchema, ReplacementSplitsSchema from collaborator.utils import logging from collaborator.utils.error import OwsError from tests.testutils import db def _replacement(identifier, split_type_id, splits): """Build a ReplacementSplitsSchema from plain dicts.""" return ReplacementSplitsSchema( identifier=identifier, split_type_id=split_type_id, splits=[ReplacementSplitSchema(**split) for split in splits], ) @db.test_schema_default_seed @pytest.mark.parametrize( "split_type_id, identifier, expected_results", [ ( 1, "ABCD1234", [ { "id": 1, "identifier": "ABCD1234", "split_rate": 0.1, "split_type_id": 1, "collaborator_id": 1, "rate_type": "NET", "created_date": ANY, "updated_date": None, "source": None, }, { "id": 2, "identifier": "ABCD1234", "split_rate": 0.1, "split_type_id": 1, "collaborator_id": 2, "rate_type": "NET", "created_date": ANY, "updated_date": None, "source": None, }, ], ), ( 2, "12341234", [ { "id": 5, "identifier": "12341234", "split_rate": 0.2, "split_type_id": 2, "collaborator_id": 1, "rate_type": "NET", "created_date": ANY, "updated_date": None, "source": None, }, { "id": 4, "identifier": "12341234", "split_rate": 0.1, "split_type_id": 2, "collaborator_id": 2, "rate_type": "NET", "created_date": ANY, "updated_date": None, "source": None, }, ], ), (1, "NOTEXIST", []), ], ) def test_get_for_account_and_identifier(split_type_id, identifier, expected_results): """Test getting splits based on an split type and identifier.""" results = SplitPersister.get_for_identifiers(split_type_id, [identifier]) results = [{**item, "created_date": ANY} for item in results] assert results == expected_results @db.test_schema_default_seed @pytest.mark.parametrize( "split_ids, expected_results", [ ( [1, 2], [ { "id": 1, "identifier": "ABCD1234", "split_rate": 0.1, "split_type_id": 1, "collaborator_id": 1, "rate_type": "NET", "created_date": ANY, "updated_date": None, "source": None, }, { "id": 2, "identifier": "ABCD1234", "split_rate": 0.1, "split_type_id": 1, "collaborator_id": 2, "rate_type": "NET", "created_date": ANY, "updated_date": None, "source": None, }, ], ), ( [3], [ { "id": 3, "identifier": "12341234", "split_rate": 0.1, "split_type_id": 1, "collaborator_id": 1, "rate_type": "NET", "created_date": ANY, "updated_date": None, "source": None, }, ], ), ([420], []), ([], []), ], ) def test_get_for_ids(split_ids, expected_results): """Test getting splits based on their IDs.""" results = SplitPersister.get_for_ids(split_ids) results = [{**item, "created_date": ANY} for item in results] assert results == expected_results @db.test_schema_default_seed @pytest.mark.parametrize( "collaborator_id, split_types, expected_results", [ # All splits for collaborator 1 (no type filter): seed has IDs 1, 3, 5, 1000. ( 1, None, [ { "id": 1, "identifier": "ABCD1234", "split_rate": 0.1, "split_type_id": 1, "collaborator_id": 1, "rate_type": "NET", "created_date": ANY, "updated_date": None, "source": None, }, { "id": 3, "identifier": "12341234", "split_rate": 0.1, "split_type_id": 1, "collaborator_id": 1, "rate_type": "NET", "created_date": ANY, "updated_date": None, "source": None, }, { "id": 5, "identifier": "12341234", "split_rate": 0.2, "split_type_id": 2, "collaborator_id": 1, "rate_type": "NET", "created_date": ANY, "updated_date": None, "source": None, }, { "id": 1000, "identifier": "1", "split_rate": 0.25, "split_type_id": 2, "collaborator_id": 1, "rate_type": "NET", "created_date": ANY, "updated_date": None, "source": None, }, ], ), # Filter by TRACK only: IDs 5 and 1000 for collaborator 1. ( 1, [SplitTypeId.TRACK], [ { "id": 5, "identifier": "12341234", "split_rate": 0.2, "split_type_id": 2, "collaborator_id": 1, "rate_type": "NET", "created_date": ANY, "updated_date": None, "source": None, }, { "id": 1000, "identifier": "1", "split_rate": 0.25, "split_type_id": 2, "collaborator_id": 1, "rate_type": "NET", "created_date": ANY, "updated_date": None, "source": None, }, ], ), # Filter by SOUND_RECORDING only: IDs 1 and 3 for collaborator 1. ( 1, [SplitTypeId.SOUND_RECORDING], [ { "id": 1, "identifier": "ABCD1234", "split_rate": 0.1, "split_type_id": 1, "collaborator_id": 1, "rate_type": "NET", "created_date": ANY, "updated_date": None, "source": None, }, { "id": 3, "identifier": "12341234", "split_rate": 0.1, "split_type_id": 1, "collaborator_id": 1, "rate_type": "NET", "created_date": ANY, "updated_date": None, "source": None, }, ], ), # Collaborator with no splits returns empty. (999, None, []), # Filter excludes all of the collaborator's splits. (5, [SplitTypeId.SOUND_RECORDING], []), ], ) def test_get_for_collaborator(collaborator_id, split_types, expected_results): """Test getting splits for a collaborator, optionally filtered by split type.""" results = SplitPersister.get_for_collaborator( collaborator_id=collaborator_id, split_types=split_types ) results = [{**item.to_dict(), "created_date": ANY} for item in results] sort_key = lambda s: s["id"] # noqa: E731 assert sorted(results, key=sort_key) == sorted(expected_results, key=sort_key) @db.test_schema_default_seed @pytest.mark.parametrize( "split_data, expected_results, has_dp", [ ( # Not modifying any splits - Vendor with DP { "dp_splits_agreed": True, "vendor_id": 25153, "tracks": [ { "tuid": "12341234", "splits": [ { "id": 4, "collaborator_id": 2, "split_rate": 0.1, "split_type_id": 2, "rate_type": "NET", }, { "id": 5, "collaborator_id": 1, "split_rate": 0.1, "split_type_id": 2, "rate_type": "NET", }, ], }, { "tuid": "1", "splits": [ { "id": 1000, "collaborator_id": 1, "split_rate": 0.25, "split_type_id": 2, "rate_type": "NET", } ], }, ], }, [ { "id": 1000, "identifier": "1", "split_rate": 0.25, "split_type_id": 2, "collaborator_id": 1, "rate_type": "NET", "created_date": "2024-01-01T11:22:33", "updated_date": "2024-01-02T11:22:33", "source": None, }, { "id": 4, "identifier": "12341234", "split_rate": 0.1, "split_type_id": 2, "collaborator_id": 2, "rate_type": "NET", "created_date": "2024-01-01T11:22:33", "updated_date": "2024-01-02T11:22:33", "source": None, }, { "id": 5, "identifier": "12341234", "split_rate": 0.1, "split_type_id": 2, "collaborator_id": 1, "rate_type": "NET", "created_date": "2024-01-01T11:22:33", "updated_date": "2024-01-02T11:22:33", "source": None, }, ], True, ), ( # Not modifying any splits - Vendor without DP { "vendor_id": 25153, "tracks": [ { "tuid": "12341234", "splits": [ { "id": 4, "collaborator_id": 2, "split_rate": 0.1, "split_type_id": 2, "rate_type": "NET", }, { "id": 5, "collaborator_id": 1, "split_rate": 0.1, "split_type_id": 2, "rate_type": "NET", }, ], }, { "tuid": "1", "splits": [ { "id": 1000, "collaborator_id": 1, "split_rate": 0.25, "split_type_id": 2, "rate_type": "NET", } ], }, ], }, [ { "id": 1000, "identifier": "1", "split_rate": 0.25, "split_type_id": 2, "collaborator_id": 1, "rate_type": "NET", "created_date": "2024-01-01T11:22:33", "updated_date": "2024-01-02T11:22:33", "source": None, }, { "id": 4, "identifier": "12341234", "split_rate": 0.1, "split_type_id": 2, "collaborator_id": 2, "rate_type": "NET", "created_date": "2024-01-01T11:22:33", "updated_date": "2024-01-02T11:22:33", "source": None, }, { "id": 5, "identifier": "12341234", "split_rate": 0.1, "split_type_id": 2, "collaborator_id": 1, "rate_type": "NET", "created_date": "2024-01-01T11:22:33", "updated_date": "2024-01-02T11:22:33", "source": None, }, ], False, ), ( # Adding a split { "dp_splits_agreed": True, "vendor_id": 25153, "tracks": [ { "tuid": "12341234", "splits": [ { "id": 4, "collaborator_id": 2, "split_rate": 0.1, "split_type_id": 2, "rate_type": "NET", }, { "id": 5, "collaborator_id": 1, "split_rate": 0.1, "split_type_id": 2, "rate_type": "NET", }, ], }, { "tuid": "1", "splits": [ { "id": 1000, "collaborator_id": 1, "split_rate": 0.25, "split_type_id": 2, "rate_type": "NET", }, { "collaborator_id": 2, "split_rate": 0.10, "split_type_id": 2, "rate_type": "NET", }, ], }, ], }, [ { "id": 1000, "identifier": "1", "split_rate": 0.25, "split_type_id": 2, "collaborator_id": 1, "rate_type": "NET", "created_date": "2024-01-01T11:22:33", "updated_date": "2024-01-02T11:22:33", "source": None, }, { "id": 1001, "identifier": "1", "split_rate": 0.10, "split_type_id": 2, "collaborator_id": 2, "rate_type": "NET", "created_date": "2024-01-01T11:22:33", "updated_date": "2024-01-02T11:22:33", "source": None, }, { "id": 4, "identifier": "12341234", "split_rate": 0.1, "split_type_id": 2, "collaborator_id": 2, "rate_type": "NET", "created_date": "2024-01-01T11:22:33", "updated_date": "2024-01-02T11:22:33", "source": None, }, { "id": 5, "identifier": "12341234", "split_rate": 0.1, "split_type_id": 2, "collaborator_id": 1, "rate_type": "NET", "created_date": "2024-01-01T11:22:33", "updated_date": "2024-01-02T11:22:33", "source": None, }, ], True, ), ( # Updating a split { "dp_splits_agreed": True, "vendor_id": 25153, "tracks": [ { "tuid": "12341234", "splits": [ { "id": 4, "collaborator_id": 2, "split_rate": 0.2, "split_type_id": 2, "rate_type": "NET", }, { "id": 5, "collaborator_id": 1, "split_rate": 0.1, "split_type_id": 2, "rate_type": "NET", }, ], }, { "tuid": "1", "splits": [ { "id": 1000, "collaborator_id": 1, "split_rate": 0.25, "split_type_id": 2, "rate_type": "NET", } ], }, ], }, [ { "id": 1000, "identifier": "1", "split_rate": 0.25, "split_type_id": 2, "collaborator_id": 1, "rate_type": "NET", "created_date": "2024-01-01T11:22:33", "updated_date": "2024-01-02T11:22:33", "source": None, }, { "id": 4, "identifier": "12341234", "split_rate": 0.2, "split_type_id": 2, "collaborator_id": 2, "rate_type": "NET", "created_date": "2024-01-01T11:22:33", "updated_date": "2024-01-02T11:22:33", "source": None, }, { "id": 5, "identifier": "12341234", "split_rate": 0.1, "split_type_id": 2, "collaborator_id": 1, "rate_type": "NET", "created_date": "2024-01-01T11:22:33", "updated_date": "2024-01-02T11:22:33", "source": None, }, ], True, ), ( # Removing all splits { "dp_splits_agreed": True, "vendor_id": 25153, "tracks": [ { "tuid": "12341234", "splits": [], }, { "tuid": "1", "splits": [], }, ], }, [], True, ), ( # Updating, creating and deleting splits { "dp_splits_agreed": True, "vendor_id": 25153, "tracks": [ { "tuid": "12341234", "splits": [ { "id": 4, "collaborator_id": 2, "split_rate": 0.2, "split_type_id": 2, "rate_type": "NET", }, ], }, { "tuid": "1", "splits": [ { "id": 1000, "collaborator_id": 1, "split_rate": 0.25, "split_type_id": 2, "rate_type": "NET", }, { "collaborator_id": 2, "split_rate": 0.10, "split_type_id": 2, "rate_type": "NET", }, ], }, ], }, [ { "id": 1000, "identifier": "1", "split_rate": 0.25, "split_type_id": 2, "collaborator_id": 1, "rate_type": "NET", "created_date": "2024-01-01T11:22:33", "updated_date": "2024-01-02T11:22:33", "source": None, }, { "id": 1001, "identifier": "1", "split_rate": 0.10, "split_type_id": 2, "collaborator_id": 2, "rate_type": "NET", "created_date": "2024-01-01T11:22:33", "updated_date": "2024-01-02T11:22:33", "source": None, }, { "id": 4, "identifier": "12341234", "split_rate": 0.2, "split_type_id": 2, "collaborator_id": 2, "rate_type": "NET", "created_date": "2024-01-01T11:22:33", "updated_date": "2024-01-02T11:22:33", "source": None, }, ], True, ), ( # When no collabs with DP are affected, request succeeds though Ts&Cs are not agreed { "vendor_id": 25153, "tracks": [ { "tuid": "1", "splits": [ { "id": 1000, "collaborator_id": 1, "split_rate": 0.35, "split_type_id": 2, "rate_type": "NET", }, ], }, ], }, [ { "id": 1000, "identifier": "1", "split_rate": 0.35, "split_type_id": 2, "collaborator_id": 1, "rate_type": "NET", "created_date": "2024-01-01T11:22:33", "updated_date": "2024-01-02T11:22:33", "source": None, }, ], True, ), ( # When vendor has NOT DP, request succeeds though total splits are over 100% { "dp_splits_agreed": True, "vendor_id": 25153, "tracks": [ { "tuid": "12341234", "splits": [ { "id": 4, "collaborator_id": 2, "split_rate": 0.5, "split_type_id": 2, "rate_type": "NET", }, { "id": 5, "collaborator_id": 1, "split_rate": 0.6, "split_type_id": 2, "rate_type": "NET", }, ], } ], }, [ { "id": 4, "identifier": "12341234", "split_rate": 0.5, "split_type_id": 2, "collaborator_id": 2, "rate_type": "NET", "created_date": "2024-01-01T11:22:33", "updated_date": "2024-01-02T11:22:33", "source": None, }, { "id": 5, "identifier": "12341234", "split_rate": 0.6, "split_type_id": 2, "collaborator_id": 1, "rate_type": "NET", "created_date": "2024-01-01T11:22:33", "updated_date": "2024-01-02T11:22:33", "source": None, }, ], False, ), ], ) def test_replace_track_splits_success( split_data, expected_results, has_dp, mocker, mock_user ): """Test replace split data success.""" bulk_log_events_mock = mocker.patch.object(logging, "bulk_log_events") split_data_to_insert, _ = SplitPersister.replace_track_splits( split_data, mock_user, has_direct_payments=has_dp ) split_data_to_insert = [ {**item, "created_date": ANY, "updated_date": ANY} for item in split_data_to_insert ] # Order from get_for_identifiers is DB-implementation-defined; sort both # lists by (identifier, collaborator_id) for a stable comparison. sort_key = lambda s: (s["identifier"], s["split_type_id"], s["collaborator_id"]) # noqa: E731 assert sorted(split_data_to_insert, key=sort_key) == sorted( expected_results, key=sort_key ) bulk_log_events_mock.assert_called_once() call_args = bulk_log_events_mock.call_args[0] assert call_args[0] == logging.LOG_EVENT_UPDATE assert call_args[1] == "split" assert len(call_args[2]) == len(expected_results) assert call_args[3] == mock_user @db.test_schema_default_seed @pytest.mark.parametrize( "split_data, has_dp", [ # No DP Splits Agreed when modifying a split with for a Collaborator with DP ( { "vendor_id": 25153, "tracks": [ { "tuid": "12341234", "splits": [ { "collaborator_id": 2, "split_rate": 0.2, "split_type_id": 2, "rate_type": "NET", }, ], }, ], }, True, ), # No DP Splits Agreed when deleting a split with for a Collaborator with DP ( { "vendor_id": 25153, "tracks": [ { "tuid": "12341234", "splits": [ { "collaborator_id": 1, "split_rate": 0.1, "split_type_id": 2, "rate_type": "NET", }, { "collaborator_id": 3, "split_rate": 0.2, "split_type_id": 2, "rate_type": "NET", }, { "collaborator_id": 4, "split_rate": 0.25, "split_type_id": 2, "rate_type": "NET", }, ], }, ], }, True, ), # No DP Splits Agreed when creating a split with for a Collaborator with DP ( { "vendor_id": 25153, "tracks": [ { "tuid": "12345", "splits": [ { "collaborator_id": 2, "split_rate": 0.2, "split_type_id": 2, "rate_type": "NET", }, ], }, ], }, True, ), # Total splits over 100% when a collaborator has DP ( { "dp_splits_agreed": True, "vendor_id": 25153, "tracks": [ { "tuid": "12341234", "splits": [ { "collaborator_id": 1, "split_rate": 0.5, "split_type_id": 2, "rate_type": "NET", }, { "collaborator_id": 2, "split_rate": 0.5, "split_type_id": 2, "rate_type": "NET", }, { "collaborator_id": 3, "split_rate": 0.1, "split_type_id": 2, "rate_type": "NET", }, { "collaborator_id": 4, "split_rate": 0.25, "split_type_id": 2, "rate_type": "NET", }, ], }, ], }, True, ), ], ) def test_replace_splits_fail(split_data, has_dp, mocker, mock_user): """Test replace split data fail.""" bulk_log_events_mock = mocker.patch.object(logging, "bulk_log_events") with pytest.raises(OwsError) as err: SplitPersister.replace_track_splits(split_data, mock_user, has_dp) assert err.value.status == status.BAD_REQUEST assert err.value.code == error.ERROR_CODE_BAD_PARAMS bulk_log_events_mock.assert_not_called() # --------------------------------------------------------------------------- # get_for_replacement_splits # --------------------------------------------------------------------------- @db.test_schema_default_seed def test_get_for_replacement_splits_returns_matching_identifier_type_pairs( mock_account, ): """Returns existing splits matching the (identifier, split_type_id) pairs.""" replacements = [ _replacement("12341234", SplitTypeId.TRACK, []), _replacement("1", SplitTypeId.TRACK, []), ] results = SplitPersister.get_for_replacement_splits(replacements, mock_account.id) # Seed track splits for vendor 24601: id 4 + 5 for "12341234", id 1000 for "1". assert sorted(split.split_id for split in results) == [4, 5, 1000] @db.test_schema_default_seed def test_get_for_replacement_splits_is_scoped_by_split_type_and_vendor(mock_account): """Only splits of the requested type owned by the vendor are returned.""" # "12341234" SOUND_RECORDING splits: id 3 (collaborator 1, vendor 24601) and # id 6 (collaborator 4, vendor 90210). Only id 3 belongs to vendor 24601. replacements = [ _replacement("12341234", SplitTypeId.SOUND_RECORDING, []), ] results = SplitPersister.get_for_replacement_splits(replacements, mock_account.id) assert sorted(split.split_id for split in results) == [3] @db.test_schema_default_seed def test_get_for_replacement_splits_empty_for_unknown_identifier(mock_account): """Returns nothing when no split matches the identifier/type pairs.""" replacements = [ _replacement("DOES-NOT-EXIST", SplitTypeId.SUBACCOUNT, []), ] assert ( SplitPersister.get_for_replacement_splits(replacements, mock_account.id) == [] ) # --------------------------------------------------------------------------- # replace_splits # --------------------------------------------------------------------------- @db.test_schema_default_seed @pytest.mark.parametrize( "replacements, has_dp, dp_splits_agreed, expected_results, expected_deleted", [ ( # Update collaborator 2's split (id 4, preserved) and delete # collaborator 1's split (id 5), for a vendor without DP. [ _replacement( "12341234", SplitTypeId.TRACK, [ { "collaborator_id": 2, "split_rate": 0.3, "rate_type": RateType.NET, } ], ) ], False, False, [ { # Existing split id is preserved (upsert, not delete + insert). "id": 4, "identifier": "12341234", "split_rate": 0.3, "split_type_id": SplitTypeId.TRACK, "collaborator_id": 2, "rate_type": RateType.NET, "created_date": ANY, "updated_date": ANY, "source": None, }, ], [5], ), ( # Removing all splits for an identifier. [ _replacement("1", SplitTypeId.TRACK, []), ], False, False, [], [1000], ), ( # Creating brand new SUBACCOUNT splits (none existed before). # Both collaborators (1 and 2) belong to vendor 24601. [ _replacement( "555", SplitTypeId.SUBACCOUNT, [ { "collaborator_id": 1, "split_rate": 0.5, "rate_type": RateType.NET, }, { "collaborator_id": 2, "split_rate": 0.5, "rate_type": RateType.NET, }, ], ) ], False, False, [ { "id": ANY, "identifier": "555", "split_rate": 0.5, "split_type_id": SplitTypeId.SUBACCOUNT, "collaborator_id": 1, "rate_type": RateType.NET, "created_date": ANY, "updated_date": ANY, "source": None, }, { "id": ANY, "identifier": "555", "split_rate": 0.5, "split_type_id": SplitTypeId.SUBACCOUNT, "collaborator_id": 2, "rate_type": RateType.NET, "created_date": ANY, "updated_date": ANY, "source": None, }, ], [], ), ( # Mixed TRACK + SUBACCOUNT replacement in a single call. [ _replacement( "1", SplitTypeId.TRACK, [ { "collaborator_id": 1, "split_rate": 0.4, "rate_type": RateType.NET, } ], ), _replacement( "555", SplitTypeId.SUBACCOUNT, [ { "collaborator_id": 1, "split_rate": 1.0, "rate_type": RateType.NET, } ], ), ], False, False, [ { # Existing split id for "1" is preserved (upsert). "id": 1000, "identifier": "1", "split_rate": 0.4, "split_type_id": SplitTypeId.TRACK, "collaborator_id": 1, "rate_type": RateType.NET, "created_date": ANY, "updated_date": ANY, "source": None, }, { "id": ANY, "identifier": "555", "split_rate": 1.0, "split_type_id": SplitTypeId.SUBACCOUNT, "collaborator_id": 1, "rate_type": RateType.NET, "created_date": ANY, "updated_date": ANY, "source": None, }, ], [], ), ], ) def test_replace_splits_success( replacements, has_dp, dp_splits_agreed, expected_results, expected_deleted, mocker, mock_user, mock_account, ): """Test the new replace_splits persister method (track + subaccount).""" bulk_log_events_mock = mocker.patch.object(logging, "bulk_log_events") final_splits, deleted_split_ids = SplitPersister.replace_splits( replacements=replacements, vendor_id=mock_account.id, has_direct_payments=has_dp, dp_splits_agreed=dp_splits_agreed, user=mock_user, ) results = [ {**split.to_dict(), "created_date": ANY, "updated_date": ANY} for split in final_splits ] sort_key = lambda s: (s["identifier"], s["split_type_id"], s["collaborator_id"]) # noqa: E731 assert sorted(results, key=sort_key) == sorted(expected_results, key=sort_key) assert sorted(deleted_split_ids) == sorted(expected_deleted) bulk_log_events_mock.assert_called_once() call_args = bulk_log_events_mock.call_args[0] assert call_args[0] == logging.LOG_EVENT_UPDATE assert call_args[1] == "split" assert len(call_args[2]) == len(expected_results) assert call_args[3] == mock_user @db.test_schema_default_seed def test_replace_splits_dp_requires_tcs_agreement(mocker, mock_user, mock_account): """A DP collaborator's split cannot be modified without agreeing T&Cs.""" bulk_log_events_mock = mocker.patch.object(logging, "bulk_log_events") # Collaborator 2 has a dp_enabled_date in the seed. replacements = [ _replacement( "12341234", SplitTypeId.TRACK, [{"collaborator_id": 2, "split_rate": 0.2, "rate_type": RateType.NET}], ) ] with pytest.raises(OwsError) as err: SplitPersister.replace_splits( replacements=replacements, vendor_id=mock_account.id, has_direct_payments=True, dp_splits_agreed=False, user=mock_user, ) assert err.value.status == status.BAD_REQUEST assert err.value.code == error.ERROR_CODE_BAD_PARAMS bulk_log_events_mock.assert_not_called() @db.test_schema_default_seed def test_replace_splits_dp_subaccount_over_100_rejected( mocker, mock_user, mock_account ): """A subaccount split over 100% is rejected when the vendor has DP.""" bulk_log_events_mock = mocker.patch.object(logging, "bulk_log_events") replacements = [ _replacement( "555", SplitTypeId.SUBACCOUNT, [{"collaborator_id": 1, "split_rate": 1.5, "rate_type": RateType.NET}], ) ] with pytest.raises(OwsError) as err: SplitPersister.replace_splits( replacements=replacements, vendor_id=mock_account.id, has_direct_payments=True, dp_splits_agreed=True, user=mock_user, ) assert err.value.status == status.BAD_REQUEST assert err.value.code == error.ERROR_CODE_BAD_PARAMS bulk_log_events_mock.assert_not_called() @db.test_schema_default_seed def test_replace_splits_dp_with_agreement_succeeds(mocker, mock_user, mock_account): """Modifying a DP collaborator's split succeeds once T&Cs are agreed.""" bulk_log_events_mock = mocker.patch.object(logging, "bulk_log_events") replacements = [ _replacement( "12341234", SplitTypeId.TRACK, [{"collaborator_id": 2, "split_rate": 0.2, "rate_type": RateType.NET}], ) ] final_splits, deleted_split_ids = SplitPersister.replace_splits( replacements=replacements, vendor_id=mock_account.id, has_direct_payments=True, dp_splits_agreed=True, user=mock_user, ) results = [ {**split.to_dict(), "created_date": ANY, "updated_date": ANY} for split in final_splits ] assert results == [ { # Collaborator 2's existing split (id 4) is preserved via upsert; # collaborator 1's split (id 5) is deleted. "id": 4, "identifier": "12341234", "split_rate": 0.2, "split_type_id": SplitTypeId.TRACK, "collaborator_id": 2, "rate_type": RateType.NET, "created_date": ANY, "updated_date": ANY, "source": None, } ] assert sorted(deleted_split_ids) == [5] bulk_log_events_mock.assert_called_once()