"""Run controller contract logic.""" from abacus_common_logic.constants.error import ERROR_FIELD_MISSING from marshmallow import ValidationError from owsresponse import response from abacus_contract.models import Contract, ContractLifecycle from royalties import models from royalties.constants.constants import CONTRACT_LIFECYCLE_STATUSES from royalties.constants.error import ( ERROR_DIFFERENT_RUN_CONTROLLER, ERROR_RUN_CONTROLLER_DOES_NOT_MATCH_CONTRACT_TYPE, ) from royalties.features import is_abacus_contract_page_update_run_controller_ff_enabled from royalties.logic.run_controller import add_run_controller_to_acct_period from royalties.schemas.run_controller import RunControllerDetailSchema from royalties.schemas.run_controller_contract import RunControllerContractSchema from royalties.utils.format_error import validation_error from royalties.utils.response import prepare_dataload_response class RunControllerValidationError(ValidationError): """Exception class for Run Controller Validation.""" pass def create_run_controller_contract( account_id, contract_id, run_controller_id, contract_type ): """Wrap create new run_controller_contract logic.""" try: run_controller_contract = _base_create_run_controller_contract( account_id, contract_id, run_controller_id, contract_type ) models.RunControllerContract.commit_changes() except RunControllerValidationError as e: return validation_error({'run_controller_id': str(e)}) except Exception as e: return validation_error(message=str(e)) return response.Response( message=RunControllerContractSchema().dump(run_controller_contract), status=201 ) def get_contracts_run_controllers_with_dataload_format( contract_ids, ) -> response.Response: """Get Run Controller Contracts by their identifiers with dataload format.""" run_controller_contracts = models.RunControllerContract.get_by_contract_ids( contract_ids ) run_controller_contracts_list = RunControllerContractSchema(many=True).dump( run_controller_contracts ) result = prepare_dataload_response( contract_ids, run_controller_contracts_list, 'contract_id' ) return response.Response(message=result, status=200) def get_run_controller_contracts_by_account(account_id, contract_type): """Get run controller contracts belonging to the specified account_id.""" if not account_id: return validation_error({'account_id': ERROR_FIELD_MISSING}) run_controller_contracts = [ dict(c) for c in models.RunControllerContract.get_by_account_id( account_id, contract_type ) ] for rcc in run_controller_contracts: rcc['run_controller'] = RunControllerDetailSchema().dump(rcc) message = RunControllerContractSchema( exclude=('run_controller_contract_id', 'contract_id'), many=True ).dump(run_controller_contracts) return response.Response( message={'items': message, 'total_count': len(message)}, status=200 ) def _base_create_run_controller_contract( account_id, contract_id, run_controller_id, contract_type ): """Validate and create a new run_controller_contract.""" run_controller = models.RunController.get_by_id_or_error(run_controller_id) _validate_run_controller_contract(account_id, run_controller, contract_type) if not models.RunController.count_contracts(run_controller_id): add_run_controller_to_acct_period(run_controller) run_controller_contract = models.RunControllerContract.build( contract_id=contract_id, run_controller_id=run_controller_id ) return run_controller_contract def _validate_run_controller_contract(account_id, run_controller, contract_type): """Validate run_controller_contract. Check that an account's other contracts of the same contract_type belong to the same run_controller. """ if not account_id: return existing_run_controller_contracts = models.RunControllerContract.get_by_account_id( account_id, contract_type ) if any( run_controller_contract.run_controller_id != run_controller.run_controller_id for run_controller_contract in existing_run_controller_contracts ): raise RunControllerValidationError( message=ERROR_DIFFERENT_RUN_CONTROLLER.format( contract_type=contract_type.capitalize(), run_controller_name=existing_run_controller_contracts[ 0 ].run_controller_name, ) ) def update_run_controller_contract(contract_id, run_controller_id): """Update run controller contract. Assumes the user acknowledges that sibling contracts of the same contract_type will be updated to the same run_controller. """ if not is_abacus_contract_page_update_run_controller_ff_enabled(): return response.create_error_response( code='Unauthorized', message='Unauthorized to update run controller contract.', status=403, ) try: contract_lifecycle = ContractLifecycle.get_by_contract_id(contract_id) if not contract_lifecycle: return response.create_fatal_response( message=f'Contract lifecycle not found for contract {contract_id}.' ) contract_lifecycle_status = contract_lifecycle.lifecycle_status if contract_lifecycle_status == CONTRACT_LIFECYCLE_STATUSES.TERMINATED: return validation_error( { 'contract_id': f'Contract {contract_id} is already terminated. Run controller cannot be updated.' } ) except Exception as e: return response.create_fatal_response( message=f'Error fetching contract lifecycle: {str(e)}', ) try: contract = Contract.get_by_id(contract_id) if not contract: return validation_error({'contract_id': 'Contract not found.'}) if not contract.account_contract: return response.create_fatal_response( message=f'Account ID not found in contract {contract_id}.' ) account_id = contract.account_id contract_type = contract.contract_type if not contract_type: return response.create_fatal_response( message='Contract type not found in contract.' ) except Exception as e: return response.create_fatal_response( message=f'Error fetching contract:{str(e)}', ) proposed_run_controller = models.RunController.get_by_id_or_error(run_controller_id) if proposed_run_controller.contract_type != contract_type: return validation_error( { 'run_controller_id': ERROR_RUN_CONTROLLER_DOES_NOT_MATCH_CONTRACT_TYPE.format( run_controller_name=proposed_run_controller.run_controller_name, run_controller_contract_type=proposed_run_controller.contract_type, contract_id=contract_id, contract_type=contract_type, ) } ) update_params = { 'run_controller_id': run_controller_id, } try: contracts_by_account_id = Contract.get_by_accounts([account_id]) if not contracts_by_account_id or len(contracts_by_account_id) == 0: return response.create_fatal_response( message=f'No contracts found for this account `{account_id}`.' ) except Exception as e: return response.create_fatal_response( message=f'Error fetching contracts by account ID: {str(e)}', ) filtered_contract_ids_by_contract_type = [] for contract in contracts_by_account_id: if contract.contract_type == contract_type: filtered_contract_ids_by_contract_type.append(contract.contract_id) try: contract_lifecycles_by_contract_ids = ContractLifecycle.get_by_contract_ids( filtered_contract_ids_by_contract_type ) except Exception as e: return response.create_fatal_response( message=f'Error fetching contract lifecycles by contract IDs: {str(e)}', ) filtered_contract_ids_by_contract_type_and_lifecycle_status = [] for contract_lifecycle in contract_lifecycles_by_contract_ids: contract_lifecycle_status = contract_lifecycle.lifecycle_status if ( contract_lifecycle_status and contract_lifecycle_status != CONTRACT_LIFECYCLE_STATUSES.TERMINATED ): filtered_contract_ids_by_contract_type_and_lifecycle_status.append( contract_lifecycle.contract_id ) run_controller_contracts = models.RunControllerContract.get_by_contract_ids( filtered_contract_ids_by_contract_type_and_lifecycle_status ) for run_controller_contract in run_controller_contracts: run_controller_contract.update_attributes(**update_params) models.RunControllerContract.commit_changes() return response.Response( message=RunControllerContractSchema().dump(run_controller_contracts, many=True), status=200, )