"""Logic for Earnings Transfer.""" from decimal import Decimal from abacus_common_logic.connectors.database import db from marshmallow import ValidationError from owsresponse import response from abacus_contract.models import Contract from royalties.constants.constants import ( EARNINGS_TRANSFER_RATE_TYPES, EARNINGS_TRANSFER_TYPES, ) from royalties.models import EarningsTransfer from royalties.schemas import EarningsTransferDetailSchema, EarningsTransferFilterSchema from royalties.schemas.earnings_transfer import validate_amount_and_input from royalties.utils.format_error import validation_error from royalties.utils.request import validate_post_request_contains_unique_data def get_earnings_transfers(request_params: dict) -> response.Response: """Get the list of earnings transfers. Args: request_params (dict)(Optional): query string parameters - reference_payment_entities (str): comma separated list of payment entities - payment_schedules (str): comma separated list of payment schedules - limit (int): the size of page - offset (int): the number of items to skip before returning results - sort_by (str): column name by which the results should be sorted - sort_order (str): order direction (asc or desc) Returns: a list of earnings transfers. """ try: params = EarningsTransferFilterSchema().load(request_params) items, total_count = EarningsTransfer.get_earnings_transfers(**params) message = dict( items=EarningsTransferDetailSchema().dump(items, many=True), total_count=total_count, ) except ValidationError as exc: return validation_error(str(exc)) except Exception as e: raise e return response.Response(message=message, status=200) def get_earnings_transfers_by_contract_id( contract_id: int, transfer_type: str, request_params: dict ) -> response.Response: """Get the list of earnings transfers. Args: contract_id (int): id of the contract transfer_type (str)(Optional): either reclass, override or transfer request_params (dict)(Optional): query string parameters - limit (int): the size of page - offset (int): the number of items to skip before returning results - sort_by (str): column name by which the results should be sorted - sort_order (str): order direction (asc or desc) Returns: a list of earnings transfers. """ Contract.get_by_id_or_error(contract_id, 404) try: params = EarningsTransferFilterSchema( exclude=('reference_payment_entities', 'payment_schedules') ).load(request_params) if transfer_type: transfer_type = transfer_type.replace('-', '_') if transfer_type not in [ EARNINGS_TRANSFER_TYPES.RECLASS, EARNINGS_TRANSFER_TYPES.OVERRIDE, EARNINGS_TRANSFER_TYPES.TRANSFER, ]: raise ValidationError( 'Invalid transfer_type. Must be one of: reclass, override, or transfer.' ) params.update({'contract_id': contract_id, 'transfer_type': transfer_type}) items, total_count = EarningsTransfer.get_earnings_transfers_by_contract_id( **params ) message = dict( items=EarningsTransferDetailSchema().dump(items, many=True), total_count=total_count, ) except ValidationError as exc: return validation_error(str(exc)) return response.Response(message=message, status=200) def get_earnings_transfer_by_id(earnings_transfer_id: int) -> response.Response: """Get an earnings transfer by ID. Args: earnings_transfer_id (int): The ID of the earnings transfer. Returns: A response containing the earnings transfer. """ transfer = EarningsTransfer.get_by_id_or_error(earnings_transfer_id, 404) return response.Response( message=EarningsTransferDetailSchema().dump(transfer), status=200 ) def bulk_create_earnings_transfers(request_body: list) -> response.Response: """Create one or more earnings transfer. Args: request_body (list): A list of dict, where each dict contains - from_contract_id (int): ID of the source contract from which the amount will be debited. - to_contract_id (int): ID of the destination contract that will receive the amount. - transfer_type (enum): Type of transfer either 'cross_recoup', 'reclass', 'override', 'transfer' or 'nr_transfer'. - rate_type (enum)(Optional): Type of rate either 'percent', or 'flat_rate'. - transfer_amount (decimal): The amount to be transferred. - transfer_source (enum): Source of transfer either 'net_revenue', 'gross_revenue' or 'closing_balance'. - negative (bool): Indicates whether negative balance can be processed. - active (bool): Indicates whether the transfer is active or inactive. - comment (str): Additional notes or remarks related to the transfer. Returns: an ows response """ try: validate_post_request_contains_unique_data(request_body) earnings_transfers_entries = list() contract_ids = set() for record in request_body: contract_ids.add(record['from_contract_id']) contract_ids.add(record['to_contract_id']) validate_contracts(contract_ids) for record in request_body: record['input'] = record.pop('transfer_source', None) earnings_transfers_entries.append(EarningsTransfer(**record)) validate_from_contract(earnings_transfers_entries) db.session.add_all(earnings_transfers_entries) db.session.commit() except ValidationError as exc: return validation_error(str(exc)) except Exception as e: db.session.rollback() raise e return response.Response( message=EarningsTransferDetailSchema(many=True).dump( earnings_transfers_entries ), status=201, ) def bulk_update_earnings_transfers(request_body: list) -> response.Response: """Update one or more earnings transfer. Args: request_body (list): A list of dict, where each dict contains - earnings_transfer_id (int): The unique ID of the earnings transfer. - from_contract_id (int)(Optional): ID of the source contract. - to_contract_id (int)(Optional): ID of the destination contract. - transfer_type (enum)(Optional): Type of transfer. - rate_type (enum)(Optional): Type of rate. - transfer_amount (decimal)(Optional): The amount to be transferred. - input (enum)(Optional): Source of transfer. - negative (bool)(Optional): Indicates whether negative balance can be processed. - active (bool)(Optional): Indicates whether the transfer is active or inactive. - comment (str)(Optional): Additional notes or remarks. Returns: an ows response """ try: transfer_ids = [record['earnings_transfer_id'] for record in request_body] existing_transfers = { et.earnings_transfer_id: et for et in EarningsTransfer.query.filter( EarningsTransfer.earnings_transfer_id.in_(transfer_ids) ).all() } validate_update_entries(existing_transfers, request_body, transfer_ids) updated_entries = [] for record in request_body: earning_transfer_id = record.pop('earnings_transfer_id') existing_earning_transfer = existing_transfers[earning_transfer_id] for key, value in record.items(): setattr(existing_earning_transfer, key, value) updated_entries.append(existing_earning_transfer) validate_from_contract(updated_entries) db.session.commit() except ValidationError as exc: return validation_error(str(exc)) except Exception as e: db.session.rollback() raise e return response.Response( message=EarningsTransferDetailSchema(many=True).dump(updated_entries), status=200, ) def validate_update_entries( existing_transfers: dict[int, EarningsTransfer], request_body: list, transfer_ids: list[int], ): """Validate the update entries.""" missing_ids = set(transfer_ids) - set(existing_transfers.keys()) if missing_ids: raise ValidationError( f'The following earnings transfer IDs do not exist: {list(missing_ids)}' ) contract_ids = set() ids_with_contract_duplicates = set() for record in request_body: earning_transfer_id = record['earnings_transfer_id'] existing_earning_transfer = existing_transfers[earning_transfer_id] from_id = record.get( 'from_contract_id', existing_earning_transfer.from_contract_id ) to_id = record.get('to_contract_id', existing_earning_transfer.to_contract_id) if from_id == to_id: ids_with_contract_duplicates.add(earning_transfer_id) contract_ids.add(from_id) contract_ids.add(to_id) if ids_with_contract_duplicates: raise ValidationError( f'Source and destination contracts must be different for ID {list(ids_with_contract_duplicates)}.' ) validate_contracts(contract_ids) errors = [] for record in request_body: earning_transfer_id = record['earnings_transfer_id'] existing_earning_transfer = existing_transfers[earning_transfer_id] error = validate_amount_and_input( record.get('rate_type', existing_earning_transfer.rate_type), record.get('transfer_amount', existing_earning_transfer.transfer_amount), record.get('input', existing_earning_transfer.input), ) if error: errors.append(error) if errors: raise ValidationError(errors) def validate_contracts(contract_ids: set) -> bool: """Verify that contract exist. Args: contract_ids (list): a list of contract ids Response: returns true if all provided contracts ids are exist otherwise raises an validation error """ existing_contracts = Contract.query.filter( Contract.contract_id.in_(contract_ids) ).all() existing_ids = {contract.contract_id for contract in existing_contracts} missing_ids = list(contract_ids - set(existing_ids)) if len(missing_ids) > 0: raise ValidationError(f'The following contract IDs do not exist: {missing_ids}') return True def validate_from_contract(new_records: list[EarningsTransfer]): """Validate entries based on the `from_contract_id` field. This function validates that: - A from_contract cannot have more than one transfer for the same to_contract - The sum of all the percentage for the same from_contract is not greater than 100 """ records_by_contract = {} for record in new_records: from_contract_id = record.from_contract_id if from_contract_id not in records_by_contract.keys(): records_by_contract[from_contract_id] = [] records_by_contract[from_contract_id].append(record) for contract_id, new_contract_records in records_by_contract.items(): existing_contract_records = ( EarningsTransfer.get_earnings_transfers_from_contract(contract_id) ) contract_records = set(new_contract_records).union( set(existing_contract_records) ) to_contract_ids = [] total_percentage_per_input = {} for record in contract_records: to_contract_id = record.to_contract_id rate_type = record.rate_type amount = record.transfer_amount source = record.input if to_contract_id in to_contract_ids: raise ValidationError( f'Contract {contract_id} cannot have more than one transfer of earnings configured for Contract {to_contract_id}' ) to_contract_ids.append(to_contract_id) if rate_type == EARNINGS_TRANSFER_RATE_TYPES.PERCENT: if source not in total_percentage_per_input.keys(): total_percentage_per_input[source] = Decimal('0') total_percentage_per_input[source] = total_percentage_per_input[ source ] + Decimal(amount) if total_percentage_per_input[source] > Decimal('100'): raise ValidationError( f'Contract {contract_id} cannot transfer more than 100% of its earnings' )