"""Logic for Contract Mechanical Deduction.""" import json from typing import Any, Dict, List from abacus_common_logic.connectors.database import db from marshmallow import ValidationError from owsresponse import response from sqlalchemy.exc import SQLAlchemyError from abacus_contract.constants.constants import ( CONTRACT_LIFECYCLE_STATUSES, MECHANICAL_DEDUCTION_TERRITORIES, MECHANICAL_DEDUCTION_TYPES, ) from abacus_contract.constants.error import ( ERROR_MECH_DEDUCTION_CAN_NOT_HAVE_SAME_TERRITORY, ERROR_MECHANICAL_DEDUCTION_ALREADY_EXIST, ERROR_MECHANICAL_TYPE_PHYSICAL_ONLY_ALLOW, ) from abacus_contract.models.contract import Contract from abacus_contract.models.contract_mechanical_deduction import ( ContractMechanicalDeduction, ) from abacus_contract.schemas.contract_mechanical_deduction import ( ActiveContractsByDateSchema, ContractMechanicalDeductionDetailSchema, ) from abacus_contract.utils.format_error import validation_error def get_contract_mechanical_deductions_by_contract_id( contract_id: int, ) -> tuple[response.Response, Contract]: """Get list of contract mechanical deductions for specified contract_id. Args: contract_id(int): id of the contract Returns: a list of contract_mechanical_deduction records """ contract = Contract.get_by_id_or_error(contract_id, 404) return ( response.Response( message=ContractMechanicalDeductionDetailSchema(many=True).dump( contract.contract_mechanical_deductions ), status=200, ), contract, ) def create_contract_mechanical_deduction( contract_id: int, admin_fee: str, admin_type: str, mechanical_type: list, territory: str, ) -> response.Response: """Create a contract_mechcanical_deduction. Args: contract_id (int): ID of the contract admin_fee (str): The amount, if any, The Orchard deducts for distribution. admin_type (str): The entity responsible for paying the mechanical deduction. One of 'business', 'customer', or 'both'. mechanical_type (list): List of mechanical deduction types. Any of 'digital' or 'physical'. territory (str): 3 character territory code. Returns: a contract_mechcanical_deduction record """ contract = Contract.get_by_id_or_error(contract_id, 404) try: _validate_territory_uniqueness(contract, territory) _validate_mechanical_type(mechanical_type, territory) contract_mechanical_deduction = ContractMechanicalDeduction.build( contract_id=contract_id, admin_fee=admin_fee, admin_type=admin_type, mechanical_type=mechanical_type, territory=territory, ) db.session.commit() except ValidationError as e: return validation_error(str(e)) except SQLAlchemyError as e: db.session.rollback() raise e return response.Response( message=ContractMechanicalDeductionDetailSchema().dump( contract_mechanical_deduction ), status=201, ) def _validate_territory_uniqueness(contract: Contract, territory: str) -> bool: """Validate that the contract does not have a mechanical deduction with the same territory. Args: contract (Contract): Contract object territory (str): 3 character territory code. Returns: a true value on successful validation """ existing_mech_deductions_territories = list() contract_mechanical_deductions = contract.contract_mechanical_deductions contract_id = contract.contract_id if contract_mechanical_deductions: existing_mech_deductions_territories = [ mech_deduction.territory for mech_deduction in contract_mechanical_deductions ] if territory in existing_mech_deductions_territories: raise ValidationError( ERROR_MECH_DEDUCTION_CAN_NOT_HAVE_SAME_TERRITORY.format( contract_id, territory ) ) return True def update_contract_mechanical_deduction( contract_mechanical_deduction: ContractMechanicalDeduction, **put_request_body: dict ) -> response.Response: """Update contract_mechanical_deduction's fields. Args: contract_mechanical_deduction(ContractMechanicalDeduction): ContractMechanicalDeduction object put_request_body(dict): PUT request body - admin_fee (Optional) - admin_type (Optional) - mechanical_type (Optional) Returns: updated contract_mechanical_deduction data """ try: mechanical_type = put_request_body.get('mechanical_type') if mechanical_type: territory = contract_mechanical_deduction.territory _validate_mechanical_type(mechanical_type, territory) contract_mechanical_deduction.update_attributes(**put_request_body) db.session.commit() except ValidationError as e: return validation_error(str(e)) except SQLAlchemyError as e: db.session.rollback() raise e return response.Response( message=ContractMechanicalDeductionDetailSchema().dump( contract_mechanical_deduction ), status=200, ) def _validate_mechanical_type(mechanical_type: list, territory: str) -> bool: """Validate mechanical_type field. Args: mechanical_type(list): list of mechanical deduction types. Either 'digital' or 'physical'. territory(str): 3 character territory code. """ if territory in [ MECHANICAL_DEDUCTION_TERRITORIES.CAN, MECHANICAL_DEDUCTION_TERRITORIES.ROW, ]: if len(mechanical_type) > 1 or ( len(mechanical_type) == 1 and mechanical_type[0] != MECHANICAL_DEDUCTION_TYPES.PHYSICAL ): raise ValidationError(ERROR_MECHANICAL_TYPE_PHYSICAL_ONLY_ALLOW) return True def soft_delete_contract_mechanical_deduction( contract_mechanical_deduction: ContractMechanicalDeduction, ) -> response.Response: """Soft delete contract mechanical deduction record. Args: contract_mechanical_deduction: a contract_mechanical_deduction Returns: response object with status """ try: contract_mechanical_deduction._soft_delete() ContractMechanicalDeduction.commit_changes() return response.Response(status=204) except SQLAlchemyError as e: raise e def create_contract_mechanical_deductions_worldwide( contract_id: int, admin_fee: str, admin_type: str, mechanical_type: list, ) -> response.Response: """Create contract mechanical deductions worldwide for specified contract_id. Args: contract_id (int): ID of the contract admin_fee (str): The amount, if any, The Orchard deducts for distribution. admin_type (str): The entity responsible for paying the mechanical deduction. One of 'business', 'customer', or 'both'. mechanical_type (list): List of mechanical deduction types. Either 'digital' or 'physical'. Returns: a list of contract_mechanical_deduction records """ contract = Contract.get_by_id_or_error(contract_id, 404) existing_contract_mechanical_deductions = contract.contract_mechanical_deductions contract_mechanical_deductions = list() try: if existing_contract_mechanical_deductions: raise ValidationError( ERROR_MECHANICAL_DEDUCTION_ALREADY_EXIST.format(contract_id) ) for territory in MECHANICAL_DEDUCTION_TERRITORIES: params = { 'admin_fee': admin_fee, 'admin_type': admin_type, 'contract_id': contract_id, 'mechanical_type': mechanical_type, 'territory': territory, } if territory in [ MECHANICAL_DEDUCTION_TERRITORIES.CAN, MECHANICAL_DEDUCTION_TERRITORIES.ROW, ]: params.update( {'mechanical_type': [MECHANICAL_DEDUCTION_TYPES.PHYSICAL]} ) contract_mechanical_deduction = ContractMechanicalDeduction.build(**params) contract_mechanical_deductions.append(contract_mechanical_deduction) db.session.commit() except ValidationError as e: return validation_error(str(e)) except SQLAlchemyError as e: db.session.rollback() raise e return response.Response( message=ContractMechanicalDeductionDetailSchema(many=True).dump( contract_mechanical_deductions ), status=201, ) def get_active_contracts_with_mechanical_deductions(date: str) -> List[Dict[str, Any]]: """Fetch active contracts with mechanical deductions for a given date. Args: date (str): A date string (format: YYYY-MM-DD) used to filter active contracts. Returns: response.Response: A Flask-compatible response object with a list of contracts. """ res = ContractMechanicalDeduction.get_active_contracts_with_mechanical_deductions( date ) if not res: return response.Response(message=[], status=200) merged_data = {} for row in res: row_dict = dict(row) contract_id = row_dict['contract_id'] term_type = row_dict['term_type'] key = (contract_id, term_type) attachments = row_dict.get('attachments') parsed_attachments = ( [str(a) for a in json.loads(attachments)] if attachments else [] ) mechanical_types = ( [s.strip() for s in row_dict['mechanical_type'].split(',')] if row_dict.get('mechanical_type') else [] ) if key not in merged_data: merged_data[key] = { 'account_id': row_dict['account_id'], 'contract_id': contract_id, 'term_type': term_type, 'attachments': set(parsed_attachments), 'mechanical_type': mechanical_types, } else: merged_data[key]['attachments'].update(parsed_attachments) final_data = [] for value in merged_data.values(): value['attachments'] = list(value['attachments']) final_data.append(value) result = ActiveContractsByDateSchema(many=True).dump(final_data) return response.Response(message=result, status=200) _MECHADMIN_ACTIVE_STATUSES = { CONTRACT_LIFECYCLE_STATUSES.ACTIVE, CONTRACT_LIFECYCLE_STATUSES.TO_BE_TERMINATED, CONTRACT_LIFECYCLE_STATUSES.IN_COLLECTION_PERIOD, } def get_mechadmin_for_account(account_id: int) -> response.Response: """Get whether or not an account is a mechadmin.""" contracts = Contract.get_by_accounts([account_id]) mechadmin_physical = False mechadmin_digital = False for contract in contracts: lifecycle = contract.contract_lifecycle if lifecycle and lifecycle.lifecycle_status not in _MECHADMIN_ACTIVE_STATUSES: continue for mech in contract.contract_mechanical_deductions: if MECHANICAL_DEDUCTION_TYPES.PHYSICAL in mech.mechanical_type: mechadmin_physical = True if ( MECHANICAL_DEDUCTION_TYPES.DIGITAL in mech.mechanical_type and mech.territory == MECHANICAL_DEDUCTION_TERRITORIES.USA ): mechadmin_digital = True results = { 'mechadmin_physical': mechadmin_physical, 'mechadmin_digital': mechadmin_digital, } return response.Response(message=results, status=200)