"""Functions to validate an adjustment.""" from abacus_common_logic.adjustments_validation.constants import ( ACCOUNT_EXPENSE_ADJUSTMENT_TYPE, ACTION_STATUS_COMPLETE, FLOWTHROUGH_ADJUSTMENT_TYPE, VALID_CURRENCIES, VALID_DISTRIBUTION_TYPES, VALID_STATEMENT_PERIOD_STATUSES, VALIDATION_ERRORS, ) from abacus_common_logic.adjustments_validation.utils import ( clean_upc, has_intersection, is_float, is_integer, ) from abacus_common_logic.utils.request import BooleanFilter def validate_close_balance_status( account_id: str | None, account_payment_entity_map: dict[str, str], payment_entity_close_balance_status_map: dict[str, str], ): """Validate that the account's payment entity balance is not closed.""" if not account_id: return VALIDATION_ERRORS.ACCOUNT_REQUIRED payment_entity_id = account_payment_entity_map.get(account_id) if not payment_entity_id: return VALIDATION_ERRORS.ACCOUNT_NO_PAYMENT_ENTITY close_balance_status = payment_entity_close_balance_status_map.get( payment_entity_id ) if not close_balance_status: return VALIDATION_ERRORS.ACCOUNT_NO_CLOSE_BALANCE if close_balance_status == ACTION_STATUS_COMPLETE: return VALIDATION_ERRORS.ACCOUNT_INVALID_CLOSE_BALANCE return True def validate_account(account_id: str | None, account_ids: set[str]) -> bool | str: """Validate the `Account ID` field of an adjustment.""" if not account_id: return VALIDATION_ERRORS.ACCOUNT_REQUIRED if not is_integer(account_id): return VALIDATION_ERRORS.ACCOUNT_INVALID if account_id not in account_ids: return VALIDATION_ERRORS.ACCOUNT_MISSING return True def validate_contract( account_id: str | None, contract_id: str | None, account_contract_map: dict[str, set[str]], ) -> bool | str: """Validate the `Contract ID` field of an adjustment.""" if not account_id: return VALIDATION_ERRORS.ACCOUNT_REQUIRED if not contract_id: return VALIDATION_ERRORS.CONTRACT_REQUIRED if not is_integer(contract_id): return VALIDATION_ERRORS.CONTRACT_INVALID if (account_id not in account_contract_map) or ( contract_id not in account_contract_map[account_id] ): return VALIDATION_ERRORS.CONTRACT_MISSING return True def validate_upc( upc: str | None, distribution_type: str | None, contract_id: str | None, contract_product_map: dict[str, set[str]], contract_label_map: dict[str, set[str]], display_upc_upc_map: dict[str, set[str]], account_upc_map: dict[str, set[str]], ) -> bool | str: """Validate the `UPC` field of an adjustment.""" # The `UPC` field is optional if not upc: return True if not contract_id: return VALIDATION_ERRORS.CONTRACT_REQUIRED if not is_integer(upc): return VALIDATION_ERRORS.UPC_INVALID if len(upc) < 11 or len(upc) > 13: return VALIDATION_ERRORS.UPC_LENGTH if not distribution_type: return VALIDATION_ERRORS.UPC_BLANK is_on_contract_product_terms = _is_upc_on_contract_product_terms( upc, contract_id, contract_product_map, display_upc_upc_map ) is_on_contract_label_terms = _is_upc_on_contract_label_terms( upc, contract_id, contract_label_map, account_upc_map ) if not is_on_contract_product_terms and not is_on_contract_label_terms: return VALIDATION_ERRORS.UPC_MISSING return True def _is_upc_on_contract_product_terms( upc: str, contract_id: str, contract_product_map: dict[str, set[str]], display_upc_upc_map: dict[str, set[str]], ) -> bool: """Validate that a UPC is attached to a contract through a product term.""" cleaned_upc = clean_upc(upc) upc_variants = {upc, cleaned_upc} display_upcs = display_upc_upc_map.get(upc) display_upcs_clean = display_upc_upc_map.get(cleaned_upc) if display_upcs: upc_variants.update(display_upcs) if display_upcs_clean: upc_variants.update(display_upcs_clean) for _contract_id, upcs in contract_product_map.items(): contract_upcs = set([clean_upc(_upc) for _upc in upcs]) contract_upcs.update(upcs) if contract_id == _contract_id and has_intersection( upc_variants, contract_upcs ): return True return False def _is_upc_on_contract_label_terms( upc: str, contract_id: str, contract_label_map: dict[str, set[str]], account_upc_map: dict[str, set[str]], ) -> bool: """Validate that a UPC is attached to a contract through a label term.""" label_ids = contract_label_map.get(contract_id) if not label_ids: return False for label_id in label_ids: upcs = account_upc_map.get(label_id) if upcs and (upc in upcs or clean_upc(upc) in upcs): return True return False def validate_amount(amount: str | None) -> bool | str: """Validate the `Amount` field of an adjustment.""" if amount is None: return VALIDATION_ERRORS.AMOUNT_REQUIRED if not is_float(amount): return VALIDATION_ERRORS.AMOUNT_INVALID if float(amount) == 0: return VALIDATION_ERRORS.AMOUNT_ZERO return True def validate_currency(currency: str | None) -> bool | str: """Validate the `Currency` field of an adjustment.""" if not currency: return VALIDATION_ERRORS.CURRENCY_REQUIRED if not currency.isalpha(): return VALIDATION_ERRORS.CURRENCY_INVALID if currency.upper() not in VALID_CURRENCIES: return VALIDATION_ERRORS.CURRENCY_UNSUPPORTED return True def validate_activity_date( activity_month: str | None, activity_year: str | None, statement_periods: dict[str, str], ) -> bool | str: """Validate the `Activity Month` and `Activity Year` fields of an adjustment.""" if not activity_month: return VALIDATION_ERRORS.ACTIVITY_MONTH_REQUIRED if not is_integer(activity_month): return VALIDATION_ERRORS.ACTIVITY_MONTH_INVALID month = int(activity_month) if month < 1 or month > 12: return VALIDATION_ERRORS.ACTIVITY_MONTH_LENGTH if not activity_year: return VALIDATION_ERRORS.ACTIVITY_YEAR_REQUIRED if not is_integer(activity_year): return VALIDATION_ERRORS.ACTIVITY_YEAR_INVALID if len(activity_year) != 4: return VALIDATION_ERRORS.ACTIVITY_YEAR_LENGTH key = f'{activity_month}/{activity_year}' statement_period_status = statement_periods.get(key) if statement_period_status is not None: return True return VALIDATION_ERRORS.ACTIVITY_PERIOD_MISSING def validate_statement_date( statement_month: str | None, statement_year: str | None, statement_periods: dict[str, str], ) -> bool | str: """Validate the `Statement Month` and `Statement Year` fields of an adjustment.""" if not statement_month: return VALIDATION_ERRORS.STATEMENT_MONTH_REQUIRED if not is_integer(statement_month): return VALIDATION_ERRORS.STATEMENT_MONTH_INVALID month = int(statement_month) if month < 1 or month > 12: return VALIDATION_ERRORS.STATEMENT_MONTH_LENGTH if not statement_year: return VALIDATION_ERRORS.STATEMENT_YEAR_REQUIRED if not is_integer(statement_year): return VALIDATION_ERRORS.STATEMENT_YEAR_INVALID if len(statement_year) != 4: return VALIDATION_ERRORS.STATEMENT_YEAR_LENGTH key = f'{statement_month}/{statement_year}' statement_period_status = statement_periods.get(key) if statement_period_status in VALID_STATEMENT_PERIOD_STATUSES: return True return VALIDATION_ERRORS.STATEMENT_PERIOD_MISSING def validate_adjustment_type( adjustment_type: str | None, adjustment_types: set[str] ) -> bool | str: """Validate the `Adjustment Type` field of an adjustment.""" if not adjustment_type: return VALIDATION_ERRORS.ADJUSTMENT_TYPE_REQUIRED if adjustment_type.lower() not in adjustment_types: return VALIDATION_ERRORS.ADJUSTMENT_TYPE_UNSUPPORTED # REVIEW: This is probably never reached if adjustment_type.lower() == ACCOUNT_EXPENSE_ADJUSTMENT_TYPE: return VALIDATION_ERRORS.ADJUSTMENT_TYPE_EXPENSE return True def validate_client_facing_comments(comments: str | None) -> bool | str: """Validate the `Client Facing Comments` field of an adjustment.""" if not comments: return VALIDATION_ERRORS.COMMENTS_REQUIRED if len(comments) > 180: return VALIDATION_ERRORS.COMMENTS_LENGTH return True def validate_distribution_type( distribution_type: str | None, upc: str | None ) -> bool | str: """Validate the `Distribution Type` field of an adjustment.""" if distribution_type and not upc: return VALIDATION_ERRORS.DISTRIBUTION_TYPE_BLANK if distribution_type and distribution_type.lower() not in VALID_DISTRIBUTION_TYPES: return VALIDATION_ERRORS.DISTRIBUTION_TYPE_UNSUPPORTED return True def validate_flowthrough_payment( flowthrough_payment: str | bool | None, adjustment_type: str | None ): """Validate the `Apply To Flowthrough Payment` field of an adjustment.""" if flowthrough_payment is None or flowthrough_payment == '': # The field is required for a "Flowthrough" adjustment if adjustment_type and adjustment_type.lower() == FLOWTHROUGH_ADJUSTMENT_TYPE: return VALIDATION_ERRORS.FLOWTHROUGH_PAYMENT_REQUIRED return True value: BooleanFilter | None = None try: value = BooleanFilter.parse(flowthrough_payment) except ValueError: pass if value is None or value == BooleanFilter.ALL: return VALIDATION_ERRORS.FLOWTHROUGH_PAYMENT_INVALID return True