"""Contract Reserve logic.""" import logging from decimal import Decimal from typing import List, Type from owsresponse import response from abacus_contract import models from abacus_contract.constants.error import ( ERROR_INVALID_RELEASE_SCHEDULE, ERROR_RESERVE_ALREADY_EXISTS, ERROR_RESERVE_DOES_NOT_EXIST, ERROR_RESERVE_MUST_BE_ACTIVE_TO_UPDATE, ) from abacus_contract.schemas.contract_reserve import ( ContractReserveSchema, ContractReserveWithAccountIdSchema, ) from abacus_contract.utils.format_error import validation_error from core.config import Config logger = logging.getLogger(Config.LOGGER_NAME) def create_contract_reserve( contract_id: int, reserve_rate: float, reserve_release_offset_in_months: int, installments_in_months: int, release_schedule: list = None, ) -> Type[response.Response]: """Create a contract reserve. Args: contract_id(int): Id of the contract reserve_rate(float): The percentage of reserves to be held reserve_release_offset_in_months(int): The number of months from when reserves are taken that they begin to be released; 1 - 24 installments_in_months(int): The number of months that reserves taken will be divided across; 1 - 24 release_schedule(list): List of splitted rates Returns: A dict of contract reserve. """ contract = models.Contract.get_by_id_or_error(contract_id) if contract.contract_reserve: return validation_error(ERROR_RESERVE_ALREADY_EXISTS) return _create_contract_reserve( contract_id, reserve_rate, reserve_release_offset_in_months, installments_in_months, release_schedule, ) def update_contract_reserve_by_contract_id( contract_id: int, reserve_rate: float, reserve_release_offset_in_months: int, installments_in_months: int, release_schedule: list = None, ) -> Type[response.Response]: """Update a contract reserve by contract_id. Args: contract_id (int): Id of the contract reserve_rate(float): The percentage of reserves to be held reserve_release_offset_in_months(int): The number of months from when reserves are taken that they begin to be released; 1 - 24 installments_in_months(int): The number of months that reserves taken will be divided across; 1 - 24 release_schedule(list): List of splitted rates Returns: A dict of contract reserve. """ contract = models.Contract.get_by_id_or_error(contract_id) # Validate the action is on an active contract reserve if not contract.contract_reserve: logger.error(f'Reserve for contract {contract_id} must be active to update') return validation_error(ERROR_RESERVE_MUST_BE_ACTIVE_TO_UPDATE) # Soft delete the active contract reserve models.ContractReserve.delete_by_id_or_error( contract.contract_reserve.contract_reserve_id, soft_delete=True ) return _create_contract_reserve( contract.contract_id, reserve_rate, reserve_release_offset_in_months, installments_in_months, release_schedule, ) def _create_contract_reserve( contract_id: int, reserve_rate: float, reserve_release_offset_in_months: int, installments_in_months: int, release_schedule: list = None, ) -> Type[response.Response]: try: if release_schedule is None: release_schedule = _calculate_release_schedule(installments_in_months) else: release_schedule = _validate_release_schedule(release_schedule) contract_reserve = models.ContractReserve.create( contract_id=contract_id, reserve_rate=reserve_rate, reserve_release_offset_in_months=reserve_release_offset_in_months, installments_in_months=installments_in_months, release_schedule=release_schedule, ) except Exception as e: return validation_error(str(e)) return response.Response( message=ContractReserveSchema().dump(contract_reserve), status=201 ) def delete_contract_reserve_by_contract_id( contract_id: int, ) -> Type[response.Response]: """Soft delete the active contract reserve for a contract. Args: contract_id: ID of the contract whose reserve should be deleted Returns: Empty 204 response """ contract = models.Contract.get_by_id_or_error(contract_id) if not contract.contract_reserve: return validation_error(ERROR_RESERVE_DOES_NOT_EXIST) models.ContractReserve.delete_by_id_or_error( contract.contract_reserve.contract_reserve_id, soft_delete=True ) models.ContractReserve.commit_changes() return response.Response(status=204) def get_reserve_by_contract_id(contract_id: int) -> Type[response.Response]: """GET reserve for a specified contract. Args: contract_id(int): ID of the contract Returns: A dict of contract reserve """ contract = models.Contract.get_by_id_or_error(contract_id) message = ContractReserveSchema().dump(contract.contract_reserve) return response.Response(message=message, status=200) def _calculate_release_schedule(installments_in_months: int) -> List[str]: """Calculate release_schedule. Args: installments_in_months(int): The number of months that reserves taken will be divided across; 1 - 24 Returns: List of splitted rates """ one_month_installment = format((1 / installments_in_months), '.12f') release_schedule = [str(one_month_installment)] * (installments_in_months - 1) last_month_installment = 1 - Decimal(one_month_installment) * ( installments_in_months - 1 ) release_schedule.append(str(last_month_installment)) return release_schedule def _validate_release_schedule(release_schedule): """Validate if the sum of all release_schedule values equals 1 or not.""" total = sum(map(Decimal, release_schedule)) if total != 1: raise Exception(ERROR_INVALID_RELEASE_SCHEDULE) return list(map(str, release_schedule)) def get_contract_reserves_by_ids(contract_reserve_ids: list) -> Type[response.Response]: """Get reserves by contract_reserve_ids. Args: contract_reserve_ids(list): List of contract reserve ids. Response: List of contract reserves """ contract_reserves = models.ContractReserve.get_by_ids(contract_reserve_ids) return response.Response( message=ContractReserveWithAccountIdSchema(many=True).dump(contract_reserves), status=200, ) def get_contract_reserves_by_contract_ids( contract_ids: list, ) -> Type[response.Response]: """Get reserves associated with specified contract_ids. Args: contract_ids(list): List of contract ids. Response: List of contract reserves """ contract_reserves = models.ContractReserve.get_by_contract_ids(contract_ids) return response.Response( message=ContractReserveSchema(many=True).dump(contract_reserves), status=200 )