"""Functional tests for replacing splits.""" import json from unittest.mock import ANY from oto import status import pytest from collaborator.models.ows import ows_account, ows_product, ows_track from collaborator.models.snowflake import split_persister as snowflake_split_persister from tests.testutils import db, mock_auth, snowflake_db def _mock_vendor_lookups(mocker, vendor_id: int): """Make ows_track + ows_product return a single ``vendor_id`` for any tuid.""" mocker.patch.object( ows_track, "get_tracks_batched", side_effect=lambda tuids: [ {"tuid": tuid, "upc": f"UPC-{tuid}"} for tuid in tuids ], ) mocker.patch.object( ows_product, "get_products_by_upc", side_effect=lambda upcs: { "items": [{"upc": upc, "vendor_id": vendor_id} for upc in upcs] }, ) previous_splits_mock = [ { "id": 1000, "identifier": "1", "split_rate": 0.25, "split_type_id": 2, "collaborator_id": 1, "rate_type": "NET", }, { "id": 4, "identifier": "12341234", "split_rate": 0.1, "split_type_id": 2, "collaborator_id": 2, "rate_type": "NET", }, { "id": 5, "identifier": "12341234", "split_rate": 0.1, "split_type_id": 2, "collaborator_id": 1, "rate_type": "NET", }, ] @snowflake_db.test_schema_default_seed @db.test_schema_default_seed @pytest.mark.parametrize( ( "split_data", "expected_results", "splits_to_insert", "splits_to_update", "splits_to_delete", "has_dp", ), [ ( # Not modifying any splits - With DP { "dp_splits_agreed": True, "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, }, ], [], [ { "id": 1000, "identifier": "1", "split_rate": 0.25, "split_type_id": 2, "collaborator_id": 1, "rate_type": "NET", "created_date": ANY, "updated_date": ANY, "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": ANY, "source": None, }, { "id": 5, "identifier": "12341234", "split_rate": 0.1, "split_type_id": 2, "collaborator_id": 1, "rate_type": "NET", "created_date": ANY, "updated_date": ANY, "source": None, }, ], [], True, ), ( # Not modifying any splits - Without DP { "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, }, ], [], [ { "id": 1000, "identifier": "1", "split_rate": 0.25, "split_type_id": 2, "collaborator_id": 1, "rate_type": "NET", "created_date": ANY, "updated_date": ANY, "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": ANY, "source": None, }, { "id": 5, "identifier": "12341234", "split_rate": 0.1, "split_type_id": 2, "collaborator_id": 1, "rate_type": "NET", "created_date": ANY, "updated_date": ANY, "source": None, }, ], [], False, ), ( # Adding a split { "dp_splits_agreed": True, "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, }, ], [ { "id": 1001, "identifier": "1", "split_rate": 0.10, "split_type_id": 2, "collaborator_id": 2, "rate_type": "NET", "created_date": ANY, "updated_date": ANY, "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": ANY, "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": ANY, "source": None, }, { "id": 5, "identifier": "12341234", "split_rate": 0.1, "split_type_id": 2, "collaborator_id": 1, "rate_type": "NET", "created_date": ANY, "updated_date": ANY, "source": None, }, ], [], True, ), ( # Updating a split { "dp_splits_agreed": True, "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, }, ], [], [ { "id": 1000, "identifier": "1", "split_rate": 0.25, "split_type_id": 2, "collaborator_id": 1, "rate_type": "NET", "created_date": ANY, "updated_date": ANY, "source": None, }, { "id": 4, "identifier": "12341234", "split_rate": 0.2, "split_type_id": 2, "collaborator_id": 2, "rate_type": "NET", "created_date": ANY, "updated_date": ANY, "source": None, }, { "id": 5, "identifier": "12341234", "split_rate": 0.1, "split_type_id": 2, "collaborator_id": 1, "rate_type": "NET", "created_date": ANY, "updated_date": ANY, "source": None, }, ], [], True, ), ( # Removing all splits { "dp_splits_agreed": True, "tracks": [ { "tuid": "12341234", "splits": [], }, { "tuid": "1", "splits": [], }, ], }, [], [], [], [1000, 4, 5], True, ), ( # Updating, creating and deleting splits { "dp_splits_agreed": True, "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, }, ], [ { "collaborator_id": 2, "created_date": ANY, "id": 1001, "identifier": "1", "rate_type": "NET", "source": None, "split_rate": 0.1, "split_type_id": 2, "updated_date": None, }, ], [ { "collaborator_id": 1, "created_date": ANY, "id": 1000, "identifier": "1", "rate_type": "NET", "source": None, "split_rate": 0.25, "split_type_id": 2, "updated_date": None, }, { "collaborator_id": 2, "created_date": ANY, "id": 4, "identifier": "12341234", "rate_type": "NET", "source": None, "split_rate": 0.2, "split_type_id": 2, "updated_date": None, }, ], [5], True, ), ], ) def test_replace_splits_success( auth_client, mocker, split_data, expected_results, splits_to_insert, splits_to_update, splits_to_delete, has_dp, ): """Test successfully replacing splits with a CollaboratorProfile.""" mock_auth(mocker, 24601) _mock_vendor_lookups(mocker, 24601) has_dp_mock = mocker.patch.object( ows_account, "has_direct_payments", return_value=has_dp ) # Mock the uwsgi_spool_task function to execute the task synchronously def mock_uwsgi_spool_task(func, *args, **kwargs): return func(*args, **kwargs) mocker.patch( "collaborator.logic.split.uwsgi_spool_task", side_effect=mock_uwsgi_spool_task ) mocker.patch.object( snowflake_split_persister, "_get_splits", return_value=previous_splits_mock ) sf_insert_splits_mock = mocker.patch.object( snowflake_split_persister, "_insert_splits" ) sf_update_splits_mock = mocker.patch.object( snowflake_split_persister, "_update_splits" ) sf_delete_splits_mock = mocker.patch.object( snowflake_split_persister, "_delete_splits" ) replace_response = auth_client.put( "/splits/replace", data=json.dumps(split_data), content_type="application/json" ) assert replace_response.status_code == status.OK result = [ {**item, "created_date": ANY, "updated_date": ANY} for item in replace_response.get_json() ] # Order from the persister's get_for_identifiers is DB-defined; sort # everything by (identifier, collaborator_id) before comparing. sort_key = lambda s: (s["identifier"], s["collaborator_id"]) # noqa: E731 assert has_dp_mock.call_count == 1 assert sf_insert_splits_mock.call_count == 1 insert_splits_args = sf_insert_splits_mock.call_args[0] assert sorted(insert_splits_args[0], key=sort_key) == sorted( splits_to_insert, key=sort_key ) assert sf_update_splits_mock.call_count == 1 update_splits_args = sf_update_splits_mock.call_args[0] assert sorted(update_splits_args[0], key=sort_key) == sorted( splits_to_update, key=sort_key ) assert sf_delete_splits_mock.call_count == 1 delete_splits_args = sf_delete_splits_mock.call_args[0] assert sorted(delete_splits_args[0]) == sorted(splits_to_delete) assert sorted(result, key=sort_key) == sorted(expected_results, key=sort_key) @db.test_schema_default_seed @pytest.mark.parametrize( "user_vendor_id, body_vendor_id, split_data, expected_status", [ # Empty tracks list — fails Pydantic validation (min_length=1) ( 24601, 24601, {"dp_splits_agreed": True, "tracks": []}, status.BAD_REQUEST, ), # DP collaborator without dp_splits_agreed ( 24601, 24601, { "tracks": [ { "tuid": "12341234", "splits": [ { "collaborator_id": 2, "split_rate": 0.2, "split_type_id": 2, "rate_type": "NET", }, ], }, ], }, status.BAD_REQUEST, ), # Cross-vendor body: track resolves to vendor 90210 but collab 1 is on # vendor 24601 — must reject as split_vendor_mismatch. ( 24601, 90210, { "tracks": [ { "tuid": "12341234", "splits": [ { "collaborator_id": 1, "split_rate": 0.5, "split_type_id": 2, "rate_type": "NET", }, ], }, ], }, status.BAD_REQUEST, ), # Caller is on vendor 25153 but the body resolves to vendor 24601 — # must reject as forbidden (caller not authorised for the body's vendor). ( 25153, 24601, { "tracks": [ { "tuid": "12341234", "splits": [ { "collaborator_id": 1, "split_rate": 0.5, "split_type_id": 2, "rate_type": "NET", }, ], }, ], }, status.FORBIDDEN, ), ], ) def test_replace_splits_fail( auth_client, mocker, user_vendor_id, body_vendor_id, split_data, expected_status ): """Test failing to replace splits with a CollaboratorProfile.""" mock_auth(mocker, user_vendor_id) _mock_vendor_lookups(mocker, body_vendor_id) mocker.patch.object(ows_account, "has_direct_payments", return_value=True) result = auth_client.put( "/splits/replace", data=json.dumps(split_data), content_type="application/json" ) assert result.status_code == expected_status