"""Logic for contract_lifecycle.""" from datetime import date from typing import List 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 dateutil.relativedelta import relativedelta from flask import g from marshmallow import ValidationError from owsresponse import response from sqlalchemy.exc import SQLAlchemyError from werkzeug.exceptions import HTTPException from abacus_contract.constants import constants, error from abacus_contract.constants.constants import ( CONTRACT_LIFECYCLE_SCHEDULE_RENEWAL_TYPES, CONTRACT_LIFECYCLE_STATUSES, CONTRACT_LIFECYCLE_UPDATEABLE_FIELDS, SCHEDULE_DETAIL_PERIOD_TYPES_MAPPING, ) from abacus_contract.constants.error import ( ERROR_CAN_NOT_TERMINATE_CONTRACT_LIFECYCLE, ERROR_CONTRACT_LIFECYCLE_ALREADY_EXIST, ERROR_CONTRACT_LIFECYCLE_SCHEDULE_NOT_MATCHED, ERROR_CONTRACT_LIFECYCLE_SCHEDULES_NOT_EXIST, ERROR_CONTRACT_LIFECYCLE_START_AND_END_DATE_NOT_VALID, ERROR_CONTRACT_REACTIVATION_INVALID_STATUS, ERROR_CURRENT_CONTRACT_LIFECYCLE_SCHEDULE_NOT_FOUND, ERROR_INVALID_STATUS_FOR_TERMINATING_CONTRACT_LIFECYCLE, ERROR_SUBSEQUENT_LIFECYCLE_SCHEDULE_NOT_EXIST, ) 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.models.contract_lifecycle_schedule_detail import ( ContractLifecycleScheduleDetail, ) from abacus_contract.schemas.contract_lifecycle import ContractLifecycleDetailSchema from abacus_contract.utils.format_error import validation_error from abacus_contract.utils.format_response import prepare_dataload_response def get_contract_lifecycle_by_contract_id(contract_id: int) -> response.Response: """Get a contract lifecycle for a contract_id. Args: contract_id(int): id of the contract Returns: a contract_lifecycle record """ Contract.get_by_id_or_error(contract_id) contract_lifecycle = ContractLifecycle.get_by_contract_id(contract_id) return response.Response( message=ContractLifecycleDetailSchema().dump(contract_lifecycle), status=200 ) def update_contract_lifecycle_by_contract_id( contract_id: int, lifecycle: dict ) -> response.Response: """Update a contract lifecycle for a contract_id. Args: contract_id(int): id of the contract lifecycle: a ContractLifecycle dict with updated values Returns: The updated contract_lifecycle record """ Contract.get_by_id_or_error(contract_id) contract_lifecycle = ContractLifecycle.get_by_contract_id(contract_id) if any(key not in CONTRACT_LIFECYCLE_UPDATEABLE_FIELDS for key in lifecycle): raise ValidationError('Non-updatable fields provided in lifecycle.') _update_contract_lifecycle(contract_lifecycle, **lifecycle) return response.Response( message=ContractLifecycleDetailSchema().dump(contract_lifecycle), status=200 ) def get_contract_lifecycles_by_contract_ids(contract_ids: list) -> response.Response: """Get a contract lifecycle for a contract_id. Args: contract_ids(list): id of the contract Returns: a dataloaded list of contract_lifecycle records """ contract_lifecycles = ContractLifecycle.get_by_contract_ids(contract_ids) contract_lifecycles_list = ContractLifecycleDetailSchema(many=True).dump( contract_lifecycles ) result = prepare_dataload_response( contract_ids, contract_lifecycles_list, 'contract_id' ) return response.Response(message=result, status=200) def get_contract_lifecycles_by_ids(contract_lifecycle_ids: list) -> response.Response: """Get contract lifecycles by their identifiers. Args: contract_lifecycle_ids(list): a list of ids of the contract_lifecycle Returns: a list of contract_lifecycle records """ contract_lifecycles = ContractLifecycle.get_by_ids(contract_lifecycle_ids) contract_lifecycles_list = ContractLifecycleDetailSchema(many=True).dump( contract_lifecycles ) result = prepare_dataload_response( contract_lifecycle_ids, contract_lifecycles_list, 'contract_lifecycle_id' ) return response.Response(message=result, status=200) def get_contract_lifecycle_by_schedule_id( contract_lifecycle_schedule_id: int, ) -> response.Response: """Get a contract lifecycle for a contract_lifecycle_schedule_id. Args: contract_lifecycle_schedule_id(int): id of the contract_lifecycle_schedule Returns: a contract_lifecycle record """ contract_lifecycle_schedule = ContractLifecycleSchedule.get_by_id_or_error( contract_lifecycle_schedule_id ) if not contract_lifecycle_schedule or ( contract_lifecycle_schedule.deleted_at is not None or contract_lifecycle_schedule.deleted_by is not None ): return response.create_not_found_response( message=ERROR_ENTITY_DOES_NOT_EXIST.format( object_type='ContractLifecycleSchedule', object_id=contract_lifecycle_schedule_id, ) ) contract_lifecycle = contract_lifecycle_schedule.contract_lifecycle if contract_lifecycle is not None and ( contract_lifecycle.deleted_by is not None or contract_lifecycle.deleted_at is not None ): contract_lifecycle = dict() return response.Response( message=ContractLifecycleDetailSchema().dump(contract_lifecycle), status=200 ) def create_contract_lifecycle( contract_id: int, contract_lifecycle_schedule_id: int, lifecycle_term_start: str ) -> response.Response: """Create a contract_lifecycle. Args: contract_id (int): id of the contract contract_lifecycle_schedule_id (int): id of the contract_lifecycle_schedule lifecycle_term_start (str): start date of the contract Returns: a contract_lifecycle record. """ try: contract_lifecycle_schedule = _validate_and_get_contract_lifecycle_schedule( contract_id, contract_lifecycle_schedule_id ) contract_lifecycle = _create_contract_lifecycle( contract_id, lifecycle_term_start, contract_lifecycle_schedule ) ContractLifecycle.commit_changes() except (ValidationError, HTTPException) as e: return validation_error(str(e)) except SQLAlchemyError as e: db.session.rollback() raise e except Exception as e: return response.create_error_response(code='error', status=409, message=str(e)) return response.Response( message=ContractLifecycleDetailSchema().dump(contract_lifecycle), status=201 ) def _create_contract_lifecycle( contract_id: int, lifecycle_term_start: str, contract_lifecycle_schedule: object ) -> object: """Create contract lifecycle. Args: contract_id (int): id of the contract lifecycle_term_start (str): start date of the contract contract_lifecycle_schedule (object): ContractLifecycleSchedule Object """ contract_lifecycle_params = _generate_contract_lifecycle_params( contract_id, lifecycle_term_start, contract_lifecycle_schedule ) contract_lifecycle = ContractLifecycle.build(**contract_lifecycle_params) return contract_lifecycle def _validate_and_get_contract_lifecycle_schedule( contract_id, contract_lifecycle_schedule_id ): """Check if contract_id and contract_lifecycle_schedule_id exist. This function also checks - if the contract_lifecycle for the specified contract_id and contract_lifecycle_schedule_id exist. - if the specified contract_lifecycle_schedule_id is associated with a contract or not and accordingly returns the contract_lifecycle_schedule. Args: contract_id (int): id of the contract contract_lifecycle_schedule_id (int): id of the contract_lifecycle_schedule Returns: a contract_lifecycle_schedule if exist """ contract = Contract.get_by_id_or_error(contract_id) ContractLifecycleSchedule.get_by_id_or_error(contract_lifecycle_schedule_id) existing_contract_lifecycle = ContractLifecycle.get_by_contract_id_and_schedule_id( contract_id, contract_lifecycle_schedule_id ) if existing_contract_lifecycle: raise Exception( ERROR_CONTRACT_LIFECYCLE_ALREADY_EXIST.format( contract_id, contract_lifecycle_schedule_id ) ) contract_lifecycle_schedules = [ schedule for schedule in contract.contract_lifecycle_schedules if schedule.contract_lifecycle_schedule_id == contract_lifecycle_schedule_id ] if contract_lifecycle_schedules and ( contract_lifecycle_schedules[0].deleted_at is not None or contract_lifecycle_schedules[0].deleted_by is not None ): raise ValidationError( ERROR_ENTITY_DOES_NOT_EXIST.format( object_type='ContractLifecycleSchedule', object_id=contract_lifecycle_schedule_id, ) ) if not contract_lifecycle_schedules: raise ValidationError( ERROR_CONTRACT_LIFECYCLE_SCHEDULE_NOT_MATCHED.format( contract_lifecycle_schedule_id, contract_id ) ) return contract_lifecycle_schedules[0] def _get_lifecycle_term_end( lifecycle_term_start: str, contract_lifecycle_schedule: object ) -> str: """Get lifecycle_term_end date by renewal_type. Args: contract_lifecycle_schedule (object): ContractLifecycleSchedule object lifecycle_term_start (str): start date of the contract Returns: a date or null for lifecycle_term_end field depends on renewal_type """ lifecycle_term_end = None renewal_type = contract_lifecycle_schedule.renewal_type if ( renewal_type == CONTRACT_LIFECYCLE_SCHEDULE_RENEWAL_TYPES.RENEW_AFTER_CERTAIN_DATE ): lifecycle_term_end = contract_lifecycle_schedule.schedule_end if renewal_type == CONTRACT_LIFECYCLE_SCHEDULE_RENEWAL_TYPES.RENEW_PERIODICALLY: renewal_offset_detail_id = contract_lifecycle_schedule.renewal_offset_detail_id renewal_offset_detail = ContractLifecycleScheduleDetail.get_by_id_or_error( renewal_offset_detail_id ) attributes = { SCHEDULE_DETAIL_PERIOD_TYPES_MAPPING[ renewal_offset_detail.period_type ]: renewal_offset_detail.period_interval } renewal_schedule = relativedelta(**attributes) lifecycle_term_end = lifecycle_term_start + renewal_schedule return lifecycle_term_end def _get_termination_notice_deadline( lifecycle_term_end: str, termination_notice_detail_id: int ) -> str: """Get the termination_notice_deadline date. Args: lifecycle_term_end (str): end date of the contract termination_notice_detail_id (int): contract_lifecycle_schedule_id of the termination_notice_detail Returns: a date or null for termination_notice_detail depends on lifecycle_term_end """ if not lifecycle_term_end: return None termination_notice_detail = ContractLifecycleScheduleDetail.get_by_id_or_error( termination_notice_detail_id ) attributes = { SCHEDULE_DETAIL_PERIOD_TYPES_MAPPING[ termination_notice_detail.period_type ]: termination_notice_detail.period_interval } termination_notice_schedule = relativedelta(**attributes) return lifecycle_term_end - termination_notice_schedule def _get_lifecycle_status(lifecycle_term_start: str, lifecycle_term_end: str) -> str: """Get lifecycle_status by lifecycle_term_start/lifecycle_term_end. Args: lifecycle_term_start (str): start date of the contract lifecycle_term_end (str): end date of the contract Returns: either active or init status """ if lifecycle_term_start > current_timestamp().date(): return CONTRACT_LIFECYCLE_STATUSES.INIT if lifecycle_term_start <= current_timestamp().date() and ( lifecycle_term_end is None or lifecycle_term_end > current_timestamp().date() ): return CONTRACT_LIFECYCLE_STATUSES.ACTIVE def _update_contract_lifecycle( contract_lifecycle: ContractLifecycle, **params: dict, ): """Update a contract_lifecycle's fields. Args: contract_lifecycle (ContractLifecycle): Contract Lifecycle model object params (dict): key value pairs of fields to be updated - Optional fields: - lifecycle_status (str): contract lifecycle status - lifecycle_term_start (date): lifecycle term start date - lifecycle_term_end (date): lifecycle term end date - renewal_effective (date): renewal effective date - termination_notice_deadline (date): termination notice deadline date - termination_notice_received (date): termination notice received date - termination_effective (date): termination effective date - collection_start (date): collection start date - collection_end (date): collection end date """ _validate_contract_lifecycle_dates(contract_lifecycle, params) contract_lifecycle.update_attributes(**params) ContractLifecycle.commit_changes() def _validate_contract_lifecycle_dates( contract_lifecycle: ContractLifecycle, params: dict ) -> bool: """Validate contract_lifecycle dates. Args: contract_lifecycle (ContractLifecycle): Contract Lifecycle model object params (dict): key value pairs of fields to be updated - Optional fields: - termination_effective (date): termination effective date Returns: true if dates are valid """ if ( params.get('termination_effective') and contract_lifecycle.lifecycle_term_end and params['termination_effective'] > contract_lifecycle.lifecycle_term_end ): raise ValidationError( error.ERROR_TERMINATION_EFFECTIVE_DATE_GREATER_THAN_TERM_END ) return True def terminate_contract_lifecycle( contract_id: int, termination_effective: date, termination_notice_received: date, ): """Terminate contract lifecycle. Args: contract_id (int): ID of the contract termination_effective (date): termination effective date termination_notice_received (date): termination notice received date """ contract_lifecycle = ContractLifecycle.get_by_contract_id(contract_id) if not contract_lifecycle: raise ValidationError( error.ERROR_CONTRACT_LIFECYCLE_NOT_FOUND.format(contract_id) ) _validate_contract_lifecycle_termination(contract_lifecycle) updated_fields = _updated_fields_for_contract_lifecycle_termination( contract_lifecycle, termination_effective, termination_notice_received ) return _update_contract_lifecycle(contract_lifecycle, **updated_fields) def _validate_contract_lifecycle_termination( contract_lifecycle: ContractLifecycle, ) -> bool: """Validate that a contract can be terminated.""" if contract_lifecycle.lifecycle_status not in [ constants.CONTRACT_LIFECYCLE_STATUSES.ACTIVE, constants.CONTRACT_LIFECYCLE_STATUSES.INIT, ]: raise ValidationError(error.ERROR_CONTRACT_TERMINATION_INVALID_STATUS) return True def _updated_fields_for_contract_lifecycle_termination( contract_lifecycle: ContractLifecycle, termination_effective: date, termination_notice_received: date, ) -> dict: """Get updated fields to terminate contract lifecycle.""" contract_lifecycle_schedule = ContractLifecycleSchedule.get_by_id( contract_lifecycle.contract_lifecycle_schedule_id ) status = constants.CONTRACT_LIFECYCLE_STATUSES.TERMINATED if termination_effective > date.today(): status = constants.CONTRACT_LIFECYCLE_STATUSES.TO_BE_TERMINATED if ( contract_lifecycle_schedule and contract_lifecycle_schedule.deleted_at is None and contract_lifecycle_schedule.deleted_by is None and contract_lifecycle_schedule.collection_period_detail is not None ): collection_period = contract_lifecycle_schedule.collection_period_detail relativedelta_params = { SCHEDULE_DETAIL_PERIOD_TYPES_MAPPING[ collection_period.period_type ]: collection_period.period_interval } collection_end_date = termination_effective + relativedelta( **relativedelta_params ) if termination_effective <= date.today() < collection_end_date: status = constants.CONTRACT_LIFECYCLE_STATUSES.IN_COLLECTION_PERIOD return { 'termination_effective': termination_effective, 'termination_notice_received': termination_notice_received, 'lifecycle_term_end': termination_effective, 'renewal_effective': None, 'collection_start': termination_effective, 'collection_end': collection_end_date, 'lifecycle_status': status, } return { 'termination_effective': termination_effective, 'termination_notice_received': termination_notice_received, 'lifecycle_term_end': termination_effective, 'renewal_effective': None, 'lifecycle_status': status, } def reactivate_contract_lifecycle(contract_id: int) -> ContractLifecycle: """Reactivate contract_lifecycle. Args: contract_id (int): id of the contract Returns: a contract_lifecycle record """ # get the contract_lifecycle by contract_id contract_lifecycle = ContractLifecycle.get_by_contract_id(contract_id) if not contract_lifecycle: raise ValidationError( error.ERROR_CONTRACT_LIFECYCLE_NOT_FOUND.format(contract_id) ) # validate that contract_lifecycle status is "terminated", "to_be_terminated" # or "in_collection_period" contract_lifecycle_status = contract_lifecycle.lifecycle_status _validate_contract_lifecycle_reactivation(contract_lifecycle_status) # get the contract_lifecycle_schedules by contract_id contract_lifecycle_schedules = ( ContractLifecycleSchedule.get_contract_lifecycle_schedules_by_contract_id( contract_id ) ) if not contract_lifecycle_schedules: raise ValidationError(error.ERROR_CONTRACT_LIFECYCLE_SCHEDULES_NOT_FOUND) # get the current contract_lifecycle_schedule current_contract_lifecycle_schedule = _get_current_contract_lifecycle_schedule( contract_lifecycle, contract_lifecycle_schedules ) # delete contract_lifecycle and create new one new_contract_lifecycle = _soft_delete_and_create_contract_lifecycle( contract_id, contract_lifecycle, contract_lifecycle_schedules, current_contract_lifecycle_schedule, ) return new_contract_lifecycle def _validate_contract_lifecycle_reactivation(contract_lifecycle_status: str) -> bool: """Validate that contract can reactivated. Args: contract_lifecycle_status (str): status of the contract_lifecycle Returns: a boolean value """ VALID_LIFECYCLE_STATUSES_FOR_REACTIVATE = [ constants.CONTRACT_LIFECYCLE_STATUSES.TERMINATED, constants.CONTRACT_LIFECYCLE_STATUSES.TO_BE_TERMINATED, constants.CONTRACT_LIFECYCLE_STATUSES.IN_COLLECTION_PERIOD, ] if contract_lifecycle_status not in VALID_LIFECYCLE_STATUSES_FOR_REACTIVATE: raise ValidationError(ERROR_CONTRACT_REACTIVATION_INVALID_STATUS) return True def _get_current_contract_lifecycle_schedule( contract_lifecycle: ContractLifecycle, contract_lifecycle_schedules: List[ContractLifecycleSchedule], ) -> ContractLifecycleSchedule: """Get a current contract_lifecycle_schedule. Args: contract_lifecycle (ContractLifecycle): contract_lifecycle details contract_lifecycle_schedules (List[ContractLifecycleSchedule]): a list of contract_lifecycle_schedules Returns: a current contract_lifecycle_schedule """ contract_lifecycle_schedule_id = contract_lifecycle.contract_lifecycle_schedule_id current_lifecycle_schedule = [ lifecycle_schedule for lifecycle_schedule in contract_lifecycle_schedules if lifecycle_schedule.contract_lifecycle_schedule_id == contract_lifecycle_schedule_id ] if not current_lifecycle_schedule: raise ValidationError(ERROR_CURRENT_CONTRACT_LIFECYCLE_SCHEDULE_NOT_FOUND) return current_lifecycle_schedule[0] def _soft_delete_and_create_contract_lifecycle( contract_id: int, contract_lifecycle: ContractLifecycle, contract_lifecycle_schedules: List[ContractLifecycleSchedule], current_contract_lifecycle_schedule: ContractLifecycleSchedule, ) -> ContractLifecycle: """Delete and create contract_lifecycle/contract_lifecycle_schedules. Args: contract_id (int): id of the contract contract_lifecycle (ContractLifecycle): contract_lifecycle details contract_lifecycle_schedules (List[ContractLifecycleSchedule]): a list of contract_lifecycle_schedules current_contract_lifecycle_schedule (ContractLifecycleSchedule): current contract_lifecycle_schedule details Returns: a contract_lifecycle record """ continuously_active_renewal_type = ( CONTRACT_LIFECYCLE_SCHEDULE_RENEWAL_TYPES.CONTINUOUSLY_ACTIVE ) renewal_after_certain_date_renewal_type = ( CONTRACT_LIFECYCLE_SCHEDULE_RENEWAL_TYPES.RENEW_AFTER_CERTAIN_DATE ) lifecycle_schedule_renewal_type = current_contract_lifecycle_schedule.renewal_type schedule_end_date = current_contract_lifecycle_schedule.schedule_end current_date = current_timestamp().date() if ( lifecycle_schedule_renewal_type == renewal_after_certain_date_renewal_type and schedule_end_date <= current_date ): # added import here to avoid circular references from abacus_contract.logic.contract_lifecycle_schedule import ( _delete_and_create_contract_lifecycle_and_schedules, ) # create contract_lifecycle_schedules and contract_lifecycle continuously_active_schedule = [ { 'renewal_type': continuously_active_renewal_type, 'termination_notice_detail_interval': 30, 'termination_notice_detail_type': 'day', } ] _, new_contract_lifecycle = _delete_and_create_contract_lifecycle_and_schedules( contract_id, continuously_active_schedule, contract_lifecycle, contract_lifecycle_schedules, current_date, True, ) else: try: new_contract_lifecycle = _create_contract_lifecycle( contract_id, current_date, current_contract_lifecycle_schedule ) contract_lifecycle._soft_delete() db.session.commit() except SQLAlchemyError as e: db.session.rollback() raise e except ValidationError as e: db.session.rollback() raise e return new_contract_lifecycle def terminate_contract_lifecycles( contract_lifecycle_ids: List[int], ) -> response.Response: """Terminate contract lifecycle records for specified ids. Args: contract_lifecycle_ids (list): a list of contract_lifecycle ids. Returns: a list of contract_lifecycle records that are "terminated"/"in_collection_period" """ contract_lifecycles_update_params = list() updated_contract_lifecycles = list() try: for contract_lifecycle_id in contract_lifecycle_ids: contract_lifecycle = ContractLifecycle.get_by_id_or_error( contract_lifecycle_id ) if ( not contract_lifecycle or contract_lifecycle.deleted_at is not None or contract_lifecycle.deleted_by is not None ): return response.create_not_found_response( message=ERROR_ENTITY_DOES_NOT_EXIST.format( object_type='ContractLifecycle', object_id=contract_lifecycle_id ) ) _validate_contract_lifecycle_termination_status(contract_lifecycle) contract_lifecycle_update_params = ( _get_updated_contract_lifecycle_termination_status(contract_lifecycle) ) contract_lifecycles_update_params.append(contract_lifecycle_update_params) updated_contract_lifecycles.append(contract_lifecycle) except (ValidationError, HTTPException) as e: return validation_error(str(e)) except Exception as e: raise e ContractLifecycle.bulk_update(contract_lifecycles_update_params) return response.Response( message=ContractLifecycleDetailSchema(many=True).dump( updated_contract_lifecycles ), status=200, ) def _validate_contract_lifecycle_termination_status( contract_lifecycle: ContractLifecycle, ) -> bool: """Validate the status of the contract_lifecycle that needs to be terminated. Args: contract_lifecycle (ContractLifecycle): contract_lifecycle record Returns: a True value if the contract_lifecycle status is "to_be_terminated" or "in_collection_period" """ lifecycle_status = [ CONTRACT_LIFECYCLE_STATUSES.IN_COLLECTION_PERIOD, CONTRACT_LIFECYCLE_STATUSES.TO_BE_TERMINATED, ] if contract_lifecycle.lifecycle_status not in lifecycle_status: raise ValidationError( ERROR_INVALID_STATUS_FOR_TERMINATING_CONTRACT_LIFECYCLE.format( contract_lifecycle.contract_lifecycle_id ) ) return True def _get_updated_contract_lifecycle_termination_status( contract_lifecycle: ContractLifecycle, ) -> dict: """Update the contract_lifecycle status to either "terminated" or "in_collection_period". Args: contract_lifecycle (ContractLifecycle): contract_lifecycle record Returns: a dict record having contract_lifecycle_id and termination status """ contract_lifecycle_id = contract_lifecycle.contract_lifecycle_id if ( contract_lifecycle.termination_effective is not None and contract_lifecycle.termination_effective <= date.today() ): if ( contract_lifecycle.collection_start is not None and contract_lifecycle.collection_end is not None and contract_lifecycle.collection_end > date.today() ): return { 'contract_lifecycle_id': contract_lifecycle_id, 'lifecycle_status': CONTRACT_LIFECYCLE_STATUSES.IN_COLLECTION_PERIOD, } return { 'contract_lifecycle_id': contract_lifecycle_id, 'lifecycle_status': CONTRACT_LIFECYCLE_STATUSES.TERMINATED, } else: raise ValidationError( ERROR_CAN_NOT_TERMINATE_CONTRACT_LIFECYCLE.format(contract_lifecycle_id) ) def activate_contract_lifecycles( contract_lifecycle_ids: List[int], ) -> response.Response: """Activate contract lifecycles based on the provided ids. Args: contract_lifecycle_ids (list): list of contract_lifecycle_ids Returns: a response object containing a list of contract lifecycles that are now activated """ contract_lifecycles_update_params = list() updated_contract_lifecycles = list() try: for contract_lifecycle_id in contract_lifecycle_ids: contract_lifecycle = ContractLifecycle.get_by_id_or_error( contract_lifecycle_id ) if ( not contract_lifecycle or contract_lifecycle.deleted_at is not None or contract_lifecycle.deleted_by is not None ): return response.create_not_found_response( message=ERROR_ENTITY_DOES_NOT_EXIST.format( object_type='Contract Lifecycle', object_id=contract_lifecycle_id, ) ) _validate_contract_lifecycle_activation_status(contract_lifecycle) contract_lifecycle_update_params = { 'contract_lifecycle_id': contract_lifecycle_id, 'lifecycle_status': constants.CONTRACT_LIFECYCLE_STATUSES.ACTIVE, } contract_lifecycles_update_params.append(contract_lifecycle_update_params) updated_contract_lifecycles.append(contract_lifecycle) except (ValidationError, HTTPException) as e: return validation_error(str(e)) except Exception as e: raise e # bulk update contract_lifecycle records ContractLifecycle.bulk_update(contract_lifecycles_update_params) return response.Response( message=ContractLifecycleDetailSchema().dump( updated_contract_lifecycles, many=True ), status=200, ) def _validate_contract_lifecycle_activation_status( contract_lifecycle: ContractLifecycle, ) -> bool: """Validate the status of the contract_lifecycle that needs to be activated. Args: contract_lifecycle (ContractLifecycle): contract_lifecycle record Returns: a True value if the contract_lifecycle status is "init" """ if contract_lifecycle.lifecycle_status != CONTRACT_LIFECYCLE_STATUSES.INIT: raise ValidationError( error.ERROR_INVALID_STATUS_FOR_ACTIVATING_CONTRACT_LIFECYCLE.format( contract_lifecycle.contract_lifecycle_id ) ) return True def renew_contract_lifecycles(contract_lifecycle_ids: List[int]) -> response.Response: """Renew contract lifecycle records for specified ids. Args: contract_lifecycle_ids (list): a list of contract_lifecycle ids. Returns: a list of contract_lifecycle records that have been renewed. """ new_contract_lifecycles = list() new_subsequent_contract_lifecycle_schedules = list() delete_contract_lifecycles = list() delete_current_contract_lifecycle_schedules = list() try: for contract_lifecycle_id in contract_lifecycle_ids: contract_lifecycle = ContractLifecycle.get_by_id_or_error( contract_lifecycle_id ) if ( not contract_lifecycle or contract_lifecycle.deleted_at is not None or contract_lifecycle.deleted_by is not None ): return response.create_not_found_response( message=ERROR_ENTITY_DOES_NOT_EXIST.format( object_type='ContractLifecycle', object_id=contract_lifecycle_id ) ) _validate_contract_lifecycle_renewal_status_and_date(contract_lifecycle) contract_lifecycle_schedules = ContractLifecycleSchedule.get_contract_lifecycle_schedules_by_contract_id( contract_lifecycle.contract_id ) if not contract_lifecycle_schedules: raise ValidationError( ERROR_CONTRACT_LIFECYCLE_SCHEDULES_NOT_EXIST.format( contract_lifecycle.contract_id ) ) new_contract_lifecycle, new_subsequent_contract_lifecycle_schedule = ( _renew_contract_lifecycle_and_schedule_from_subsequent_schedule( contract_lifecycle, contract_lifecycle_schedules ) ) new_contract_lifecycles.append(new_contract_lifecycle) if new_subsequent_contract_lifecycle_schedule: new_subsequent_contract_lifecycle_schedules.append( new_subsequent_contract_lifecycle_schedule ) current_contract_lifecycle_schedule = ( _get_current_contract_lifecycle_schedule( contract_lifecycle, contract_lifecycle_schedules ) ) delete_contract_lifecycles.append( _generate_contract_lifecycle_soft_delete_params(contract_lifecycle) ) delete_current_contract_lifecycle_schedules.append( _generate_contract_lifecycle_schedule_soft_delete_params( current_contract_lifecycle_schedule ) ) db.session.bulk_save_objects(new_contract_lifecycles, return_defaults=True) db.session.bulk_update_mappings(ContractLifecycle, delete_contract_lifecycles) if len(new_subsequent_contract_lifecycle_schedules) > 0: db.session.bulk_save_objects(new_subsequent_contract_lifecycle_schedules) db.session.bulk_update_mappings( ContractLifecycleSchedule, delete_current_contract_lifecycle_schedules ) db.session.commit() except ValidationError as e: return validation_error(str(e)) except Exception as e: db.session.rollback() raise e finally: db.session.close() return response.Response( message=ContractLifecycleDetailSchema().dump( new_contract_lifecycles, many=True ), status=200, ) def _validate_contract_lifecycle_renewal_status_and_date( contract_lifecycle: ContractLifecycle, ) -> bool: """Validate the status and renewal_effective date of the contract_lifecycle that needs to be renewed. Args: contract_lifecycle (ContractLifecycle): contract_lifecycle record Returns: a True value if the contract_lifecycle status is "active" and renewal_effective <= today """ if contract_lifecycle.lifecycle_status != CONTRACT_LIFECYCLE_STATUSES.ACTIVE: raise ValidationError( error.ERROR_INVALID_STATUS_FOR_RENEWING_CONTRACT_LIFECYCLE.format( contract_lifecycle.contract_lifecycle_id ) ) if ( contract_lifecycle.renewal_effective is None or contract_lifecycle.renewal_effective > date.today() ): raise ValidationError( error.ERROR_INVALID_RENEWAL_EFFECTIVE_DATE.format( contract_lifecycle.contract_lifecycle_id ) ) return True def _renew_contract_lifecycle_and_schedule_from_subsequent_schedule( contract_lifecycle: ContractLifecycle, contract_lifecycle_schedules: List[ContractLifecycleSchedule], ) -> tuple: """Build a new contract lifecycle record from the subsequent contract_lifecycle_schedule. This function will also create a subsequent contract_lifecycle schedule if renewal_type is "renew_periodically". Args: contract_lifecycle (ContractLifecycle): contract_lifecycle record contract_lifecycle_schedules (List[ContractLifecycleSchedule]): a list of contract_lifecycle_schedules Returns: a tuple containing new contract_lifecycle and subsequent contract_lifecycle_schedule record """ subsequent_contract_lifecycle_schedule = None new_subsequent_contract_lifecycle_schedule = None contract_lifecycle_schedule_id = contract_lifecycle.contract_lifecycle_schedule_id contract_id = contract_lifecycle.contract_id renewal_effective = contract_lifecycle.renewal_effective lifecycle_term_start = renewal_effective + relativedelta(days=1) for schedule in contract_lifecycle_schedules: if contract_lifecycle_schedule_id != schedule.contract_lifecycle_schedule_id: subsequent_contract_lifecycle_schedule = schedule if subsequent_contract_lifecycle_schedule is None: raise ValidationError( ERROR_SUBSEQUENT_LIFECYCLE_SCHEDULE_NOT_EXIST.format(contract_id) ) contract_lifecycle_params = _generate_contract_lifecycle_params( contract_id, lifecycle_term_start, subsequent_contract_lifecycle_schedule ) contract_lifecycle_params.update( { 'lifecycle_status': CONTRACT_LIFECYCLE_STATUSES.ACTIVE, 'last_renewed': current_timestamp(), } ) if ( subsequent_contract_lifecycle_schedule.renewal_type == CONTRACT_LIFECYCLE_SCHEDULE_RENEWAL_TYPES.RENEW_PERIODICALLY ): new_subsequent_contract_lifecycle_schedule = ContractLifecycleSchedule( contract_id=subsequent_contract_lifecycle_schedule.contract_id, termination_notice_detail_id=subsequent_contract_lifecycle_schedule.termination_notice_detail_id, renewal_offset_detail_id=subsequent_contract_lifecycle_schedule.renewal_offset_detail_id, collection_period_detail_id=subsequent_contract_lifecycle_schedule.collection_period_detail_id, renewal_type=subsequent_contract_lifecycle_schedule.renewal_type, schedule_end=subsequent_contract_lifecycle_schedule.schedule_end, ) return ContractLifecycle( **contract_lifecycle_params ), new_subsequent_contract_lifecycle_schedule def _generate_contract_lifecycle_params( contract_id: int, lifecycle_term_start: str, contract_lifecycle_schedule: ContractLifecycleSchedule, ) -> dict: """Generate contract_lifecycle params. Args: contract_id (int): id of the contract lifecycle_term_start (str): start date of the contract contract_lifecycle_schedule (ContractLifecycleSchedule): contract_lifecycle_schedule record Returns: a dict of contract_lifecycle params """ lifecycle_term_end = _get_lifecycle_term_end( lifecycle_term_start, contract_lifecycle_schedule ) if lifecycle_term_end is not None and (lifecycle_term_end <= lifecycle_term_start): raise ValidationError( ERROR_CONTRACT_LIFECYCLE_START_AND_END_DATE_NOT_VALID.format( lifecycle_term_start, lifecycle_term_end ) ) termination_notice_deadline = _get_termination_notice_deadline( lifecycle_term_end, contract_lifecycle_schedule.termination_notice_detail_id ) lifecycle_status = _get_lifecycle_status(lifecycle_term_start, lifecycle_term_end) renewal_effective_date = lifecycle_term_end return { 'contract_id': contract_id, 'contract_lifecycle_schedule_id': contract_lifecycle_schedule.contract_lifecycle_schedule_id, 'lifecycle_status': lifecycle_status, 'lifecycle_term_start': lifecycle_term_start, 'lifecycle_term_end': lifecycle_term_end, 'renewal_effective': renewal_effective_date, 'termination_notice_deadline': termination_notice_deadline, } def _generate_contract_lifecycle_soft_delete_params( contract_lifecycle: ContractLifecycle, ) -> dict: """Generate the fields required to soft delete contract_lifecycle record. Args: contract_lifecycle (ContractLifecycle): contract_lifecycle record Returns: a dict of contract_lifecycle soft delete params """ user_id = g.user_details.get('id') timestamp = current_timestamp() return { 'contract_lifecycle_id': contract_lifecycle.contract_lifecycle_id, 'lifecycle_status': CONTRACT_LIFECYCLE_STATUSES.INACTIVE, 'last_modified': timestamp, 'last_modified_by': user_id, 'deleted_by': user_id, 'deleted_at': timestamp, } def _generate_contract_lifecycle_schedule_soft_delete_params( contract_lifecycle_schedule: ContractLifecycleSchedule, ) -> dict: """Generate the fields required to soft delete contract_lifecycle_schedule record. Args: contract_lifecycle_schedule (ContractLifecycleSchedule): contract_lifecycle_schedule record Returns: a dict of contract_lifecycle_schedule soft delete params """ user_id = g.user_details.get('id') timestamp = current_timestamp() return { 'contract_lifecycle_schedule_id': contract_lifecycle_schedule.contract_lifecycle_schedule_id, 'last_modified': timestamp, 'last_modified_by': user_id, 'deleted_by': user_id, 'deleted_at': timestamp, }