"""Logic for Contract Advance.""" from datetime import date, datetime from decimal import Decimal from typing import Type from owsresponse import response from abacus_contract import models from abacus_contract.constants import error from abacus_contract.constants.constants import ( ADVANCE_STATUSES, ADVANCE_STATUSES_EXTRA_VALIDATION_REQUIRED, ADVANCE_STATUSES_PENDING_MARK, DEFAULT_PAGE_LIMIT, DEFAULT_PAGE_OFFSET, ) from abacus_contract.constants.error import ( ERROR_CONTRACT_ADVANCE_PAID_DELETE, ERROR_INVALID_CONTRACT_ADVANCE_STATUS, ERROR_INVALID_REFERENCE_PAYMENT_TYPE_ID, ) from abacus_contract.schemas.contract_advance import ( ContractAdvancePaidSchema, ContractAdvanceSchema, ) from abacus_contract.utils.format_error import validation_error from abacus_contract.utils.request import validate_pagination_params def create_contract_advance( contract_id: str, advance_description: str, amount: str, currency_code: str, milestone: str, milestone_date: datetime, milestone_description: str, reference_payment_type_id: int = None, advance_status: str = None, note: str = None, vat_amount: Decimal = None, withholding_tax_amount: Decimal = None, us_source_income_rate: Decimal = None, ) -> Type[response.Response]: """Create contract advance. Args: contract_id (str): ID of the related contract advance_description (str): Description for the advance amount (str): Decimal dollar amount of the advance currency_code (str): ISO currency code for the advance milestone (str): Milestone to justify the advance milestone_description (str): Description for the milestone milestone_date (str): Date of the specified milestone reference_payment_type_id (int): Payment type of the advance advance_status (str): Completion status of the milestone note (str): A note attached to the advance vat_amount (Decimal): VAT amount withholding_tax_amount (Decimal): Withholding tax amount us_source_income_rate (Decimal): US source income rate Returns: a response.Response """ try: _validate_create_contract_advance( milestone_date, note, advance_status, vat_amount, withholding_tax_amount ) except Exception as exc: return validation_error(str(exc)) models.Contract.get_by_id_or_error(contract_id) contract_advance = models.ContractAdvance.build( contract_id=contract_id, advance_description=advance_description, amount=amount, currency_code=currency_code, milestone=milestone, milestone_date=milestone_date, milestone_description=milestone_description, advance_status=advance_status, note=note, reference_payment_type_id=reference_payment_type_id, vat_amount=vat_amount, withholding_tax_amount=withholding_tax_amount, us_source_income_rate=us_source_income_rate, ) _calculate_contract_advance_fields(contract_advance) models.ContractAdvance.commit_changes() message = ContractAdvanceSchema().dump(contract_advance) return response.Response(message=message, status=201) def _validate_create_contract_advance( milestone_date, note, advance_status, vat_amount, withholding_tax_amount ): """Validate create request parameters. Args: contract_advance_object (ContractAdvance): the contract_advance object loaded from the database. advance_description (str): Description for the advance amount (str): Decimal dollar amount of the advance currency_code (str): ISO currency code for the advance milestone (str): Milestone to justify the advance milestone_description (str): Description for the milestone milestone_date (str): Date of the specified milestone advance_status (str): Completion status of the milestone note (str): A note attached to the advance vat_amount (Decimal): VAT amount withholding_tax_amount (Decimal): Withholding tax amount """ if ( advance_status not in [ADVANCE_STATUSES.NOT_QUALIFIED, ADVANCE_STATUSES.DELETED] and not milestone_date ): raise Exception( error.ERROR_CONTRACT_ADVANCE_NO_MILESTONE_DATE.format(status=advance_status) ) current_date = date.today() if milestone_date is not None and milestone_date > current_date: raise Exception(error.ERROR_CONTRACT_ADVANCE_MILESTONE_DATE) if advance_status in ADVANCE_STATUSES_EXTRA_VALIDATION_REQUIRED and ( vat_amount is None or withholding_tax_amount is None ): raise Exception(error.ERROR_CONTRACT_ADVANCE_TAXES_FIELDS_REQUIRED) def get_contract_advances( contract_id: int, status: str, request_params: dict ) -> response.Response: """Get contract advances by contract_id. Arg: contract_id(int): id of the contract status (str)(Optional): either pending or paid request_params (dict)(Optional): dict of query string passed to the url - limit(int): the size of page - offset(int): the page number Returns: A List of contract advances """ try: params = _validate_request_params(contract_id, status, request_params) items, total_count = models.ContractAdvance.get_by_contract_id(**params) schema = ( ContractAdvancePaidSchema if (status == ADVANCE_STATUSES.PAID) else ContractAdvanceSchema ) message = {'items': schema(many=True).dump(items), 'total_count': total_count} return response.Response(message=message, status=200) except Exception as e: return validation_error(str(e)) def _validate_request_params( contract_id: int, contract_advance_status: str, request_params: dict ) -> dict: """Validate request parameters. Args: contract_id(int): id of the contract contract_advance_status(str): either pending or paid request_params (dict)(Optional): dict of query string passed to the url - limit(int): the size of page - offset(int): the page number Returns: A dict of request parameters """ limit = request_params.get('limit', DEFAULT_PAGE_LIMIT) offset = request_params.get('offset', DEFAULT_PAGE_OFFSET) valid_advance_statuses = [ ADVANCE_STATUSES_PENDING_MARK, ADVANCE_STATUSES.PAID, ADVANCE_STATUSES.QUALIFIED, ADVANCE_STATUSES.IN_REVIEW, ] pagination_params = validate_pagination_params(limit, offset) if ( contract_advance_status and contract_advance_status not in valid_advance_statuses ): raise Exception( ERROR_INVALID_CONTRACT_ADVANCE_STATUS.format( status=', '.join(valid_advance_statuses) ) ) reference_payment_type_id = request_params.get('reference_payment_type_id') if reference_payment_type_id: try: reference_payment_type_id = int(reference_payment_type_id) except ValueError: raise Exception(ERROR_INVALID_REFERENCE_PAYMENT_TYPE_ID) models.ReferencePaymentType.get_by_id_or_error(reference_payment_type_id) models.Contract.get_by_id_or_error(contract_id) return { 'contract_advance_status': contract_advance_status, 'contract_id': contract_id, 'reference_payment_type_id': reference_payment_type_id, **pagination_params, } def delete_contract_advance( contract_advance: Type[models.ContractAdvance], ) -> Type[response.Response]: """Soft delete specified contract_advance. Args: contract_advance (class): ContractAdvance instance """ if contract_advance.advance_status == ADVANCE_STATUSES.PAID: return validation_error(ERROR_CONTRACT_ADVANCE_PAID_DELETE) contract_advance_id = contract_advance.contract_advance_id models.ContractAdvance.delete_by_id_or_error(contract_advance_id, soft_delete=True) contract_advance.update_attributes(advance_status=ADVANCE_STATUSES.DELETED) models.ContractAdvance.commit_changes() return response.Response(status=204) def update_contract_advance(contract_advance_object, **params): """Update contract advance. Args: contract_advance_object (ContractAdvance): the contract_advance object loaded from the database. **params: - advance_description (str): Description for the advance - amount (str): Decimal dollar amount of the advance - currency_code (str): ISO currency code for the advance - milestone (str): Milestone to justify the advance - milestone_description (str): Description for the milestone - milestone_date (str): Date of the specified milestone - advance_status (str): Completion status of the milestone - note (str): A note attached to the advance - reference_payment_type_id (int): Payment type of the advance - vat_amount (Decimal): VAT amount - withholding_tax_amount (Decimal): Withholding tax amount - us_source_income_rate (Decimal): US source income rate Returns: a response.Response """ try: _validate_update_contract_advance_params(contract_advance_object, **params) contract_advance_object.update_attributes(**params) _calculate_contract_advance_fields(contract_advance_object) models.ContractAdvance.commit_changes() except Exception as exc: return validation_error(str(exc)) return response.Response( message=ContractAdvanceSchema().dump(contract_advance_object), status=200 ) def _validate_update_contract_advance_params(contract_advance_object, **params): """Validate update request parameters. Args: contract_advance_object (ContractAdvance): the contract_advance object loaded from the database. **params: - advance_description (str): Description for the advance - amount (str): Decimal dollar amount of the advance - currency_code (str): ISO currency code for the advance - milestone (str): Milestone to justify the advance - milestone_description (str): Description for the milestone - milestone_date (str): Date of the specified milestone - advance_status (str): Completion status of the milestone - note (str): A note attached to the advance - reference_payment_type_id (int): Payment type of the advance - vat_amount (Decimal): VAT amount - withholding_tax_amount (Decimal): Withholding tax amount - us_source_income_rate (Decimal): US source income rate """ if ( contract_advance_object.deleted_at is not None or contract_advance_object.advance_status == ADVANCE_STATUSES.DELETED ): raise Exception(error.ERROR_CONTRACT_ADVANCE_DELETED) if ( params.get('advance_status') is not None and params.get('advance_status') not in [ADVANCE_STATUSES.NOT_QUALIFIED, ADVANCE_STATUSES.DELETED] and not params.get('milestone_date', contract_advance_object.milestone_date) ): raise Exception( error.ERROR_CONTRACT_ADVANCE_NO_MILESTONE_DATE.format( status=params['advance_status'] ) ) current_date = date.today() if params.get('milestone_date') and params.get('milestone_date') > current_date: raise Exception(error.ERROR_CONTRACT_ADVANCE_MILESTONE_DATE) if ( params.get('advance_status') == ADVANCE_STATUSES.IN_REVIEW and contract_advance_object.advance_status != ADVANCE_STATUSES.QUALIFIED ): raise Exception(error.ERROR_CONTRACT_ADVANCE_QUALIFIED_STATUS_REQUIRED) if ( params.get('advance_status') == ADVANCE_STATUSES.PENDING_PAYMENT and contract_advance_object.advance_status != ADVANCE_STATUSES.IN_REVIEW ): raise Exception(error.ERROR_CONTRACT_ADVANCE_IN_REVIEW_STATUS_REQUIRED) advance_status = ( params.get('advance_status') or contract_advance_object.advance_status ) if advance_status in ADVANCE_STATUSES_EXTRA_VALIDATION_REQUIRED and ( ( contract_advance_object.vat_amount is None and params.get('vat_amount') is None ) or ( contract_advance_object.withholding_tax_amount is None and params.get('withholding_tax_amount') is None ) ): raise Exception(error.ERROR_CONTRACT_ADVANCE_TAXES_FIELDS_REQUIRED) return params def _calculate_contract_advance_fields(contract_advance: models.ContractAdvance): """Calculate contract advance fields. Fields: - amount_after_withholding_and_vat Args: contract_advance (ContractAdvance): the contract_advance object """ if ( contract_advance.amount is not None and contract_advance.withholding_tax_amount is not None and contract_advance.vat_amount is not None ): contract_advance.amount_after_withholding_and_vat = ( contract_advance.amount + contract_advance.withholding_tax_amount + contract_advance.vat_amount )