"""Shared logic to validate manual adjustments.""" from copy import deepcopy from abacus_common_logic.adjustments_validation.adjustments_validation_data import ( AdjustmentsValidationData, ) from abacus_common_logic.adjustments_validation.logger import logger from abacus_common_logic.adjustments_validation.snowflake.executor import ( AdjustmentsValidationSnowflakeExecutor, ) from abacus_common_logic.adjustments_validation.types_definitions import ( Adjustment, ) from abacus_common_logic.adjustments_validation.utils import ( clean_upc, is_integer, ) from abacus_common_logic.adjustments_validation.validation_functions import ( validate_account, validate_activity_date, validate_adjustment_type, validate_amount, validate_client_facing_comments, validate_close_balance_status, validate_contract, validate_currency, validate_distribution_type, validate_flowthrough_payment, validate_statement_date, validate_upc, ) def validate_adjustments( adjustments: list[Adjustment], statement_period_id: int, sf_executor: AdjustmentsValidationSnowflakeExecutor, ) -> dict[int, set[str]]: """Validate a list of manual adjustments. 1. Go through the adjustments to collect the entities to validate. 2. Fetch those entities from Snowflake to make sure they exist. 3. Go through the adjustments once again to validate them. Args: adjustments: The list of adjustments to validate. statement_period_id: The ID of the "Apply To" statement period. sf_executor: An instance of AdjustmentsValidationSnowflakeExecutor. Returns: A dict with the adjustment's index as key and a list of errors as value. """ logger.info(f'Starting validation for {len(adjustments)} adjustments') data_to_validate = _collect_data_to_validate(adjustments) logger.info(f'Collected data to validate') existing_data = _fetch_existing_data( data_to_validate, statement_period_id, sf_executor ) logger.info(f'Fetched existing data') errors = {} for index, adjustment in enumerate(adjustments): adjustment_errors = _validate_adjustment(adjustment, existing_data) if adjustment_errors: errors[index] = set(adjustment_errors) logger.info(f'Found errors for {len(errors.keys())} adjustments') return errors def _collect_data_to_validate( adjustments: list[Adjustment], ) -> AdjustmentsValidationData: """Go through the list of adjustments and collect the entities to validate.""" data = AdjustmentsValidationData() for adjustment in adjustments: account_id = adjustment.get('account_id') or '' contract_id = adjustment.get('contract_id') or '' upc = adjustment.get('upc') or '' cleaned_upc = clean_upc(upc) activity_year = adjustment.get('activity_year') or '' statement_year = adjustment.get('statement_year') or '' is_account_id_int = is_integer(account_id) is_contract_id_int = is_integer(contract_id) is_upc_int = is_integer(upc) and is_integer(cleaned_upc) is_activity_year_int = is_integer(activity_year) is_statement_year_int = is_integer(statement_year) if is_account_id_int: data.add_account_id(account_id) if is_account_id_int and is_contract_id_int: data.add_account_contract_mapping(account_id, contract_id) if is_upc_int: data.add_upc(upc) data.add_upc(cleaned_upc) if is_account_id_int and is_upc_int: data.add_account_upc_mapping(account_id, upc) data.add_account_upc_mapping(account_id, cleaned_upc) if is_account_id_int and is_contract_id_int and is_upc_int: data.add_account_contract_upc_mapping(account_id, contract_id, upc) data.add_account_contract_upc_mapping(account_id, contract_id, cleaned_upc) if is_activity_year_int: data.add_statement_year(activity_year) if is_statement_year_int: data.add_statement_year(statement_year) return data def _fetch_existing_data( data_to_validate: AdjustmentsValidationData, statement_period_id: int, sf_executor: AdjustmentsValidationSnowflakeExecutor, ) -> AdjustmentsValidationData: """Fetch the entities to validate from Snowflake to make sure they exist.""" existing_data = AdjustmentsValidationData() contract_ids = data_to_validate.get_contract_ids() existing_data.payment_entity_close_balance_status_map = ( sf_executor.fetch_close_balance_statuses(statement_period_id) ) existing_data.account_ids = sf_executor.fetch_accounts(data_to_validate.account_ids) existing_data.account_contract_map = sf_executor.fetch_account_contracts( data_to_validate.account_contract_map ) existing_data.account_payment_entity_map = sf_executor.fetch_payment_entities( data_to_validate.account_ids ) existing_data.display_upc_upc_map = sf_executor.fetch_display_upcs( data_to_validate.upcs ) existing_data.contract_product_map = sf_executor.fetch_product_terms(contract_ids) existing_data.contract_label_map = sf_executor.fetch_label_terms(contract_ids) full_account_upc_map = _build_full_account_upc_map( data_to_validate.account_upc_map, data_to_validate.account_contract_upc_map, data_to_validate.get_contract_account_map(), existing_data.contract_label_map, ) existing_data.account_upc_map = sf_executor.fetch_account_upcs(full_account_upc_map) existing_data.statement_periods = sf_executor.fetch_statement_periods( data_to_validate.statement_years ) existing_data.adjustment_types = sf_executor.fetch_adjustment_types() return existing_data def _build_full_account_upc_map( account_upc_map: dict[str, set[str]], account_contract_upc_map: dict[str, dict[str, set[str]]], contract_account_map: dict[str, str], contract_label_map: dict[str, set[str]], ) -> dict[str, set[str]]: """Build the full map of UPCs by account. Args: account_upc_map: The map of UPCs by account. account_contract_upc_map: The map of UPCs by account and contract. contract_account_map: The map of accounts by contract. contract_label_map: The map of labels associated to a contract through a label term. """ results = deepcopy(account_upc_map) for contract_id, label_ids in contract_label_map.items(): account_id = contract_account_map.get(contract_id) if not account_id: continue upcs = account_contract_upc_map.get(account_id, {}).get(contract_id) if not upcs: continue for label_id in label_ids: if label_id == account_id: continue if label_id not in results: results[label_id] = set() results[label_id].update(upcs) return results def _validate_adjustment( adjustment: Adjustment, existing_data: AdjustmentsValidationData, ) -> list[str]: """Validate an adjustment.""" account_id = adjustment.get('account_id') contract_id = adjustment.get('contract_id') upc = adjustment.get('upc') or '' amount = adjustment.get('amount') currency = adjustment.get('currency') activity_month = adjustment.get('activity_month') activity_year = adjustment.get('activity_year') statement_month = adjustment.get('statement_month') statement_year = adjustment.get('statement_year') adjustment_type = adjustment.get('adjustment_type') client_facing_comments = adjustment.get('client_facing_comments') distribution_type = adjustment.get('distribution_type') flowthrough_payment = adjustment.get('apply_to_flowthrough_payment') results = [ validate_close_balance_status( account_id, existing_data.account_payment_entity_map, existing_data.payment_entity_close_balance_status_map, ), validate_account(account_id, existing_data.account_ids), validate_contract(account_id, contract_id, existing_data.account_contract_map), validate_upc( upc, distribution_type, contract_id, existing_data.contract_product_map, existing_data.contract_label_map, existing_data.display_upc_upc_map, existing_data.account_upc_map, ), validate_amount(amount), validate_currency(currency), validate_activity_date( activity_month, activity_year, existing_data.statement_periods ), validate_statement_date( statement_month, statement_year, existing_data.statement_periods ), validate_adjustment_type(adjustment_type, existing_data.adjustment_types), validate_client_facing_comments(client_facing_comments), validate_distribution_type(distribution_type, upc), validate_flowthrough_payment(flowthrough_payment, adjustment_type), ] errors = [result for result in results if isinstance(result, str)] return errors