"""Unit testcases for EarningsTransfer schema.""" from datetime import datetime from decimal import Decimal import pytest from abacus_common_logic.constants.constants import SYSTEM_TIMEZONE from freezegun import freeze_time from marshmallow import ValidationError from royalties.constants.constants import ( EARNINGS_TRANSFER_SORT_OPTIONS, PAYMENT_SCHEDULES, SORT_ORDER_OPTIONS, ) from royalties.constants.error import ( ERROR_INVALID_CRITERIA_DUPLICATE_PAYMENT_ENTITIES, ERROR_INVALID_CRITERIA_DUPLICATE_PAYMENT_SCHEDULES, ) from royalties.schemas.earnings_transfer import ( EarningsTransferBaseSchema, EarningsTransferDetailSchema, EarningsTransferFilterSchema, EarningsTransferPostSchema, EarningsTransferPutSchema, ) from royalties.tests.utils.factories import EarningsTransferFactory def test_earnings_transfer_base_schema(): """Test base schema for earnings transfer.""" mock_earnings_transfer = EarningsTransferFactory.create() result = EarningsTransferBaseSchema().dump(mock_earnings_transfer) assert result == { 'from_contract_id': mock_earnings_transfer.from_contract_id, 'to_contract_id': mock_earnings_transfer.to_contract_id, 'transfer_type': mock_earnings_transfer.transfer_type, 'rate_type': mock_earnings_transfer.rate_type, 'transfer_amount': str(mock_earnings_transfer.transfer_amount), 'input': mock_earnings_transfer.input, 'negative': bool(mock_earnings_transfer.negative), 'use_static_balance': bool(mock_earnings_transfer.use_static_balance), 'comment': mock_earnings_transfer.comment, 'active': False, } @freeze_time(datetime(2026, 5, 5, 0, 0, 0, tzinfo=SYSTEM_TIMEZONE)) def test_earnings_transfer_detail_schema(): """Test detail schema for earnings transfer.""" mock_earnings_transfer = EarningsTransferFactory.create() result = EarningsTransferDetailSchema().dump(mock_earnings_transfer) assert result == { 'earnings_transfer_id': mock_earnings_transfer.earnings_transfer_id, 'from_contract_id': mock_earnings_transfer.from_contract_id, 'to_contract_id': mock_earnings_transfer.to_contract_id, 'transfer_type': mock_earnings_transfer.transfer_type, 'rate_type': mock_earnings_transfer.rate_type, 'transfer_amount': str(mock_earnings_transfer.transfer_amount), 'input': mock_earnings_transfer.input, 'negative': bool(mock_earnings_transfer.negative), 'use_static_balance': bool(mock_earnings_transfer.use_static_balance), 'comment': mock_earnings_transfer.comment, 'active': False, 'created_at': '2026-05-05', } def test_earnings_transfer_filter_schema(): """Test filter schema for earnings transfer.""" mock_input = { 'limit': 10, 'offset': 0, 'sort_by': EARNINGS_TRANSFER_SORT_OPTIONS.EARNINGS_TRANSFER_ID, 'sort_order': SORT_ORDER_OPTIONS.DESC, 'reference_payment_entities': '1,2,3', 'payment_schedules': '30_days_after_month_end', } result = EarningsTransferFilterSchema().dump(mock_input) assert result == { 'limit': 10, 'offset': 0, 'sort_by': EARNINGS_TRANSFER_SORT_OPTIONS.EARNINGS_TRANSFER_ID, 'sort_order': SORT_ORDER_OPTIONS.DESC, 'reference_payment_entities': '1,2,3', 'payment_schedules': '30_days_after_month_end', } def test_filter_with_duplicate_payment_entities(): """Test earnings transfer filter schema with duplicate payment entities.""" with pytest.raises(ValidationError) as exc_info: EarningsTransferFilterSchema().load( { 'limit': 10, 'offset': 0, 'sort_by': EARNINGS_TRANSFER_SORT_OPTIONS.EARNINGS_TRANSFER_ID, 'sort_order': SORT_ORDER_OPTIONS.DESC, 'reference_payment_entities': '1,2,3,1,2', 'payment_schedules': '30_days_after_month_end,60_days_after_month_end', } ) assert exc_info.value.messages == { 'reference_payment_entities': [ ERROR_INVALID_CRITERIA_DUPLICATE_PAYMENT_ENTITIES ] } def test_filter_with_duplicate_payment_schedules(): """Test earnings transfer filter schema with duplicate payment schedules.""" with pytest.raises(ValidationError) as exc_info: EarningsTransferFilterSchema().load( { 'limit': 10, 'offset': 0, 'sort_by': EARNINGS_TRANSFER_SORT_OPTIONS.EARNINGS_TRANSFER_ID, 'sort_order': SORT_ORDER_OPTIONS.DESC, 'reference_payment_entities': '1,2,3', 'payment_schedules': '30_days_after_month_end,30_days_after_month_end', } ) assert exc_info.value.messages == { 'payment_schedules': [ERROR_INVALID_CRITERIA_DUPLICATE_PAYMENT_SCHEDULES] } def test_earnings_transfer_filter_schema_with_null_values(): """Test earnings transfer filter schema with null values.""" mock_input = { 'limit': 100, 'offset': 0, 'sort_by': EARNINGS_TRANSFER_SORT_OPTIONS.EARNINGS_TRANSFER_ID, 'sort_order': SORT_ORDER_OPTIONS.DESC, 'reference_payment_entities': None, 'payment_schedules': None, } result = EarningsTransferFilterSchema().load(mock_input) assert result == { 'limit': 100, 'offset': 0, 'sort_by': EARNINGS_TRANSFER_SORT_OPTIONS.EARNINGS_TRANSFER_ID, 'sort_order': SORT_ORDER_OPTIONS.DESC, 'reference_payment_entities': None, 'payment_schedules': None, } def test_earnings_transfer_filter_schema_with_missing_fields(): """Test earnings transfer filter schema with missing fields.""" mock_input = { 'reference_payment_entities': '1,2', 'payment_schedules': '30_days_after_month_end,60_days_after_month_end', } result = EarningsTransferFilterSchema().load(mock_input) assert result == { 'limit': 100, 'offset': 0, 'sort_by': EARNINGS_TRANSFER_SORT_OPTIONS.EARNINGS_TRANSFER_ID, 'sort_order': SORT_ORDER_OPTIONS.ASC, 'reference_payment_entities': '1,2', 'payment_schedules': '30_days_after_month_end,60_days_after_month_end', } def test_filter_schema_with_invalid_payment_entities(): """Test earnings transfer filter schema with invalid payment entities.""" with pytest.raises(ValidationError) as exc_info: EarningsTransferFilterSchema().load( { 'limit': 10, 'offset': 0, 'sort_by': EARNINGS_TRANSFER_SORT_OPTIONS.EARNINGS_TRANSFER_ID, 'sort_order': SORT_ORDER_OPTIONS.DESC, 'reference_payment_entities': '1,2,test', 'payment_schedules': '60_days_after_month_end', } ) assert exc_info.value.messages == { 'reference_payment_entities': ['Must be a comma-separated list of digits.'] } def test_filter_schema_with_invalid_payment_schedules(): """Test earnings transfer filter schema with invalid payment entity.""" with pytest.raises(ValidationError) as exc_info: EarningsTransferFilterSchema().load( { 'limit': 10, 'offset': 0, 'sort_by': EARNINGS_TRANSFER_SORT_OPTIONS.EARNINGS_TRANSFER_ID, 'sort_order': SORT_ORDER_OPTIONS.DESC, 'reference_payment_entities': '1,2', 'payment_schedules': 'test,60_days_after_month_end', } ) assert exc_info.value.messages == { 'payment_schedules': [f'Must be one of: {", ".join(PAYMENT_SCHEDULES)}'] } def test_earnings_transfer_post_schema(): """Test post schema for earnings transfer.""" mock_input = { 'from_contract_id': 317051, 'to_contract_id': 317057, 'transfer_type': 'override', 'rate_type': 'flat_rate', 'transfer_amount': '1', 'input': 'closing_balance', 'negative': True, 'active': True, 'comment': 'Test comment', } result = EarningsTransferPostSchema().load(mock_input) assert result == { 'from_contract_id': mock_input['from_contract_id'], 'to_contract_id': mock_input['to_contract_id'], 'transfer_type': mock_input['transfer_type'], 'rate_type': mock_input['rate_type'], 'transfer_amount': Decimal(mock_input['transfer_amount']), 'transfer_source': mock_input['input'], 'negative': mock_input['negative'], 'active': mock_input['active'], 'comment': mock_input['comment'], } def test_earnings_transfer_post_schema_with_required_fields(): """Test post schema for earnings transfer with only required fields.""" mock_input = { 'from_contract_id': 317051, 'to_contract_id': 317057, 'transfer_type': 'override', 'transfer_amount': '1', 'negative': True, 'active': True, } result = EarningsTransferPostSchema().load(mock_input) assert result == { 'from_contract_id': mock_input['from_contract_id'], 'to_contract_id': mock_input['to_contract_id'], 'transfer_type': mock_input['transfer_type'], 'transfer_amount': Decimal(mock_input['transfer_amount']), 'negative': mock_input['negative'], 'active': mock_input['active'], } def test_earnings_transfer_post_schema_with_invalid_enums(): """Test throws an error when enum values are invalid.""" mock_input = { 'from_contract_id': 317051, 'to_contract_id': 317057, 'transfer_type': 'invalid_transfer_type', 'rate_type': 'invalid_flat_rate', 'transfer_amount': '1', 'input': 'invalid_gross_revenue', 'negative': True, 'active': True, 'comment': 'Test comment', } with pytest.raises(ValidationError) as exc_info: EarningsTransferPostSchema().load(mock_input) assert exc_info.value.messages == { 'transfer_type': [ 'Must be one of cross_recoup, reclass, override, transfer, nr_transfer' ], 'rate_type': ['Must be one of flat_rate, percent'], 'input': ['Must be one of closing_balance, gross_revenue, net_revenue'], } def test_earnings_transfer_post_schema_for_percent_rate_type(): """Test throws an error when rate_type is percent and transfer_amount is not between 0-100.""" mock_input = { 'from_contract_id': 317051, 'to_contract_id': 317057, 'transfer_type': 'cross_recoup', 'rate_type': 'percent', 'transfer_amount': '190.90', 'negative': True, 'active': True, } with pytest.raises(ValidationError) as exc_info: EarningsTransferPostSchema().load(mock_input) assert exc_info.value.messages == { 'transfer_amount': [ 'Transfer amount must be between 0 and 100 when rate type is percent.' ] } def test_earnings_transfer_post_schema_for_flat_rate_type(): """Test throws an error when rate_type is flat_rate and transfer_amount is negative.""" mock_input = { 'from_contract_id': 317051, 'to_contract_id': 317057, 'transfer_type': 'cross_recoup', 'rate_type': 'flat_rate', 'transfer_amount': '-190.90', 'negative': True, 'active': True, } with pytest.raises(ValidationError) as exc_info: EarningsTransferPostSchema().load(mock_input) assert exc_info.value.messages == { 'transfer_amount': [ 'Transfer amount must be a positive value when rate type is flat_rate.' ] } def test_earnings_transfer_post_schema_for_flat_rate_input(): """Test throws an error when rate_type is flat_rate and input is net_revenue.""" mock_input = { 'from_contract_id': 317051, 'to_contract_id': 317057, 'transfer_type': 'cross_recoup', 'rate_type': 'flat_rate', 'transfer_amount': '190.90', 'negative': True, 'active': True, 'input': 'net_revenue', } with pytest.raises(ValidationError) as exc_info: EarningsTransferPostSchema().load(mock_input) assert exc_info.value.messages == { 'input': [ 'Only closing_balance transfer source is allowed for flat_rate.', ] } def test_earnings_transfer_post_schema_for_same_contracts(): """Test throws an error when from/to contract ids are same.""" mock_input = { 'from_contract_id': 317051, 'to_contract_id': 317051, 'transfer_type': 'cross_recoup', 'rate_type': 'percent', 'transfer_amount': '50', 'negative': True, 'active': True, } with pytest.raises(ValidationError) as exc_info: EarningsTransferPostSchema().load(mock_input) assert exc_info.value.messages == { 'to_contract_id': ['Source and destination contracts must be different.'] } def test_earnings_transfer_put_schema_with_required_fields(): """Test successful validation of required fields in earnings transfer update schema.""" mock_input = { 'earnings_transfer_id': 12, 'to_contract_id': 317057, 'transfer_type': 'override', 'transfer_amount': '100.00', } result = EarningsTransferPutSchema().load(mock_input) assert result == { 'earnings_transfer_id': mock_input['earnings_transfer_id'], 'to_contract_id': mock_input['to_contract_id'], 'transfer_type': mock_input['transfer_type'], 'transfer_amount': Decimal(mock_input['transfer_amount']), } def test_earnings_transfer_put_schema_invalid_enum(): """Test throws an error for invalid enumeration values in earnings transfer update schema.""" mock_input = { 'earnings_transfer_id': 12, 'to_contract_id': 317057, 'transfer_type': 'invalid_transfer_type', 'transfer_amount': '100.00', } with pytest.raises(ValidationError) as exc_info: EarningsTransferPutSchema().load(mock_input) assert exc_info.value.messages == { 'transfer_type': [ 'Must be one of cross_recoup, reclass, override, transfer, nr_transfer' ], } def test_earnings_transfer_put_schema_invalid_numeric_range(): """Test throws an error for transfer_amount out of range in earnings transfer update schema.""" mock_input = { 'earnings_transfer_id': 12, 'rate_type': 'flat_rate', 'to_contract_id': 317057, 'transfer_type': 'override', 'transfer_amount': '-50.00', } with pytest.raises(ValidationError) as exc_info: EarningsTransferPutSchema().load(mock_input) assert exc_info.value.messages == { 'transfer_amount': [ 'Transfer amount must be a positive value when rate type is flat_rate.' ] } def test_earnings_transfer_put_schema_null_optional_fields(): """Test successful validation with null optional fields in earnings transfer update schema.""" mock_input = { 'earnings_transfer_id': 12, 'to_contract_id': 317057, 'transfer_type': 'override', 'transfer_amount': '100.00', 'comment': None, } result = EarningsTransferPutSchema().load(mock_input) assert result == { 'earnings_transfer_id': mock_input['earnings_transfer_id'], 'to_contract_id': mock_input['to_contract_id'], 'transfer_type': mock_input['transfer_type'], 'transfer_amount': Decimal(mock_input['transfer_amount']), 'comment': mock_input['comment'], } def test_earnings_transfer_put_schema_identical_contracts(): """Test throws an error when from/to contract ids are identical in update schema.""" mock_input = { 'earnings_transfer_id': 12, 'from_contract_id': 317051, 'to_contract_id': 317051, 'transfer_type': 'override', 'transfer_amount': '100.00', } with pytest.raises(ValidationError) as exc_info: EarningsTransferPutSchema().load(mock_input) assert exc_info.value.messages == { 'to_contract_id': ['Source and destination contracts must be different.'] }