"""Logic for Abacus Event.""" from abacus_common_logic.connectors.database import db from botocore.exceptions import ClientError from flask import g from owsresponse import response from werkzeug.exceptions import BadRequest, HTTPException, NotFound from abacus_event.connectors.airflow import handle_event_actions from abacus_event.connectors.aws import invoke_lambda from abacus_event.constants.constants import ( ABACUS_PAYMENTS_GENERATE_LAMBDA_NAME, ALLOWED_EVENTS_FOR_RETRY, EVENT_LAMBDA_NAMES, EVENT_NAMES, EVENT_TARGET_LAMBDA_NAMES, UNPROVISIONED_LAMBDAS, ) from abacus_event.models.abacus_event import AbacusEvent from abacus_event.schemas.abacus_event import ( AbacusEventByTargetTypeSchema, AbacusEventPaymentApprovalCountSchema, AbacusEventSchema, ) from abacus_event.utils.format_response import ( prepare_dataload_response, prepare_dataload_with_data_as_list_response, ) from abacus_event.utils.ows_error import OwsError schema = AbacusEventSchema() def create_event(**params) -> dict: """Create abacus event.""" event_name = params.get('event_name') target_id = params.get('target_id') target_type = params.get('target_type') if event_name not in EVENT_NAMES: raise BadRequest(f"Unknown event name: '{event_name}'") if _is_duplicate_event(event_name, target_id, target_type): raise BadRequest(f'{event_name} for {target_type} {target_id} already exists.') new_event = AbacusEvent.create(**params) _execute_event(new_event) return schema.dump(new_event) def get_payment_approval_count(payment_group_payment_id): """Get payment_group_payment's approval count.""" result = AbacusEvent.get_payment_approval_count(payment_group_payment_id) return AbacusEventPaymentApprovalCountSchema().dump(result) def update_rollback_time(target_type, target_id, event_name): """Update datetime when event is rollback.""" result = AbacusEvent.update_rollback_time(target_type, target_id, event_name) return schema.dump(result, many=True) def get_events_by_target_type(target_type, target_id): """Get event records for a specified target type and target ID.""" result = AbacusEvent.get_events_by_target_type(target_type, target_id) return AbacusEventByTargetTypeSchema().dump(result, many=True) def dataload_events_by_target(target_type, target_ids): """Dataload active events for a target type across many target ids. Returns an ordered response, one entry per requested target id (a list of that id's active events, or None), matching the /dataloader convention. """ events = AbacusEvent.get_events_by_target_ids(target_type, target_ids) events_list = AbacusEventByTargetTypeSchema().dump(events, many=True) if events_list and not any( event.get('target_id') is not None for event in events_list ): # A non-empty fetch where no record carries target_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_event dataloader: %s event(s) returned, none carrying ' 'target_id; likely a schema field mismatch. All entries shape to null.', len(events_list), ) result = prepare_dataload_with_data_as_list_response( target_ids, events_list, 'target_id' ) return response.Response({'items': result}) def dataload_events_by_ids(events_ids): """Dataload events by ids.""" stmt = AbacusEvent.get_filtered_query(event_ids=events_ids) events_by_params = db.session.execute(stmt).scalars().all() events_list = schema.dump(events_by_params, many=True) result = prepare_dataload_response(events_ids, events_list, 'abacus_event_id') return response.Response({'items': result}) def _is_duplicate_event(event_name: str, target_id: str, target_type: str) -> bool: """Validate that the event being created does not already exist for target. Args: event_name (str): name of the event target_id (str): ID or UUID of parent object tied to the event target_type (str): the name of the parent object tied to the event """ if event_name in [ EVENT_NAMES.ACCOUNTING_PERIOD_CLOSE, EVENT_NAMES.ADJUSTMENT_FILE_UPLOAD, EVENT_NAMES.ADJUSTMENT_FILE_WORKSHEET_IMPORT, *ALLOWED_EVENTS_FOR_RETRY, ]: existing_events = AbacusEvent.get_events_by_target_type(target_type, target_id) return any(event for event in existing_events if event.event_name == event_name) return False def retry_events(target_type: str, target_id: int, event_name: str): """Retry event.""" events = AbacusEvent.get_events_by_target_type(target_type, target_id, event_name) if not events: raise NotFound( f"No events found for '{event_name}' on {target_type} '{target_id}'." ) if not _is_retry_allowed(events): raise BadRequest( f"Retry not allowed for '{event_name}' on {target_type} '{target_id}'." ) for event in events: _execute_event(event, is_retry=True) return schema.dump(events, many=True) def _is_retry_allowed(events: list[AbacusEvent]) -> bool: """Check if retry is allowed for the given events.""" return bool(events) and all( event.event_name in ALLOWED_EVENTS_FOR_RETRY for event in events ) def _execute_event(event: AbacusEvent, is_retry: bool = False): """Execute the event by invoking the corresponding Lambda function or DAG. Args: event (AbacusEvent): The event to be executed is_retry (bool): Flag indicating if this is a retry call """ try: payload = schema.dump(event) if is_retry: payload['is_retry'] = True if lambda_function := EVENT_TARGET_LAMBDA_NAMES.get( (event.event_name, event.target_type) ): invoke_lambda(lambda_function, payload) return if lambda_function := EVENT_LAMBDA_NAMES.get(event.event_name): if lambda_function in UNPROVISIONED_LAMBDAS: invoke_lambda(lambda_function, payload, '$LATEST') else: invoke_lambda(lambda_function, payload) return if event.event_name == EVENT_NAMES.PAYMENTS_GENERATE: invoke_lambda(ABACUS_PAYMENTS_GENERATE_LAMBDA_NAME, payload) return handle_event_actions(event) except (ClientError, HTTPException, OwsError) as e: update_rollback_time(event.target_type, event.target_id, event.event_name) raise e