"""Logic for Payment Group Payment Account.""" import collections from decimal import Decimal import typing from typing import Iterable, List from abacus_common_logic.connectors.database import db from marshmallow import ValidationError from payment import models from payment.constants.constants import ( DEFAULT_PAGE_LIMIT, DEFAULT_PAGE_OFFSET, PAYMENT_TYPES, ) from payment.constants.error import ERROR_PAYMENT_UPDATE_INVALID_STATUS from payment.logic.exceptions import EntityDoesNotExist, LogicError from payment.models import Items, PaymentGroupPaymentAccount from payment.models.worksheet_payable_balance_after_tax import ( get_payments_matching_closing_balances, ) from payment.repository import ( payment_allocation as payment_allocation_repository, payment_group_payment_account as payment_group_payment_account_repository, worksheet_account_contract_payable_details as payable_details_repository, ) from payment.utils.decorators import no_autoflush from payment.utils.features import is_feature_tap_automatic_tax_calculation_enabled from payment.utils.format_error import validate_currency from payment.utils.format_response import prepare_dataload_with_data_as_list_response @no_autoflush def bulk_create( payment_group_payment_id: int, create_params: Iterable[dict] ) -> list[PaymentGroupPaymentAccount]: """Create one or more payment_group_payment_account records. Args: payment_group_payment_id (int): id of parent payment_group_payment create_params (List[dict]): list of POST parameters """ payment_group_payment = models.PaymentGroupPayment.get_by_id_or_error( payment_group_payment_id ) automatic_tax_calc_ff = is_feature_tap_automatic_tax_calculation_enabled() account_ids_worksheets = models.WorksheetPayableBalanceAfterTax.get_by_payment_group_payment_and_account_ids( payment_group_payment_id, [p.get('account_id') for p in create_params], (not automatic_tax_calc_ff), ) all_worksheets = [] for account_worksheets in account_ids_worksheets.values(): all_worksheets.extend(account_worksheets) account_ids_payments = _get_matching_payments(all_worksheets) for params in create_params: try: _validate_params(params) account_id = params.get('account_id') pending_payments = account_ids_payments.get(account_id) pending_payment = pending_payments[0] if pending_payments else None pending_payment_id = ( pending_payment and pending_payment.payment_group_payment_id ) params.update( { 'payment_group_payment_id': payment_group_payment_id, 'prior_payment_group_payment_id': pending_payment_id, 'current_statement_period_id': payment_group_payment.statement_period_id, } ) payment_group_payment_account = models.PaymentGroupPaymentAccount.build( **params ) details = _create_payment_group_payment_account_details( account_ids_worksheets.get(account_id, []) ) payment_group_payment_account.details = details except ValidationError as e: raise LogicError(e.messages[0]) db.session.commit() return payment_group_payment.payment_accounts def get_payment_group_payment_account( object_id: int, ) -> models.PaymentGroupPaymentAccount: """Get a payment_group_payment_account by id.""" payment_group_payment_account = models.PaymentGroupPaymentAccount.get_by_id( object_id ) if not payment_group_payment_account: raise EntityDoesNotExist(models.PaymentGroupPaymentAccount, object_id) return payment_group_payment_account def delete_payment_group_payment_account(object_id: int) -> None: """Soft delete a payment_group_payment_account by updating deleted fields.""" payment_group_payment_account = models.PaymentGroupPaymentAccount.get_by_id( object_id ) if not payment_group_payment_account: raise EntityDoesNotExist(models.PaymentGroupPaymentAccount, object_id) payment_group_payment = payment_group_payment_account.payment_group_payment if payment_group_payment.is_sent(): raise LogicError(ERROR_PAYMENT_UPDATE_INVALID_STATUS) payment_allocation_repository.reset_to_init_by_payment_group_payment_account( payment_group_payment_account.payment_group_payment_account_id ) models.PaymentGroupPaymentAccount.delete_by_id_or_error( payment_group_payment_account.payment_group_payment_account_id, soft_delete=True ) models.PaymentGroupPaymentAccountDetail.soft_delete_by_payment_group_payment_account( # noqa payment_group_payment_account.payment_group_payment_account_id ) models.WorksheetPayableBalanceAfterTax.soft_delete_by_payment_group_payment_account( payment_group_payment_account.payment_group_payment_account_id ) payable_details_repository.soft_delete_by_payment_group_payment_account( payment_group_payment_account.payment_group_payment_account_id ) def get_accounts_list( limit: int, offset: int, payment_group_payment_id: int, sort_by: str, sort_order: str, is_pending: bool = False, search_term: str = None, ) -> Items[models.PaymentGroupPaymentAccount]: """Get page of payment_group_payment_accounts. Args: limit (int): The number of items to return; the size of the page offset (int): The number of items to skip before returning results; the page num payment_group_payment_id (int): id of the parent payment_group_payment sort_by (str): column name by which to order results sort_order (str): order by which to sort results (asc or desc) is_pending (bool): whether to get only pending or only non-pending results """ models.PaymentGroupPayment.get_by_id_or_error(payment_group_payment_id) params = { 'is_pending': is_pending, 'limit': max(limit, 1), 'offset': max(offset, 0), 'order_by': sort_by.lower(), 'order_dir': sort_order.lower(), 'payment_group_payment_id': payment_group_payment_id, 'search_term': search_term, } payment_accounts = models.PaymentGroupPaymentAccount.get_by_payment_group_payment( **params ) total_count = models.PaymentGroupPaymentAccount.get_by_payment_group_payment_count( payment_group_payment_id, is_pending, search_term ) return Items(payment_accounts, total_count) def get_last_posted_payment_by_account( account_id: int, ) -> models.PaymentGroupPaymentAccount | None: """Get an account's last posted payment, if any.""" return models.PaymentGroupPaymentAccount.last_posted_payment_by_account(account_id) def get_pending_payment_by_account( account_id: int, ) -> models.PaymentGroupPaymentAccount | None: """Get an account's pending (AKA 'prior' or 'unposted') payment, if any.""" return _get_pending_payment(account_id) def get_totals_by_payment_group_payment( payment_group_payment_id: int, ) -> list[dict]: """Get total amounts and accounts per currency_code in a payment_group_payment.""" models.PaymentGroupPayment.get_by_id_or_error(payment_group_payment_id) return models.PaymentGroupPaymentAccount.group_by_currency_code( payment_group_payment_id ) def get_totals_by_payment_group_payment_ids(ids: List[int]): """Get total amounts and accounts per currency_code in many payment_group_payments.""" totals = models.PaymentGroupPaymentAccount.group_by_currency_code_for_many_payment_group_payments( ids ) serializable = [dict(row) if row else None for row in totals] result = prepare_dataload_with_data_as_list_response( ids, serializable, 'payment_group_payment_id' ) return {'items': result} def get_totals_grouped_by_program_id( payment_group_payment_id: int, ) -> list[dict]: """Get total amounts per Payoneer Program ID.""" models.PaymentGroupPayment.get_by_id_or_error(payment_group_payment_id) return models.PaymentGroupPaymentAccount.get_payment_totals_grouped_by_program_id( payment_group_payment_id ) def update_payment_group_payment_account( object_id: int, note: str ) -> models.PaymentGroupPaymentAccount: """Update payment_group_payment_account's note. Args: object_id (int): id of the PaymentGroupPaymentAccount instance note (str): note to be added to instance of payment_group_payment_account """ payment_group_payment_account = models.PaymentGroupPaymentAccount.get_by_id( object_id ) if not payment_group_payment_account: raise EntityDoesNotExist(models.PaymentGroupPaymentAccount, object_id) payment_group_payment = payment_group_payment_account.payment_group_payment if payment_group_payment.is_sent(): raise LogicError(ERROR_PAYMENT_UPDATE_INVALID_STATUS) payment_group_payment_account.update_attributes(note=note) models.PaymentGroupPaymentAccount.commit_changes() return payment_group_payment_account def _get_pending_payment(account_id: int): """Get an account's pending (AKA 'prior' or 'unposted' or 'processing') payment, if any.""" # noqa: E501 pending_payments = models.PaymentGroupPaymentAccount.pending_payments_by_account( account_id ) if pending_payments: return pending_payments[0] def _get_matching_payments( payable_after_tax_entries: List[models.WorksheetPayableBalanceAfterTax], ) -> typing.Dict[int, list]: """ Get payments that have the same closing balance data as the given worksheets. Returns map by account_id """ payable_after_tax_ids = [ entry.worksheet_account_contract_payable_after_tax_id for entry in payable_after_tax_entries ] payments = get_payments_matching_closing_balances(payable_after_tax_ids) account_ids_payments = collections.defaultdict(list) for payment in payments: account_ids_payments[payment.account_id].append(payment) return account_ids_payments def _validate_params(params: dict, close_balance_enabled: bool = False): """Validate payment_group_payment_account parameters.""" currency_error = validate_currency(params.get('currency_code')) if currency_error is not None: raise ValidationError(currency_error.errors.get('message')) def _create_payment_group_payment_account_details( worksheets: List[models.WorksheetPayableBalanceAfterTax], ): """ Create PaymentGroupPaymentAccountDetail based on parent instance. Populates the data using the WorksheetPayableBalanceAfterTax """ details = [] for worksheet in worksheets: details.append( models.PaymentGroupPaymentAccountDetail.build( worksheet_account_contract_payable_after_tax_id=worksheet.worksheet_account_contract_payable_after_tax_id, # noqa account_id=worksheet.account_id, contract_id=worksheet.contract_id, payable_amount_pre_tax=worksheet.payable_amount_pre_tax, tax_withholding_amount=worksheet.tax_withholding_amount, vat_amount=worksheet.vat_amount, payable_amount_post_tax=worksheet.payable_amount_post_tax, currency_code=worksheet.currency_code, ) ) return details def get_credit_data( payment_group_payment_account: int, payment_type: str ) -> List[models.PaymentDebitCreditDataEntry]: """Get credit data. Arg: payment_group_payment_account(int): payment group payment account id payment_type(str): `wht` or `payment` """ details = ( models.PaymentGroupPaymentAccountDetail.get_by_payment_group_payment_account( # noqa payment_group_payment_account ) ) return [ models.PaymentDebitCreditDataEntry( d.account_id, d.contract_id, d.currency_code, _get_credit_amount_using_type(payment_type, d), d.worksheet_account_contract_payable_after_tax_id, ) for d in details # This should be flexible filter for any case, including corrections. if _get_credit_amount_using_type(payment_type, d) ] def _get_credit_amount_using_type( payment_type: str, account_detail: models.PaymentGroupPaymentAccountDetail ) -> Decimal: """Get credit amount. Debit of vat should be positive, while payment and wht are negative. Then it’s flipped for credit. Arg: payment_type(int): payment type `wht`, `vat` or `payment` account_detail(str): detail record """ match payment_type: case PAYMENT_TYPES.WHT: # negative according to formula, make positive return ( account_detail.tax_withholding_amount * -1 if account_detail.tax_withholding_amount else account_detail.tax_withholding_amount ) case PAYMENT_TYPES.VAT: # positive according to formula, make negative return ( account_detail.vat_amount * -1 if account_detail.vat_amount else account_detail.vat_amount ) case _: # positive according to formula, return as is return account_detail.payable_amount_post_tax def get_last_payments( limit: int, offset: int, account_ids: List[int], ) -> dict: """Get last payments by accounts ids and payment_statuses.""" items, total_count = payment_group_payment_account_repository.get_last_payments( limit=limit, offset=offset, account_ids=account_ids, ) return {'items': items, 'total_count': total_count} def payments_search( account_ids: set[int] | None = None, contract_ids: set[int] | None = None, payment_statuses: set[str] | None = None, limit: int = DEFAULT_PAGE_LIMIT, offset: int = DEFAULT_PAGE_OFFSET, ) -> dict: """Search payments by criteria.""" items, total_count = payment_group_payment_account_repository.payments_search( account_ids=account_ids, contract_ids=contract_ids, payment_statuses=payment_statuses, limit=limit, offset=offset, ) return {'items': items, 'total_count': total_count}