"""Contract Reserve logic.""" from decimal import Decimal import logging from typing import List, Type from owsresponse import response from abacus_contract import models from abacus_contract.config import Config from abacus_contract.constants.error import ERROR_INVALID_RELEASE_SCHEDULE from abacus_contract.constants.error import ERROR_RESERVE_ALREADY_EXISTS from abacus_contract.constants.error import ERROR_RESERVE_MUST_BE_ACTIVE_TO_UPDATE from abacus_contract.schemas.contract_reserve import ContractReserveSchema from abacus_contract.schemas.contract_reserve import ContractReserveWithAccountIdSchema from abacus_contract.utils.format_error import validation_error 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. """ # noqa: E501 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(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. """ # noqa: E501 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 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 """ # noqa: E501 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 )