"""ledger_contract_flowthrough Endpoint Logic.""" import copy from decimal import Decimal from typing import Dict, Iterable, List, Tuple from abacus_common_logic.connectors.database import db from owsresponse import response from sqlalchemy import exc from werkzeug.exceptions import abort from ledger.constants.error import ( ERROR_INTEGRITY_CONFLICT, ERROR_INVALID_DATA, ERROR_INVALID_MODEL_TYPE, ERROR_INVALID_SYNTAX, ) from ledger.models.ledger_contract_flowthrough import LedgerContractFlowthrough from ledger.models.ledger_deposit import LedgerDeposit from ledger.utils.currency import currency_exists from ledger.utils.retry import retry_on_exception @retry_on_exception(retries=5, delay_secs=0.333) def handle_bulk_ledger_entries(request: List[Dict]): """Handle bulk creation of ledger_contract_flowthrough, and ledger_deposit entries. Retries a maximum of 5 times, with exponential backoff, for a total of 5 seconds. Args: request (list): a list of data for contract flowthrough and deposit ledger entries Returns: An ows Response """ contract_entries, deposit_entries = validate_and_group_ledger_entries(request) try: if len(contract_entries) > 0: l_contract_entries = build_contract_flowthrough_ledger_entries( contract_entries ) db.session.add_all(l_contract_entries) if len(deposit_entries) > 0: db.session.add_all(build_deposit_ledger_entries(deposit_entries)) db.session.commit() except exc.IntegrityError: db.session.rollback() abort(status=409, description=ERROR_INTEGRITY_CONFLICT) except exc.DataError: db.session.rollback() abort(status=422, description=ERROR_INVALID_DATA) except Exception as e: db.session.rollback() raise e finally: db.session.close() return response.Response(message={'message': 'OK'}, status=201) def validate_and_group_ledger_entries( records: List[Dict], ) -> Tuple[List[Dict], List[Dict]]: """Validate and sort ledger entries by type. Args: records (list[dict]): a list of data for ledger_contract_flowthrough or ledger_deposit entries Returns: a tuple containing a list of dict of ledger_contract_flowthrough and ledger_deposit records """ contract_records = list() deposit_records = list() for record in records: validate_record(record) record = copy.copy(record) model_type = record.pop('model_type') if model_type == 'contract': contract_records.append(record) elif model_type == 'deposit': deposit_records.append(record) else: abort( status=400, description=ERROR_INVALID_MODEL_TYPE.format(ledger_type=model_type), ) return contract_records, deposit_records def validate_record(record: dict): """Validate record. Args: record (dict): data for a ledger_contract_flowthrough or ledger_deposit entry """ if 'model_type' not in record: abort(status=400, description=ERROR_INVALID_SYNTAX) if not currency_exists(record.get('currency_code')): abort(status=400, description='unrecognized currency code') def build_contract_flowthrough_ledger_entries( records: List[dict], ) -> List[LedgerContractFlowthrough]: """Build ledger_contract_flowthrough entries. Args: records (list[dict]): a list of ledger_contract_flowthrough entries Returns: a list of LedgerContractFlowthrough object for new ledger_contract_flowthrough entries """ contract_ids = set([record['contract_id'] for record in records]) balances_by_contract_ids = get_ledger_flowthrough_current_balances_by_contracts( contract_ids, True ) ledger_contract_flowthrough_entries = [] zero = Decimal(0) for record in records: contract_flowthrough_balance = balances_by_contract_ids.get( record['contract_id'], zero ) ledger_contract_flowthrough_entries.append( build_ledger_contract_flowthrough_entry( record, contract_flowthrough_balance ) ) return ledger_contract_flowthrough_entries def get_ledger_flowthrough_current_balances_by_contracts( contract_ids: Iterable[int], for_update: bool = False ) -> Dict[int, Decimal]: """Get current balance for given contract ids. Args: contract_ids: a list of contract ids for_update: true if the existing record needs to be updated; otherwise, false Returns: a dict containing flowthrough contracts along with their respective current balances """ query = ( LedgerContractFlowthrough.get_ledger_contract_flowthrough_balance_by_contracts( contract_ids ) ) if for_update: query = query.with_for_update() entries = query.all() balances_dict = {} for entry in entries: balances_dict[entry.contract_id] = Decimal(entry.current_balance) return balances_dict def build_ledger_contract_flowthrough_entry( record: Dict, current_balance: Decimal ) -> LedgerContractFlowthrough: """Build ledger_contract_flowthrough entry. Args: record: new ledger_contract_flowthrough entry current_balance: current balance for contract flowthrough Returns: LedgerContractFlowthrough object for new ledger_contract_flowthrough entry """ decimal_amount = Decimal(record['currency_amount']) record.update( { 'previous_balance': current_balance, 'current_balance': current_balance + decimal_amount, } ) return LedgerContractFlowthrough(**record) def build_deposit_ledger_entries(records: List[Dict]): """Build ledger_deposit entries. Args: records (list[dict]): a list of ledger_deposit entries Returns: a list of LederDeposit object for new ledger_deposit entries """ formatted_record = [] for record in records: record['remaining_amount'] = Decimal(record['remaining_amount']) formatted_record.append(LedgerDeposit(**record)) return formatted_record