"""Logic for Abacus State.""" import sys from typing import Any, Iterable, cast from flask import g from marshmallow import ValidationError from owsresponse import response from abacus_state.constants import constants, error from abacus_state.logic.account_payee_state import ( update_account_payee_state, ) from abacus_state.logic.accounting_period_state import ( update_accounting_period_state, ) from abacus_state.logic.accounting_run_state import ( update_accounting_run_state, ) from abacus_state.logic.contract_state import ( update_contract_state, ) from abacus_state.logic.payee_state import ( update_payee_state, ) from abacus_state.logic.payment_group_payment_account_state import ( bulk_update_payment_group_payment_account_states, update_payment_group_payment_account_state, ) from abacus_state.logic.payment_group_payment_batch_state import ( update_payment_group_payment_batch_state, ) from abacus_state.logic.payment_group_payment_state import ( update_payment_group_payment_state, ) from abacus_state.logic.sales_file_state import ( update_sales_file_state, ) from abacus_state.logic.statement_period_adjustment_file_state import ( update_statement_period_adjustment_file_state, ) from abacus_state.logic.statement_period_payment_entity import ( update_statement_period_payment_entity_state, ) from abacus_state.logic.statement_period_state import ( update_statement_period_state, ) from abacus_state.logic.worksheet_payment_contract_advance_state import ( update_worksheet_payment_contract_advance_state, ) from abacus_state.logic.worksheet_payment_custom_state import ( update_worksheet_payment_custom_state, ) from abacus_state.models.abacus_state import AbacusState from abacus_state.models.statement_period_adjustment_file import ( StatementPeriodAdjustmentFile, ) from abacus_state.schemas.abacus_state import AbacusStateDetailSchema from abacus_state.utils.features import is_abacus_flowthrough_automation_enabled from abacus_state.utils.format_error import validation_error from abacus_state.utils.format_response import ( prepare_dataload_response, prepare_dataload_with_data_as_list_response, ) current_module = sys.modules[__name__] schema = AbacusStateDetailSchema() def create_abacus_states(params): """Create an abacus states.""" new_states = [] for item in params: if item['parent_table_name'] not in constants.PARENT_TABLE_NAMES: raise ValidationError(error.ERROR_INVALID_PARENT_TABLE_NAME) new_state = { 'parent_table_id': item['parent_table_id'], 'parent_table_name': item['parent_table_name'], 'action_name': item['action_name'], } new_states.append(AbacusState.build(**new_state)) AbacusState.commit_changes() message = AbacusStateDetailSchema(many=True).dump(new_states) return response.Response(message=message, status=201) def create_abacus_states_by_parent_table( parent_table_name: str, parent_table_id: int, contract_type: str | None = None ): """Create all abacus_state records for the specified parent_table_name. Args: parent_table_name (str): a valid royalty_accounting table name parent_table_id (int): valid ID of a record in the parent_table contract_type (str): accounting period contract type """ try: _validate_create_params(parent_table_name, parent_table_id) actions = constants.ACTIONS_BY_PARENT_TABLE.get(parent_table_name) # when creating accounting_period abacus_states, # remove prep_mechanical_deductions if contract_type is 'neighbouring_rights' if parent_table_name == constants.PARENT_TABLE_NAMES.ACCOUNTING_PERIOD: if contract_type == constants.CONTRACT_TYPES.NEIGHBOURING_RIGHTS: actions = [ action for action in constants.ACCOUNTING_PERIOD_ACTION_NAMES if action not in constants.ACCOUNTING_PERIOD_ACTION_NAMES[2:3] ] if ( parent_table_name == constants.PARENT_TABLE_NAMES.STATEMENT_PERIOD_ADJUSTMENT_FILE ): if is_abacus_flowthrough_automation_enabled(): adjustment_file = ( StatementPeriodAdjustmentFile.get_statement_period_adjustment_file( parent_table_id ) ) if adjustment_file and adjustment_file['batch_type'] == 'auto': actions = constants.AUTO_GENERATED_STATEMENT_PERIOD_ADJUSTMENT_FILE_ACTION_NAMES else: actions = constants.STATEMENT_PERIOD_ADJUSTMENT_FILE_ACTION_NAMES new_abacus_states = [] for action in cast(Iterable[str], actions): new_abacus_state = AbacusState.build( action_name=action, parent_table_id=parent_table_id, parent_table_name=parent_table_name, ) new_abacus_states.append(new_abacus_state) AbacusState.commit_changes() message = AbacusStateDetailSchema(many=True).dump(new_abacus_states) return response.Response(message=message, status=201) except ValidationError as e: return validation_error(e.messages[0]) def get_action_status_list(parent_table_name, parent_table_id): """Get action statuses list by specified parent table.""" result = AbacusState.get_action_status_list(parent_table_name, parent_table_id) return AbacusStateDetailSchema(many=True).dump(result) def update_abacus_state(abacus_state, **params): """Dynamically call an update action_status method by action_state_id.""" function_name = f'update_{abacus_state.parent_table_name}_state' callable_function = getattr(current_module, function_name) return callable_function(abacus_state, **params) def _validate_create_params(parent_table_name: str, parent_table_id: int): """Validate the parent_table_name is correct and that records don't already exist. Args: parent_table_name (str): a valid royalty_accounting table name parent_table_id (int): valid ID of a record in the parent_table """ if parent_table_name not in constants.PARENT_TABLE_NAMES: raise ValidationError(error.ERROR_INVALID_PARENT_TABLE_NAME) if AbacusState.get_action_status_list(parent_table_name, parent_table_id): raise ValidationError( error.ERROR_ACTION_STATES_ALREADY_EXIST.format( parent_table_name=parent_table_name, parent_table_id=parent_table_id ) ) def bulk_update_abacus_states_by_parent_table( parent_table_name: str, params: list[dict[str, Any]] ): """Dynamically call bulk update action_status method by parent_table_name.""" function_name = f'bulk_update_{parent_table_name}_states' callable_function = getattr(current_module, function_name) return callable_function(parent_table_name, params) def dataload_states_by_ids(states_ids): """Dataload states by ids.""" states_by_params = AbacusState.get_filtered_query(state_ids=states_ids).all() states_list = schema.dump(states_by_params, many=True) result = prepare_dataload_response(states_ids, states_list, 'abacus_state_id') return response.Response({'items': result}) def dataload_states_by_target(parent_table_name, parent_table_ids): """Dataload states for a parent table across many parent ids. Returns an ordered response, one entry per requested parent id (a list of that parent's states, or None), matching the /dataloader convention. """ states = AbacusState.get_filtered_query( parent_table_name=parent_table_name, parent_table_ids=parent_table_ids ).all() states_list = schema.dump(states, many=True) if states_list and not any( state.get('parent_table_id') is not None for state in states_list ): # A non-empty fetch where no record carries parent_table_id would shape # every id to null silently. That only happens on a serializer field-name # mismatch, so surface it loudly rather than shipping an all-null 200. g.log.error( 'abacus_state dataloader: %s state(s) returned, none carrying ' 'parent_table_id; likely a schema field mismatch. All entries shape ' 'to null.', len(states_list), ) result = prepare_dataload_with_data_as_list_response( parent_table_ids, states_list, 'parent_table_id' ) return response.Response({'items': result}) def bulk_query_states( parent_table_name=None, parent_table_ids=None, action_name=None, limit=100, offset=0, ): """Bulk query states with filters and pagination. Args: parent_table_name (str): Parent table name to filter by parent_table_ids (list): List of parent table IDs to filter by (requires parent_table_name) action_name (str): Action name to filter by limit (int): Maximum number of results to return (default: 100, max: 300) offset (int): Number of results to skip (default: 0) Returns: Response: Response object with filtered states and pagination metadata """ if ( parent_table_name is not None and parent_table_name not in constants.PARENT_TABLE_NAMES ): raise ValidationError(error.ERROR_INVALID_PARENT_TABLE_NAME) if parent_table_ids is not None and parent_table_name is None: raise ValidationError(error.ERROR_PARENT_TABLE_IDS_REQUIRE_PARENT_TABLE_NAME) count_query = AbacusState.get_filtered_query( parent_table_name=parent_table_name, parent_table_ids=parent_table_ids, action_name=action_name, ) total = count_query.count() states_query = AbacusState.get_filtered_query( parent_table_name=parent_table_name, parent_table_ids=parent_table_ids, action_name=action_name, limit=limit, offset=offset, ) states = states_query.all() states_list = schema.dump(states, many=True) result = { 'items': states_list, 'total': total, 'limit': limit, 'offset': offset, } return response.Response(result) def reset_abacus_state_actions( parent_table_name: str, parent_table_id: int ) -> response.Response: """Reset all action states for specified parent table name and id. Args: parent_table_name (str): name of the parent table parent_table_id (int): id of the parent table Returns: Response: Response object with updated states """ actions = AbacusState.reset_abacus_state_actions(parent_table_name, parent_table_id) result = AbacusStateDetailSchema(many=True).dump(actions) return response.Response(message=result, status=200)