"""Logic for contract_lifecycle_schedule.""" from abacus_common_logic.connectors.database import db from abacus_common_logic.constants.error import ERROR_ENTITY_DOES_NOT_EXIST from abacus_common_logic.utils.dates import current_timestamp from marshmallow import ValidationError from owsresponse import response import sqlalchemy from abacus_contract.constants.constants import \ CONTRACT_LIFECYCLE_SCHEDULE_RENEWAL_TYPES from abacus_contract.constants.constants import CONTRACT_TYPES from abacus_contract.constants.error import \ ERROR_COLLECTION_PERIOD_DETAIL_NOT_REQUIRED, \ ERROR_CONTRACT_LIFECYCLE_START_NOT_VALID from abacus_contract.constants.error import \ ERROR_COLLECTION_PERIOD_DETAIL_REQUIRED from abacus_contract.constants.error import \ ERROR_CONTRACT_LIFECYCLE_SCHEDULE_NOT_MATCHED from abacus_contract.constants.error import \ ERROR_CONTRACT_LIFECYCLE_SCHEDULES_ALREADY_EXIST from abacus_contract.constants.error import \ ERROR_MORE_THAN_TWO_CONTRACT_LIFECYCLE_SCHEDULES_NOT_ALLOWED from abacus_contract.constants.error import ERROR_SCHEDULE_END_REQUIRED from abacus_contract.constants.error import ERROR_SCHEDULE_END_SHOULD_BE_IN_FUTURE from abacus_contract.constants.error import \ ERROR_SUBSEQUENT_CONTRACT_LIFECYCLE_SCHEDULES_CAN_NOT_CREATED from abacus_contract.constants.error import \ ERROR_SUBSEQUENT_RENEW_AFTER_CERTAIN_DATE_CAN_NOT_CREATED from abacus_contract.constants.error import \ ERROR_TWO_CONTRACT_LIFECYCLE_SCHEDULES_ALLOWED from abacus_contract.logic.contract_lifecycle import _create_contract_lifecycle from abacus_contract.logic.contract_lifecycle_schedule_detail import \ find_or_create_contract_lifecycle_schedule_detail from abacus_contract.models.contract import Contract from abacus_contract.models.contract_lifecycle import ContractLifecycle from abacus_contract.models.contract_lifecycle_schedule import ContractLifecycleSchedule from abacus_contract.schemas.contract_lifecycle_schedule \ import ContractLifecycleScheduleSchema from abacus_contract.utils.format_error import validation_error from abacus_contract.utils.format_response import prepare_dataload_response continuously_active_renewal_type = \ CONTRACT_LIFECYCLE_SCHEDULE_RENEWAL_TYPES.CONTINUOUSLY_ACTIVE renew_after_certain_date_renewal_type = \ CONTRACT_LIFECYCLE_SCHEDULE_RENEWAL_TYPES.RENEW_AFTER_CERTAIN_DATE def get_contract_lifecycle_schedules_by_contract_id( contract_id: int ) -> response.Response: """Get a list of contract lifecycle schedules for a contract_id. Args: contract_id(int): id of the contract Returns: a list of contract_lifecycle_schedule records """ Contract.get_by_id_or_error(contract_id) result = ContractLifecycleSchedule.get_contract_lifecycle_schedules_by_contract_id( contract_id ) return response.Response( message=ContractLifecycleScheduleSchema(many=True).dump(result), status=200 ) def get_contract_lifecycle_schedules_by_ids( contract_lifecycle_schedule_ids: list ) -> response.Response: """Get contract lifecycle schedules by their identifiers. Args: contract_lifecycle_schedule_ids(list): a list of ids of the contract_lifecycle_schedule Returns: a list of contract_lifecycle_schedule records """ contract_lifecycle_schedules = ContractLifecycleSchedule.get_by_ids( contract_lifecycle_schedule_ids ) contract_lifecycle_schedules_list = ContractLifecycleScheduleSchema(many=True) \ .dump(contract_lifecycle_schedules) result = prepare_dataload_response( contract_lifecycle_schedule_ids, contract_lifecycle_schedules_list, 'contract_lifecycle_schedule_id' ) return response.Response( message=result, status=200 ) def create_contract_lifecycle_schedules( contract_id: int, post_request_body: list, allow_end_date_in_past: bool = False ) -> response.Response: """Create one or two contract_lifecycle_schedules. A contract_lifecycle_schedule_detail is also created if there is no corresponding record that has the given period_interval and period_type. Args: contract_id (int): ID of the contract post_request_body (list): POST request payload having following fields - renewal_type - termination_notice_detail_interval - termination_notice_detail_type - renewal_offset_detail_interval (Optional) - renewal_offset_detail_type (Optional) - collection_period_detail_interval (Optional) - collection_period_detail_type (Optional) - schedule_end (Optional) - allow_end_date_in_past (Optional) default False. Allows schedule end date in the past for backfilling the contracts. - contract_lifecycle (Optional) Returns: a list of contract_lifecycle_schedule records """ contract = Contract.get_by_id_or_error(contract_id) existing_contract_lifecycle_schedules = \ ContractLifecycleSchedule.get_contract_lifecycle_schedules_by_contract_id(contract_id) # noqa: E501 try: _validate_request_payload( contract_id, contract.contract_type, existing_contract_lifecycle_schedules, post_request_body, allow_end_date_in_past=allow_end_date_in_past ) except ValidationError as e: return validation_error(str(e)) except Exception as e: return response.create_error_response( code='error', status=409, message=str(e) ) try: contract_lifecycle_schedules = _create_contract_lifecycle_schedules( contract_id, post_request_body ) ContractLifecycleSchedule.commit_changes() except sqlalchemy.exc.SQLAlchemyError as e: db.session.rollback() raise e return response.Response( message=ContractLifecycleScheduleSchema(many=True).dump( contract_lifecycle_schedules ), status=201 ) def _create_contract_lifecycle_schedules(contract_id: int, post_request_body: list): """Create contract lifecycle schedules. Args: contract_id (int): ID of the contract post_request_body (list): POST request body having following fields - renewal_type - termination_notice_detail_interval - termination_notice_detail_type - renewal_offset_detail_interval (Optional) - renewal_offset_detail_type (Optional) - collection_period_detail_interval (Optional) - collection_period_detail_type (Optional) - schedule_end (Optional) - contract_lifecycle (Optional) """ contract_lifecycle_schedules = list() for request_body in post_request_body: renewal_type = request_body.get('renewal_type') schedule_end = request_body.get('schedule_end') contract_lifecycle = request_body.get('contract_lifecycle') termination_notice_detail_id, renewal_offset_detail_id, collection_period_detail_id = _get_detail_ids(request_body) # noqa: E501 # creates contract_lifecycle_schedule records contract_lifecycle_schedule = ContractLifecycleSchedule.build( contract_id=contract_id, termination_notice_detail_id=termination_notice_detail_id, renewal_offset_detail_id=renewal_offset_detail_id, collection_period_detail_id=collection_period_detail_id, renewal_type=renewal_type, schedule_end=schedule_end, ) if contract_lifecycle is not None: contract_lifecycle_schedule.contract_lifecycle = \ ContractLifecycle.build( contract_id=contract_id, lifecycle_term_start=contract_lifecycle.get('lifecycle_term_start'), ) contract_lifecycle_schedules.append(contract_lifecycle_schedule) return contract_lifecycle_schedules def _validate_request_payload( contract_id: int, contract_type: str, existing_contract_lifecycle_schedules: list, request_body: list, allow_one_record_in_payload: bool = False, allow_end_date_in_past: bool = False ) -> bool: """Validate contract_lifecycle_schedule's POST/PUT request params. Args: contract_id (int): ID of the contract contract_type (str): Type of the contract existing_contract_lifecycle_schedules (list): list of contract_lifecycle_schedules request_body (list): request body having following fields - renewal_type - termination_notice_detail_interval - termination_notice_detail_type - renewal_offset_detail_interval (Optional) - renewal_offset_detail_type (Optional) - collection_period_detail_interval (Optional) - collection_period_detail_type (Optional) - schedule_end (Optional) - allow_one_record_in_payload (bool): indicates whether a request payload can contain one record or not. This is specifically for PUT request - allow_end_date_in_past (bool): allows schedule end date in the past for backfilling the contracts. - contract_lifecycle (Optional): if present, it should have a valid lifecycle_term_start date """ # validate if contract_lifecycle_schedules already associated # with a specified contract if len(existing_contract_lifecycle_schedules) != 0: raise Exception( ERROR_CONTRACT_LIFECYCLE_SCHEDULES_ALREADY_EXIST.format(contract_id) ) # Check if request payload has more than two records if len(request_body) > 2: raise ValidationError( ERROR_MORE_THAN_TWO_CONTRACT_LIFECYCLE_SCHEDULES_NOT_ALLOWED ) # Check if request payload has one record if len(request_body) == 1: # if renewal_type is not "continuously_active", then throw an error if not allow_one_record_in_payload and request_body[0]['renewal_type'] \ != continuously_active_renewal_type: raise ValidationError(ERROR_TWO_CONTRACT_LIFECYCLE_SCHEDULES_ALLOWED) if allow_one_record_in_payload and request_body[0]['renewal_type'] \ == renew_after_certain_date_renewal_type: raise ValidationError( ERROR_SUBSEQUENT_RENEW_AFTER_CERTAIN_DATE_CAN_NOT_CREATED.format( renew_after_certain_date_renewal_type ) ) if 'contract_lifecycle' in request_body[0]: lifecycle = request_body[0]['contract_lifecycle'] _validate_contract_lifecycle_create_params(lifecycle) # Check if request payload has two records if len(request_body) == 2: first_lifecycle_schedule_renewal_type = \ request_body[0].get('renewal_type') second_lifecycle_schedule_renewal_type = \ request_body[1].get('renewal_type') # check the renewal_type of first record if first_lifecycle_schedule_renewal_type == continuously_active_renewal_type: raise ValidationError( ERROR_SUBSEQUENT_CONTRACT_LIFECYCLE_SCHEDULES_CAN_NOT_CREATED.format( continuously_active_renewal_type ) ) # check the renewal_type of second record if second_lifecycle_schedule_renewal_type == renew_after_certain_date_renewal_type: # noqa: E501 raise ValidationError( ERROR_SUBSEQUENT_RENEW_AFTER_CERTAIN_DATE_CAN_NOT_CREATED.format( renew_after_certain_date_renewal_type ) ) # if renewal_type of first record is "renew_after_certain_date" # then check whether schedule_end date is provided if first_lifecycle_schedule_renewal_type == renew_after_certain_date_renewal_type: # noqa: E501 schedule_end = request_body[0].get('schedule_end') if schedule_end is None: raise ValidationError( ERROR_SCHEDULE_END_REQUIRED.format( renew_after_certain_date_renewal_type ) ) # validate if schedule_end date is future date if (schedule_end <= current_timestamp().date() and not allow_end_date_in_past): raise ValidationError(ERROR_SCHEDULE_END_SHOULD_BE_IN_FUTURE) for i in range(2): if 'contract_lifecycle' in request_body[i]: lifecycle = request_body[i]['contract_lifecycle'] _validate_contract_lifecycle_create_params(lifecycle) _validate_collection_period_details(contract_type, request_body) return True def _validate_contract_lifecycle_create_params(contract_lifecycle: dict) -> bool: """Validate contract_lifecycle create params. Args: contract_lifecycle (dict): contract_lifecycle field from request body """ if contract_lifecycle is None: return True value = contract_lifecycle.get('lifecycle_term_start') if value is None: raise ValidationError(ERROR_CONTRACT_LIFECYCLE_START_NOT_VALID) return True def _validate_collection_period_details( contract_type: str, post_request_body: list ) -> bool: """Validate collection period detail fields. Args: contract_type (str): type of contract post_request_body (list): POST request payload """ for request_body in post_request_body: collection_period_detail_interval = \ request_body.get('collection_period_detail_interval') collection_period_detail_type = \ request_body.get('collection_period_detail_type') if contract_type != CONTRACT_TYPES.NEIGHBOURING_RIGHTS: if collection_period_detail_interval is not None or \ collection_period_detail_type is not None: raise ValidationError( ERROR_COLLECTION_PERIOD_DETAIL_NOT_REQUIRED.format(contract_type) ) else: if ( collection_period_detail_interval is not None and collection_period_detail_type is None ) or ( collection_period_detail_interval is None and collection_period_detail_type is not None ): raise ValidationError( ERROR_COLLECTION_PERIOD_DETAIL_REQUIRED.format(contract_type) ) return True def _get_detail_ids(request_body: dict) -> tuple: """Get the contract_lifecycle_schedule_detail_ids. Args: request_body (dict): POST request payload Returns a tuple having termination_notice_detail_id, renewal_offset_detail_id, collection_period_detail_id fields """ termination_notice_detail_interval = \ request_body.get('termination_notice_detail_interval') termination_notice_detail_type = \ request_body.get('termination_notice_detail_type') termination_notice_detail_id = \ find_or_create_contract_lifecycle_schedule_detail( termination_notice_detail_interval, termination_notice_detail_type ).contract_lifecycle_schedule_detail_id renewal_offset_detail_interval = \ request_body.get('renewal_offset_detail_interval') renewal_offset_detail_type = \ request_body.get('renewal_offset_detail_type') renewal_offset_detail_id = None if renewal_offset_detail_interval is not None and \ renewal_offset_detail_type is not None: renewal_offset_detail_id = \ find_or_create_contract_lifecycle_schedule_detail( renewal_offset_detail_interval, renewal_offset_detail_type ).contract_lifecycle_schedule_detail_id collection_period_detail_interval = \ request_body.get('collection_period_detail_interval') collection_period_detail_type = \ request_body.get('collection_period_detail_type') collection_period_detail_id = None if collection_period_detail_interval is not None and \ collection_period_detail_type is not None: collection_period_detail_id = \ find_or_create_contract_lifecycle_schedule_detail( collection_period_detail_interval, collection_period_detail_type ).contract_lifecycle_schedule_detail_id return termination_notice_detail_id, renewal_offset_detail_id, collection_period_detail_id # noqa: E501 def update_contract_lifecycle_schedules( contract_id: int, put_request_body: list, allow_end_date_in_past: bool = False ) -> response.Response: """Update contract_lifecycle_schedules. This function will delete the existing contract_lifecycle_schedules and create new ones.   If the contract_lifecycle_schedule being updated is current schedule, then the corresponding contract_lifecycle will also be deleted and recreated. Args: contract_id (int): id of the contract put_request_body (list): PUT request payload having following fields - contract_lifecycle_schedule_id (Optional) - renewal_type - termination_notice_detail_interval - termination_notice_detail_type - renewal_offset_detail_interval (Optional) - renewal_offset_detail_type (Optional) - collection_period_detail_interval (Optional) - collection_period_detail_type (Optional) - schedule_end (Optional) - allow_end_date_in_past (Optional) default False. Allows schedule end date in the past for backfilling the contracts. Returns: a list of contract_lifecycle_schedule records """ try: contract = Contract.get_by_id_or_error(contract_id) existing_contract_lifecycle = ContractLifecycle.get_by_contract_id(contract_id) is_updating_current_lifecycle_schedule = any([ existing_contract_lifecycle.contract_lifecycle_schedule_id == request.get('contract_lifecycle_schedule_id') for request in put_request_body ]) existing_contract_lifecycle_schedules = \ _validate_and_get_existing_contract_lifecycle_schedules( contract_id, put_request_body, is_updating_current_lifecycle_schedule ) _validate_request_payload( contract_id, contract.contract_type, [], put_request_body, not is_updating_current_lifecycle_schedule, allow_end_date_in_past ) contract_lifecycle_schedules, _ = _delete_and_create_contract_lifecycle_and_schedules( # noqa: E501 contract_id, put_request_body, existing_contract_lifecycle, existing_contract_lifecycle_schedules, existing_contract_lifecycle.lifecycle_term_start, is_updating_current_lifecycle_schedule ) except ValidationError as e: return validation_error(str(e)) return response.Response( message=ContractLifecycleScheduleSchema(many=True).dump( contract_lifecycle_schedules ), status=200 ) def _validate_and_get_existing_contract_lifecycle_schedules( contract_id: int, put_request_body: list, is_updating_current_lifecycle_schedule ): """Check if contract_lifecycle_schedule_id exist. Args: contract_id (int): id of the contract put_request_body (list): PUT request payload is_updating_current_lifecycle_schedule (bool): indicates whether the current lifecycle schedule is being updated Returns: existing contract_lifecycle_schedule records """ schedules = list() contract_associated_lifecycle_schedules = \ ContractLifecycleSchedule.get_contract_lifecycle_schedules_by_contract_id(contract_id) # noqa: 501 contract_lifecycle_schedule_ids = set( [request.get('contract_lifecycle_schedule_id') for request in put_request_body] ) for contract_lifecycle_schedule_id in contract_lifecycle_schedule_ids: contract_lifecycle_schedule = ContractLifecycleSchedule.get_by_id( contract_lifecycle_schedule_id ) if not contract_lifecycle_schedule: continue if contract_lifecycle_schedule and ( contract_lifecycle_schedule.deleted_at is not None or contract_lifecycle_schedule.deleted_by is not None ): raise ValidationError( ERROR_ENTITY_DOES_NOT_EXIST.format( object_type='ContractLifecycleSchedule', object_id=contract_lifecycle_schedule_id ) ) existing_contract_lifecycle_schedules = [ schedule for schedule in contract_associated_lifecycle_schedules if schedule.contract_lifecycle_schedule_id == contract_lifecycle_schedule_id ] if not existing_contract_lifecycle_schedules: raise ValidationError( ERROR_CONTRACT_LIFECYCLE_SCHEDULE_NOT_MATCHED.format( contract_lifecycle_schedule_id, contract_id ) ) schedules.append(contract_lifecycle_schedule) return contract_associated_lifecycle_schedules \ if is_updating_current_lifecycle_schedule \ else schedules def _delete_and_create_contract_lifecycle_and_schedules( contract_id: int, put_request_body: list, existing_contract_lifecycle: object, existing_contract_lifecycle_schedules: list, lifecycle_term_start: str, is_updating_current_lifecycle_schedule: bool ) -> tuple: """Delete and create contract_lifecycle_schedules/contract_lifecycle. Args: contract_id (int): id of the contract put_request_body (list): PUT request payload existing_contract_lifecycle (obj): ContractLifecycle Object existing_contract_lifecycle_schedules (list): a list of ContractLifecycleSchedule Object lifecycle_term_start (str): start date of the contract is_updating_current_lifecycle_schedule (bool): indicates whether the current lifecycle schedule is being updated Returns: a tuple containing a list of contract_lifecycle_schedule records and a contract_lifecycle """ try: # create contract_lifecycle_schedules contract_lifecycle_schedules = _create_contract_lifecycle_schedules( contract_id, put_request_body ) db.session.flush() # create contract_lifecycle if current lifecycle schedule is updated contract_lifecycle = existing_contract_lifecycle if is_updating_current_lifecycle_schedule: contract_lifecycle = _create_contract_lifecycle( contract_id, lifecycle_term_start, contract_lifecycle_schedules[0] ) # delete existing contract_lifecycle existing_contract_lifecycle._soft_delete() db.session.flush() # delete existing contract_lifecycle_schedules for schedule in existing_contract_lifecycle_schedules: schedule._soft_delete() db.session.flush() db.session.commit() except sqlalchemy.exc.SQLAlchemyError as e: db.session.rollback() raise e except ValidationError as e: db.session.rollback() raise e return contract_lifecycle_schedules, contract_lifecycle