"""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 import sqlalchemy from abacus_contract.constants.constants import MECHANICAL_DEDUCTION_TERRITORIES from abacus_contract.constants.constants import MECHANICAL_DEDUCTION_TYPES from abacus_contract.constants.error import \ ERROR_MECH_DEDUCTION_CAN_NOT_HAVE_SAME_TERRITORY from abacus_contract.constants.error import \ ERROR_MECHANICAL_DEDUCTION_ALREADY_EXIST from abacus_contract.constants.error import \ 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 sqlalchemy.exc.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 """ # noqa: E501 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 sqlalchemy.exc.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 # noqa: E501 )): 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 sqlalchemy.exc.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 sqlalchemy.exc.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)